From eb1ada611575a6ae6f5ff01c428adb2a3c939239 Mon Sep 17 00:00:00 2001 From: Arash Outadi Date: Tue, 16 Jun 2020 12:12:04 -0700 Subject: [PATCH 01/27] issue1332 - Fix for yamls with unquoted double curly braces (#1988) * Gnarly hardcoded fix * Whoops remove println --- crates/nu-cli/src/commands/from_yaml.rs | 96 +++++++++++++++++++------ crates/nu-plugin/src/test_helpers.rs | 9 +++ 2 files changed, 85 insertions(+), 20 deletions(-) diff --git a/crates/nu-cli/src/commands/from_yaml.rs b/crates/nu-cli/src/commands/from_yaml.rs index f28e004482..bafb8bf17e 100644 --- a/crates/nu-cli/src/commands/from_yaml.rs +++ b/crates/nu-cli/src/commands/from_yaml.rs @@ -59,27 +59,19 @@ fn convert_yaml_value_to_nu_value( ) -> Result { let tag = tag.into(); + let err_not_compatible_number = ShellError::labeled_error( + "Expected a compatible number", + "expected a compatible number", + &tag, + ); Ok(match v { serde_yaml::Value::Bool(b) => UntaggedValue::boolean(*b).into_value(tag), serde_yaml::Value::Number(n) if n.is_i64() => { - UntaggedValue::int(n.as_i64().ok_or_else(|| { - ShellError::labeled_error( - "Expected a compatible number", - "expected a compatible number", - &tag, - ) - })?) - .into_value(tag) + UntaggedValue::int(n.as_i64().ok_or_else(|| err_not_compatible_number)?).into_value(tag) } serde_yaml::Value::Number(n) if n.is_f64() => { - UntaggedValue::decimal(n.as_f64().ok_or_else(|| { - ShellError::labeled_error( - "Expected a compatible number", - "expected a compatible number", - &tag, - ) - })?) - .into_value(tag) + UntaggedValue::decimal(n.as_f64().ok_or_else(|| err_not_compatible_number)?) + .into_value(tag) } serde_yaml::Value::String(s) => UntaggedValue::string(s).into_value(tag), serde_yaml::Value::Sequence(a) => { @@ -93,11 +85,39 @@ fn convert_yaml_value_to_nu_value( let mut collected = TaggedDictBuilder::new(&tag); for (k, v) in t.iter() { - match k { - serde_yaml::Value::String(k) => { + // A ShellError that we re-use multiple times in the Mapping scenario + let err_unexpected_map = ShellError::labeled_error( + format!("Unexpected YAML:\nKey: {:?}\nValue: {:?}", k, v), + "unexpected", + tag.clone(), + ); + match (k, v) { + (serde_yaml::Value::String(k), _) => { collected.insert_value(k.clone(), convert_yaml_value_to_nu_value(v, &tag)?); } - _ => unimplemented!("Unknown key type"), + // Hard-code fix for cases where "v" is a string without quotations with double curly braces + // e.g. k = value + // value: {{ something }} + // Strangely, serde_yaml returns + // "value" -> Mapping(Mapping { map: {Mapping(Mapping { map: {String("something"): Null} }): Null} }) + (serde_yaml::Value::Mapping(m), serde_yaml::Value::Null) => { + return m + .iter() + .take(1) + .collect_vec() + .first() + .and_then(|e| match e { + (serde_yaml::Value::String(s), serde_yaml::Value::Null) => Some( + UntaggedValue::string("{{ ".to_owned() + &s + " }}") + .into_value(tag), + ), + _ => None, + }) + .ok_or(err_unexpected_map); + } + (_, _) => { + return Err(err_unexpected_map); + } } } @@ -151,7 +171,9 @@ async fn from_yaml( #[cfg(test)] mod tests { - use super::FromYAML; + use super::*; + use nu_plugin::row; + use nu_plugin::test_helpers::value::string; #[test] fn examples_work_as_expected() { @@ -159,4 +181,38 @@ mod tests { test_examples(FromYAML {}) } + + #[test] + fn test_problematic_yaml() { + struct TestCase { + description: &'static str, + input: &'static str, + expected: Result, + } + let tt: Vec = vec![ + TestCase { + description: "Double Curly Braces With Quotes", + input: r#"value: "{{ something }}""#, + expected: Ok(row!["value".to_owned() => string("{{ something }}")]), + }, + TestCase { + description: "Double Curly Braces Without Quotes", + input: r#"value: {{ something }}"#, + expected: Ok(row!["value".to_owned() => string("{{ something }}")]), + }, + ]; + for tc in tt.into_iter() { + let actual = from_yaml_string_to_value(tc.input.to_owned(), Tag::default()); + if actual.is_err() { + assert!( + tc.expected.is_err(), + "actual is Err for test:\nTest Description {}\nErr: {:?}", + tc.description, + actual + ); + } else { + assert_eq!(actual, tc.expected, "{}", tc.description); + } + } + } } diff --git a/crates/nu-plugin/src/test_helpers.rs b/crates/nu-plugin/src/test_helpers.rs index f64c01ef52..2ef4661e2b 100644 --- a/crates/nu-plugin/src/test_helpers.rs +++ b/crates/nu-plugin/src/test_helpers.rs @@ -215,4 +215,13 @@ pub mod value { )) .into_untagged_value()) } + + #[macro_export] + macro_rules! row { + ($( $key: expr => $val: expr ),*) => {{ + let mut map = indexmap::IndexMap::new(); + $( map.insert($key, $val); )* + UntaggedValue::row(map).into_untagged_value() + }} + } } From 3d63901b3bf7dddc0829eeaccb5b30e3bfaae98b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BD=C3=A1dn=C3=ADk?= Date: Tue, 16 Jun 2020 22:58:41 +0300 Subject: [PATCH 02/27] Add 'every' command to select (or skip) every nth row (#1992) * Add 'every' command * Add --skip option to 'every' command This option skips instead of selects every nth row * Fix descriptions for 'every' command * Add docummentation for 'every' command * Check actual filenames in 'every' command tests --- crates/nu-cli/src/cli.rs | 1 + crates/nu-cli/src/commands.rs | 2 + crates/nu-cli/src/commands/every.rs | 105 +++++++++++ crates/nu-cli/tests/commands/every.rs | 245 ++++++++++++++++++++++++++ crates/nu-cli/tests/commands/mod.rs | 1 + docs/commands/every.md | 46 +++++ 6 files changed, 400 insertions(+) create mode 100644 crates/nu-cli/src/commands/every.rs create mode 100644 crates/nu-cli/tests/commands/every.rs create mode 100644 docs/commands/every.md diff --git a/crates/nu-cli/src/cli.rs b/crates/nu-cli/src/cli.rs index 14b423c454..05add78bc5 100644 --- a/crates/nu-cli/src/cli.rs +++ b/crates/nu-cli/src/cli.rs @@ -320,6 +320,7 @@ pub fn create_default_context( whole_stream_command(GroupByDate), whole_stream_command(First), whole_stream_command(Last), + whole_stream_command(Every), whole_stream_command(Nth), whole_stream_command(Drop), whole_stream_command(Format), diff --git a/crates/nu-cli/src/commands.rs b/crates/nu-cli/src/commands.rs index 51acae5929..dbb6b605f2 100644 --- a/crates/nu-cli/src/commands.rs +++ b/crates/nu-cli/src/commands.rs @@ -30,6 +30,7 @@ pub(crate) mod echo; pub(crate) mod enter; #[allow(unused)] pub(crate) mod evaluate_by; +pub(crate) mod every; pub(crate) mod exit; pub(crate) mod first; pub(crate) mod format; @@ -160,6 +161,7 @@ pub(crate) mod touch; pub(crate) use enter::Enter; #[allow(unused_imports)] pub(crate) use evaluate_by::EvaluateBy; +pub(crate) use every::Every; pub(crate) use exit::Exit; pub(crate) use first::First; pub(crate) use format::Format; diff --git a/crates/nu-cli/src/commands/every.rs b/crates/nu-cli/src/commands/every.rs new file mode 100644 index 0000000000..2019ce3a5a --- /dev/null +++ b/crates/nu-cli/src/commands/every.rs @@ -0,0 +1,105 @@ +use crate::commands::WholeStreamCommand; +use crate::context::CommandRegistry; +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::{ReturnSuccess, Signature, SyntaxShape, UntaggedValue}; +use nu_source::Tagged; + +pub struct Every; + +#[derive(Deserialize)] +pub struct EveryArgs { + stride: Tagged, + skip: Tagged, +} + +#[async_trait] +impl WholeStreamCommand for Every { + fn name(&self) -> &str { + "every" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required( + "stride", + SyntaxShape::Int, + "how many rows to skip between (and including) each row returned", + ) + .switch( + "skip", + "skip the rows that would be returned, instead of selecting them", + Some('s'), + ) + } + + fn usage(&self) -> &str { + "Show (or skip) every n-th row, starting from the first one." + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + every(args, registry).await + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Get every second row", + example: "echo [1 2 3 4 5] | every 2", + result: Some(vec![ + UntaggedValue::int(1).into(), + UntaggedValue::int(3).into(), + UntaggedValue::int(5).into(), + ]), + }, + Example { + description: "Skip every second row", + example: "echo [1 2 3 4 5] | every 2 --skip", + result: Some(vec![ + UntaggedValue::int(2).into(), + UntaggedValue::int(4).into(), + ]), + }, + ] + } +} + +async fn every(args: CommandArgs, registry: &CommandRegistry) -> Result { + let registry = registry.clone(); + let (EveryArgs { stride, skip }, input) = args.process(®istry).await?; + let v: Vec<_> = input.into_vec().await; + + let stride_desired = if stride.item < 1 { 1 } else { stride.item } as usize; + + let mut values_vec_deque = VecDeque::new(); + + for (i, x) in v.iter().enumerate() { + let should_include = if skip.item { + i % stride_desired != 0 + } else { + i % stride_desired == 0 + }; + + if should_include { + values_vec_deque.push_back(ReturnSuccess::value(x.clone())); + } + } + + Ok(futures::stream::iter(values_vec_deque).to_output_stream()) +} + +#[cfg(test)] +mod tests { + use super::Every; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(Every {}) + } +} diff --git a/crates/nu-cli/tests/commands/every.rs b/crates/nu-cli/tests/commands/every.rs new file mode 100644 index 0000000000..9992713460 --- /dev/null +++ b/crates/nu-cli/tests/commands/every.rs @@ -0,0 +1,245 @@ +use nu_test_support::fs::Stub::EmptyFile; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn gets_all_rows_by_every_zero() { + Playground::setup("every_test_1", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("los.txt"), + EmptyFile("tres.txt"), + EmptyFile("amigos.txt"), + EmptyFile("arepas.clu"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | get name + | every 0 + "# + )); + + let expected = nu!( + cwd: dirs.test(), pipeline( + r#" + echo [ amigos.txt arepas.clu los.txt tres.txt ] + "# + )); + + assert_eq!(actual.out, expected.out); + }) +} + +#[test] +fn gets_no_rows_by_every_skip_zero() { + Playground::setup("every_test_2", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("los.txt"), + EmptyFile("tres.txt"), + EmptyFile("amigos.txt"), + EmptyFile("arepas.clu"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | get name + | every 0 --skip + "# + )); + + let expected = nu!( + cwd: dirs.test(), pipeline( + r#" + echo [ ] + "# + )); + + assert_eq!(actual.out, expected.out); + }) +} + +#[test] +fn gets_all_rows_by_every_one() { + Playground::setup("every_test_3", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("los.txt"), + EmptyFile("tres.txt"), + EmptyFile("amigos.txt"), + EmptyFile("arepas.clu"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | get name + | every 1 + "# + )); + + let expected = nu!( + cwd: dirs.test(), pipeline( + r#" + echo [ amigos.txt arepas.clu los.txt tres.txt ] + "# + )); + + assert_eq!(actual.out, expected.out); + }) +} + +#[test] +fn gets_no_rows_by_every_skip_one() { + Playground::setup("every_test_4", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("los.txt"), + EmptyFile("tres.txt"), + EmptyFile("amigos.txt"), + EmptyFile("arepas.clu"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | get name + | every 1 --skip + "# + )); + + let expected = nu!( + cwd: dirs.test(), pipeline( + r#" + echo [ ] + "# + )); + + assert_eq!(actual.out, expected.out); + }) +} + +#[test] +fn gets_first_row_by_every_too_much() { + Playground::setup("every_test_5", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("los.txt"), + EmptyFile("tres.txt"), + EmptyFile("amigos.txt"), + EmptyFile("arepas.clu"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | get name + | every 999 + "# + )); + + let expected = nu!( + cwd: dirs.test(), pipeline( + r#" + echo [ amigos.txt ] + "# + )); + + assert_eq!(actual.out, expected.out); + }) +} + +#[test] +fn gets_all_rows_except_first_by_every_skip_too_much() { + Playground::setup("every_test_6", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("los.txt"), + EmptyFile("tres.txt"), + EmptyFile("amigos.txt"), + EmptyFile("arepas.clu"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | get name + | every 999 --skip + "# + )); + + let expected = nu!( + cwd: dirs.test(), pipeline( + r#" + echo [ arepas.clu los.txt tres.txt ] + "# + )); + + assert_eq!(actual.out, expected.out); + }) +} + +#[test] +fn gets_every_third_row() { + Playground::setup("every_test_7", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("los.txt"), + EmptyFile("tres.txt"), + EmptyFile("quatro.txt"), + EmptyFile("amigos.txt"), + EmptyFile("arepas.clu"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | get name + | every 3 + "# + )); + + let expected = nu!( + cwd: dirs.test(), pipeline( + r#" + echo [ amigos.txt quatro.txt ] + "# + )); + + assert_eq!(actual.out, expected.out); + }) +} + +#[test] +fn skips_every_third_row() { + Playground::setup("every_test_8", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("los.txt"), + EmptyFile("tres.txt"), + EmptyFile("quatro.txt"), + EmptyFile("amigos.txt"), + EmptyFile("arepas.clu"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | get name + | every 3 --skip + "# + )); + + let expected = nu!( + cwd: dirs.test(), pipeline( + r#" + echo [ arepas.clu los.txt tres.txt ] + "# + )); + + assert_eq!(actual.out, expected.out); + }) +} diff --git a/crates/nu-cli/tests/commands/mod.rs b/crates/nu-cli/tests/commands/mod.rs index e131e19b21..dd43d76201 100644 --- a/crates/nu-cli/tests/commands/mod.rs +++ b/crates/nu-cli/tests/commands/mod.rs @@ -10,6 +10,7 @@ mod default; mod drop; mod each; mod enter; +mod every; mod first; mod format; mod get; diff --git a/docs/commands/every.md b/docs/commands/every.md new file mode 100644 index 0000000000..1e93c42f5e --- /dev/null +++ b/docs/commands/every.md @@ -0,0 +1,46 @@ +# every + +Selects every n-th row of a table, starting from the first one. With the `--skip` flag, every n-th row will be skipped, inverting the original functionality. + +Syntax: `> [input-command] | every {flags}` + +## Flags + +* `--skip`, `-s`: Skip the rows that would be returned, instead of selecting them + + +## Examples + +```shell +> open contacts.csv +───┬─────────┬──────┬───────────────── + # │ first │ last │ email +───┼─────────┼──────┼───────────────── + 0 │ John │ Doe │ doe.1@email.com + 1 │ Jane │ Doe │ doe.2@email.com + 2 │ Chris │ Doe │ doe.3@email.com + 3 │ Francis │ Doe │ doe.4@email.com + 4 │ Stella │ Doe │ doe.5@email.com +───┴─────────┴──────┴───────────────── +``` + +```shell +> open contacts.csv | every 2 +───┬─────────┬──────┬───────────────── + # │ first │ last │ email +───┼─────────┼──────┼───────────────── + 0 │ John │ Doe │ doe.1@email.com + 2 │ Chris │ Doe │ doe.3@email.com + 4 │ Stella │ Doe │ doe.5@email.com +───┴─────────┴──────┴───────────────── +``` + +```shell +> open contacts.csv | every 2 --skip +───┬─────────┬──────┬───────────────── + # │ first │ last │ email +───┼─────────┼──────┼───────────────── + 1 │ Jane │ Doe │ doe.2@email.com + 3 │ Francis │ Doe │ doe.4@email.com +───┴─────────┴──────┴───────────────── +``` From 1b6f94b46c971ba9f810724c9abd899885809c1b Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 16 Jun 2020 16:00:49 -0400 Subject: [PATCH 03/27] Cal command code cleanup (#1990) * Cal command code cleanup * Reverting "index_map" back to "indexmap", since that is the convention used in the system --- crates/nu-cli/src/commands/cal.rs | 108 ++++++++++-------------------- 1 file changed, 34 insertions(+), 74 deletions(-) diff --git a/crates/nu-cli/src/commands/cal.rs b/crates/nu-cli/src/commands/cal.rs index bd80c2a18f..db459d63b0 100644 --- a/crates/nu-cli/src/commands/cal.rs +++ b/crates/nu-cli/src/commands/cal.rs @@ -112,90 +112,48 @@ fn get_invalid_year_shell_error(year_tag: &Tag) -> ShellError { } struct MonthHelper { - day_number_month_starts_on: u32, - number_of_days_in_month: u32, selected_year: i32, selected_month: u32, + day_number_of_week_month_starts_on: u32, + number_of_days_in_month: u32, + quarter_number: u32, + month_name: String, } impl MonthHelper { pub fn new(selected_year: i32, selected_month: u32) -> Result { - let mut month_helper = MonthHelper { - day_number_month_starts_on: 0, - number_of_days_in_month: 0, + let naive_date = NaiveDate::from_ymd_opt(selected_year, selected_month, 1).ok_or(())?; + let number_of_days_in_month = + MonthHelper::calculate_number_of_days_in_month(selected_year, selected_month)?; + + Ok(MonthHelper { selected_year, selected_month, - }; - - let chosen_date_result_one = month_helper.update_day_number_month_starts_on(); - let chosen_date_result_two = month_helper.update_number_of_days_in_month(); - - if chosen_date_result_one.is_ok() && chosen_date_result_two.is_ok() { - return Ok(month_helper); - } - - Err(()) + day_number_of_week_month_starts_on: naive_date.weekday().num_days_from_sunday(), + number_of_days_in_month, + quarter_number: ((selected_month - 1) / 3) + 1, + month_name: naive_date.format("%B").to_string().to_ascii_lowercase(), + }) } - pub fn get_month_name(&self) -> String { - let month_name = match self.selected_month { - 1 => "january", - 2 => "february", - 3 => "march", - 4 => "april", - 5 => "may", - 6 => "june", - 7 => "july", - 8 => "august", - 9 => "september", - 10 => "october", - 11 => "november", - _ => "december", - }; - - month_name.to_string() - } - - fn update_day_number_month_starts_on(&mut self) -> Result<(), ()> { - let naive_date_result = - MonthHelper::get_naive_date(self.selected_year, self.selected_month); - - match naive_date_result { - Ok(naive_date) => { - self.day_number_month_starts_on = naive_date.weekday().num_days_from_sunday(); - Ok(()) - } - _ => Err(()), - } - } - - fn update_number_of_days_in_month(&mut self) -> Result<(), ()> { + fn calculate_number_of_days_in_month( + mut selected_year: i32, + mut selected_month: u32, + ) -> Result { // Chrono does not provide a method to output the amount of days in a month // This is a workaround taken from the example code from the Chrono docs here: // https://docs.rs/chrono/0.3.0/chrono/naive/date/struct.NaiveDate.html#example-30 - let (adjusted_year, adjusted_month) = if self.selected_month == 12 { - (self.selected_year + 1, 1) + if selected_month == 12 { + selected_year += 1; + selected_month = 1; } else { - (self.selected_year, self.selected_month + 1) + selected_month += 1; }; - let naive_date_result = MonthHelper::get_naive_date(adjusted_year, adjusted_month); + let next_month_naive_date = + NaiveDate::from_ymd_opt(selected_year, selected_month, 1).ok_or(())?; - match naive_date_result { - Ok(naive_date) => { - self.number_of_days_in_month = naive_date.pred().day(); - Ok(()) - } - _ => Err(()), - } - } - - fn get_naive_date(selected_year: i32, selected_month: u32) -> Result { - if let Some(naive_date) = NaiveDate::from_ymd_opt(selected_year, selected_month, 1) { - return Ok(naive_date); - } - - Err(()) + Ok(next_month_naive_date.pred().day()) } } @@ -268,7 +226,8 @@ fn add_month_to_table( }, }; - let day_limit = month_helper.number_of_days_in_month + month_helper.day_number_month_starts_on; + let day_limit = + month_helper.number_of_days_in_month + month_helper.day_number_of_week_month_starts_on; let mut day_count: u32 = 1; let days_of_the_week = [ @@ -282,8 +241,8 @@ fn add_month_to_table( ]; let should_show_year_column = args.has("year"); - let should_show_month_column = args.has("month"); let should_show_quarter_column = args.has("quarter"); + let should_show_month_column = args.has("month"); let should_show_month_names = args.has("month-names"); while day_count <= day_limit { @@ -299,13 +258,13 @@ fn add_month_to_table( if should_show_quarter_column { indexmap.insert( "quarter".to_string(), - UntaggedValue::int(((month_helper.selected_month - 1) / 3) + 1).into_value(tag), + UntaggedValue::int(month_helper.quarter_number).into_value(tag), ); } if should_show_month_column { let month_value = if should_show_month_names { - UntaggedValue::string(month_helper.get_month_name()).into_value(tag) + UntaggedValue::string(month_helper.month_name.clone()).into_value(tag) } else { UntaggedValue::int(month_helper.selected_month).into_value(tag) }; @@ -314,13 +273,14 @@ fn add_month_to_table( } for day in &days_of_the_week { - let should_add_day_number_to_table = - (day_count <= day_limit) && (day_count > month_helper.day_number_month_starts_on); + let should_add_day_number_to_table = (day_count <= day_limit) + && (day_count > month_helper.day_number_of_week_month_starts_on); let mut value = UntaggedValue::nothing().into_value(tag); if should_add_day_number_to_table { - let day_count_with_offset = day_count - month_helper.day_number_month_starts_on; + let day_count_with_offset = + day_count - month_helper.day_number_of_week_month_starts_on; value = UntaggedValue::int(day_count_with_offset).into_value(tag); From 6914099e28957acdbd1e06adb3e081a0bc589fca Mon Sep 17 00:00:00 2001 From: Darren Schroeder Date: Tue, 16 Jun 2020 16:17:32 -0500 Subject: [PATCH 04/27] Cat with wings (#1993) * WIP - Modified textview to use bat crate * use input_from_bytes_with_name instead of input_file * removed old paging added prettyprint on else blocks duplicated too much code hard coded defaults Co-authored-by: Darren Schroeder --- Cargo.lock | 322 ++++++++++++++++++++++ crates/nu_plugin_textview/Cargo.toml | 3 + crates/nu_plugin_textview/src/textview.rs | 307 +++++---------------- 3 files changed, 391 insertions(+), 241 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 660f439981..6940bb757c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,6 +24,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "ansi_colours" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d0f302a81afc6a7f4350c04f0ba7cfab529cc009bca3324b3fb5764e6add8b6" +dependencies = [ + "cc", +] + [[package]] name = "ansi_term" version = "0.11.0" @@ -42,6 +51,12 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "anymap" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33954243bd79057c2de7338850b85983a44588021f8a5fee574a8888c6de4344" + [[package]] name = "app_dirs" version = "1.2.1" @@ -212,6 +227,35 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d1ccbaf7d9ec9537465a97bf19edc1a4e158ecb49fc16178202238c569cc42" +[[package]] +name = "bat" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91f17c2d9e1cee447a788a15fa6819c0cb488fb2935e3e8c4e7120e1678b7aa8" +dependencies = [ + "ansi_colours", + "ansi_term 0.12.1", + "atty", + "clap", + "console", + "content_inspector", + "dirs 2.0.2", + "encoding", + "error-chain", + "git2", + "globset", + "lazy_static 1.4.0", + "liquid", + "path_abs", + "semver 0.9.0", + "serde 1.0.110", + "serde_yaml", + "shell-words", + "syntect", + "unicode-width", + "wild", +] + [[package]] name = "battery" version = "0.7.5" @@ -433,6 +477,7 @@ dependencies = [ "atty", "bitflags", "strsim", + "term_size", "textwrap", "unicode-width", "vec_map", @@ -505,12 +550,38 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "console" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c0994e656bba7b922d8dd1245db90672ffb701e684e45be58f20719d69abc5a" +dependencies = [ + "encode_unicode", + "lazy_static 1.4.0", + "libc", + "regex", + "terminal_size", + "termios", + "unicode-width", + "winapi 0.3.8", + "winapi-util", +] + [[package]] name = "constant_time_eq" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "content_inspector" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38" +dependencies = [ + "memchr", +] + [[package]] name = "core-foundation" version = "0.6.4" @@ -914,6 +985,70 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "encoding" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +dependencies = [ + "encoding-index-japanese", + "encoding-index-korean", + "encoding-index-simpchinese", + "encoding-index-singlebyte", + "encoding-index-tradchinese", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" + [[package]] name = "encoding_rs" version = "0.8.23" @@ -945,6 +1080,15 @@ dependencies = [ "serde 1.0.110", ] +[[package]] +name = "error-chain" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d371106cc88ffdfb1eabd7111e432da544f16f3e2d7bf1dfe8bf575f1df045cd" +dependencies = [ + "version_check 0.9.1", +] + [[package]] name = "failure" version = "0.1.8" @@ -1334,6 +1478,19 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +[[package]] +name = "globset" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ad1da430bd7281dde2576f44c84cc3f0f7b475e7202cd503042dff01a8c8120" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + [[package]] name = "heim" version = "0.1.0-beta.2" @@ -1740,6 +1897,15 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "kstring" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6382df53100fd22e149030b6634720c94a151076db8d727b3274d7070975d609" +dependencies = [ + "serde 1.0.110", +] + [[package]] name = "kv-log-macro" version = "1.0.5" @@ -1856,6 +2022,64 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" +[[package]] +name = "liquid" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503b7cd741bf1a6c01bfdf697ba13f67e2c8e152920af25596763bb0dbcd6215" +dependencies = [ + "doc-comment", + "kstring", + "liquid-core", + "liquid-derive", + "liquid-lib", + "serde 1.0.110", +] + +[[package]] +name = "liquid-core" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea59709d9530bab4e9cc0ece12f20fe4999fdef90f7d89391b0fc9ff563b62b" +dependencies = [ + "anymap", + "chrono", + "itertools", + "kstring", + "liquid-derive", + "num-traits 0.2.11", + "pest", + "pest_derive", + "serde 1.0.110", +] + +[[package]] +name = "liquid-derive" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfef35f37f019e5dfc550517045078317f5d37afa64cbf246ecde616a7091cb0" +dependencies = [ + "proc-macro2", + "proc-quote", + "syn", +] + +[[package]] +name = "liquid-lib" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c4aa47dc08fd8c6c8aea70a0da2a98c0f0416d49e8b03c5c46354ef559bee3c" +dependencies = [ + "chrono", + "itertools", + "kstring", + "liquid-core", + "once_cell", + "percent-encoding", + "regex", + "unicode-segmentation", +] + [[package]] name = "lock_api" version = "0.3.4" @@ -2540,6 +2764,7 @@ name = "nu_plugin_textview" version = "0.15.1" dependencies = [ "ansi_term 0.12.1", + "bat", "crossterm", "nu-build", "nu-errors", @@ -2547,6 +2772,7 @@ dependencies = [ "nu-protocol", "nu-source", "syntect", + "textwrap", "url", ] @@ -2686,6 +2912,28 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b631f7e854af39a1739f401cf34a8a013dfe09eac4fa4dba91e9768bd28168d" +[[package]] +name = "onig" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd91ccd8a02fce2f7e8a86655aec67bc6c171e6f8e704118a0e8c4b866a05a8a" +dependencies = [ + "bitflags", + "lazy_static 1.4.0", + "libc", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3814583fad89f3c60ae0701d80e87e1fd3028741723deda72d0d4a0ecf0cb0db" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "opaque-debug" version = "0.2.3" @@ -2790,6 +3038,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0858af4d9136275541f4eac7be1af70add84cf356d901799b065ac1b8ff6e2f" +[[package]] +name = "path_abs" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb6b8e6dede0bf94e9300e669f335ba92d5fc9fc8be7f4b1ca8a05206489388c" +dependencies = [ + "std_prelude", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -3029,6 +3286,30 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "proc-quote" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea4226882439d07839be9c7f683e13d6d69d9c2fe960d61f637d1e2fa4c081" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "proc-quote-impl", + "quote", + "syn", +] + +[[package]] +name = "proc-quote-impl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb3ec628b063cdbcf316e06a8b8c1a541d28fa6c0a8eacd2bfb2b7f49e88aa0" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", +] + [[package]] name = "ptree" version = "0.2.1" @@ -3575,6 +3856,12 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "shell-words" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fa3938c99da4914afedd13bf3d79bcb6c277d1b2c398d23257a304d9e1b074" + [[package]] name = "shell32-sys" version = "0.1.2" @@ -3724,6 +4011,12 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f3eb36b47e512f8f1c9e3d10c2c1965bc992bd9cdb024fa581e2194501c83d3" +[[package]] +name = "std_prelude" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8207e78455ffdf55661170876f88daf85356e4edd54e0a3dbc79586ca1e50cbe" + [[package]] name = "strip-ansi-escapes" version = "0.1.0" @@ -3808,6 +4101,7 @@ dependencies = [ "fnv", "lazy_static 1.4.0", "lazycell", + "onig", "plist", "regex-syntax", "serde 1.0.110", @@ -3876,6 +4170,25 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal_size" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8038f95fc7a6f351163f4b964af631bd26c9e828f7db085f2a84aca56f70d13b" +dependencies = [ + "libc", + "winapi 0.3.8", +] + +[[package]] +name = "termios" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0fcee7b24a25675de40d5bb4de6e41b0df07bc9856295e7e2b3a3600c400c2" +dependencies = [ + "libc", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -4323,6 +4636,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "effc0e4ff8085673ea7b9b2e3c73f6bd4d118810c9009ed8f1e16bd96c331db6" +[[package]] +name = "wild" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "035793abb854745033f01a07647a79831eba29ec0be377205f2a25b0aa830020" +dependencies = [ + "glob", +] + [[package]] name = "winapi" version = "0.2.8" diff --git a/crates/nu_plugin_textview/Cargo.toml b/crates/nu_plugin_textview/Cargo.toml index da145fac8f..65e8019645 100644 --- a/crates/nu_plugin_textview/Cargo.toml +++ b/crates/nu_plugin_textview/Cargo.toml @@ -14,11 +14,14 @@ nu-plugin = { path = "../nu-plugin", version = "0.15.1" } nu-protocol = { path = "../nu-protocol", version = "0.15.1" } nu-source = { path = "../nu-source", version = "0.15.1" } nu-errors = { path = "../nu-errors", version = "0.15.1" } +#nu-cli = { path = "../nu-cli", version = "0.15.1" } crossterm = "0.17.5" syntect = { version = "4.2", default-features = false, features = ["default-fancy"]} ansi_term = "0.12.1" url = "2.1.1" +bat = "0.15.4" +textwrap = {version = "0.11.0", features = ["term_size"]} [build-dependencies] nu-build = { version = "0.15.1", path = "../nu-build" } diff --git a/crates/nu_plugin_textview/src/textview.rs b/crates/nu_plugin_textview/src/textview.rs index 7e06d5378c..89e422a9bf 100644 --- a/crates/nu_plugin_textview/src/textview.rs +++ b/crates/nu_plugin_textview/src/textview.rs @@ -1,23 +1,7 @@ -use crossterm::{ - event::{KeyCode, KeyEvent}, - ExecutableCommand, -}; - use nu_protocol::{Primitive, UntaggedValue, Value}; use nu_source::AnchorLocation; - -use syntect::easy::HighlightLines; -use syntect::highlighting::{Style, ThemeSet}; -use syntect::parsing::SyntaxSet; - -use std::io::Write; use std::path::Path; -enum DrawCommand { - DrawString(Style, String), - NextLine, -} - #[derive(Default)] pub struct TextView; @@ -27,209 +11,14 @@ impl TextView { } } -fn paint_textview( - draw_commands: &[DrawCommand], - starting_row: usize, - use_color_buffer: bool, -) -> usize { - let size = crossterm::terminal::size().unwrap_or_else(|_| (80, 24)); - - // render - let mut pos = 0; - let width = size.0 as usize; - let height = size.1 as usize - 1; - let mut frame_buffer = vec![]; - - for command in draw_commands { - match command { - DrawCommand::DrawString(style, string) => { - for chr in string.chars() { - if chr == '\t' { - for _ in 0..8 { - frame_buffer.push(( - ' ', - style.foreground.r, - style.foreground.g, - style.foreground.b, - )); - } - pos += 8; - } else { - frame_buffer.push(( - chr, - style.foreground.r, - style.foreground.g, - style.foreground.b, - )); - pos += 1; - } - } - } - DrawCommand::NextLine => { - for _ in 0..(width - pos % width) { - frame_buffer.push((' ', 0, 0, 0)); - } - pos += width - pos % width; - } - } - } - - let num_frame_buffer_rows = frame_buffer.len() / width; - let buffer_needs_scrolling = num_frame_buffer_rows > height; - - // display - let mut ansi_strings = vec![]; - let mut normal_chars = vec![]; - - for c in - &frame_buffer[starting_row * width..std::cmp::min(pos, (starting_row + height) * width)] - { - if use_color_buffer { - ansi_strings.push(ansi_term::Colour::RGB(c.1, c.2, c.3).paint(format!("{}", c.0))); - } else { - normal_chars.push(c.0); - } - } - - if buffer_needs_scrolling { - let _ = std::io::stdout().execute(crossterm::cursor::MoveTo(0, 0)); - } - - if use_color_buffer { - print!("{}", ansi_term::ANSIStrings(&ansi_strings)); - } else { - let s: String = normal_chars.into_iter().collect(); - print!("{}", s); - } - - if buffer_needs_scrolling { - let _ = std::io::stdout().execute(crossterm::cursor::MoveTo(0, size.1)); - print!( - "{}", - ansi_term::Colour::Blue.paint("[ESC to quit, arrow keys to move]") - ); - } - - let _ = std::io::stdout().flush(); - - num_frame_buffer_rows -} - -fn scroll_view_lines_if_needed(draw_commands: Vec, use_color_buffer: bool) { - let mut starting_row = 0; - - if let Ok(_raw) = crossterm::terminal::enable_raw_mode() { - let mut size = crossterm::terminal::size().unwrap_or_else(|_| (80, 24)); - let height = size.1 as usize - 1; - - let mut max_bottom_line = paint_textview(&draw_commands, starting_row, use_color_buffer); - - // Only scroll if needed - if max_bottom_line > height as usize { - let _ = std::io::stdout().execute(crossterm::cursor::Hide); - - loop { - if let Ok(ev) = crossterm::event::read() { - if let crossterm::event::Event::Key(KeyEvent { code, modifiers }) = ev { - match code { - KeyCode::Esc => { - break; - } - KeyCode::Up | KeyCode::Char('k') => { - if starting_row > 0 { - starting_row -= 1; - max_bottom_line = paint_textview( - &draw_commands, - starting_row, - use_color_buffer, - ); - } - } - KeyCode::Down | KeyCode::Char('j') => { - if starting_row < (max_bottom_line - height) { - starting_row += 1; - } - max_bottom_line = - paint_textview(&draw_commands, starting_row, use_color_buffer); - } - KeyCode::Char('b') - if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) => - { - starting_row -= std::cmp::min(height, starting_row); - max_bottom_line = - paint_textview(&draw_commands, starting_row, use_color_buffer); - } - KeyCode::PageUp => { - starting_row -= std::cmp::min(height, starting_row); - max_bottom_line = - paint_textview(&draw_commands, starting_row, use_color_buffer); - } - KeyCode::Char('f') - if modifiers.contains(crossterm::event::KeyModifiers::CONTROL) => - { - if starting_row < (max_bottom_line - height) { - starting_row += height; - - if starting_row > (max_bottom_line - height) { - starting_row = max_bottom_line - height; - } - } - max_bottom_line = - paint_textview(&draw_commands, starting_row, use_color_buffer); - } - KeyCode::PageDown | KeyCode::Char(' ') => { - if starting_row < (max_bottom_line - height) { - starting_row += height; - - if starting_row > (max_bottom_line - height) { - starting_row = max_bottom_line - height; - } - } - max_bottom_line = - paint_textview(&draw_commands, starting_row, use_color_buffer); - } - _ => {} - } - } - } - - if let Ok(new_size) = crossterm::terminal::size() { - if size != new_size { - size = new_size; - let _ = std::io::stdout().execute(crossterm::terminal::Clear( - crossterm::terminal::ClearType::All, - )); - max_bottom_line = - paint_textview(&draw_commands, starting_row, use_color_buffer); - } - } - } - } - - let _ = std::io::stdout().execute(crossterm::cursor::Show); - let _ = crossterm::terminal::disable_raw_mode(); - } - - println!() -} - -fn scroll_view(s: &str) { - let mut v = vec![]; - for line in s.lines() { - v.push(DrawCommand::DrawString(Style::default(), line.to_string())); - v.push(DrawCommand::NextLine); - } - scroll_view_lines_if_needed(v, false); -} - pub fn view_text_value(value: &Value) { let value_anchor = value.anchor(); if let UntaggedValue::Primitive(Primitive::String(ref s)) = &value.value { if let Some(source) = value_anchor { - let extension: Option = match source { + let file_path: Option = match source { AnchorLocation::File(file) => { let path = Path::new(&file); - path.extension().map(|x| x.to_string_lossy().to_string()) + Some(path.to_string_lossy().to_string()) } AnchorLocation::Url(url) => { let url = url::Url::parse(&url); @@ -237,7 +26,7 @@ pub fn view_text_value(value: &Value) { if let Some(mut segments) = url.path_segments() { if let Some(file) = segments.next_back() { let path = Path::new(file); - path.extension().map(|x| x.to_string_lossy().to_string()) + Some(path.to_string_lossy().to_string()) } else { None } @@ -252,38 +41,74 @@ pub fn view_text_value(value: &Value) { AnchorLocation::Source(_source) => None, }; - match extension { - Some(extension) => { - // Load these once at the start of your program - let ps: SyntaxSet = - syntect::dumps::from_binary(include_bytes!("assets/syntaxes.bin")); - - if let Some(syntax) = ps.find_syntax_by_extension(&extension) { - let ts: ThemeSet = - syntect::dumps::from_binary(include_bytes!("assets/themes.bin")); - let mut h = HighlightLines::new(syntax, &ts.themes["OneHalfDark"]); - - let mut v = vec![]; - for line in s.lines() { - let ranges: Vec<(Style, &str)> = h.highlight(line, &ps); - - for range in ranges { - v.push(DrawCommand::DrawString(range.0, range.1.to_string())); - } - - v.push(DrawCommand::NextLine); - } - scroll_view_lines_if_needed(v, true); - } else { - scroll_view(s); - } + match file_path { + Some(file_path) => { + // Let bat do it's thing + bat::PrettyPrinter::new() + .input_from_bytes_with_name(s.as_bytes(), file_path) + .term_width(textwrap::termwidth()) + .tab_width(Some(4)) + .colored_output(true) + .true_color(true) + .header(true) + .line_numbers(true) + .grid(true) + .vcs_modification_markers(true) + .snip(true) + .wrapping_mode(bat::WrappingMode::NoWrapping) + .use_italics(true) + .paging_mode(bat::PagingMode::QuitIfOneScreen) + .pager("less") + .line_ranges(bat::line_range::LineRanges::all()) + .highlight_range(0, 0) + .theme("OneHalfDark") + .print() + .expect("Error with bat PrettyPrint"); } _ => { - scroll_view(s); + bat::PrettyPrinter::new() + .input_from_bytes(s.as_bytes()) + .term_width(textwrap::termwidth()) + .tab_width(Some(4)) + .colored_output(true) + .true_color(true) + .header(true) + .line_numbers(true) + .grid(true) + .vcs_modification_markers(true) + .snip(true) + .wrapping_mode(bat::WrappingMode::NoWrapping) + .use_italics(true) + .paging_mode(bat::PagingMode::QuitIfOneScreen) + .pager("less") + .line_ranges(bat::line_range::LineRanges::all()) + .highlight_range(0, 0) + .theme("OneHalfDark") + .print() + .expect("Error with bat PrettyPrint"); } } } else { - scroll_view(s); + bat::PrettyPrinter::new() + .input_from_bytes(s.as_bytes()) + .term_width(textwrap::termwidth()) + .tab_width(Some(4)) + .colored_output(true) + .true_color(true) + .header(true) + .line_numbers(true) + .grid(true) + .vcs_modification_markers(true) + .snip(true) + .wrapping_mode(bat::WrappingMode::NoWrapping) + .use_italics(true) + .paging_mode(bat::PagingMode::QuitIfOneScreen) + .pager("less") + .line_ranges(bat::line_range::LineRanges::all()) + .highlight_range(0, 0) + .theme("OneHalfDark") + .print() + .expect("Error with bat PrettyPrint"); } } } From 778e497903d58fa702930d2f2a1117538b41e171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20N=2E=20Robalino?= Date: Wed, 17 Jun 2020 12:33:50 -0500 Subject: [PATCH 05/27] Refactoring and more group-by flexibility. --- crates/nu-cli/src/cli.rs | 13 +- crates/nu-cli/src/commands/group_by.rs | 91 +++++++++-- crates/nu-cli/src/commands/group_by_date.rs | 99 +++++++++--- crates/nu-cli/src/commands/histogram.rs | 10 +- crates/nu-cli/src/commands/split_by.rs | 160 +++++++++----------- crates/nu-cli/src/commands/t_sort_by.rs | 2 +- crates/nu-cli/src/utils/data/group.rs | 53 ++----- crates/nu-cli/src/utils/data/mod.rs | 2 + crates/nu-cli/src/utils/data_processing.rs | 17 ++- crates/nu-protocol/src/value.rs | 8 + 10 files changed, 266 insertions(+), 189 deletions(-) diff --git a/crates/nu-cli/src/cli.rs b/crates/nu-cli/src/cli.rs index 05add78bc5..5136e875b0 100644 --- a/crates/nu-cli/src/cli.rs +++ b/crates/nu-cli/src/cli.rs @@ -49,8 +49,9 @@ fn load_plugin(path: &std::path::Path, context: &mut Context) -> Result<(), Shel let mut input = String::new(); let result = match reader.read_line(&mut input) { Ok(count) => { - trace!("processing response ({} bytes)", count); - trace!("response: {}", input); + trace!(target: "nu::load", "plugin infrastructure -> config response"); + trace!(target: "nu::load", "plugin infrastructure -> processing response ({} bytes)", count); + trace!(target: "nu::load", "plugin infrastructure -> response: {}", input); let response = serde_json::from_str::>>(&input); match response { @@ -58,13 +59,13 @@ fn load_plugin(path: &std::path::Path, context: &mut Context) -> Result<(), Shel Ok(params) => { let fname = path.to_string_lossy(); - trace!("processing {:?}", params); + trace!(target: "nu::load", "plugin infrastructure -> processing {:?}", params); let name = params.name.clone(); let fname = fname.to_string(); if context.get_command(&name).is_some() { - trace!("plugin {:?} already loaded.", &name); + trace!(target: "nu::load", "plugin infrastructure -> {:?} already loaded.", &name); } else if params.is_filter { context.add_commands(vec![whole_stream_command(PluginCommand::new( name, fname, params, @@ -79,7 +80,7 @@ fn load_plugin(path: &std::path::Path, context: &mut Context) -> Result<(), Shel Err(e) => Err(e), }, Err(e) => { - trace!("incompatible plugin {:?}", input); + trace!(target: "nu::load", "plugin infrastructure -> incompatible {:?}", input); Err(ShellError::untagged_runtime_error(format!( "Error: {:?}", e @@ -188,7 +189,7 @@ pub fn load_plugins(context: &mut Context) -> Result<(), ShellError> { }; if is_valid_name && is_executable { - trace!("Trying {:?}", path.display()); + trace!(target: "nu::load", "plugin infrastructure -> Trying {:?}", path.display()); // we are ok if this plugin load fails let _ = load_plugin(&path, &mut context.clone()); diff --git a/crates/nu-cli/src/commands/group_by.rs b/crates/nu-cli/src/commands/group_by.rs index 4b63ed8233..c762f05a3a 100644 --- a/crates/nu-cli/src/commands/group_by.rs +++ b/crates/nu-cli/src/commands/group_by.rs @@ -4,6 +4,7 @@ use indexmap::indexmap; use nu_errors::ShellError; use nu_protocol::{ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value}; use nu_source::Tagged; +use nu_value_ext::as_string; pub struct GroupBy; @@ -71,6 +72,10 @@ impl WholeStreamCommand for GroupBy { } } +enum Grouper { + ByColumn(Option>), +} + pub async fn group_by( args: CommandArgs, registry: &CommandRegistry, @@ -81,30 +86,84 @@ pub async fn group_by( let values: Vec = input.collect().await; if values.is_empty() { - Err(ShellError::labeled_error( + return Err(ShellError::labeled_error( "Expected table from pipeline", "requires a table input", name, - )) + )); + } + + let values = UntaggedValue::table(&values).into_value(&name); + + match group(&column_name, &values, name) { + Ok(grouped) => Ok(OutputStream::one(ReturnSuccess::value(grouped))), + Err(reason) => Err(reason), + } +} + +pub fn suggestions(tried: Tagged<&str>, for_value: &Value) -> ShellError { + let possibilities = for_value.data_descriptors(); + + let mut possible_matches: Vec<_> = possibilities + .iter() + .map(|x| (natural::distance::levenshtein_distance(x, &tried), x)) + .collect(); + + possible_matches.sort(); + + if !possible_matches.is_empty() { + ShellError::labeled_error( + "Unknown column", + format!("did you mean '{}'?", possible_matches[0].1), + tried.tag(), + ) } else { - match crate::utils::data::group(column_name, &values, None, &name) { - Ok(grouped) => Ok(OutputStream::one(ReturnSuccess::value(grouped))), - Err(err) => Err(err), - } + ShellError::labeled_error( + "Unknown column", + "row does not contain this column", + tried.tag(), + ) } } pub fn group( - column_name: &Tagged, - values: Vec, + column_name: &Option>, + values: &Value, tag: impl Into, ) -> Result { - crate::utils::data::group(Some(column_name.clone()), &values, None, tag) + let name = tag.into(); + + let grouper = if let Some(column_name) = column_name { + Grouper::ByColumn(Some(column_name.clone())) + } else { + Grouper::ByColumn(None) + }; + + match grouper { + Grouper::ByColumn(Some(column_name)) => { + let block = Box::new(move |row: &Value| { + match row.get_data_by_key(column_name.borrow_spanned()) { + Some(group_key) => Ok(as_string(&group_key)?), + None => Err(suggestions(column_name.borrow_tagged(), &row)), + } + }); + + crate::utils::data::group(&values, &Some(block), &name) + } + Grouper::ByColumn(None) => { + let block = Box::new(move |row: &Value| match as_string(row) { + Ok(group_key) => Ok(group_key), + Err(reason) => Err(reason), + }); + + crate::utils::data::group(&values, &Some(block), &name) + } + } } #[cfg(test)] mod tests { - use crate::commands::group_by::group; + use super::group; use indexmap::IndexMap; use nu_errors::ShellError; use nu_protocol::{UntaggedValue, Value}; @@ -122,7 +181,7 @@ mod tests { UntaggedValue::table(list).into_untagged_value() } - fn nu_releases_commiters() -> Vec { + fn nu_releases_committers() -> Vec { vec![ row( indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("August 23-2019")}, @@ -156,10 +215,11 @@ mod tests { #[test] fn groups_table_by_date_column() -> Result<(), ShellError> { - let for_key = String::from("date").tagged_unknown(); + let for_key = Some(String::from("date").tagged_unknown()); + let sample = table(&nu_releases_committers()); assert_eq!( - group(&for_key, nu_releases_commiters(), Tag::unknown())?, + group(&for_key, &sample, Tag::unknown())?, row(indexmap! { "August 23-2019".into() => table(&[ row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("August 23-2019")}), @@ -184,10 +244,11 @@ mod tests { #[test] fn groups_table_by_country_column() -> Result<(), ShellError> { - let for_key = String::from("country").tagged_unknown(); + let for_key = Some(String::from("country").tagged_unknown()); + let sample = table(&nu_releases_committers()); assert_eq!( - group(&for_key, nu_releases_commiters(), Tag::unknown())?, + group(&for_key, &sample, Tag::unknown())?, row(indexmap! { "EC".into() => table(&[ row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("August 23-2019")}), diff --git a/crates/nu-cli/src/commands/group_by_date.rs b/crates/nu-cli/src/commands/group_by_date.rs index f361deb207..b4c0391d86 100644 --- a/crates/nu-cli/src/commands/group_by_date.rs +++ b/crates/nu-cli/src/commands/group_by_date.rs @@ -1,7 +1,7 @@ use crate::commands::WholeStreamCommand; use crate::prelude::*; use nu_errors::ShellError; -use nu_protocol::{ReturnSuccess, Signature, SyntaxShape, Value}; +use nu_protocol::{ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value}; use nu_source::Tagged; pub struct GroupByDate; @@ -55,7 +55,11 @@ impl WholeStreamCommand for GroupByDate { } enum Grouper { - ByDate(Option), + ByDate(Option>), +} + +enum GroupByColumn { + Name(Option>), } pub async fn group_by_date( @@ -80,31 +84,63 @@ pub async fn group_by_date( name, )) } else { - let grouper = if let Some(Tagged { item: fmt, tag: _ }) = format { - Grouper::ByDate(Some(fmt)) + let values = UntaggedValue::table(&values).into_value(&name); + + let grouper_column = if let Some(column_name) = column_name { + GroupByColumn::Name(Some(column_name)) + } else { + GroupByColumn::Name(None) + }; + + let grouper_date = if let Some(date_format) = format { + Grouper::ByDate(Some(date_format)) } else { Grouper::ByDate(None) }; - match grouper { - Grouper::ByDate(None) => { - match crate::utils::data::group( - column_name, - &values, - Some(Box::new(|row: &Value| row.format("%Y-%b-%d"))), - &name, - ) { + match (grouper_date, grouper_column) { + (Grouper::ByDate(None), GroupByColumn::Name(None)) => { + let block = Box::new(move |row: &Value| row.format("%Y-%b-%d")); + + match crate::utils::data::group(&values, &Some(block), &name) { Ok(grouped) => Ok(OutputStream::one(ReturnSuccess::value(grouped))), Err(err) => Err(err), } } - Grouper::ByDate(Some(fmt)) => { - match crate::utils::data::group( - column_name, - &values, - Some(Box::new(move |row: &Value| row.format(&fmt))), - &name, - ) { + (Grouper::ByDate(None), GroupByColumn::Name(Some(column_name))) => { + let block = Box::new(move |row: &Value| { + let group_key = match row.get_data_by_key(column_name.borrow_spanned()) { + Some(group_key) => Ok(group_key), + None => Err(suggestions(column_name.borrow_tagged(), &row)), + }; + + group_key?.format("%Y-%b-%d") + }); + + match crate::utils::data::group(&values, &Some(block), &name) { + Ok(grouped) => Ok(OutputStream::one(ReturnSuccess::value(grouped))), + Err(err) => Err(err), + } + } + (Grouper::ByDate(Some(fmt)), GroupByColumn::Name(None)) => { + let block = Box::new(move |row: &Value| row.format(&fmt)); + + match crate::utils::data::group(&values, &Some(block), &name) { + Ok(grouped) => Ok(OutputStream::one(ReturnSuccess::value(grouped))), + Err(err) => Err(err), + } + } + (Grouper::ByDate(Some(fmt)), GroupByColumn::Name(Some(column_name))) => { + let block = Box::new(move |row: &Value| { + let group_key = match row.get_data_by_key(column_name.borrow_spanned()) { + Some(group_key) => Ok(group_key), + None => Err(suggestions(column_name.borrow_tagged(), &row)), + }; + + group_key?.format(&fmt) + }); + + match crate::utils::data::group(&values, &Some(block), &name) { Ok(grouped) => Ok(OutputStream::one(ReturnSuccess::value(grouped))), Err(err) => Err(err), } @@ -113,6 +149,31 @@ pub async fn group_by_date( } } +pub fn suggestions(tried: Tagged<&str>, for_value: &Value) -> ShellError { + let possibilities = for_value.data_descriptors(); + + let mut possible_matches: Vec<_> = possibilities + .iter() + .map(|x| (natural::distance::levenshtein_distance(x, &tried), x)) + .collect(); + + possible_matches.sort(); + + if !possible_matches.is_empty() { + ShellError::labeled_error( + "Unknown column", + format!("did you mean '{}'?", possible_matches[0].1), + tried.tag(), + ) + } else { + ShellError::labeled_error( + "Unknown column", + "row does not contain this column", + tried.tag(), + ) + } +} + #[cfg(test)] mod tests { use super::GroupByDate; diff --git a/crates/nu-cli/src/commands/histogram.rs b/crates/nu-cli/src/commands/histogram.rs index 8a8c3b70ad..1e6fb4244e 100644 --- a/crates/nu-cli/src/commands/histogram.rs +++ b/crates/nu-cli/src/commands/histogram.rs @@ -76,14 +76,14 @@ pub async fn histogram( ) -> Result { let registry = registry.clone(); let name = args.call_info.name_tag.clone(); + let (HistogramArgs { column_name, rest }, input) = args.process(®istry).await?; let values: Vec = input.collect().await; + let values = UntaggedValue::table(&values).into_value(&name); - let Tagged { item: group_by, .. } = column_name.clone(); - - let groups = group(&column_name, values, &name)?; - let group_labels = columns_sorted(Some(group_by.clone()), &groups, &name); - let sorted = t_sort(Some(group_by), None, &groups, &name)?; + let groups = group(&Some(column_name.clone()), &values, &name)?; + let group_labels = columns_sorted(Some(column_name.clone()), &groups, &name); + let sorted = t_sort(Some(column_name.clone()), None, &groups, &name)?; let evaled = evaluate(&sorted, None, &name)?; let reduced = reduce(&evaled, None, &name)?; let maxima = map_max(&reduced, None, &name)?; diff --git a/crates/nu-cli/src/commands/split_by.rs b/crates/nu-cli/src/commands/split_by.rs index 6a35a178c9..ba44d32cdc 100644 --- a/crates/nu-cli/src/commands/split_by.rs +++ b/crates/nu-cli/src/commands/split_by.rs @@ -1,16 +1,15 @@ use crate::commands::WholeStreamCommand; use crate::prelude::*; use nu_errors::ShellError; -use nu_protocol::{ - Signature, SpannedTypeName, SyntaxShape, TaggedDictBuilder, UntaggedValue, Value, -}; +use nu_protocol::{ReturnSuccess, Signature, SyntaxShape, Value}; use nu_source::Tagged; +use nu_value_ext::as_string; pub struct SplitBy; #[derive(Deserialize)] pub struct SplitByArgs { - column_name: Tagged, + column_name: Option>, } #[async_trait] @@ -20,7 +19,7 @@ impl WholeStreamCommand for SplitBy { } fn signature(&self) -> Signature { - Signature::build("split-by").required( + Signature::build("split-by").optional( "column_name", SyntaxShape::String, "the name of the column within the nested table to split by", @@ -53,108 +52,84 @@ pub async fn split_by( return Err(ShellError::labeled_error( "Expected table from pipeline", "requires a table input", - column_name.span(), + name, )); } - match split(&column_name, &values[0], name) { - Ok(split) => Ok(OutputStream::one(split)), + match split(&column_name, &values[0], &name) { + Ok(splits) => Ok(OutputStream::one(ReturnSuccess::value(splits))), Err(err) => Err(err), } } +enum Grouper { + ByColumn(Option>), +} + pub fn split( - column_name: &Tagged, - value: &Value, + column_name: &Option>, + values: &Value, tag: impl Into, ) -> Result { - let origin_tag = tag.into(); + let name = tag.into(); - let mut splits = indexmap::IndexMap::new(); + let grouper = if let Some(column_name) = column_name { + Grouper::ByColumn(Some(column_name.clone())) + } else { + Grouper::ByColumn(None) + }; - match value { - Value { - value: UntaggedValue::Row(group_sets), - .. - } => { - for (group_key, group_value) in group_sets.entries.iter() { - match *group_value { - Value { - value: UntaggedValue::Table(ref dataset), - .. - } => { - let group = crate::commands::group_by::group( - &column_name, - dataset.to_vec(), - &origin_tag, - )?; - - match group { - Value { - value: UntaggedValue::Row(o), - .. - } => { - for (split_label, subset) in o.entries.into_iter() { - match subset { - Value { - value: UntaggedValue::Table(subset), - tag, - } => { - let s = splits - .entry(split_label.clone()) - .or_insert(indexmap::IndexMap::new()); - s.insert( - group_key.clone(), - UntaggedValue::table(&subset).into_value(tag), - ); - } - other => { - return Err(ShellError::type_error( - "a table value", - other.spanned_type_name(), - )) - } - } - } - } - _ => { - return Err(ShellError::type_error( - "a table value", - group.spanned_type_name(), - )) - } - } - } - ref other => { - return Err(ShellError::type_error( - "a table value", - other.spanned_type_name(), - )) - } + match grouper { + Grouper::ByColumn(Some(column_name)) => { + let block = Box::new(move |row: &Value| { + match row.get_data_by_key(column_name.borrow_spanned()) { + Some(group_key) => Ok(as_string(&group_key)?), + None => Err(suggestions(column_name.borrow_tagged(), &row)), } - } + }); + + crate::utils::data::split(&values, &Some(block), &name) } - _ => { - return Err(ShellError::type_error( - "a table value", - value.spanned_type_name(), - )) + Grouper::ByColumn(None) => { + let block = Box::new(move |row: &Value| match as_string(row) { + Ok(group_key) => Ok(group_key), + Err(reason) => Err(reason), + }); + + crate::utils::data::split(&values, &Some(block), &name) } } - - let mut out = TaggedDictBuilder::new(&origin_tag); - - for (k, v) in splits.into_iter() { - out.insert_untagged(k, UntaggedValue::row(v)); - } - - Ok(out.into_value()) } + +pub fn suggestions(tried: Tagged<&str>, for_value: &Value) -> ShellError { + let possibilities = for_value.data_descriptors(); + + let mut possible_matches: Vec<_> = possibilities + .iter() + .map(|x| (natural::distance::levenshtein_distance(x, &tried), x)) + .collect(); + + possible_matches.sort(); + + if !possible_matches.is_empty() { + return ShellError::labeled_error( + "Unknown column", + format!("did you mean '{}'?", possible_matches[0].1), + tried.tag(), + ); + } else { + return ShellError::labeled_error( + "Unknown column", + "row does not contain this column", + tried.tag(), + ); + } +} + #[cfg(test)] mod tests { - + use super::split; use crate::commands::group_by::group; - use crate::commands::split_by::split; use indexmap::IndexMap; use nu_errors::ShellError; use nu_protocol::{UntaggedValue, Value}; @@ -173,11 +148,12 @@ mod tests { } fn nu_releases_grouped_by_date() -> Result { - let key = String::from("date").tagged_unknown(); - group(&key, nu_releases_commiters(), Tag::unknown()) + let key = Some(String::from("date").tagged_unknown()); + let sample = table(&nu_releases_committers()); + group(&key, &sample, Tag::unknown()) } - fn nu_releases_commiters() -> Vec { + fn nu_releases_committers() -> Vec { vec![ row( indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("August 23-2019")}, @@ -211,7 +187,7 @@ mod tests { #[test] fn splits_inner_tables_by_key() -> Result<(), ShellError> { - let for_key = String::from("country").tagged_unknown(); + let for_key = Some(String::from("country").tagged_unknown()); assert_eq!( split(&for_key, &nu_releases_grouped_by_date()?, Tag::unknown())?, @@ -257,7 +233,7 @@ mod tests { #[test] fn errors_if_key_within_some_inner_table_is_missing() { - let for_key = String::from("country").tagged_unknown(); + let for_key = Some(String::from("country").tagged_unknown()); let nu_releases = row(indexmap! { "August 23-2019".into() => table(&[ diff --git a/crates/nu-cli/src/commands/t_sort_by.rs b/crates/nu-cli/src/commands/t_sort_by.rs index 98d76ec27a..a1aac2e91e 100644 --- a/crates/nu-cli/src/commands/t_sort_by.rs +++ b/crates/nu-cli/src/commands/t_sort_by.rs @@ -78,7 +78,7 @@ async fn t_sort_by( let values: Vec = input.collect().await; let column_grouped_by_name = if let Some(grouped_by) = group_by { - Some(grouped_by.item().clone()) + Some(grouped_by) } else { None }; diff --git a/crates/nu-cli/src/utils/data/group.rs b/crates/nu-cli/src/utils/data/group.rs index e7b6161e05..e1f0dd7859 100644 --- a/crates/nu-cli/src/utils/data/group.rs +++ b/crates/nu-cli/src/utils/data/group.rs @@ -1,61 +1,28 @@ use indexmap::IndexMap; use nu_errors::ShellError; use nu_protocol::{TaggedDictBuilder, UntaggedValue, Value}; -use nu_source::{Tag, Tagged, TaggedItem}; -use nu_value_ext::{as_string, get_data_by_key}; +use nu_source::Tag; +use nu_value_ext::as_string; #[allow(clippy::type_complexity)] pub fn group( - column_name: Option>, - values: &[Value], - grouper: Option Result + Send>>, + values: &Value, + grouper: &Option Result + Send>>, tag: impl Into, ) -> Result { let tag = tag.into(); let mut groups: IndexMap> = IndexMap::new(); - for value in values { - let group_key = if let Some(ref column_name) = column_name { - get_data_by_key(&value, column_name.borrow_spanned()) + for value in values.table_entries() { + let group_key = if let Some(ref grouper) = grouper { + grouper(&value) } else { - Some(value.clone()) + as_string(&value) }; - if let Some(group_key) = group_key { - let group_key = if let Some(ref grouper) = grouper { - grouper(&group_key) - } else { - as_string(&group_key) - }; - let group = groups.entry(group_key?).or_insert(vec![]); - group.push((*value).clone()); - } else { - let column_name = column_name.unwrap_or_else(|| String::from("").tagged(&tag)); - - let possibilities = value.data_descriptors(); - - let mut possible_matches: Vec<_> = possibilities - .iter() - .map(|x| (natural::distance::levenshtein_distance(x, &column_name), x)) - .collect(); - - possible_matches.sort(); - - if !possible_matches.is_empty() { - return Err(ShellError::labeled_error( - "Unknown column", - format!("did you mean '{}'?", possible_matches[0].1), - column_name.tag(), - )); - } else { - return Err(ShellError::labeled_error( - "Unknown column", - "row does not contain this column", - column_name.tag(), - )); - } - } + let group = groups.entry(group_key?).or_insert(vec![]); + group.push((*value).clone()); } let mut out = TaggedDictBuilder::new(&tag); diff --git a/crates/nu-cli/src/utils/data/mod.rs b/crates/nu-cli/src/utils/data/mod.rs index f90d93bdbb..8e98210695 100644 --- a/crates/nu-cli/src/utils/data/mod.rs +++ b/crates/nu-cli/src/utils/data/mod.rs @@ -1,3 +1,5 @@ pub mod group; +pub mod split; pub use crate::utils::data::group::group; +pub use crate::utils::data::split::split; diff --git a/crates/nu-cli/src/utils/data_processing.rs b/crates/nu-cli/src/utils/data_processing.rs index 82966903cf..b17f714326 100644 --- a/crates/nu-cli/src/utils/data_processing.rs +++ b/crates/nu-cli/src/utils/data_processing.rs @@ -12,7 +12,7 @@ use num_traits::Zero; const ERR_EMPTY_DATA: &str = "Cannot perform aggregate math operation on empty data"; pub fn columns_sorted( - _group_by_name: Option, + _group_by_name: Option>, value: &Value, tag: impl Into, ) -> Vec> { @@ -61,7 +61,7 @@ pub fn columns_sorted( } pub fn t_sort( - group_by_name: Option, + group_by_name: Option>, split_by_name: Option, value: &Value, tag: impl Into, @@ -454,12 +454,13 @@ mod tests { } fn nu_releases_grouped_by_date() -> Result { - let key = String::from("date").tagged_unknown(); - group(&key, nu_releases_commiters(), Tag::unknown()) + let key = Some(String::from("date").tagged_unknown()); + let sample = table(&nu_releases_committers()); + group(&key, &sample, Tag::unknown()) } fn nu_releases_sorted_by_date() -> Result { - let key = String::from("date"); + let key = String::from("date").tagged(Tag::unknown()); t_sort( Some(key), @@ -481,7 +482,7 @@ mod tests { ) } - fn nu_releases_commiters() -> Vec { + fn nu_releases_committers() -> Vec { vec![ row( indexmap! {"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("August 23-2019")}, @@ -515,7 +516,7 @@ mod tests { #[test] fn show_columns_sorted_given_a_column_to_sort_by() -> Result<(), ShellError> { - let by_column = String::from("date"); + let by_column = String::from("date").tagged(Tag::unknown()); assert_eq!( columns_sorted( @@ -535,7 +536,7 @@ mod tests { #[test] fn sorts_the_tables() -> Result<(), ShellError> { - let group_by = String::from("date"); + let group_by = String::from("date").tagged(Tag::unknown()); assert_eq!( t_sort( diff --git a/crates/nu-protocol/src/value.rs b/crates/nu-protocol/src/value.rs index acef9d86cf..c2315e2e8e 100644 --- a/crates/nu-protocol/src/value.rs +++ b/crates/nu-protocol/src/value.rs @@ -91,6 +91,14 @@ impl UntaggedValue { } } + /// Returns true if this value represents a table + pub fn is_table(&self) -> bool { + match self { + UntaggedValue::Table(_) => true, + _ => false, + } + } + /// Returns true if the value represents something other than Nothing pub fn is_some(&self) -> bool { !self.is_none() From 9f54d238ba6e36c35092a98d7ed73406e4cbf270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20N=2E=20Robalino?= Date: Wed, 17 Jun 2020 12:34:26 -0500 Subject: [PATCH 06/27] Refactoring and more split-by flexibility. --- crates/nu-cli/src/commands/split_by.rs | 8 ++-- crates/nu-cli/src/utils/data/split.rs | 53 ++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 crates/nu-cli/src/utils/data/split.rs diff --git a/crates/nu-cli/src/commands/split_by.rs b/crates/nu-cli/src/commands/split_by.rs index ba44d32cdc..d7c299c993 100644 --- a/crates/nu-cli/src/commands/split_by.rs +++ b/crates/nu-cli/src/commands/split_by.rs @@ -112,17 +112,17 @@ pub fn suggestions(tried: Tagged<&str>, for_value: &Value) -> ShellError { possible_matches.sort(); if !possible_matches.is_empty() { - return ShellError::labeled_error( + ShellError::labeled_error( "Unknown column", format!("did you mean '{}'?", possible_matches[0].1), tried.tag(), - ); + ) } else { - return ShellError::labeled_error( + ShellError::labeled_error( "Unknown column", "row does not contain this column", tried.tag(), - ); + ) } } diff --git a/crates/nu-cli/src/utils/data/split.rs b/crates/nu-cli/src/utils/data/split.rs new file mode 100644 index 0000000000..86bfeeb107 --- /dev/null +++ b/crates/nu-cli/src/utils/data/split.rs @@ -0,0 +1,53 @@ +use nu_errors::ShellError; +use nu_protocol::{SpannedTypeName, TaggedDictBuilder, UntaggedValue, Value}; +use nu_source::Tag; + +use crate::utils::data::group; + +#[allow(clippy::type_complexity)] +pub fn split( + value: &Value, + splitter: &Option Result + Send>>, + tag: impl Into, +) -> Result { + let tag = tag.into(); + + let mut splits = indexmap::IndexMap::new(); + + for (column, value) in value.row_entries() { + if !&value.is_table() { + return Err(ShellError::type_error( + "a table value", + value.spanned_type_name(), + )); + } + + match group(&value, splitter, &tag) { + Ok(grouped) => { + for (split_label, subset) in grouped.row_entries() { + let s = splits + .entry(split_label.clone()) + .or_insert(indexmap::IndexMap::new()); + + if !&subset.is_table() { + return Err(ShellError::type_error( + "a table value", + subset.spanned_type_name(), + )); + } + + s.insert(column.clone(), subset.clone()); + } + } + Err(err) => return Err(err), + } + } + + let mut out = TaggedDictBuilder::new(&tag); + + for (k, v) in splits.into_iter() { + out.insert_untagged(k, UntaggedValue::row(v)); + } + + Ok(out.into_value()) +} From 94aac0e8ddcea3d177d80965901a800bd755c7b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20N=2E=20Robalino?= Date: Wed, 17 Jun 2020 12:35:13 -0500 Subject: [PATCH 07/27] Remove unused pattern matched tag fields. --- crates/nu-cli/src/env/directory_specific_environment.rs | 4 ++-- crates/nu-cli/src/env/environment.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/nu-cli/src/env/directory_specific_environment.rs b/crates/nu-cli/src/env/directory_specific_environment.rs index 27d7509cfe..3ffa405cf3 100644 --- a/crates/nu-cli/src/env/directory_specific_environment.rs +++ b/crates/nu-cli/src/env/directory_specific_environment.rs @@ -18,7 +18,7 @@ impl DirectorySpecificEnvironment { pub fn new(allowed_directories: Option) -> DirectorySpecificEnvironment { let mut allowed_directories = if let Some(Value { value: UntaggedValue::Table(ref wrapped_directories), - tag: _, + .. }) = allowed_directories { wrapped_directories @@ -26,7 +26,7 @@ impl DirectorySpecificEnvironment { .filter_map(|dirval| { if let Value { value: UntaggedValue::Primitive(Primitive::String(ref dir)), - tag: _, + .. } = dirval { return Some(PathBuf::from(&dir)); diff --git a/crates/nu-cli/src/env/environment.rs b/crates/nu-cli/src/env/environment.rs index 41e3270a61..abb276d985 100644 --- a/crates/nu-cli/src/env/environment.rs +++ b/crates/nu-cli/src/env/environment.rs @@ -79,7 +79,7 @@ impl Environment { fn remove_env(&mut self, key: &str) { if let Some(Value { value: UntaggedValue::Row(ref mut envs), - tag: _, + .. }) = self.environment_vars { envs.entries.remove(key); From 96d58094cf93962ae46729090bed057e478117f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20N=2E=20Robalino?= Date: Wed, 17 Jun 2020 13:36:50 -0500 Subject: [PATCH 08/27] Fix regression. skip-until 'skips' until condition is met. --- crates/nu-cli/src/commands/skip_until.rs | 4 ++-- crates/nu-cli/tests/commands/skip_until.rs | 26 +++++++++++----------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/crates/nu-cli/src/commands/skip_until.rs b/crates/nu-cli/src/commands/skip_until.rs index 7cbcbb9f39..8eddae37e0 100644 --- a/crates/nu-cli/src/commands/skip_until.rs +++ b/crates/nu-cli/src/commands/skip_until.rs @@ -100,8 +100,8 @@ impl WholeStreamCommand for SkipUntil { trace!("RESULT = {:?}", result); match result { - Ok(ref v) if v.is_true() => true, - _ => false, + Ok(ref v) if v.is_true() => false, // stop skipping + _ => true, } } }) diff --git a/crates/nu-cli/tests/commands/skip_until.rs b/crates/nu-cli/tests/commands/skip_until.rs index f901e8b655..4748766caa 100644 --- a/crates/nu-cli/tests/commands/skip_until.rs +++ b/crates/nu-cli/tests/commands/skip_until.rs @@ -12,20 +12,20 @@ fn condition_is_met() { -------------------------------------------------------------------- Chicken Collection,29/04/2020,30/04/2020,31/04/2020, Yellow Chickens,,, - Andrés,1,1,1 - Jonathan,1,1,1 - Jason,1,1,1 - Yehuda,1,1,1 + Andrés,0,0,1 + Jonathan,0,0,1 + Jason,0,0,1 + Yehuda,0,0,1 Blue Chickens,,, - Andrés,1,1,2 - Jonathan,1,1,2 - Jason,1,1,2 - Yehuda,1,1,2 + Andrés,0,0,1 + Jonathan,0,0,1 + Jason,0,0,1 + Yehuda,0,0,2 Red Chickens,,, - Andrés,1,1,3 - Jonathan,1,1,3 - Jason,1,1,3 - Yehuda,1,1,3 + Andrés,0,0,1 + Jonathan,0,0,1 + Jason,0,0,1 + Yehuda,0,0,3 "#, )]); @@ -45,6 +45,6 @@ fn condition_is_met() { "# )); - assert_eq!(actual.out, "12"); + assert_eq!(actual.out, "6"); }) } From 353b33be1b537203dce28f2821d4c4bb74f3bf5d Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 18 Jun 2020 13:34:51 -0400 Subject: [PATCH 09/27] Add support to allow the week day start in cal to be configured via a flag (#1996) * Add support to allow the week day start in cal to be configurable * Fix variable name * Use a flag instead of a configuration setting for specifying the starting day of the week --- crates/nu-cli/src/commands/cal.rs | 63 +++++++++++++++++++++++------ crates/nu-cli/tests/commands/cal.rs | 14 +++++++ docs/commands/cal.md | 14 +++++++ 3 files changed, 78 insertions(+), 13 deletions(-) diff --git a/crates/nu-cli/src/commands/cal.rs b/crates/nu-cli/src/commands/cal.rs index db459d63b0..1970ba5f06 100644 --- a/crates/nu-cli/src/commands/cal.rs +++ b/crates/nu-cli/src/commands/cal.rs @@ -24,6 +24,12 @@ impl WholeStreamCommand for Cal { "Display a year-long calendar for the specified year", None, ) + .named( + "week-start", + SyntaxShape::String, + "Display the calendar with the specified day as the first day of the week", + None, + ) .switch( "month-names", "Display the month names instead of integers", @@ -55,6 +61,11 @@ impl WholeStreamCommand for Cal { example: "cal --full-year 2012", result: None, }, + Example { + description: "This month's calendar with the week starting on monday", + example: "cal --week-start monday", + result: None, + }, ] } } @@ -226,11 +237,7 @@ fn add_month_to_table( }, }; - let day_limit = - month_helper.number_of_days_in_month + month_helper.day_number_of_week_month_starts_on; - let mut day_count: u32 = 1; - - let days_of_the_week = [ + let mut days_of_the_week = [ "sunday", "monday", "tuesday", @@ -240,12 +247,43 @@ fn add_month_to_table( "saturday", ]; + let mut week_start_day = days_of_the_week[0].to_string(); + + if let Some(week_start_value) = args.get("week-start") { + if let Ok(day) = week_start_value.as_string() { + if days_of_the_week.contains(&day.as_str()) { + week_start_day = day; + } else { + return Err(ShellError::labeled_error( + "The specified week start day is invalid", + "invalid week start day", + week_start_value.tag(), + )); + } + } + } + + let week_start_day_offset = days_of_the_week.len() + - days_of_the_week + .iter() + .position(|day| *day == week_start_day) + .unwrap_or(0); + + days_of_the_week.rotate_right(week_start_day_offset); + + let mut total_start_offset: u32 = + month_helper.day_number_of_week_month_starts_on + week_start_day_offset as u32; + total_start_offset %= days_of_the_week.len() as u32; + + let mut day_number: u32 = 1; + let day_limit: u32 = total_start_offset + month_helper.number_of_days_in_month; + let should_show_year_column = args.has("year"); let should_show_quarter_column = args.has("quarter"); let should_show_month_column = args.has("month"); let should_show_month_names = args.has("month-names"); - while day_count <= day_limit { + while day_number <= day_limit { let mut indexmap = IndexMap::new(); if should_show_year_column { @@ -273,19 +311,18 @@ fn add_month_to_table( } for day in &days_of_the_week { - let should_add_day_number_to_table = (day_count <= day_limit) - && (day_count > month_helper.day_number_of_week_month_starts_on); + let should_add_day_number_to_table = + (day_number > total_start_offset) && (day_number <= day_limit); let mut value = UntaggedValue::nothing().into_value(tag); if should_add_day_number_to_table { - let day_count_with_offset = - day_count - month_helper.day_number_of_week_month_starts_on; + let adjusted_day_number = day_number - total_start_offset; - value = UntaggedValue::int(day_count_with_offset).into_value(tag); + value = UntaggedValue::int(adjusted_day_number).into_value(tag); if let Some(current_day) = current_day_option { - if current_day == day_count_with_offset { + if current_day == adjusted_day_number { // TODO: Update the value here with a color when color support is added // This colors the current day } @@ -294,7 +331,7 @@ fn add_month_to_table( indexmap.insert((*day).to_string(), value); - day_count += 1; + day_number += 1; } calendar_vec_deque diff --git a/crates/nu-cli/tests/commands/cal.rs b/crates/nu-cli/tests/commands/cal.rs index afb20e26bb..b5fad15686 100644 --- a/crates/nu-cli/tests/commands/cal.rs +++ b/crates/nu-cli/tests/commands/cal.rs @@ -52,6 +52,20 @@ fn cal_rows_in_2020() { assert!(actual.out.contains("62")); } +#[test] +fn cal_week_day_start_monday() { + let actual = nu!( + cwd: ".", pipeline( + r#" + cal --full-year 2020 -m --month-names --week-start monday | where month == january | to json + "# + )); + + let cal_january_json = r#"[{"month":"january","monday":null,"tuesday":null,"wednesday":1,"thursday":2,"friday":3,"saturday":4,"sunday":5},{"month":"january","monday":6,"tuesday":7,"wednesday":8,"thursday":9,"friday":10,"saturday":11,"sunday":12},{"month":"january","monday":13,"tuesday":14,"wednesday":15,"thursday":16,"friday":17,"saturday":18,"sunday":19},{"month":"january","monday":20,"tuesday":21,"wednesday":22,"thursday":23,"friday":24,"saturday":25,"sunday":26},{"month":"january","monday":27,"tuesday":28,"wednesday":29,"thursday":30,"friday":31,"saturday":null,"sunday":null}]"#; + + assert_eq!(actual.out, cal_january_json); +} + #[test] fn cal_sees_pipeline_year() { let actual = nu!( diff --git a/docs/commands/cal.md b/docs/commands/cal.md index 7b02501b90..99fd7cb8cc 100644 --- a/docs/commands/cal.md +++ b/docs/commands/cal.md @@ -8,6 +8,7 @@ Use `cal` to display a calendar. * `-q`, `--quarter`: Display the quarter column * `-m`, `--month`: Display the month column * `--full-year` \: Display a year-long calendar for the specified year +* `--week-start` \: Display the calendar with the specified day as the first day of the week * `--month-names`: Display the month names instead of integers ## Examples @@ -188,3 +189,16 @@ Use `cal` to display a calendar. 1 │ 2020 │ november │ 8 │ 9 │ 10 │ 11 │ 12 │ 13 │ 14 ───┴──────┴──────────┴────────┴────────┴─────────┴───────────┴──────────┴────────┴────────── ``` + +```shell +> cal -ymq --month-names --week-start-day monday +───┬──────┬─────────┬───────┬────────┬─────────┬───────────┬──────────┬────────┬──────────┬──────── + # │ year │ quarter │ month │ monday │ tuesday │ wednesday │ thursday │ friday │ saturday │ sunday +───┼──────┼─────────┼───────┼────────┼─────────┼───────────┼──────────┼────────┼──────────┼──────── + 0 │ 2020 │ 2 │ june │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 + 1 │ 2020 │ 2 │ june │ 8 │ 9 │ 10 │ 11 │ 12 │ 13 │ 14 + 2 │ 2020 │ 2 │ june │ 15 │ 16 │ 17 │ 18 │ 19 │ 20 │ 21 + 3 │ 2020 │ 2 │ june │ 22 │ 23 │ 24 │ 25 │ 26 │ 27 │ 28 + 4 │ 2020 │ 2 │ june │ 29 │ 30 │ │ │ │ │ +───┴──────┴─────────┴───────┴────────┴─────────┴───────────┴──────────┴────────┴──────────┴──────── +``` From 5f9de80d9b8ff427c94cb448f467efff0e9eb25b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20N=2E=20Robalino?= Date: Thu, 18 Jun 2020 16:37:18 -0500 Subject: [PATCH 10/27] Math#media - ability to compute median value. --- crates/nu-cli/src/cli.rs | 7 +- crates/nu-cli/src/commands.rs | 3 +- crates/nu-cli/src/commands/math/average.rs | 8 +- crates/nu-cli/src/commands/math/median.rs | 193 ++++++++++++++++++ crates/nu-cli/src/commands/math/mod.rs | 8 +- crates/nu-cli/src/commands/sort_by.rs | 34 ++- .../tests/commands/{ => math}/average.rs | 2 +- crates/nu-cli/tests/commands/math/median.rs | 29 +++ .../tests/commands/{math.rs => math/mod.rs} | 3 + crates/nu-cli/tests/commands/mod.rs | 1 - 10 files changed, 266 insertions(+), 22 deletions(-) create mode 100644 crates/nu-cli/src/commands/math/median.rs rename crates/nu-cli/tests/commands/{ => math}/average.rs (94%) create mode 100644 crates/nu-cli/tests/commands/math/median.rs rename crates/nu-cli/tests/commands/{math.rs => math/mod.rs} (99%) diff --git a/crates/nu-cli/src/cli.rs b/crates/nu-cli/src/cli.rs index 5136e875b0..e3cf9100ca 100644 --- a/crates/nu-cli/src/cli.rs +++ b/crates/nu-cli/src/cli.rs @@ -348,9 +348,10 @@ pub fn create_default_context( // Data processing whole_stream_command(Histogram), whole_stream_command(Math), - whole_stream_command(Average), - whole_stream_command(Minimum), - whole_stream_command(Maximum), + whole_stream_command(MathAverage), + whole_stream_command(MathMedian), + whole_stream_command(MathMinimum), + whole_stream_command(MathMaximum), whole_stream_command(Sum), // File format output whole_stream_command(To), diff --git a/crates/nu-cli/src/commands.rs b/crates/nu-cli/src/commands.rs index dbb6b605f2..d24be55728 100644 --- a/crates/nu-cli/src/commands.rs +++ b/crates/nu-cli/src/commands.rs @@ -151,7 +151,6 @@ pub(crate) use du::Du; pub(crate) use each::Each; pub(crate) use echo::Echo; pub(crate) use is_empty::IsEmpty; -pub(crate) use math::Math; pub(crate) use update::Update; pub(crate) mod kill; pub(crate) use kill::Kill; @@ -200,7 +199,7 @@ pub(crate) use lines::Lines; pub(crate) use ls::Ls; #[allow(unused_imports)] pub(crate) use map_max_by::MapMaxBy; -pub(crate) use math::{Average, Maximum, Minimum}; +pub(crate) use math::{Math, MathAverage, MathMaximum, MathMedian, MathMinimum}; pub(crate) use merge::Merge; pub(crate) use mkdir::Mkdir; pub(crate) use mv::Move; diff --git a/crates/nu-cli/src/commands/math/average.rs b/crates/nu-cli/src/commands/math/average.rs index 627a072d1d..0e02ab1a3f 100644 --- a/crates/nu-cli/src/commands/math/average.rs +++ b/crates/nu-cli/src/commands/math/average.rs @@ -58,7 +58,13 @@ impl WholeStreamCommand for SubCommand { pub fn average(values: &[Value], name: &Tag) -> Result { let sum = reducer_for(Reduce::Sum); - let number = BigDecimal::from_usize(values.len()).expect("expected a usize-sized bigdecimal"); + let number = BigDecimal::from_usize(values.len()).ok_or_else(|| { + ShellError::labeled_error( + "could not convert to big decimal", + "could not convert to big decimal", + &name.span, + ) + })?; let total_rows = UntaggedValue::decimal(number); let total = sum(Value::zero(), values.to_vec())?; diff --git a/crates/nu-cli/src/commands/math/median.rs b/crates/nu-cli/src/commands/math/median.rs new file mode 100644 index 0000000000..5a212be957 --- /dev/null +++ b/crates/nu-cli/src/commands/math/median.rs @@ -0,0 +1,193 @@ +use crate::commands::math::utils::calculate; +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use crate::utils::data_processing::{reducer_for, Reduce}; +use bigdecimal::{FromPrimitive, Zero}; +use nu_errors::ShellError; +use nu_protocol::{ + hir::{convert_number_to_u64, Number, Operator}, + Primitive, Signature, UntaggedValue, Value, +}; + +pub struct SubCommand; + +#[async_trait] +impl WholeStreamCommand for SubCommand { + fn name(&self) -> &str { + "math median" + } + + fn signature(&self) -> Signature { + Signature::build("math median") + } + + fn usage(&self) -> &str { + "Gets the median of a list of numbers" + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + calculate( + RunnableContext { + input: args.input, + registry: registry.clone(), + shell_manager: args.shell_manager, + host: args.host, + ctrl_c: args.ctrl_c, + current_errors: args.current_errors, + name: args.call_info.name_tag, + raw_input: args.raw_input, + }, + median, + ) + .await + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Get the median of a list of numbers", + example: "echo [3 8 9 12 12 15] | math median", + result: Some(vec![UntaggedValue::decimal(10.5).into()]), + }] + } +} + +enum Pick { + MedianAverage, + Median, +} + +pub fn median(values: &[Value], name: &Tag) -> Result { + let take = if values.len() % 2 == 0 { + Pick::MedianAverage + } else { + Pick::Median + }; + + let mut sorted = vec![]; + + for item in values { + sorted.push(item.clone()); + } + + crate::commands::sort_by::sort(&mut sorted, &[], name)?; + + match take { + Pick::Median => { + let idx = (values.len() as f64 / 2.0).floor() as usize; + let out = sorted.get(idx).ok_or_else(|| { + ShellError::labeled_error( + "could not extract value", + "could not extract value", + &name.span, + ) + })?; + Ok(out.clone()) + } + Pick::MedianAverage => { + let idx_end = (values.len() / 2) as usize; + let idx_start = idx_end - 1; + + let left = sorted + .get(idx_start) + .ok_or_else(|| { + ShellError::labeled_error( + "could not extract value", + "could not extract value", + &name.span, + ) + })? + .clone(); + + let right = sorted + .get(idx_end) + .ok_or_else(|| { + ShellError::labeled_error( + "could not extract value", + "could not extract value", + &name.span, + ) + })? + .clone(); + + compute_average(&[left, right], name) + } + } +} + +fn compute_average(values: &[Value], name: impl Into) -> Result { + let name = name.into(); + + let sum = reducer_for(Reduce::Sum); + let number = BigDecimal::from_usize(2).ok_or_else(|| { + ShellError::labeled_error( + "could not convert to big decimal", + "could not convert to big decimal", + &name, + ) + })?; + let total_rows = UntaggedValue::decimal(number); + let total = sum(Value::zero(), values.to_vec())?; + + match total { + Value { + value: UntaggedValue::Primitive(Primitive::Bytes(num)), + .. + } => { + let left = UntaggedValue::from(Primitive::Int(num.into())); + let result = crate::data::value::compute_values(Operator::Divide, &left, &total_rows); + + match result { + Ok(UntaggedValue::Primitive(Primitive::Decimal(result))) => { + let number = Number::Decimal(result); + let number = convert_number_to_u64(&number); + Ok(UntaggedValue::bytes(number).into_value(name)) + } + Ok(_) => Err(ShellError::labeled_error( + "could not calculate median of non-numeric or unrelated types", + "source", + name, + )), + Err((left_type, right_type)) => Err(ShellError::coerce_error( + left_type.spanned(name.span), + right_type.spanned(name.span), + )), + } + } + Value { + value: UntaggedValue::Primitive(other), + .. + } => { + let left = UntaggedValue::from(other); + let result = crate::data::value::compute_values(Operator::Divide, &left, &total_rows); + + match result { + Ok(value) => Ok(value.into_value(name)), + Err((left_type, right_type)) => Err(ShellError::coerce_error( + left_type.spanned(name.span), + right_type.spanned(name.span), + )), + } + } + _ => Err(ShellError::labeled_error( + "could not calculate median of non-numeric or unrelated types", + "source", + name, + )), + } +} + +#[cfg(test)] +mod tests { + use super::SubCommand; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-cli/src/commands/math/mod.rs b/crates/nu-cli/src/commands/math/mod.rs index c8fee95f2c..2b0ae6d5da 100644 --- a/crates/nu-cli/src/commands/math/mod.rs +++ b/crates/nu-cli/src/commands/math/mod.rs @@ -1,10 +1,12 @@ pub mod average; pub mod command; pub mod max; +pub mod median; pub mod min; pub mod utils; -pub use average::SubCommand as Average; +pub use average::SubCommand as MathAverage; pub use command::Command as Math; -pub use max::SubCommand as Maximum; -pub use min::SubCommand as Minimum; +pub use max::SubCommand as MathMaximum; +pub use median::SubCommand as MathMedian; +pub use min::SubCommand as MathMinimum; diff --git a/crates/nu-cli/src/commands/sort_by.rs b/crates/nu-cli/src/commands/sort_by.rs index cde0c5fc87..bf273d1152 100644 --- a/crates/nu-cli/src/commands/sort_by.rs +++ b/crates/nu-cli/src/commands/sort_by.rs @@ -70,15 +70,33 @@ async fn sort_by( let (SortByArgs { rest }, mut input) = args.process(®istry).await?; let mut vec = input.drain_vec().await; + sort(&mut vec, &rest, &tag)?; + + let mut values_vec_deque: VecDeque = VecDeque::new(); + + for item in vec { + values_vec_deque.push_back(item); + } + + Ok(futures::stream::iter(values_vec_deque).to_output_stream()) +} + +pub fn sort( + vec: &mut [Value], + keys: &[Tagged], + tag: impl Into, +) -> Result<(), ShellError> { + let tag = tag.into(); + if vec.is_empty() { return Err(ShellError::labeled_error( - "Error performing sort-by command", - "sort-by error", + "no values to work with", + "no values to work with", tag, )); } - for sort_arg in rest.iter() { + for sort_arg in keys.iter() { let match_test = get_data_by_key(&vec[0], sort_arg.borrow_spanned()); if match_test == None { return Err(ShellError::labeled_error( @@ -98,7 +116,7 @@ async fn sort_by( } _ => { let calc_key = |item: &Value| { - rest.iter() + keys.iter() .map(|f| get_data_by_key(item, f.borrow_spanned())) .collect::>>() }; @@ -106,13 +124,7 @@ async fn sort_by( } }; - let mut values_vec_deque: VecDeque = VecDeque::new(); - - for item in vec { - values_vec_deque.push_back(item); - } - - Ok(futures::stream::iter(values_vec_deque).to_output_stream()) + Ok(()) } #[cfg(test)] diff --git a/crates/nu-cli/tests/commands/average.rs b/crates/nu-cli/tests/commands/math/average.rs similarity index 94% rename from crates/nu-cli/tests/commands/average.rs rename to crates/nu-cli/tests/commands/math/average.rs index 7c1e8a60b8..caa586ff7c 100644 --- a/crates/nu-cli/tests/commands/average.rs +++ b/crates/nu-cli/tests/commands/math/average.rs @@ -11,7 +11,7 @@ fn can_average_numbers() { | echo $it "# )); - println!("{:?}", actual.err); + assert_eq!(actual.out, "101.5") } diff --git a/crates/nu-cli/tests/commands/math/median.rs b/crates/nu-cli/tests/commands/math/median.rs new file mode 100644 index 0000000000..79f4da6d4a --- /dev/null +++ b/crates/nu-cli/tests/commands/math/median.rs @@ -0,0 +1,29 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn median_numbers_with_even_rows() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo [10 6 19 21 4] + | math median + | echo $it + "# + )); + + assert_eq!(actual.out, "10") +} + +#[test] +fn median_numbers_with_odd_rows() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo [3 8 9 12 12 15] + | math median + | echo $it + "# + )); + + assert_eq!(actual.out, "10.5") +} diff --git a/crates/nu-cli/tests/commands/math.rs b/crates/nu-cli/tests/commands/math/mod.rs similarity index 99% rename from crates/nu-cli/tests/commands/math.rs rename to crates/nu-cli/tests/commands/math/mod.rs index 680b1b641b..ff682ab3ad 100644 --- a/crates/nu-cli/tests/commands/math.rs +++ b/crates/nu-cli/tests/commands/math/mod.rs @@ -1,3 +1,6 @@ +mod average; +mod median; + use nu_test_support::{nu, pipeline}; #[test] diff --git a/crates/nu-cli/tests/commands/mod.rs b/crates/nu-cli/tests/commands/mod.rs index dd43d76201..22febe997d 100644 --- a/crates/nu-cli/tests/commands/mod.rs +++ b/crates/nu-cli/tests/commands/mod.rs @@ -1,6 +1,5 @@ mod alias; mod append; -mod average; mod cal; mod calc; mod cd; From 53a6e9f0bda804d2caec17c8f73075e1220f3b1b Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 18 Jun 2020 22:02:01 -0400 Subject: [PATCH 11/27] Convert sum command into subcommand of the math command (#2004) * Convert sum command into subcommand of the math command * Add bullet points to math.md documentation --- crates/nu-cli/src/cli.rs | 2 +- crates/nu-cli/src/commands.rs | 4 +- crates/nu-cli/src/commands/each.rs | 2 +- crates/nu-cli/src/commands/math/average.rs | 4 +- crates/nu-cli/src/commands/math/command.rs | 24 +++- crates/nu-cli/src/commands/math/max.rs | 2 +- crates/nu-cli/src/commands/math/median.rs | 2 +- crates/nu-cli/src/commands/math/mod.rs | 2 + crates/nu-cli/src/commands/{ => math}/sum.rs | 81 ++++++------ crates/nu-cli/src/utils/data_processing.rs | 8 +- crates/nu-cli/tests/commands/drop.rs | 2 +- crates/nu-cli/tests/commands/each.rs | 2 +- crates/nu-cli/tests/commands/is_empty.rs | 6 +- crates/nu-cli/tests/commands/keep.rs | 2 +- crates/nu-cli/tests/commands/keep_until.rs | 2 +- crates/nu-cli/tests/commands/keep_while.rs | 2 +- crates/nu-cli/tests/commands/merge.rs | 2 +- crates/nu-cli/tests/commands/skip_until.rs | 2 +- crates/nu-cli/tests/commands/str_.rs | 4 +- crates/nu-cli/tests/commands/sum.rs | 10 +- crates/nu-cli/tests/commands/where_.rs | 6 +- crates/nu-test-support/src/lib.rs | 4 +- docs/commands/math.md | 129 +++++++++++-------- docs/commands/str.md | 2 +- docs/commands/sum.md | 10 +- tests/shell/pipeline/commands/internal.rs | 2 +- 26 files changed, 175 insertions(+), 143 deletions(-) rename crates/nu-cli/src/commands/{ => math}/sum.rs (50%) diff --git a/crates/nu-cli/src/cli.rs b/crates/nu-cli/src/cli.rs index e3cf9100ca..ab83a4a9d3 100644 --- a/crates/nu-cli/src/cli.rs +++ b/crates/nu-cli/src/cli.rs @@ -352,7 +352,7 @@ pub fn create_default_context( whole_stream_command(MathMedian), whole_stream_command(MathMinimum), whole_stream_command(MathMaximum), - whole_stream_command(Sum), + whole_stream_command(MathSummation), // File format output whole_stream_command(To), whole_stream_command(ToBSON), diff --git a/crates/nu-cli/src/commands.rs b/crates/nu-cli/src/commands.rs index d24be55728..cc79777f41 100644 --- a/crates/nu-cli/src/commands.rs +++ b/crates/nu-cli/src/commands.rs @@ -102,7 +102,6 @@ pub(crate) mod sort_by; pub(crate) mod split; pub(crate) mod split_by; pub(crate) mod str_; -pub(crate) mod sum; #[allow(unused)] pub(crate) mod t_sort_by; pub(crate) mod table; @@ -199,7 +198,7 @@ pub(crate) use lines::Lines; pub(crate) use ls::Ls; #[allow(unused_imports)] pub(crate) use map_max_by::MapMaxBy; -pub(crate) use math::{Math, MathAverage, MathMaximum, MathMedian, MathMinimum}; +pub(crate) use math::{Math, MathAverage, MathMaximum, MathMedian, MathMinimum, MathSummation}; pub(crate) use merge::Merge; pub(crate) use mkdir::Mkdir; pub(crate) use mv::Move; @@ -236,7 +235,6 @@ pub(crate) use str_::{ Str, StrCapitalize, StrDowncase, StrFindReplace, StrSet, StrSubstring, StrToDatetime, StrToDecimal, StrToInteger, StrTrim, StrUpcase, }; -pub(crate) use sum::Sum; #[allow(unused_imports)] pub(crate) use t_sort_by::TSortBy; pub(crate) use table::Table; diff --git a/crates/nu-cli/src/commands/each.rs b/crates/nu-cli/src/commands/each.rs index 10cae1a42f..76eb5adf63 100644 --- a/crates/nu-cli/src/commands/each.rs +++ b/crates/nu-cli/src/commands/each.rs @@ -56,7 +56,7 @@ impl WholeStreamCommand for Each { }, Example { description: "Echo the sum of each row", - example: "echo [[1 2] [3 4]] | each { echo $it | sum }", + example: "echo [[1 2] [3 4]] | each { echo $it | math sum }", result: Some(vec![ UntaggedValue::int(3).into(), UntaggedValue::int(7).into(), diff --git a/crates/nu-cli/src/commands/math/average.rs b/crates/nu-cli/src/commands/math/average.rs index 0e02ab1a3f..ecb111cd46 100644 --- a/crates/nu-cli/src/commands/math/average.rs +++ b/crates/nu-cli/src/commands/math/average.rs @@ -22,7 +22,7 @@ impl WholeStreamCommand for SubCommand { } fn usage(&self) -> &str { - "Gets the average of a list of numbers" + "Finds the average of a list of numbers or tables" } async fn run( @@ -56,7 +56,7 @@ impl WholeStreamCommand for SubCommand { } pub fn average(values: &[Value], name: &Tag) -> Result { - let sum = reducer_for(Reduce::Sum); + let sum = reducer_for(Reduce::Summation); let number = BigDecimal::from_usize(values.len()).ok_or_else(|| { ShellError::labeled_error( diff --git a/crates/nu-cli/src/commands/math/command.rs b/crates/nu-cli/src/commands/math/command.rs index 253220665d..b6609754c0 100644 --- a/crates/nu-cli/src/commands/math/command.rs +++ b/crates/nu-cli/src/commands/math/command.rs @@ -35,7 +35,7 @@ impl WholeStreamCommand for Command { mod tests { use super::*; use crate::commands::math::{ - average::average, max::maximum, min::minimum, utils::MathFunction, + average::average, max::maximum, min::minimum, sum::summation, utils::MathFunction, }; use nu_plugin::test_helpers::value::{decimal, int}; use nu_protocol::Value; @@ -67,31 +67,41 @@ mod tests { description: "Single value", values: vec![int(10)], expected_err: None, - expected_res: vec![Ok(decimal(10)), Ok(int(10)), Ok(int(10))], + expected_res: vec![Ok(decimal(10)), Ok(int(10)), Ok(int(10)), Ok(int(10))], }, TestCase { description: "Multiple Values", values: vec![int(10), int(30), int(20)], expected_err: None, - expected_res: vec![Ok(decimal(20)), Ok(int(10)), Ok(int(30))], + expected_res: vec![Ok(decimal(20)), Ok(int(10)), Ok(int(30)), Ok(int(60))], }, TestCase { description: "Mixed Values", values: vec![int(10), decimal(26.5), decimal(26.5)], expected_err: None, - expected_res: vec![Ok(decimal(21)), Ok(int(10)), Ok(decimal(26.5))], + expected_res: vec![ + Ok(decimal(21)), + Ok(int(10)), + Ok(decimal(26.5)), + Ok(decimal(63)), + ], }, TestCase { description: "Negative Values", values: vec![int(10), int(-11), int(-14)], expected_err: None, - expected_res: vec![Ok(decimal(-5)), Ok(int(-14)), Ok(int(10))], + expected_res: vec![Ok(decimal(-5)), Ok(int(-14)), Ok(int(10)), Ok(int(-15))], }, TestCase { description: "Mixed Negative Values", values: vec![int(10), decimal(-11.5), decimal(-13.5)], expected_err: None, - expected_res: vec![Ok(decimal(-5)), Ok(decimal(-13.5)), Ok(int(10))], + expected_res: vec![ + Ok(decimal(-5)), + Ok(decimal(-13.5)), + Ok(int(10)), + Ok(decimal(-15)), + ], }, // TODO-Uncomment once I figure out how to structure tables // TestCase { @@ -116,7 +126,7 @@ mod tests { for tc in tt.iter() { let tc: &TestCase = tc; // Just for type annotations - let math_functions: Vec = vec![average, minimum, maximum]; + let math_functions: Vec = vec![average, minimum, maximum, summation]; let results = math_functions .iter() .map(|mf| mf(&tc.values, &test_tag)) diff --git a/crates/nu-cli/src/commands/math/max.rs b/crates/nu-cli/src/commands/math/max.rs index db8b6ecd86..8d5354451f 100644 --- a/crates/nu-cli/src/commands/math/max.rs +++ b/crates/nu-cli/src/commands/math/max.rs @@ -18,7 +18,7 @@ impl WholeStreamCommand for SubCommand { } fn usage(&self) -> &str { - "Get the maximum of a list of numbers or tables" + "Finds the maximum within a list of numbers or tables" } async fn run( diff --git a/crates/nu-cli/src/commands/math/median.rs b/crates/nu-cli/src/commands/math/median.rs index 5a212be957..3748e0108a 100644 --- a/crates/nu-cli/src/commands/math/median.rs +++ b/crates/nu-cli/src/commands/math/median.rs @@ -121,7 +121,7 @@ pub fn median(values: &[Value], name: &Tag) -> Result { fn compute_average(values: &[Value], name: impl Into) -> Result { let name = name.into(); - let sum = reducer_for(Reduce::Sum); + let sum = reducer_for(Reduce::Summation); let number = BigDecimal::from_usize(2).ok_or_else(|| { ShellError::labeled_error( "could not convert to big decimal", diff --git a/crates/nu-cli/src/commands/math/mod.rs b/crates/nu-cli/src/commands/math/mod.rs index 2b0ae6d5da..399416fb65 100644 --- a/crates/nu-cli/src/commands/math/mod.rs +++ b/crates/nu-cli/src/commands/math/mod.rs @@ -3,6 +3,7 @@ pub mod command; pub mod max; pub mod median; pub mod min; +pub mod sum; pub mod utils; pub use average::SubCommand as MathAverage; @@ -10,3 +11,4 @@ pub use command::Command as Math; pub use max::SubCommand as MathMaximum; pub use median::SubCommand as MathMedian; pub use min::SubCommand as MathMinimum; +pub use sum::SubCommand as MathSummation; diff --git a/crates/nu-cli/src/commands/sum.rs b/crates/nu-cli/src/commands/math/sum.rs similarity index 50% rename from crates/nu-cli/src/commands/sum.rs rename to crates/nu-cli/src/commands/math/sum.rs index b326b72e0d..01fa5276cd 100644 --- a/crates/nu-cli/src/commands/sum.rs +++ b/crates/nu-cli/src/commands/math/sum.rs @@ -1,26 +1,25 @@ +use crate::commands::math::utils::calculate; use crate::commands::WholeStreamCommand; use crate::prelude::*; use crate::utils::data_processing::{reducer_for, Reduce}; use nu_errors::ShellError; -use nu_protocol::{Dictionary, ReturnSuccess, Signature, UntaggedValue, Value}; +use nu_protocol::{Dictionary, Signature, UntaggedValue, Value}; use num_traits::identities::Zero; -use indexmap::map::IndexMap; - -pub struct Sum; +pub struct SubCommand; #[async_trait] -impl WholeStreamCommand for Sum { +impl WholeStreamCommand for SubCommand { fn name(&self) -> &str { - "sum" + "math sum" } fn signature(&self) -> Signature { - Signature::build("sum") + Signature::build("math sum") } fn usage(&self) -> &str { - "Sums the values." + "Finds the sum of a list of numbers or tables" } async fn run( @@ -28,16 +27,19 @@ impl WholeStreamCommand for Sum { args: CommandArgs, registry: &CommandRegistry, ) -> Result { - sum(RunnableContext { - input: args.input, - registry: registry.clone(), - shell_manager: args.shell_manager, - host: args.host, - ctrl_c: args.ctrl_c, - current_errors: args.current_errors, - name: args.call_info.name_tag, - raw_input: args.raw_input, - }) + calculate( + RunnableContext { + input: args.input, + registry: registry.clone(), + shell_manager: args.shell_manager, + host: args.host, + ctrl_c: args.ctrl_c, + current_errors: args.current_errors, + name: args.call_info.name_tag, + raw_input: args.raw_input, + }, + summation, + ) .await } @@ -45,31 +47,28 @@ impl WholeStreamCommand for Sum { vec![ Example { description: "Sum a list of numbers", - example: "echo [1 2 3] | sum", + example: "echo [1 2 3] | math sum", result: Some(vec![UntaggedValue::int(6).into()]), }, Example { description: "Get the disk usage for the current directory", - example: "ls --all --du | get size | sum", + example: "ls --all --du | get size | math sum", result: None, }, ] } } -async fn sum( - RunnableContext { mut input, .. }: RunnableContext, -) -> Result { - let values: Vec = input.drain_vec().await; - let action = reducer_for(Reduce::Sum); +pub fn summation(values: &[Value], name: &Tag) -> Result { + let sum = reducer_for(Reduce::Summation); if values.iter().all(|v| v.is_primitive()) { - let total = action(Value::zero(), values)?; - Ok(OutputStream::one(ReturnSuccess::value(total))) + Ok(sum(Value::zero(), values.to_vec())?) } else { let mut column_values = IndexMap::new(); + for value in values { - if let UntaggedValue::Row(row_dict) = value.value { + if let UntaggedValue::Row(row_dict) = value.value.clone() { for (key, value) in row_dict.entries.iter() { column_values .entry(key.clone()) @@ -80,32 +79,28 @@ async fn sum( } let mut column_totals = IndexMap::new(); + for (col_name, col_vals) in column_values { - let sum = action(Value::zero(), col_vals); - match sum { - Ok(value) => { - column_totals.insert(col_name, value); - } - Err(err) => return Err(err), - }; + let sum = sum(Value::zero(), col_vals)?; + + column_totals.insert(col_name, sum); } - Ok(OutputStream::one(ReturnSuccess::value( - UntaggedValue::Row(Dictionary { - entries: column_totals, - }) - .into_untagged_value(), - ))) + + Ok(UntaggedValue::Row(Dictionary { + entries: column_totals, + }) + .into_value(name)) } } #[cfg(test)] mod tests { - use super::Sum; + use super::SubCommand; #[test] fn examples_work_as_expected() { use crate::examples::test as test_examples; - test_examples(Sum {}) + test_examples(SubCommand {}) } } diff --git a/crates/nu-cli/src/utils/data_processing.rs b/crates/nu-cli/src/utils/data_processing.rs index b17f714326..8b94f9047e 100644 --- a/crates/nu-cli/src/utils/data_processing.rs +++ b/crates/nu-cli/src/utils/data_processing.rs @@ -288,14 +288,14 @@ pub fn reducer_for( command: Reduce, ) -> Box) -> Result + Send + Sync + 'static> { match command { - Reduce::Sum | Reduce::Default => Box::new(formula(Value::zero(), Box::new(sum))), + Reduce::Summation | Reduce::Default => Box::new(formula(Value::zero(), Box::new(sum))), Reduce::Minimum => Box::new(|_, values| min(values)), Reduce::Maximum => Box::new(|_, values| max(values)), } } pub enum Reduce { - Sum, + Summation, Minimum, Maximum, Default, @@ -309,7 +309,7 @@ pub fn reduce( let tag = tag.into(); let reduce_with = match reducer { - Some(cmd) if cmd == "sum" => reducer_for(Reduce::Sum), + Some(cmd) if cmd == "sum" => reducer_for(Reduce::Summation), Some(cmd) if cmd == "min" => reducer_for(Reduce::Minimum), Some(cmd) if cmd == "max" => reducer_for(Reduce::Maximum), Some(_) | None => reducer_for(Reduce::Default), @@ -642,7 +642,7 @@ mod tests { fn reducer_computes_given_a_sum_command() -> Result<(), ShellError> { let subject = vec![int(1), int(1), int(1)]; - let action = reducer_for(Reduce::Sum); + let action = reducer_for(Reduce::Summation); assert_eq!(action(Value::zero(), subject)?, int(3)); diff --git a/crates/nu-cli/tests/commands/drop.rs b/crates/nu-cli/tests/commands/drop.rs index 1bdb26aac5..432f94f68f 100644 --- a/crates/nu-cli/tests/commands/drop.rs +++ b/crates/nu-cli/tests/commands/drop.rs @@ -4,7 +4,7 @@ use nu_test_support::nu; fn drop_rows() { let actual = nu!( cwd: "tests/fixtures/formats", - r#"echo '[{"foo": 3}, {"foo": 8}, {"foo": 4}]' | from json | drop 2 | get foo | sum | echo $it"# + r#"echo '[{"foo": 3}, {"foo": 8}, {"foo": 4}]' | from json | drop 2 | get foo | math sum | echo $it"# ); assert_eq!(actual.out, "3"); diff --git a/crates/nu-cli/tests/commands/each.rs b/crates/nu-cli/tests/commands/each.rs index bfffe2bf26..f12ffe3b62 100644 --- a/crates/nu-cli/tests/commands/each.rs +++ b/crates/nu-cli/tests/commands/each.rs @@ -5,7 +5,7 @@ fn each_works_separately() { let actual = nu!( cwd: "tests/fixtures/formats", pipeline( r#" - echo [1 2 3] | each { echo $it 10 | sum } | to json | echo $it + echo [1 2 3] | each { echo $it 10 | math sum } | to json | echo $it "# )); diff --git a/crates/nu-cli/tests/commands/is_empty.rs b/crates/nu-cli/tests/commands/is_empty.rs index c53192f245..6929a58d04 100644 --- a/crates/nu-cli/tests/commands/is_empty.rs +++ b/crates/nu-cli/tests/commands/is_empty.rs @@ -22,7 +22,7 @@ fn adds_value_provided_if_column_is_empty() { open likes.csv | empty? likes 1 | get likes - | sum + | math sum | echo $it "# )); @@ -43,7 +43,7 @@ fn adds_value_provided_for_columns_that_are_empty() { {"boost": 1, "check": {}}, {"boost": null, "check": ["" {} [] ""]} ] - + "#, )]); @@ -53,7 +53,7 @@ fn adds_value_provided_for_columns_that_are_empty() { open checks.json | empty? boost check 1 | get boost check - | sum + | math sum | echo $it "# )); diff --git a/crates/nu-cli/tests/commands/keep.rs b/crates/nu-cli/tests/commands/keep.rs index 540566c8c2..eb50025197 100644 --- a/crates/nu-cli/tests/commands/keep.rs +++ b/crates/nu-cli/tests/commands/keep.rs @@ -22,7 +22,7 @@ fn rows() { open caballeros.csv | keep 3 | get lucky_code - | sum + | math sum | echo $it "# )); diff --git a/crates/nu-cli/tests/commands/keep_until.rs b/crates/nu-cli/tests/commands/keep_until.rs index 55524538b0..6dab4273d7 100644 --- a/crates/nu-cli/tests/commands/keep_until.rs +++ b/crates/nu-cli/tests/commands/keep_until.rs @@ -41,7 +41,7 @@ fn condition_is_met() { | keep-until "Chicken Collection" == "Red Chickens" | str to-int "31/04/2020" | get "31/04/2020" - | sum + | math sum | echo $it "# )); diff --git a/crates/nu-cli/tests/commands/keep_while.rs b/crates/nu-cli/tests/commands/keep_while.rs index e8c3e69342..87eeaade48 100644 --- a/crates/nu-cli/tests/commands/keep_while.rs +++ b/crates/nu-cli/tests/commands/keep_while.rs @@ -41,7 +41,7 @@ fn condition_is_met() { | keep-while "Chicken Collection" != "Blue Chickens" | str to-int "31/04/2020" | get "31/04/2020" - | sum + | math sum | echo $it "# )); diff --git a/crates/nu-cli/tests/commands/merge.rs b/crates/nu-cli/tests/commands/merge.rs index dfda69ee59..cda5a9178b 100644 --- a/crates/nu-cli/tests/commands/merge.rs +++ b/crates/nu-cli/tests/commands/merge.rs @@ -33,7 +33,7 @@ fn row() { | merge { open new_caballeros.csv } | where country in: ["Guayaquil Ecuador" "New Zealand"] | get luck - | sum + | math sum | echo $it "# )); diff --git a/crates/nu-cli/tests/commands/skip_until.rs b/crates/nu-cli/tests/commands/skip_until.rs index 4748766caa..1344f49d99 100644 --- a/crates/nu-cli/tests/commands/skip_until.rs +++ b/crates/nu-cli/tests/commands/skip_until.rs @@ -40,7 +40,7 @@ fn condition_is_met() { | skip-until "Chicken Collection" == "Red Chickens" | str to-int "31/04/2020" | get "31/04/2020" - | sum + | math sum | echo $it "# )); diff --git a/crates/nu-cli/tests/commands/str_.rs b/crates/nu-cli/tests/commands/str_.rs index e25e5df28f..3f2f574ac1 100644 --- a/crates/nu-cli/tests/commands/str_.rs +++ b/crates/nu-cli/tests/commands/str_.rs @@ -108,7 +108,7 @@ fn converts_to_decimal() { echo "3.1, 0.0415" | split row "," | str to-decimal - | sum + | math sum "# )); @@ -130,7 +130,7 @@ fn sets() { cwd: dirs.test(), pipeline( r#" open sample.toml - | str set wykittenshell package.name + | str set wykittenshell package.name | get package.name | echo $it "# diff --git a/crates/nu-cli/tests/commands/sum.rs b/crates/nu-cli/tests/commands/sum.rs index 5d5e440cdf..7395ef6cf8 100644 --- a/crates/nu-cli/tests/commands/sum.rs +++ b/crates/nu-cli/tests/commands/sum.rs @@ -25,7 +25,7 @@ fn all() { open meals.json | get meals | get calories - | sum + | math sum | echo $it "# )); @@ -53,7 +53,7 @@ fn outputs_zero_with_no_input() { let actual = nu!( cwd: dirs.test(), pipeline( r#" - sum + math sum | echo $it "# )); @@ -74,7 +74,7 @@ fn compute_sum_of_individual_row() -> Result<(), String> { for (column_name, expected_value) in answers_for_columns.iter() { let actual = nu!( cwd: "tests/fixtures/formats/", - format!("open sample-ps-output.json | select {} | sum | get {}", column_name, column_name) + format!("open sample-ps-output.json | select {} | math sum | get {}", column_name, column_name) ); let result = f64::from_str(&actual.out).map_err(|_| String::from("Failed to parse float."))?; @@ -95,7 +95,7 @@ fn compute_sum_of_table() -> Result<(), String> { for (column_name, expected_value) in answers_for_columns.iter() { let actual = nu!( cwd: "tests/fixtures/formats/", - format!("open sample-ps-output.json | select cpu mem virtual | sum | get {}", column_name) + format!("open sample-ps-output.json | select cpu mem virtual | math sum | get {}", column_name) ); let result = f64::from_str(&actual.out).map_err(|_| String::from("Failed to parse float."))?; @@ -108,7 +108,7 @@ fn compute_sum_of_table() -> Result<(), String> { fn sum_of_a_row_containing_a_table_is_an_error() { let actual = nu!( cwd: "tests/fixtures/formats/", - "open sample-sys-output.json | sum" + "open sample-sys-output.json | math sum" ); assert!(actual .err diff --git a/crates/nu-cli/tests/commands/where_.rs b/crates/nu-cli/tests/commands/where_.rs index 340828185e..41a0a59453 100644 --- a/crates/nu-cli/tests/commands/where_.rs +++ b/crates/nu-cli/tests/commands/where_.rs @@ -14,7 +14,7 @@ fn filters_by_unit_size_comparison() { fn filters_with_nothing_comparison() { let actual = nu!( cwd: "tests/fixtures/formats", - r#"echo '[{"foo": 3}, {"foo": null}, {"foo": 4}]' | from json | get foo | compact | where $it > 1 | sum | echo $it"# + r#"echo '[{"foo": 3}, {"foo": null}, {"foo": 4}]' | from json | get foo | compact | where $it > 1 | math sum | echo $it"# ); assert_eq!(actual.out, "7"); @@ -24,7 +24,7 @@ fn filters_with_nothing_comparison() { fn where_in_table() { let actual = nu!( cwd: "tests/fixtures/formats", - r#"echo '[{"name": "foo", "size": 3}, {"name": "foo", "size": 2}, {"name": "bar", "size": 4}]' | from json | where name in: ["foo"] | get size | sum | echo $it"# + r#"echo '[{"name": "foo", "size": 3}, {"name": "foo", "size": 2}, {"name": "bar", "size": 4}]' | from json | where name in: ["foo"] | get size | math sum | echo $it"# ); assert_eq!(actual.out, "5"); @@ -34,7 +34,7 @@ fn where_in_table() { fn where_not_in_table() { let actual = nu!( cwd: "tests/fixtures/formats", - r#"echo '[{"name": "foo", "size": 3}, {"name": "foo", "size": 2}, {"name": "bar", "size": 4}]' | from json | where name not-in: ["foo"] | get size | sum | echo $it"# + r#"echo '[{"name": "foo", "size": 3}, {"name": "foo", "size": 2}, {"name": "bar", "size": 4}]' | from json | where name not-in: ["foo"] | get size | math sum | echo $it"# ); assert_eq!(actual.out, "4"); diff --git a/crates/nu-test-support/src/lib.rs b/crates/nu-test-support/src/lib.rs index 8850dce034..a60b75217c 100644 --- a/crates/nu-test-support/src/lib.rs +++ b/crates/nu-test-support/src/lib.rs @@ -36,14 +36,14 @@ mod tests { | from-csv | get rusty_luck | str --to-int - | sum + | math sum | echo "$it" "#, ); assert_eq!( actual, - r#"open los_tres_amigos.txt | from-csv | get rusty_luck | str --to-int | sum | echo "$it""# + r#"open los_tres_amigos.txt | from-csv | get rusty_luck | str --to-int | math sum | echo "$it""# ); } } diff --git a/docs/commands/math.md b/docs/commands/math.md index a9eba76aa2..0af856efcd 100644 --- a/docs/commands/math.md +++ b/docs/commands/math.md @@ -1,95 +1,126 @@ # math + Mathematical functions that generally only operate on a list of numbers (integers, decimals, bytes) and tables. Currently the following functions are implemented: -`math average` Get the average of a list of number -`math min` Get the minimum of a list of numbers -`math max` Get the maximum of a list of numbers + +* `math average`: Finds the average of a list of numbers or tables +* `math min`: Finds the minimum within a list of numbers or tables +* `math max`: Finds the maximum within a list of numbers or tables +* `math sum`: Finds the sum of a list of numbers or tables However, the mathematical functions like `min` and `max` are more permissive and also work on `Dates`. ## Examples + To get the average of the file sizes in a directory, simply pipe the size column from the ls command to the average command. ### List of Numbers (Integers, Decimals, Bytes) + ```shell > ls - # │ name │ type │ size │ modified + # │ name │ type │ size │ modified ────┼────────────────────┼──────┼──────────┼───────────── - 0 │ CODE_OF_CONDUCT.md │ File │ 3.4 KB │ 4 days ago - 1 │ CONTRIBUTING.md │ File │ 1.3 KB │ 4 days ago - 2 │ Cargo.lock │ File │ 106.3 KB │ 6 mins ago - 3 │ Cargo.toml │ File │ 4.6 KB │ 3 days ago - 4 │ LICENSE │ File │ 1.1 KB │ 4 days ago - 5 │ Makefile.toml │ File │ 449 B │ 4 days ago - 6 │ README.md │ File │ 16.0 KB │ 6 mins ago - 7 │ TODO.md │ File │ 0 B │ 6 mins ago - 8 │ assets │ Dir │ 128 B │ 4 days ago - 9 │ build.rs │ File │ 78 B │ 4 days ago - 10 │ crates │ Dir │ 672 B │ 3 days ago - 11 │ debian │ Dir │ 352 B │ 4 days ago - 12 │ docker │ Dir │ 288 B │ 4 days ago - 13 │ docs │ Dir │ 160 B │ 4 days ago - 14 │ features.toml │ File │ 632 B │ 4 days ago - 15 │ images │ Dir │ 160 B │ 4 days ago - 16 │ justfile │ File │ 234 B │ 3 days ago - 17 │ rustfmt.toml │ File │ 16 B │ 4 days ago - 18 │ src │ Dir │ 128 B │ 4 days ago - 19 │ target │ Dir │ 192 B │ 8 hours ago - 20 │ tests │ Dir │ 192 B │ 4 days ago + 0 │ CODE_OF_CONDUCT.md │ File │ 3.4 KB │ 4 days ago + 1 │ CONTRIBUTING.md │ File │ 1.3 KB │ 4 days ago + 2 │ Cargo.lock │ File │ 106.3 KB │ 6 mins ago + 3 │ Cargo.toml │ File │ 4.6 KB │ 3 days ago + 4 │ LICENSE │ File │ 1.1 KB │ 4 days ago + 5 │ Makefile.toml │ File │ 449 B │ 4 days ago + 6 │ README.md │ File │ 16.0 KB │ 6 mins ago + 7 │ TODO.md │ File │ 0 B │ 6 mins ago + 8 │ assets │ Dir │ 128 B │ 4 days ago + 9 │ build.rs │ File │ 78 B │ 4 days ago + 10 │ crates │ Dir │ 672 B │ 3 days ago + 11 │ debian │ Dir │ 352 B │ 4 days ago + 12 │ docker │ Dir │ 288 B │ 4 days ago + 13 │ docs │ Dir │ 160 B │ 4 days ago + 14 │ features.toml │ File │ 632 B │ 4 days ago + 15 │ images │ Dir │ 160 B │ 4 days ago + 16 │ justfile │ File │ 234 B │ 3 days ago + 17 │ rustfmt.toml │ File │ 16 B │ 4 days ago + 18 │ src │ Dir │ 128 B │ 4 days ago + 19 │ target │ Dir │ 192 B │ 8 hours ago + 20 │ tests │ Dir │ 192 B │ 4 days ago +``` +```shell > ls | get size | math average ───┬──────── - 0 │ 6.5 KB + 0 │ 7.2 KB ───┴──────── +``` +```shell > ls | get size | math min ───┬───── - 0 │ 0 B + 0 │ 0 B ───┴───── +``` + +```shell > ls | get size | math max ───┬────────── - 0 │ 106.3 KB + 0 │ 113.5 KB ───┴────────── +``` -# Dates +```shell +> ls | get size | math sum +───┬────────── + 0 │ 143.4 KB +───┴────────── +``` + +### Dates + +```shell > ls | get modified | math min 2020-06-09 17:25:51.798743222 UTC +``` +```shell > ls | get modified | math max 2020-06-14 05:49:59.637449186 UT ``` ### Operations on tables -```shell -> pwd | split row / | size -───┬───────┬───────┬───────┬──────────── - # │ lines │ words │ chars │ max length -───┼───────┼───────┼───────┼──────────── - 0 │ 0 │ 1 │ 5 │ 5 - 1 │ 0 │ 1 │ 7 │ 7 - 2 │ 0 │ 1 │ 9 │ 9 - 3 │ 0 │ 1 │ 7 │ 7 -───┴───────┴───────┴───────┴──────────── +```shell +> pwd | split row / | size +───┬───────┬───────┬───────┬──────────── + # │ lines │ words │ chars │ max length +───┼───────┼───────┼───────┼──────────── + 0 │ 0 │ 1 │ 5 │ 5 + 1 │ 0 │ 1 │ 7 │ 7 + 2 │ 0 │ 1 │ 9 │ 9 + 3 │ 0 │ 1 │ 7 │ 7 +───┴───────┴───────┴───────┴──────────── +``` + +```shell > pwd | split row / | size | math max ───────────┬─── - lines │ 0 - words │ 1 - chars │ 9 - max length │ 9 + lines │ 0 + words │ 1 + chars │ 9 + max length │ 9 ────────────┴─── +``` +```shell > pwd | split row / | size | math average ────────────┬──────── - lines │ 0.0000 - words │ 1.0000 - chars │ 7.0000 - max length │ 7.0000 + lines │ 0.0000 + words │ 1.0000 + chars │ 7.0000 + max length │ 7.0000 ────────────┴──────── ``` ## Errors + `math` functions are aggregation functions so empty lists are invalid + ```shell > echo [] | math average error: Error: Unexpected: Cannot perform aggregate math operation on empty data @@ -102,7 +133,3 @@ then unexpected results can occur. > echo [1 2 a ] | math average 0 ``` - - - - diff --git a/docs/commands/str.md b/docs/commands/str.md index ce358043db..f8eb32c475 100644 --- a/docs/commands/str.md +++ b/docs/commands/str.md @@ -41,7 +41,7 @@ Applies the subcommand to a value or a table. 1 │ │ filesystem │ ━━━┷━━━┷━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -> echo "1, 2, 3" | split row "," | str to-int | sum +> echo "1, 2, 3" | split row "," | str to-int | math sum ━━━━━━━━━ ───────── diff --git a/docs/commands/sum.md b/docs/commands/sum.md index e87dc65afb..f36d5b2655 100644 --- a/docs/commands/sum.md +++ b/docs/commands/sum.md @@ -5,7 +5,7 @@ This command allows you to calculate the sum of values in a column. To get the sum of the file sizes in a directory, simply pipe the size column from the ls command to the sum command. ```shell -> ls | get size | sum +> ls | get size | math sum ━━━━━━━━━ value ━━━━━━━━━ @@ -15,7 +15,7 @@ To get the sum of the file sizes in a directory, simply pipe the size column fro To get the sum of the characters that make up your present working directory. ```shell -> pwd | split-row / | size | get chars | sum +> pwd | split-row / | size | get chars | math sum ━━━━━━━━━ ━━━━━━━━━ @@ -27,15 +27,15 @@ Note that sum only works for integer and byte values. If the shell doesn't recog One way to solve this is to convert each row to an integer when possible and then pipe the result to `sum` ```shell -> open tests/fixtures/formats/caco3_plastics.csv | get tariff_item | sum +> open tests/fixtures/formats/caco3_plastics.csv | get tariff_item | math sum error: Unrecognized type in stream: Primitive(String("2509000000")) - shell:1:0 -1 | open tests/fixtures/formats/caco3_plastics.csv | get tariff_item | sum +1 | open tests/fixtures/formats/caco3_plastics.csv | get tariff_item | math sum | ^^^^ source ``` ```shell -> open tests/fixtures/formats/caco3_plastics.csv | get tariff_item | str --to-int | sum +> open tests/fixtures/formats/caco3_plastics.csv | get tariff_item | str --to-int | math sum ━━━━━━━━━━━━━ ───────────── diff --git a/tests/shell/pipeline/commands/internal.rs b/tests/shell/pipeline/commands/internal.rs index d5ebf7da8a..d30718cf6e 100644 --- a/tests/shell/pipeline/commands/internal.rs +++ b/tests/shell/pipeline/commands/internal.rs @@ -198,7 +198,7 @@ fn echoing_ranges() { let actual = nu!( cwd: ".", r#" - echo 1..3 | sum + echo 1..3 | math sum "# ); From bc9cc75c8a0534760056ba5bae18e396a2f2c4e8 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 19 Jun 2020 14:00:18 -0400 Subject: [PATCH 12/27] Minor Math Sum Additions (#2007) * Move sum tests into math directory * Move sum documentation over to math documentation One sum example already existed in the math examples and a few of the others were outdated and didn't work, so I only moved one over, and updated their output * Remove no-longer-in-use mod statement --- .../nu-cli/tests/commands/{ => math}/sum.rs | 0 crates/nu-cli/tests/commands/mod.rs | 1 - docs/commands/math.md | 7 +++ docs/commands/sum.md | 44 ------------------- 4 files changed, 7 insertions(+), 45 deletions(-) rename crates/nu-cli/tests/commands/{ => math}/sum.rs (100%) delete mode 100644 docs/commands/sum.md diff --git a/crates/nu-cli/tests/commands/sum.rs b/crates/nu-cli/tests/commands/math/sum.rs similarity index 100% rename from crates/nu-cli/tests/commands/sum.rs rename to crates/nu-cli/tests/commands/math/sum.rs diff --git a/crates/nu-cli/tests/commands/mod.rs b/crates/nu-cli/tests/commands/mod.rs index 22febe997d..614edd4770 100644 --- a/crates/nu-cli/tests/commands/mod.rs +++ b/crates/nu-cli/tests/commands/mod.rs @@ -44,7 +44,6 @@ mod split_by; mod split_column; mod split_row; mod str_; -mod sum; mod touch; mod trim; mod uniq; diff --git a/docs/commands/math.md b/docs/commands/math.md index 0af856efcd..fa61dd3797 100644 --- a/docs/commands/math.md +++ b/docs/commands/math.md @@ -117,6 +117,13 @@ To get the average of the file sizes in a directory, simply pipe the size column ────────────┴──────── ``` +To get the sum of the characters that make up your present working directory. + +```shell +> pwd | split row / | size | get chars | math sum +50 +``` + ## Errors `math` functions are aggregation functions so empty lists are invalid diff --git a/docs/commands/sum.md b/docs/commands/sum.md deleted file mode 100644 index f36d5b2655..0000000000 --- a/docs/commands/sum.md +++ /dev/null @@ -1,44 +0,0 @@ -# sum -This command allows you to calculate the sum of values in a column. - -## Examples -To get the sum of the file sizes in a directory, simply pipe the size column from the ls command to the sum command. - -```shell -> ls | get size | math sum -━━━━━━━━━ - value -━━━━━━━━━ - 51.0 MB -━━━━━━━━━ -``` - -To get the sum of the characters that make up your present working directory. -```shell -> pwd | split-row / | size | get chars | math sum -━━━━━━━━━ - -━━━━━━━━━ -21 -━━━━━━━━━ -``` - -Note that sum only works for integer and byte values. If the shell doesn't recognize the values in a column as one of those types, it will return an error. -One way to solve this is to convert each row to an integer when possible and then pipe the result to `sum` - -```shell -> open tests/fixtures/formats/caco3_plastics.csv | get tariff_item | math sum -error: Unrecognized type in stream: Primitive(String("2509000000")) -- shell:1:0 -1 | open tests/fixtures/formats/caco3_plastics.csv | get tariff_item | math sum - | ^^^^ source -``` - -```shell -> open tests/fixtures/formats/caco3_plastics.csv | get tariff_item | str --to-int | math sum -━━━━━━━━━━━━━ - -───────────── - 29154639996 -━━━━━━━━━━━━━ -``` From 6bfd8532e406511546f6b95e4d8ad27ef61c4742 Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Fri, 19 Jun 2020 15:08:59 -0500 Subject: [PATCH 13/27] Bat config (#2010) * WIP - changes to support bat config * added bat configuration * removed debug info * clippy fix * changed [bat] to [textview] Co-authored-by: Darren Schroeder --- Cargo.lock | 1 + crates/nu-cli/src/data.rs | 2 +- crates/nu-cli/src/data/config.rs | 2 +- crates/nu-cli/src/lib.rs | 3 +- crates/nu_plugin_textview/Cargo.toml | 2 +- crates/nu_plugin_textview/src/textview.rs | 227 +++++++++++++++++----- 6 files changed, 184 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6940bb757c..bec2113661 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2767,6 +2767,7 @@ dependencies = [ "bat", "crossterm", "nu-build", + "nu-cli", "nu-errors", "nu-plugin", "nu-protocol", diff --git a/crates/nu-cli/src/data.rs b/crates/nu-cli/src/data.rs index 5699e68480..4269aef910 100644 --- a/crates/nu-cli/src/data.rs +++ b/crates/nu-cli/src/data.rs @@ -1,6 +1,6 @@ pub(crate) mod base; pub(crate) mod command; -pub(crate) mod config; +pub mod config; pub(crate) mod dict; pub(crate) mod files; pub mod primitive; diff --git a/crates/nu-cli/src/data/config.rs b/crates/nu-cli/src/data/config.rs index fbd296f27d..391b5db1bb 100644 --- a/crates/nu-cli/src/data/config.rs +++ b/crates/nu-cli/src/data/config.rs @@ -103,7 +103,7 @@ pub fn read( } } -pub(crate) fn config(tag: impl Into) -> Result, ShellError> { +pub fn config(tag: impl Into) -> Result, ShellError> { read(tag, &None) } diff --git a/crates/nu-cli/src/lib.rs b/crates/nu-cli/src/lib.rs index cd923e2c19..0cb86aa508 100644 --- a/crates/nu-cli/src/lib.rs +++ b/crates/nu-cli/src/lib.rs @@ -16,7 +16,7 @@ extern crate quickcheck_macros; mod cli; mod commands; mod context; -mod data; +pub mod data; mod deserializer; mod env; mod evaluate; @@ -39,6 +39,7 @@ pub use crate::commands::command::{ }; pub use crate::commands::help::get_help; pub use crate::context::CommandRegistry; +pub use crate::data::config; pub use crate::data::dict::TaggedListBuilder; pub use crate::data::primitive; pub use crate::data::value; diff --git a/crates/nu_plugin_textview/Cargo.toml b/crates/nu_plugin_textview/Cargo.toml index 65e8019645..7e4ae79417 100644 --- a/crates/nu_plugin_textview/Cargo.toml +++ b/crates/nu_plugin_textview/Cargo.toml @@ -14,7 +14,7 @@ nu-plugin = { path = "../nu-plugin", version = "0.15.1" } nu-protocol = { path = "../nu-protocol", version = "0.15.1" } nu-source = { path = "../nu-source", version = "0.15.1" } nu-errors = { path = "../nu-errors", version = "0.15.1" } -#nu-cli = { path = "../nu-cli", version = "0.15.1" } +nu-cli = { path = "../nu-cli", version = "0.15.1" } crossterm = "0.17.5" syntect = { version = "4.2", default-features = false, features = ["default-fancy"]} diff --git a/crates/nu_plugin_textview/src/textview.rs b/crates/nu_plugin_textview/src/textview.rs index 89e422a9bf..5c8aec6471 100644 --- a/crates/nu_plugin_textview/src/textview.rs +++ b/crates/nu_plugin_textview/src/textview.rs @@ -1,5 +1,5 @@ use nu_protocol::{Primitive, UntaggedValue, Value}; -use nu_source::AnchorLocation; +use nu_source::{AnchorLocation, Tag}; use std::path::Path; #[derive(Default)] @@ -11,7 +11,136 @@ impl TextView { } } +#[allow(clippy::cognitive_complexity)] pub fn view_text_value(value: &Value) { + let mut term_width: u64 = textwrap::termwidth() as u64; + let mut tab_width: u64 = 4; + let mut colored_output = true; + let mut true_color = true; + let mut header = true; + let mut line_numbers = true; + let mut grid = true; + let mut vcs_modification_markers = true; + let mut snip = true; + let mut wrapping_mode = bat::WrappingMode::NoWrapping; + let mut use_italics = true; + let mut paging_mode = bat::PagingMode::QuitIfOneScreen; + let mut pager = "less".to_string(); + let mut line_ranges = bat::line_range::LineRanges::all(); + let mut _highlight_range = "0,0"; + let highlight_range_from: u64 = 0; + let highlight_range_to: u64 = 0; + let mut theme = "OneHalfDark".to_string(); + + if let Ok(config) = nu_cli::data::config::config(Tag::unknown()) { + if let Some(batvars) = config.get("textview") { + for (idx, value) in batvars.row_entries() { + match idx { + x if x == "term_width" => { + term_width = match value.as_u64() { + Ok(n) => n, + _ => textwrap::termwidth() as u64, + } + } + x if x == "tab_width" => { + tab_width = match value.as_u64() { + Ok(n) => n, + _ => 4u64, + } + } + x if x == "colored_output" => { + colored_output = match value.as_bool() { + Ok(b) => b, + _ => true, + } + } + x if x == "true_color" => { + true_color = match value.as_bool() { + Ok(b) => b, + _ => true, + } + } + x if x == "header" => { + header = match value.as_bool() { + Ok(b) => b, + _ => true, + } + } + x if x == "line_numbers" => { + line_numbers = match value.as_bool() { + Ok(b) => b, + _ => true, + } + } + x if x == "grid" => { + grid = match value.as_bool() { + Ok(b) => b, + _ => true, + } + } + x if x == "vcs_modification_markers" => { + vcs_modification_markers = match value.as_bool() { + Ok(b) => b, + _ => true, + } + } + x if x == "snip" => { + snip = match value.as_bool() { + Ok(b) => b, + _ => true, + } + } + x if x == "wrapping_mode" => { + wrapping_mode = match value.as_string() { + Ok(s) if s.to_lowercase() == "nowrapping" => { + bat::WrappingMode::NoWrapping + } + Ok(s) if s.to_lowercase() == "character" => { + bat::WrappingMode::Character + } + _ => bat::WrappingMode::NoWrapping, + } + } + x if x == "use_italics" => { + use_italics = match value.as_bool() { + Ok(b) => b, + _ => true, + } + } + x if x == "paging_mode" => { + paging_mode = match value.as_string() { + Ok(s) if s.to_lowercase() == "always" => bat::PagingMode::Always, + Ok(s) if s.to_lowercase() == "never" => bat::PagingMode::Never, + Ok(s) if s.to_lowercase() == "quitifonescreen" => { + bat::PagingMode::QuitIfOneScreen + } + _ => bat::PagingMode::QuitIfOneScreen, + } + } + x if x == "pager" => { + pager = match value.as_string() { + Ok(s) => s, + _ => "less".to_string(), + } + } + x if x == "line_ranges" => line_ranges = bat::line_range::LineRanges::all(), // not real sure what to do with this + x if x == "highlight_range" => _highlight_range = "0,0", //ignore config value for now + x if x == "theme" => { + theme = match value.as_string() { + Ok(s) => s, + _ => "OneDarkHalf".to_string(), + } + } + _ => (), + } + } + } else { + println!("Couldn't find bat section in config"); + } + } else { + println!("Error reading config!"); + } + let value_anchor = value.anchor(); if let UntaggedValue::Primitive(Primitive::String(ref s)) = &value.value { if let Some(source) = value_anchor { @@ -46,44 +175,44 @@ pub fn view_text_value(value: &Value) { // Let bat do it's thing bat::PrettyPrinter::new() .input_from_bytes_with_name(s.as_bytes(), file_path) - .term_width(textwrap::termwidth()) - .tab_width(Some(4)) - .colored_output(true) - .true_color(true) - .header(true) - .line_numbers(true) - .grid(true) - .vcs_modification_markers(true) - .snip(true) - .wrapping_mode(bat::WrappingMode::NoWrapping) - .use_italics(true) - .paging_mode(bat::PagingMode::QuitIfOneScreen) - .pager("less") - .line_ranges(bat::line_range::LineRanges::all()) - .highlight_range(0, 0) - .theme("OneHalfDark") + .term_width(term_width as usize) + .tab_width(Some(tab_width as usize)) + .colored_output(colored_output) + .true_color(true_color) + .header(header) + .line_numbers(line_numbers) + .grid(grid) + .vcs_modification_markers(vcs_modification_markers) + .snip(snip) + .wrapping_mode(wrapping_mode) + .use_italics(use_italics) + .paging_mode(paging_mode) + .pager(&pager) + .line_ranges(line_ranges) + .highlight_range(highlight_range_from as usize, highlight_range_to as usize) + .theme(&theme) .print() .expect("Error with bat PrettyPrint"); } _ => { bat::PrettyPrinter::new() .input_from_bytes(s.as_bytes()) - .term_width(textwrap::termwidth()) - .tab_width(Some(4)) - .colored_output(true) - .true_color(true) - .header(true) - .line_numbers(true) - .grid(true) - .vcs_modification_markers(true) - .snip(true) - .wrapping_mode(bat::WrappingMode::NoWrapping) - .use_italics(true) - .paging_mode(bat::PagingMode::QuitIfOneScreen) - .pager("less") - .line_ranges(bat::line_range::LineRanges::all()) - .highlight_range(0, 0) - .theme("OneHalfDark") + .term_width(term_width as usize) + .tab_width(Some(tab_width as usize)) + .colored_output(colored_output) + .true_color(true_color) + .header(header) + .line_numbers(line_numbers) + .grid(grid) + .vcs_modification_markers(vcs_modification_markers) + .snip(snip) + .wrapping_mode(wrapping_mode) + .use_italics(use_italics) + .paging_mode(paging_mode) + .pager(&pager) + .line_ranges(line_ranges) + .highlight_range(highlight_range_from as usize, highlight_range_to as usize) + .theme(&theme) .print() .expect("Error with bat PrettyPrint"); } @@ -91,22 +220,22 @@ pub fn view_text_value(value: &Value) { } else { bat::PrettyPrinter::new() .input_from_bytes(s.as_bytes()) - .term_width(textwrap::termwidth()) - .tab_width(Some(4)) - .colored_output(true) - .true_color(true) - .header(true) - .line_numbers(true) - .grid(true) - .vcs_modification_markers(true) - .snip(true) - .wrapping_mode(bat::WrappingMode::NoWrapping) - .use_italics(true) - .paging_mode(bat::PagingMode::QuitIfOneScreen) - .pager("less") - .line_ranges(bat::line_range::LineRanges::all()) - .highlight_range(0, 0) - .theme("OneHalfDark") + .term_width(term_width as usize) + .tab_width(Some(tab_width as usize)) + .colored_output(colored_output) + .true_color(true_color) + .header(header) + .line_numbers(line_numbers) + .grid(grid) + .vcs_modification_markers(vcs_modification_markers) + .snip(snip) + .wrapping_mode(wrapping_mode) + .use_italics(use_italics) + .paging_mode(paging_mode) + .pager(&pager) + .line_ranges(line_ranges) + .highlight_range(highlight_range_from as usize, highlight_range_to as usize) + .theme(&theme) .print() .expect("Error with bat PrettyPrint"); } From ed7a62bca33d60470b79d511baf63244b772e94d Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Fri, 19 Jun 2020 15:45:56 -0500 Subject: [PATCH 14/27] textview config docs (#2011) * documentation for bat config changes * renamed to textview, added fetch example Co-authored-by: Darren Schroeder --- docs/commands/textview_config.md | 55 ++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 docs/commands/textview_config.md diff --git a/docs/commands/textview_config.md b/docs/commands/textview_config.md new file mode 100644 index 0000000000..9dc4ce1429 --- /dev/null +++ b/docs/commands/textview_config.md @@ -0,0 +1,55 @@ +# textview config +The configuration for textview, which is used to autoview text files, uses [bat](https://docs.rs/bat/0.15.4/bat/struct.PrettyPrinter.html). The textview configurtion will **not** use any existing `bat` configuration you may have. + +### Configuration Points and Defaults +| config point | definition | implemented | +| - | - | - | +| term_width | The character width of the terminal (default: autodetect) | yes | +| tab_width | The width of tab characters (default: None - do not turn tabs to spaces) | yes | +| colored_output | Whether or not the output should be colorized (default: true) | yes | +| true_color | Whether or not to output 24bit colors (default: true) | yes | +| header | Whether to show a header with the file name | yes | +| line_numbers | Whether to show line numbers | yes | +| grid | Whether to paint a grid, separating line numbers, git changes and the code | yes | +| vcs_modification_markers | Whether to show modification markers for VCS changes. This has no effect if the git feature is not activated. | yes | +| snip | Whether to show "snip" markers between visible line ranges (default: no) | yes | +| wrapping_mode | Text wrapping mode (default: do not wrap), options (Character, NoWrapping) | yes | +| use_italics | Whether or not to use ANSI italics (default: off) | yes | +| paging_mode | If and how to use a pager (default: no paging), options (Always, QuitIfOneScreen, Never) | yes | +| pager | Specify the command to start the pager (default: use "less") | yes | +| line_ranges | Specify the lines that should be printed (default: all) | no | +| highlight | Specify a line that should be highlighted (default: none). This can be called multiple times to highlight more than one line. See also: highlight_range. | no | +| highlight_range | Specify a range of lines that should be highlighted (default: none). This can be called multiple times to highlight more than one range of lines. | no | +| theme | Specify the highlighting theme (default: OneHalfDark) | yes | + +### Example textview confguration for `config.toml` +```toml +[textview] +term_width = "default" +tab_width = 4 +colored_output = true +true_color = true +header = true +line_numbers = false +grid = false +vcs_modification_markers = true +snip = true +wrapping_mode = "NoWrapping" +use_italics = true +paging_mode = "QuitIfOneScreen" +pager = "less" +theme = "TwoDark" +``` +### Example Usage +``` +> open src/main.rs +``` +``` +> cat some_file.txt | textview +``` +``` +> fetch https://www.jonathanturner.org/feed.xml --raw +``` + +### Help +A more detailed description of the configuration points that textview uses, please visit the `bat` repo at https://github.com/sharkdp/bat \ No newline at end of file From 088901b24fed2814a8dc830e23ea60f56faa99fb Mon Sep 17 00:00:00 2001 From: Joseph T Lyons Date: Fri, 19 Jun 2020 19:20:25 -0400 Subject: [PATCH 15/27] Rename average to avg --- crates/nu-cli/src/commands/math/{average.rs => avg.rs} | 6 +++--- crates/nu-cli/src/commands/math/command.rs | 2 +- crates/nu-cli/src/commands/math/mod.rs | 4 ++-- .../nu-cli/tests/commands/math/{average.rs => avg.rs} | 4 ++-- crates/nu-cli/tests/commands/math/mod.rs | 2 +- docs/commands/math.md | 10 +++++----- 6 files changed, 14 insertions(+), 14 deletions(-) rename crates/nu-cli/src/commands/math/{average.rs => avg.rs} (96%) rename crates/nu-cli/tests/commands/math/{average.rs => avg.rs} (89%) diff --git a/crates/nu-cli/src/commands/math/average.rs b/crates/nu-cli/src/commands/math/avg.rs similarity index 96% rename from crates/nu-cli/src/commands/math/average.rs rename to crates/nu-cli/src/commands/math/avg.rs index ecb111cd46..23d2357d51 100644 --- a/crates/nu-cli/src/commands/math/average.rs +++ b/crates/nu-cli/src/commands/math/avg.rs @@ -14,11 +14,11 @@ pub struct SubCommand; #[async_trait] impl WholeStreamCommand for SubCommand { fn name(&self) -> &str { - "math average" + "math avg" } fn signature(&self) -> Signature { - Signature::build("math average") + Signature::build("math avg") } fn usage(&self) -> &str { @@ -49,7 +49,7 @@ impl WholeStreamCommand for SubCommand { fn examples(&self) -> Vec { vec![Example { description: "Get the average of a list of numbers", - example: "echo [-50 100.0 25] | math average", + example: "echo [-50 100.0 25] | math avg", result: Some(vec![UntaggedValue::decimal(25).into()]), }] } diff --git a/crates/nu-cli/src/commands/math/command.rs b/crates/nu-cli/src/commands/math/command.rs index b6609754c0..266b560203 100644 --- a/crates/nu-cli/src/commands/math/command.rs +++ b/crates/nu-cli/src/commands/math/command.rs @@ -35,7 +35,7 @@ impl WholeStreamCommand for Command { mod tests { use super::*; use crate::commands::math::{ - average::average, max::maximum, min::minimum, sum::summation, utils::MathFunction, + avg::average, max::maximum, min::minimum, sum::summation, utils::MathFunction, }; use nu_plugin::test_helpers::value::{decimal, int}; use nu_protocol::Value; diff --git a/crates/nu-cli/src/commands/math/mod.rs b/crates/nu-cli/src/commands/math/mod.rs index 399416fb65..f30ada16c5 100644 --- a/crates/nu-cli/src/commands/math/mod.rs +++ b/crates/nu-cli/src/commands/math/mod.rs @@ -1,4 +1,4 @@ -pub mod average; +pub mod avg; pub mod command; pub mod max; pub mod median; @@ -6,7 +6,7 @@ pub mod min; pub mod sum; pub mod utils; -pub use average::SubCommand as MathAverage; +pub use avg::SubCommand as MathAverage; pub use command::Command as Math; pub use max::SubCommand as MathMaximum; pub use median::SubCommand as MathMedian; diff --git a/crates/nu-cli/tests/commands/math/average.rs b/crates/nu-cli/tests/commands/math/avg.rs similarity index 89% rename from crates/nu-cli/tests/commands/math/average.rs rename to crates/nu-cli/tests/commands/math/avg.rs index caa586ff7c..c139e6aba6 100644 --- a/crates/nu-cli/tests/commands/math/average.rs +++ b/crates/nu-cli/tests/commands/math/avg.rs @@ -7,7 +7,7 @@ fn can_average_numbers() { r#" open sgml_description.json | get glossary.GlossDiv.GlossList.GlossEntry.Sections - | math average + | math avg | echo $it "# )); @@ -19,7 +19,7 @@ fn can_average_numbers() { fn can_average_bytes() { let actual = nu!( cwd: "tests/fixtures/formats", - "ls | sort-by name | skip 1 | first 2 | get size | math average | format \"{$it}\" | echo $it" + "ls | sort-by name | skip 1 | first 2 | get size | math avg | format \"{$it}\" | echo $it" ); assert_eq!(actual.out, "1.6 KB"); diff --git a/crates/nu-cli/tests/commands/math/mod.rs b/crates/nu-cli/tests/commands/math/mod.rs index ff682ab3ad..78c591c90a 100644 --- a/crates/nu-cli/tests/commands/math/mod.rs +++ b/crates/nu-cli/tests/commands/math/mod.rs @@ -1,4 +1,4 @@ -mod average; +mod avg; mod median; use nu_test_support::{nu, pipeline}; diff --git a/docs/commands/math.md b/docs/commands/math.md index fa61dd3797..4c2528000f 100644 --- a/docs/commands/math.md +++ b/docs/commands/math.md @@ -3,7 +3,7 @@ Mathematical functions that generally only operate on a list of numbers (integers, decimals, bytes) and tables. Currently the following functions are implemented: -* `math average`: Finds the average of a list of numbers or tables +* `math avg`: Finds the average of a list of numbers or tables * `math min`: Finds the minimum within a list of numbers or tables * `math max`: Finds the maximum within a list of numbers or tables * `math sum`: Finds the sum of a list of numbers or tables @@ -44,7 +44,7 @@ To get the average of the file sizes in a directory, simply pipe the size column ``` ```shell -> ls | get size | math average +> ls | get size | math avg ───┬──────── 0 │ 7.2 KB ───┴──────── @@ -108,7 +108,7 @@ To get the average of the file sizes in a directory, simply pipe the size column ``` ```shell -> pwd | split row / | size | math average +> pwd | split row / | size | math avg ────────────┬──────── lines │ 0.0000 words │ 1.0000 @@ -129,7 +129,7 @@ To get the sum of the characters that make up your present working directory. `math` functions are aggregation functions so empty lists are invalid ```shell -> echo [] | math average +> echo [] | math avg error: Error: Unexpected: Cannot perform aggregate math operation on empty data ``` @@ -137,6 +137,6 @@ Note `math` functions only work on list of numbers (integers, decimals, bytes) a then unexpected results can occur. ```shell -> echo [1 2 a ] | math average +> echo [1 2 a ] | math avg 0 ``` From 77e02ac1c1260d28198e10ba82cffbf689745faf Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Fri, 19 Jun 2020 20:54:25 -0500 Subject: [PATCH 16/27] Fixed grammar (#2012) --- docs/commands/textview_config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/commands/textview_config.md b/docs/commands/textview_config.md index 9dc4ce1429..8d60ffbbdf 100644 --- a/docs/commands/textview_config.md +++ b/docs/commands/textview_config.md @@ -52,4 +52,4 @@ theme = "TwoDark" ``` ### Help -A more detailed description of the configuration points that textview uses, please visit the `bat` repo at https://github.com/sharkdp/bat \ No newline at end of file +For a more detailed description of the configuration points that textview uses, please visit the `bat` repo at https://github.com/sharkdp/bat From fcbaefed52c0c91c4dcff945c34d1202f71e4e58 Mon Sep 17 00:00:00 2001 From: Jonathan Turner Date: Fri, 19 Jun 2020 20:41:53 -0700 Subject: [PATCH 17/27] Nu table (#2015) * WIP * Get ready to land nu-table * Remove unwrap --- Cargo.lock | 9 + crates/nu-cli/Cargo.toml | 1 + crates/nu-cli/src/cli.rs | 4 +- crates/nu-cli/src/commands/table.rs | 189 +++++- crates/nu-cli/src/data/config/nuconfig.rs | 2 +- crates/nu-cli/src/data/primitive.rs | 7 +- crates/nu-cli/src/data/value.rs | 5 +- crates/nu-cli/src/env/host.rs | 20 +- crates/nu-cli/src/format.rs | 8 - crates/nu-cli/src/format/table.rs | 453 --------------- crates/nu-cli/src/lib.rs | 3 +- crates/nu-cli/src/prelude.rs | 2 +- crates/nu-table/Cargo.toml | 16 + crates/nu-table/src/lib.rs | 5 + crates/nu-table/src/main.rs | 23 + crates/nu-table/src/table.rs | 673 ++++++++++++++++++++++ crates/nu-table/src/wrap.rs | 226 ++++++++ 17 files changed, 1146 insertions(+), 500 deletions(-) delete mode 100644 crates/nu-cli/src/format/table.rs create mode 100644 crates/nu-table/Cargo.toml create mode 100644 crates/nu-table/src/lib.rs create mode 100644 crates/nu-table/src/main.rs create mode 100644 crates/nu-table/src/table.rs create mode 100644 crates/nu-table/src/wrap.rs diff --git a/Cargo.lock b/Cargo.lock index bec2113661..87e87af30b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2472,6 +2472,7 @@ dependencies = [ "nu-plugin", "nu-protocol", "nu-source", + "nu-table", "nu-test-support", "nu-value-ext", "num-bigint", @@ -2611,6 +2612,14 @@ dependencies = [ "termcolor", ] +[[package]] +name = "nu-table" +version = "0.15.1" +dependencies = [ + "ansi_term 0.12.1", + "unicode-width", +] + [[package]] name = "nu-test-support" version = "0.15.1" diff --git a/crates/nu-cli/Cargo.toml b/crates/nu-cli/Cargo.toml index c55dfa655a..d9c8c36383 100644 --- a/crates/nu-cli/Cargo.toml +++ b/crates/nu-cli/Cargo.toml @@ -17,6 +17,7 @@ nu-errors = { version = "0.15.1", path = "../nu-errors" } nu-parser = { version = "0.15.1", path = "../nu-parser" } nu-value-ext = { version = "0.15.1", path = "../nu-value-ext" } nu-test-support = { version = "0.15.1", path = "../nu-test-support" } +nu-table = {version = "0.15.1", path = "../nu-table"} ansi_term = "0.12.1" app_dirs = "1.2.1" diff --git a/crates/nu-cli/src/cli.rs b/crates/nu-cli/src/cli.rs index ab83a4a9d3..bd529a0a98 100644 --- a/crates/nu-cli/src/cli.rs +++ b/crates/nu-cli/src/cli.rs @@ -739,7 +739,7 @@ fn chomp_newline(s: &str) -> &str { } } -enum LineResult { +pub enum LineResult { Success(String), Error(String, ShellError), CtrlC, @@ -747,7 +747,7 @@ enum LineResult { } /// Process the line by parsing the text to turn it into commands, classify those commands so that we understand what is being called in the pipeline, and then run this pipeline -async fn process_line( +pub async fn process_line( readline: Result, ctx: &mut Context, redirect_stdin: bool, diff --git a/crates/nu-cli/src/commands/table.rs b/crates/nu-cli/src/commands/table.rs index ea8f3da027..9953dbda93 100644 --- a/crates/nu-cli/src/commands/table.rs +++ b/crates/nu-cli/src/commands/table.rs @@ -1,8 +1,9 @@ use crate::commands::WholeStreamCommand; -use crate::format::TableView; +use crate::data::value::{format_leaf, style_leaf}; use crate::prelude::*; use nu_errors::ShellError; use nu_protocol::{Primitive, Signature, SyntaxShape, UntaggedValue, Value}; +use nu_table::{draw_table, Alignment, StyledString, TextStyle, Theme}; use std::time::Instant; const STREAM_PAGE_SIZE: usize = 1000; @@ -38,12 +39,175 @@ impl WholeStreamCommand for Table { } } +fn str_to_color(s: String) -> Option { + match s.as_str() { + "g" | "green" => Some(ansi_term::Color::Green), + "r" | "red" => Some(ansi_term::Color::Red), + "u" | "blue" => Some(ansi_term::Color::Blue), + "b" | "black" => Some(ansi_term::Color::Black), + "y" | "yellow" => Some(ansi_term::Color::Yellow), + "p" | "purple" => Some(ansi_term::Color::Purple), + "c" | "cyan" => Some(ansi_term::Color::Cyan), + "w" | "white" => Some(ansi_term::Color::White), + _ => None, + } +} + +pub fn from_list(values: &[Value], starting_idx: usize) -> nu_table::Table { + let config = crate::data::config::config(Tag::unknown()); + + let header_style = if let Ok(config) = config { + let header_align = config.get("header_align").map_or(Alignment::Left, |a| { + a.as_string() + .map_or(Alignment::Center, |a| match a.to_lowercase().as_str() { + "center" | "c" => Alignment::Center, + "right" | "r" => Alignment::Right, + _ => Alignment::Center, + }) + }); + + let header_color = match config.get("header_color") { + Some(c) => match c.as_string() { + Ok(color) => str_to_color(color.to_lowercase()).unwrap_or(ansi_term::Color::Green), + _ => ansi_term::Color::Green, + }, + _ => ansi_term::Color::Green, + }; + + let header_bold = match config.get("header_bold") { + Some(b) => match b.as_bool() { + Ok(b) => b, + _ => true, + }, + _ => true, + }; + + TextStyle { + alignment: header_align, + color: Some(header_color), + is_bold: header_bold, + } + } else { + TextStyle::default_header() + }; + + let mut headers: Vec = nu_protocol::merge_descriptors(values) + .into_iter() + .map(|x| StyledString::new(x, header_style.clone())) + .collect(); + let entries = values_to_entries(values, &mut headers, starting_idx); + + nu_table::Table { + headers, + data: entries, + theme: Theme::compact(), + } +} + +fn are_table_indexes_disabled() -> bool { + let config = crate::data::config::config(Tag::unknown()); + match config { + Ok(config) => { + let disable_indexes = config.get("disable_table_indexes"); + disable_indexes.map_or(false, |x| x.as_bool().unwrap_or(false)) + } + _ => false, + } +} + +fn values_to_entries( + values: &[Value], + headers: &mut Vec, + starting_idx: usize, +) -> Vec> { + let disable_indexes = are_table_indexes_disabled(); + let mut entries = vec![]; + + if headers.is_empty() { + headers.push(StyledString::new("".to_string(), TextStyle::basic())); + } + + for (idx, value) in values.iter().enumerate() { + let mut row: Vec = headers + .iter() + .map(|d: &StyledString| { + if d.contents == "" { + match value { + Value { + value: UntaggedValue::Row(..), + .. + } => StyledString::new( + format_leaf(&UntaggedValue::nothing()).plain_string(100_000), + style_leaf(&UntaggedValue::nothing()), + ), + _ => StyledString::new( + format_leaf(value).plain_string(100_000), + style_leaf(value), + ), + } + } else { + match value { + Value { + value: UntaggedValue::Row(..), + .. + } => { + let data = value.get_data(&d.contents); + + StyledString::new( + format_leaf(data.borrow()).plain_string(100_000), + style_leaf(data.borrow()), + ) + } + _ => StyledString::new( + format_leaf(&UntaggedValue::nothing()).plain_string(100_000), + style_leaf(&UntaggedValue::nothing()), + ), + } + } + }) + .collect(); + + // Indices are green, bold, right-aligned: + if !disable_indexes { + row.insert( + 0, + StyledString::new( + (starting_idx + idx).to_string(), + TextStyle { + alignment: Alignment::Center, + color: Some(ansi_term::Color::Green), + is_bold: true, + }, + ), + ); + } + + entries.push(row); + } + + if !disable_indexes { + headers.insert( + 0, + StyledString::new( + "#".to_owned(), + TextStyle { + alignment: Alignment::Center, + color: Some(ansi_term::Color::Green), + is_bold: true, + }, + ), + ); + } + + entries +} + async fn table(args: CommandArgs, registry: &CommandRegistry) -> Result { let registry = registry.clone(); let mut args = args.evaluate_once(®istry).await?; let mut finished = false; - let host = args.host.clone(); + // let host = args.host.clone(); let mut start_number = match args.get("start_number") { Some(Value { value: UntaggedValue::Primitive(Primitive::Int(i)), @@ -64,6 +228,8 @@ async fn table(args: CommandArgs, registry: &CommandRegistry) -> Result = VecDeque::new(); @@ -113,12 +279,9 @@ async fn table(args: CommandArgs, registry: &CommandRegistry) -> Result = new_input.into(); if !input.is_empty() { - let mut host = host.lock(); - let view = TableView::from_list(&input, start_number); + let t = from_list(&input, start_number); - if let Some(view) = view { - handle_unexpected(&mut *host, |host| crate::format::print_view(&view, host)); - } + draw_table(&t, termwidth); } start_number += input.len(); @@ -126,15 +289,3 @@ async fn table(args: CommandArgs, registry: &CommandRegistry) -> Result>>, } diff --git a/crates/nu-cli/src/data/primitive.rs b/crates/nu-cli/src/data/primitive.rs index c352fa3e76..666bd95fd9 100644 --- a/crates/nu-cli/src/data/primitive.rs +++ b/crates/nu-cli/src/data/primitive.rs @@ -1,4 +1,5 @@ use nu_protocol::{hir::Number, Primitive}; +use nu_table::TextStyle; pub fn number(number: impl Into) -> Primitive { let number = number.into(); @@ -9,9 +10,9 @@ pub fn number(number: impl Into) -> Primitive { } } -pub fn style_primitive(primitive: &Primitive) -> &'static str { +pub fn style_primitive(primitive: &Primitive) -> TextStyle { match primitive { - Primitive::Int(_) | Primitive::Bytes(_) | Primitive::Decimal(_) => "r", - _ => "", + Primitive::Int(_) | Primitive::Bytes(_) | Primitive::Decimal(_) => TextStyle::basic_right(), + _ => TextStyle::basic(), } } diff --git a/crates/nu-cli/src/data/value.rs b/crates/nu-cli/src/data/value.rs index 78506c69ff..d4521bf92b 100644 --- a/crates/nu-cli/src/data/value.rs +++ b/crates/nu-cli/src/data/value.rs @@ -7,6 +7,7 @@ use nu_protocol::hir::Operator; use nu_protocol::ShellTypeName; use nu_protocol::{Primitive, Type, UntaggedValue}; use nu_source::{DebugDocBuilder, PrettyDebug, Tagged}; +use nu_table::TextStyle; pub fn date_from_str(s: Tagged<&str>) -> Result { let date = DateTime::parse_from_rfc3339(s.item).map_err(|err| { @@ -160,10 +161,10 @@ pub fn format_leaf<'a>(value: impl Into<&'a UntaggedValue>) -> DebugDocBuilder { InlineShape::from_value(value.into()).format().pretty() } -pub fn style_leaf<'a>(value: impl Into<&'a UntaggedValue>) -> &'static str { +pub fn style_leaf<'a>(value: impl Into<&'a UntaggedValue>) -> TextStyle { match value.into() { UntaggedValue::Primitive(p) => style_primitive(p), - _ => "", + _ => TextStyle::basic(), } } diff --git a/crates/nu-cli/src/env/host.rs b/crates/nu-cli/src/env/host.rs index e6095f6ae7..6a6cce3829 100644 --- a/crates/nu-cli/src/env/host.rs +++ b/crates/nu-cli/src/env/host.rs @@ -1,7 +1,7 @@ use crate::prelude::*; #[cfg(test)] use indexmap::IndexMap; -use nu_errors::ShellError; +// use nu_errors::ShellError; use std::ffi::OsString; use std::fmt::Debug; @@ -200,13 +200,13 @@ impl Host for FakeHost { } } -pub(crate) fn handle_unexpected( - host: &mut dyn Host, - func: impl FnOnce(&mut dyn Host) -> Result, -) { - let result = func(host); +// pub(crate) fn handle_unexpected( +// host: &mut dyn Host, +// func: impl FnOnce(&mut dyn Host) -> Result, +// ) { +// let result = func(host); - if let Err(err) = result { - host.stderr(&format!("Something unexpected happened:\n{:?}", err)); - } -} +// if let Err(err) = result { +// host.stderr(&format!("Something unexpected happened:\n{:?}", err)); +// } +// } diff --git a/crates/nu-cli/src/format.rs b/crates/nu-cli/src/format.rs index 6d7710a146..a4e62f534c 100644 --- a/crates/nu-cli/src/format.rs +++ b/crates/nu-cli/src/format.rs @@ -1,14 +1,6 @@ -pub(crate) mod table; - use crate::prelude::*; use nu_errors::ShellError; -pub(crate) use table::TableView; - pub(crate) trait RenderView { fn render_view(&self, host: &mut dyn Host) -> Result<(), ShellError>; } - -pub(crate) fn print_view(view: &impl RenderView, host: &mut dyn Host) -> Result<(), ShellError> { - view.render_view(host) -} diff --git a/crates/nu-cli/src/format/table.rs b/crates/nu-cli/src/format/table.rs deleted file mode 100644 index 22b6b03739..0000000000 --- a/crates/nu-cli/src/format/table.rs +++ /dev/null @@ -1,453 +0,0 @@ -use crate::data::value::{format_leaf, style_leaf}; -use crate::format::RenderView; -use crate::prelude::*; -use derive_new::new; -use nu_errors::ShellError; -use nu_protocol::{UntaggedValue, Value}; -use textwrap::fill; - -use prettytable::format::{Alignment, FormatBuilder, LinePosition, LineSeparator}; -use prettytable::{color, Attr, Cell, Row, Table}; - -type Entries = Vec>; - -#[derive(Debug, new)] -pub struct TableView { - // List of header cell values: - headers: Vec, - - // List of rows of cells, each containing value and prettytable style-string: - entries: Entries, -} - -enum TableMode { - Light, - Normal, -} - -impl TableView { - pub fn from_list(values: &[Value], starting_idx: usize) -> Option { - if values.is_empty() { - return None; - } - - // Different platforms want different amounts of buffer, not sure why - let termwidth = std::cmp::max(textwrap::termwidth(), 20); - - let mut headers = nu_protocol::merge_descriptors(values); - let mut entries = values_to_entries(values, &mut headers, starting_idx); - let max_per_column = max_per_column(&headers, &entries, values.len()); - - maybe_truncate_columns(&mut headers, &mut entries, termwidth); - let headers_len = headers.len(); - - // Measure how big our columns need to be (accounting for separators also) - let max_naive_column_width = (termwidth - 3 * (headers_len - 1)) / headers_len; - - let column_space = - ColumnSpace::measure(&max_per_column, max_naive_column_width, headers_len); - - // This gives us the max column width - let max_column_width = column_space.max_width(termwidth); - - // This width isn't quite right, as we're rounding off some of our space - let column_space = column_space.fix_almost_column_width( - &max_per_column, - max_naive_column_width, - max_column_width, - headers_len, - ); - - // This should give us the final max column width - let max_column_width = column_space.max_width(termwidth); - - // Wrap cells as needed - let table_view = wrap_cells( - headers, - entries, - max_per_column, - max_naive_column_width, - max_column_width, - ); - Some(table_view) - } -} - -fn are_table_indexes_disabled() -> bool { - let config = crate::data::config::config(Tag::unknown()); - match config { - Ok(config) => { - let disable_indexes = config.get("disable_table_indexes"); - disable_indexes.map_or(false, |x| x.as_bool().unwrap_or(false)) - } - _ => false, - } -} - -fn values_to_entries(values: &[Value], headers: &mut Vec, starting_idx: usize) -> Entries { - let disable_indexes = are_table_indexes_disabled(); - let mut entries = vec![]; - - if headers.is_empty() { - headers.push("".to_string()); - } - - for (idx, value) in values.iter().enumerate() { - let mut row: Vec<(String, &'static str)> = headers - .iter() - .map(|d: &String| { - if d == "" { - match value { - Value { - value: UntaggedValue::Row(..), - .. - } => ( - format_leaf(&UntaggedValue::nothing()).plain_string(100_000), - style_leaf(&UntaggedValue::nothing()), - ), - _ => (format_leaf(value).plain_string(100_000), style_leaf(value)), - } - } else { - match value { - Value { - value: UntaggedValue::Row(..), - .. - } => { - let data = value.get_data(d); - ( - format_leaf(data.borrow()).plain_string(100_000), - style_leaf(data.borrow()), - ) - } - _ => ( - format_leaf(&UntaggedValue::nothing()).plain_string(100_000), - style_leaf(&UntaggedValue::nothing()), - ), - } - } - }) - .collect(); - - // Indices are green, bold, right-aligned: - if !disable_indexes { - row.insert(0, ((starting_idx + idx).to_string(), "Fgbr")); - } - - entries.push(row); - } - - if !disable_indexes { - headers.insert(0, "#".to_owned()); - } - - entries -} - -#[allow(clippy::ptr_arg)] -fn max_per_column(headers: &[String], entries: &Entries, values_len: usize) -> Vec { - let mut max_per_column = vec![]; - - for i in 0..headers.len() { - let mut current_col_max = 0; - let iter = entries.iter().take(values_len); - - for entry in iter { - let value_length = entry[i].0.chars().count(); - if value_length > current_col_max { - current_col_max = value_length; - } - } - - max_per_column.push(std::cmp::max(current_col_max, headers[i].chars().count())); - } - - max_per_column -} - -fn maybe_truncate_columns(headers: &mut Vec, entries: &mut Entries, termwidth: usize) { - // Make sure we have enough space for the columns we have - let max_num_of_columns = termwidth / 10; - - // If we have too many columns, truncate the table - if max_num_of_columns < headers.len() { - headers.truncate(max_num_of_columns); - - for entry in entries.iter_mut() { - entry.truncate(max_num_of_columns); - } - - headers.push("...".to_owned()); - - for entry in entries.iter_mut() { - entry.push(("...".to_owned(), "c")); // ellipsis is centred - } - } -} - -struct ColumnSpace { - num_overages: usize, - underage_sum: usize, - overage_separator_sum: usize, -} - -impl ColumnSpace { - /// Measure how much space we have once we subtract off the columns who are small enough - fn measure( - max_per_column: &[usize], - max_naive_column_width: usize, - headers_len: usize, - ) -> ColumnSpace { - let mut num_overages = 0; - let mut underage_sum = 0; - let mut overage_separator_sum = 0; - let iter = max_per_column.iter().enumerate().take(headers_len); - - for (i, &column_max) in iter { - if column_max > max_naive_column_width { - num_overages += 1; - if i != (headers_len - 1) { - overage_separator_sum += 3; - } - if i == 0 { - overage_separator_sum += 1; - } - } else { - underage_sum += column_max; - // if column isn't last, add 3 for its separator - if i != (headers_len - 1) { - underage_sum += 3; - } - if i == 0 { - underage_sum += 1; - } - } - } - - ColumnSpace { - num_overages, - underage_sum, - overage_separator_sum, - } - } - - fn fix_almost_column_width( - self, - max_per_column: &[usize], - max_naive_column_width: usize, - max_column_width: usize, - headers_len: usize, - ) -> ColumnSpace { - let mut num_overages = 0; - let mut overage_separator_sum = 0; - let mut underage_sum = self.underage_sum; - let iter = max_per_column.iter().enumerate().take(headers_len); - - for (i, &column_max) in iter { - if column_max > max_naive_column_width { - if column_max <= max_column_width { - underage_sum += column_max; - // if column isn't last, add 3 for its separator - if i != (headers_len - 1) { - underage_sum += 3; - } - if i == 0 { - underage_sum += 1; - } - } else { - // Column is still too large, so let's count it - num_overages += 1; - if i != (headers_len - 1) { - overage_separator_sum += 3; - } - if i == 0 { - overage_separator_sum += 1; - } - } - } - } - - ColumnSpace { - num_overages, - underage_sum, - overage_separator_sum, - } - } - - fn max_width(&self, termwidth: usize) -> usize { - let ColumnSpace { - num_overages, - underage_sum, - overage_separator_sum, - } = self; - - if *num_overages > 0 { - (termwidth - 1 - *underage_sum - *overage_separator_sum) / *num_overages - } else { - 99999 - } - } -} - -fn wrap_cells( - mut headers: Vec, - mut entries: Entries, - max_per_column: Vec, - max_naive_column_width: usize, - max_column_width: usize, -) -> TableView { - for head in 0..headers.len() { - if max_per_column[head] > max_naive_column_width { - headers[head] = fill(&headers[head], max_column_width); - - for entry in entries.iter_mut() { - entry[head].0 = fill(&entry[head].0, max_column_width); - } - } - } - - TableView { headers, entries } -} - -impl RenderView for TableView { - fn render_view(&self, host: &mut dyn Host) -> Result<(), ShellError> { - if self.entries.is_empty() { - return Ok(()); - } - - let mut table = Table::new(); - - let mut config = crate::data::config::config(Tag::unknown())?; - let header_align = config.get("header_align").map_or(Alignment::LEFT, |a| { - a.as_string() - .map_or(Alignment::LEFT, |a| match a.to_lowercase().as_str() { - "center" | "c" => Alignment::CENTER, - "right" | "r" => Alignment::RIGHT, - _ => Alignment::LEFT, - }) - }); - - let header_color = config.get("header_color").map_or(color::GREEN, |c| { - c.as_string().map_or(color::GREEN, |c| { - str_to_color(c.to_lowercase()).unwrap_or(color::GREEN) - }) - }); - - let header_style = - config - .remove("header_style") - .map_or(vec![Attr::Bold], |y| match y.value { - UntaggedValue::Table(t) => to_style_vec(t), - UntaggedValue::Primitive(p) => vec![p - .into_string(Span::unknown()) - .map_or(Attr::Bold, |s| str_to_style(s).unwrap_or(Attr::Bold))], - _ => vec![Attr::Bold], - }); - - let table_mode = if let Some(s) = config.get("table_mode") { - match s.as_string() { - Ok(typ) if typ == "light" => TableMode::Light, - _ => TableMode::Normal, - } - } else { - TableMode::Normal - }; - - match table_mode { - TableMode::Light => { - table.set_format( - FormatBuilder::new() - .separator(LinePosition::Title, LineSeparator::new('─', '─', ' ', ' ')) - .separator(LinePosition::Bottom, LineSeparator::new(' ', ' ', ' ', ' ')) - .padding(1, 1) - .build(), - ); - } - _ => { - table.set_format( - FormatBuilder::new() - .column_separator('│') - .separator(LinePosition::Top, LineSeparator::new('─', '┬', ' ', ' ')) - .separator(LinePosition::Title, LineSeparator::new('─', '┼', ' ', ' ')) - .separator(LinePosition::Bottom, LineSeparator::new('─', '┴', ' ', ' ')) - .padding(1, 1) - .build(), - ); - } - } - - let skip_headers = (self.headers.len() == 2 && self.headers[1] == "") - || (self.headers.len() == 1 && self.headers[0] == ""); - - let header: Vec = self - .headers - .iter() - .map(|h| { - let mut c = Cell::new_align(h, header_align) - .with_style(Attr::ForegroundColor(header_color)); - for &s in &header_style { - c.style(s); - } - c - }) - .collect(); - - if !skip_headers { - table.set_titles(Row::new(header)); - } - - for row in &self.entries { - table.add_row(Row::new( - row.iter() - .map(|(v, s)| Cell::new(v).style_spec(s)) - .collect(), - )); - } - - table.print_term(&mut *host.out_terminal().ok_or_else(|| ShellError::untagged_runtime_error("Could not open terminal for output"))?) - .map_err(|_| ShellError::untagged_runtime_error("Internal error: could not print to terminal (for unix systems check to make sure TERM is set)"))?; - - Ok(()) - } -} - -fn str_to_color(s: String) -> Option { - match s.as_str() { - "g" | "green" => Some(color::GREEN), - "r" | "red" => Some(color::RED), - "u" | "blue" => Some(color::BLUE), - "b" | "black" => Some(color::BLACK), - "y" | "yellow" => Some(color::YELLOW), - "m" | "magenta" => Some(color::MAGENTA), - "c" | "cyan" => Some(color::CYAN), - "w" | "white" => Some(color::WHITE), - "bg" | "bright green" => Some(color::BRIGHT_GREEN), - "br" | "bright red" => Some(color::BRIGHT_RED), - "bu" | "bright blue" => Some(color::BRIGHT_BLUE), - "by" | "bright yellow" => Some(color::BRIGHT_YELLOW), - "bm" | "bright magenta" => Some(color::BRIGHT_MAGENTA), - "bc" | "bright cyan" => Some(color::BRIGHT_CYAN), - "bw" | "bright white" => Some(color::BRIGHT_WHITE), - _ => None, - } -} - -fn to_style_vec(a: Vec) -> Vec { - let mut v: Vec = Vec::new(); - for t in a { - if let Ok(s) = t.as_string() { - if let Some(r) = str_to_style(s) { - v.push(r); - } - } - } - v -} - -fn str_to_style(s: String) -> Option { - match s.as_str() { - "b" | "bold" => Some(Attr::Bold), - "i" | "italic" | "italics" => Some(Attr::Italic(true)), - "u" | "underline" | "underlined" => Some(Attr::Underline(true)), - _ => None, - } -} diff --git a/crates/nu-cli/src/lib.rs b/crates/nu-cli/src/lib.rs index 0cb86aa508..a7bb6c7027 100644 --- a/crates/nu-cli/src/lib.rs +++ b/crates/nu-cli/src/lib.rs @@ -32,7 +32,8 @@ pub mod utils; mod examples; pub use crate::cli::{ - cli, create_default_context, load_plugins, run_pipeline_standalone, run_vec_of_pipelines, + cli, create_default_context, load_plugins, process_line, run_pipeline_standalone, + run_vec_of_pipelines, LineResult, }; pub use crate::commands::command::{ whole_stream_command, CommandArgs, EvaluatedWholeStreamCommandArgs, WholeStreamCommand, diff --git a/crates/nu-cli/src/prelude.rs b/crates/nu-cli/src/prelude.rs index e8df6041a1..ce58148b6e 100644 --- a/crates/nu-cli/src/prelude.rs +++ b/crates/nu-cli/src/prelude.rs @@ -77,7 +77,7 @@ pub(crate) use crate::context::CommandRegistry; pub(crate) use crate::context::Context; pub(crate) use crate::data::config; pub(crate) use crate::data::value; -pub(crate) use crate::env::host::handle_unexpected; +// pub(crate) use crate::env::host::handle_unexpected; pub(crate) use crate::env::Host; pub(crate) use crate::shell::filesystem_shell::FilesystemShell; pub(crate) use crate::shell::help_shell::HelpShell; diff --git a/crates/nu-table/Cargo.toml b/crates/nu-table/Cargo.toml new file mode 100644 index 0000000000..f3e25392bd --- /dev/null +++ b/crates/nu-table/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "nu-table" +version = "0.15.1" +authors = ["The Nu Project Contributors"] +edition = "2018" +description = "Nushell table printing" +license = "MIT" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[[bin]] +name = "table" +path = "src/main.rs" + +[dependencies] +unicode-width = "0.1.7" +ansi_term = "0.12.1" \ No newline at end of file diff --git a/crates/nu-table/src/lib.rs b/crates/nu-table/src/lib.rs new file mode 100644 index 0000000000..661d7ddde7 --- /dev/null +++ b/crates/nu-table/src/lib.rs @@ -0,0 +1,5 @@ +mod table; +mod wrap; + +pub use table::{draw_table, StyledString, Table, TextStyle, Theme}; +pub use wrap::Alignment; diff --git a/crates/nu-table/src/main.rs b/crates/nu-table/src/main.rs new file mode 100644 index 0000000000..f426a9d02d --- /dev/null +++ b/crates/nu-table/src/main.rs @@ -0,0 +1,23 @@ +use nu_table::{draw_table, StyledString, Table, TextStyle, Theme}; + +fn main() { + let args: Vec<_> = std::env::args().collect(); + + let width = args[1].parse::().expect("Need a width in columns"); + let msg = args[2..] + .iter() + .map(|x| StyledString::new(x.to_owned(), TextStyle::basic())) + .collect(); + + let t = Table::new( + vec![ + StyledString::new("Test me".to_owned(), TextStyle::default_header()), + StyledString::new("Long column name".to_owned(), TextStyle::default_header()), + StyledString::new("Another".to_owned(), TextStyle::default_header()), + ], + vec![msg; 2], + Theme::compact(), + ); + + draw_table(&t, width); +} diff --git a/crates/nu-table/src/table.rs b/crates/nu-table/src/table.rs new file mode 100644 index 0000000000..7ad91e9085 --- /dev/null +++ b/crates/nu-table/src/table.rs @@ -0,0 +1,673 @@ +use crate::wrap::{column_width, split_sublines, wrap, Alignment, Subline, WrappedCell}; + +enum SeparatorPosition { + Top, + Middle, + Bottom, +} + +#[derive(Debug)] +pub struct Table { + pub headers: Vec, + pub data: Vec>, + pub theme: Theme, +} + +#[derive(Debug, Clone)] +pub struct StyledString { + pub contents: String, + pub style: TextStyle, +} + +impl StyledString { + pub fn new(contents: String, style: TextStyle) -> StyledString { + StyledString { contents, style } + } +} + +#[derive(Debug, Clone)] +pub struct TextStyle { + pub is_bold: bool, + pub alignment: Alignment, + pub color: Option, +} + +impl TextStyle { + pub fn basic() -> TextStyle { + TextStyle { + is_bold: false, + alignment: Alignment::Left, + color: None, + } + } + + pub fn basic_right() -> TextStyle { + TextStyle { + is_bold: false, + alignment: Alignment::Right, + color: None, + } + } + + pub fn default_header() -> TextStyle { + TextStyle { + is_bold: true, + alignment: Alignment::Center, + color: Some(ansi_term::Colour::Green), + } + } +} + +#[derive(Debug, Clone)] +pub struct Theme { + pub top_left: char, + pub middle_left: char, + pub bottom_left: char, + pub top_center: char, + pub center: char, + pub bottom_center: char, + pub top_right: char, + pub middle_right: char, + pub bottom_right: char, + pub top_horizontal: char, + pub middle_horizontal: char, + pub bottom_horizontal: char, + pub left_vertical: char, + pub center_vertical: char, + pub right_vertical: char, + + pub separate_header: bool, + pub separate_rows: bool, + + pub print_left_border: bool, + pub print_right_border: bool, +} + +impl Theme { + #[allow(unused)] + pub fn basic() -> Theme { + Theme { + top_left: '+', + middle_left: '+', + bottom_left: '+', + top_center: '+', + center: '+', + bottom_center: '+', + top_right: '+', + middle_right: '+', + bottom_right: '+', + top_horizontal: '-', + middle_horizontal: '-', + bottom_horizontal: '-', + left_vertical: '|', + center_vertical: '|', + right_vertical: '|', + + separate_header: true, + separate_rows: true, + + print_left_border: true, + print_right_border: true, + } + } + #[allow(unused)] + pub fn thin() -> Theme { + Theme { + top_left: '┌', + middle_left: '├', + bottom_left: '└', + top_center: '┬', + center: '┼', + bottom_center: '┴', + top_right: '┐', + middle_right: '┤', + bottom_right: '┘', + + top_horizontal: '─', + middle_horizontal: '─', + bottom_horizontal: '─', + + left_vertical: '│', + center_vertical: '│', + right_vertical: '│', + + separate_header: true, + separate_rows: true, + + print_left_border: true, + print_right_border: true, + } + } + #[allow(unused)] + pub fn light() -> Theme { + Theme { + top_left: ' ', + middle_left: '─', + bottom_left: ' ', + top_center: ' ', + center: '─', + bottom_center: ' ', + top_right: ' ', + middle_right: '─', + bottom_right: ' ', + + top_horizontal: ' ', + middle_horizontal: '─', + bottom_horizontal: ' ', + + left_vertical: ' ', + center_vertical: ' ', + right_vertical: ' ', + + separate_header: true, + separate_rows: true, + + print_left_border: true, + print_right_border: true, + } + } + #[allow(unused)] + pub fn compact() -> Theme { + Theme { + top_left: '─', + middle_left: '─', + bottom_left: '─', + top_center: '┬', + center: '┼', + bottom_center: '┴', + top_right: '─', + middle_right: '─', + bottom_right: '─', + top_horizontal: '─', + middle_horizontal: '─', + bottom_horizontal: '─', + + left_vertical: ' ', + center_vertical: '│', + right_vertical: ' ', + + separate_header: true, + separate_rows: false, + + print_left_border: false, + print_right_border: false, + } + } +} + +impl Table { + pub fn new(headers: Vec, data: Vec>, theme: Theme) -> Table { + Table { + headers, + data, + theme, + } + } +} + +#[derive(Debug)] +pub struct ProcessedTable<'a> { + pub headers: Vec>, + pub data: Vec>>, + pub theme: Theme, +} + +#[derive(Debug)] +pub struct ProcessedCell<'a> { + pub contents: Vec>, + pub style: TextStyle, +} + +#[derive(Debug)] +pub struct WrappedTable { + pub column_widths: Vec, + pub headers: Vec, + pub data: Vec>, + pub theme: Theme, +} + +impl WrappedTable { + //TODO: optimize this + fn print_separator(&self, separator_position: SeparatorPosition) { + let column_count = self.column_widths.len(); + + match separator_position { + SeparatorPosition::Top => { + for column in self.column_widths.iter().enumerate() { + if column.0 == 0 && self.theme.print_left_border { + print!("{}", self.theme.top_left); + } + print!( + "{}", + std::iter::repeat(self.theme.top_horizontal) + .take(*column.1) + .collect::() + ); + + print!("{}{}", self.theme.top_horizontal, self.theme.top_horizontal); + if column.0 == column_count - 1 { + if self.theme.print_right_border { + print!("{}", self.theme.top_right); + } + } else { + print!("{}", self.theme.top_center); + } + } + } + SeparatorPosition::Middle => { + for column in self.column_widths.iter().enumerate() { + if column.0 == 0 && self.theme.print_left_border { + print!("{}", self.theme.middle_left); + } + print!( + "{}", + std::iter::repeat(self.theme.middle_horizontal) + .take(*column.1) + .collect::() + ); + + print!( + "{}{}", + self.theme.middle_horizontal, self.theme.middle_horizontal + ); + if column.0 == column_count - 1 { + if self.theme.print_right_border { + print!("{}", self.theme.middle_right); + } + } else { + print!("{}", self.theme.center); + } + } + } + SeparatorPosition::Bottom => { + for column in self.column_widths.iter().enumerate() { + if column.0 == 0 && self.theme.print_left_border { + print!("{}", self.theme.bottom_left); + } + print!( + "{}", + std::iter::repeat(self.theme.bottom_horizontal) + .take(*column.1) + .collect::() + ); + + print!( + "{}{}", + self.theme.bottom_horizontal, self.theme.bottom_horizontal + ); + if column.0 == column_count - 1 { + if self.theme.print_right_border { + print!("{}", self.theme.bottom_right); + } + } else { + print!("{}", self.theme.bottom_center); + } + } + } + } + } + + fn print_cell_contents(&self, cells: &[WrappedCell]) { + for current_line in 0.. { + let mut lines_printed = 0; + + let mut output = if self.theme.print_left_border { + self.theme.left_vertical.to_string() + } else { + String::new() + }; + for column in cells.iter().enumerate() { + if let Some(line) = (column.1).lines.get(current_line) { + let remainder = self.column_widths[column.0] - line.width; + output.push(' '); + + match column.1.style.alignment { + Alignment::Left => { + if let Some(color) = column.1.style.color { + let color = if column.1.style.is_bold { + color.bold() + } else { + color.normal() + }; + + output.push_str(&color.paint(&line.line).to_string()); + } else { + output.push_str(&line.line); + } + for _ in 0..remainder { + output.push(' '); + } + } + Alignment::Center => { + for _ in 0..remainder / 2 { + output.push(' '); + } + if let Some(color) = column.1.style.color { + let color = if column.1.style.is_bold { + color.bold() + } else { + color.normal() + }; + + output.push_str(&color.paint(&line.line).to_string()); + } else { + output.push_str(&line.line); + } + for _ in 0..(remainder / 2 + remainder % 2) { + output.push(' '); + } + } + Alignment::Right => { + for _ in 0..remainder { + output.push(' '); + } + if let Some(color) = column.1.style.color { + let color = if column.1.style.is_bold { + color.bold() + } else { + color.normal() + }; + + output.push_str(&color.paint(&line.line).to_string()); + } else { + output.push_str(&line.line); + } + } + } + output.push(' '); + lines_printed += 1; + } else { + for _ in 0..self.column_widths[column.0] + 2 { + output.push(' '); + } + } + if column.0 < cells.len() - 1 { + output.push(self.theme.center_vertical); + } else if self.theme.print_right_border { + output.push(self.theme.right_vertical); + } + } + if lines_printed == 0 { + break; + } else { + println!("{}", output); + } + } + } + fn new_print_table(&self) { + if self.data.is_empty() { + return; + } + + self.print_separator(SeparatorPosition::Top); + println!(); + + self.print_cell_contents(&self.headers); + + let mut first_row = true; + + for row in &self.data { + if !first_row { + if self.theme.separate_rows { + self.print_separator(SeparatorPosition::Middle); + println!(); + } + } else { + first_row = false; + + if self.theme.separate_header { + self.print_separator(SeparatorPosition::Middle); + println!(); + } + } + + self.print_cell_contents(row); + } + self.print_separator(SeparatorPosition::Bottom); + println!(); + } +} + +fn process_table(table: &Table) -> ProcessedTable { + let mut processed_data = vec![]; + for row in &table.data { + let mut out_row = vec![]; + for column in row { + out_row.push(ProcessedCell { + contents: split_sublines(&column.contents).collect::>(), + style: column.style.clone(), + }); + } + processed_data.push(out_row); + } + + let mut processed_headers = vec![]; + for header in &table.headers { + processed_headers.push(ProcessedCell { + contents: split_sublines(&header.contents).collect::>(), + style: header.style.clone(), + }); + } + + ProcessedTable { + headers: processed_headers, + data: processed_data, + theme: table.theme.clone(), + } +} + +fn get_max_column_widths(processed_table: &ProcessedTable) -> Vec { + use std::cmp::max; + + let mut max_num_columns = 0; + + max_num_columns = max(max_num_columns, processed_table.headers.len()); + + for row in &processed_table.data { + max_num_columns = max(max_num_columns, row.len()); + } + + let mut output = vec![0; max_num_columns]; + + for column in processed_table.headers.iter().enumerate() { + output[column.0] = max(output[column.0], column_width(column.1.contents.iter())); + } + + for row in &processed_table.data { + for column in row.iter().enumerate() { + output[column.0] = max(output[column.0], column_width(column.1.contents.iter())); + } + } + + output +} + +pub fn draw_table(table: &Table, termwidth: usize) { + // Remove the edges, if used + let termwidth = if table.theme.print_left_border && table.theme.print_right_border { + termwidth - 2 + } else if table.theme.print_left_border || table.theme.print_right_border { + termwidth - 1 + } else { + termwidth + }; + + let processed_table = process_table(table); + + let max_per_column = get_max_column_widths(&processed_table); + + // maybe_truncate_columns(&mut headers, &mut entries, termwidth); + let headers_len = table.headers.len(); + + // Measure how big our columns need to be (accounting for separators also) + let max_naive_column_width = (termwidth - 3 * (headers_len - 1)) / headers_len; + + let column_space = ColumnSpace::measure(&max_per_column, max_naive_column_width, headers_len); + + // This gives us the max column width + let max_column_width = column_space.max_width(termwidth); + + // This width isn't quite right, as we're rounding off some of our space + let column_space = column_space.fix_almost_column_width( + &max_per_column, + max_naive_column_width, + max_column_width, + headers_len, + ); + + // This should give us the final max column width + let max_column_width = column_space.max_width(termwidth); + + let wrapped_table = wrap_cells( + processed_table, + // max_per_column, + // max_naive_column_width, + max_column_width, + ); + + wrapped_table.new_print_table(); +} + +fn wrap_cells(processed_table: ProcessedTable, max_column_width: usize) -> WrappedTable { + let mut column_widths = vec![0; processed_table.headers.len()]; + let mut output_headers = vec![]; + for header in processed_table.headers.into_iter().enumerate() { + let wrapped = wrap( + max_column_width, + header.1.contents.into_iter(), + header.1.style, + ); + if column_widths[header.0] < wrapped.max_width { + column_widths[header.0] = wrapped.max_width; + } + output_headers.push(wrapped); + } + + let mut output_data = vec![]; + for row in processed_table.data.into_iter() { + let mut output_row = vec![]; + for column in row.into_iter().enumerate() { + let wrapped = wrap( + max_column_width, + column.1.contents.into_iter(), + column.1.style, + ); + if column_widths[column.0] < wrapped.max_width { + column_widths[column.0] = wrapped.max_width; + } + output_row.push(wrapped); + } + output_data.push(output_row); + } + + WrappedTable { + column_widths, + headers: output_headers, + data: output_data, + theme: processed_table.theme, + } +} + +struct ColumnSpace { + num_overages: usize, + underage_sum: usize, + overage_separator_sum: usize, +} + +impl ColumnSpace { + /// Measure how much space we have once we subtract off the columns who are small enough + fn measure( + max_per_column: &[usize], + max_naive_column_width: usize, + headers_len: usize, + ) -> ColumnSpace { + let mut num_overages = 0; + let mut underage_sum = 0; + let mut overage_separator_sum = 0; + let iter = max_per_column.iter().enumerate().take(headers_len); + + for (i, &column_max) in iter { + if column_max > max_naive_column_width { + num_overages += 1; + if i != (headers_len - 1) { + overage_separator_sum += 3; + } + if i == 0 { + overage_separator_sum += 1; + } + } else { + underage_sum += column_max; + // if column isn't last, add 3 for its separator + if i != (headers_len - 1) { + underage_sum += 3; + } + if i == 0 { + underage_sum += 1; + } + } + } + + ColumnSpace { + num_overages, + underage_sum, + overage_separator_sum, + } + } + + fn fix_almost_column_width( + self, + max_per_column: &[usize], + max_naive_column_width: usize, + max_column_width: usize, + headers_len: usize, + ) -> ColumnSpace { + let mut num_overages = 0; + let mut overage_separator_sum = 0; + let mut underage_sum = self.underage_sum; + let iter = max_per_column.iter().enumerate().take(headers_len); + + for (i, &column_max) in iter { + if column_max > max_naive_column_width { + if column_max <= max_column_width { + underage_sum += column_max; + // if column isn't last, add 3 for its separator + if i != (headers_len - 1) { + underage_sum += 3; + } + if i == 0 { + underage_sum += 1; + } + } else { + // Column is still too large, so let's count it + num_overages += 1; + if i != (headers_len - 1) { + overage_separator_sum += 3; + } + if i == 0 { + overage_separator_sum += 1; + } + } + } + } + + ColumnSpace { + num_overages, + underage_sum, + overage_separator_sum, + } + } + + fn max_width(&self, termwidth: usize) -> usize { + let ColumnSpace { + num_overages, + underage_sum, + overage_separator_sum, + } = self; + + if *num_overages > 0 { + (termwidth - 1 - *underage_sum - *overage_separator_sum) / *num_overages + } else { + 99999 + } + } +} diff --git a/crates/nu-table/src/wrap.rs b/crates/nu-table/src/wrap.rs new file mode 100644 index 0000000000..bc86eb9f3c --- /dev/null +++ b/crates/nu-table/src/wrap.rs @@ -0,0 +1,226 @@ +use crate::table::TextStyle; +use std::{fmt::Display, iter::Iterator}; +use unicode_width::UnicodeWidthStr; + +#[derive(Debug, Clone)] +pub enum Alignment { + Left, + Center, + Right, +} + +#[derive(Debug)] +pub struct Subline<'a> { + pub subline: &'a str, + pub width: usize, +} + +#[derive(Debug)] +pub struct Line<'a> { + pub sublines: Vec>, + pub width: usize, +} + +#[derive(Debug)] +pub struct WrappedLine { + pub line: String, + pub width: usize, +} + +#[derive(Debug)] +pub struct WrappedCell { + pub lines: Vec, + pub max_width: usize, + + pub style: TextStyle, +} + +impl<'a> Display for Line<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut first = true; + for subline in &self.sublines { + if !first { + write!(f, " ")?; + } else { + first = false; + } + write!(f, "{}", subline.subline)?; + } + Ok(()) + } +} + +pub fn split_sublines(input: &str) -> impl Iterator { + input.split_terminator(' ').map(|x| Subline { + subline: x, + width: UnicodeWidthStr::width(x), + }) +} + +pub fn column_width<'a>(input: impl Iterator>) -> usize { + let mut total = 0; + + let mut first = true; + for inp in input { + if !first { + // Account for the space + total += 1; + } else { + first = false; + } + + total += inp.width; + } + + total +} + +fn split_word<'a>(cell_width: usize, word: &'a str) -> Vec> { + use unicode_width::UnicodeWidthChar; + + let mut output = vec![]; + let mut current_width = 0; + let mut start_index = 0; + let mut end_index; + + for c in word.char_indices() { + if let Some(width) = c.1.width() { + end_index = c.0; + if current_width + width > cell_width { + output.push(Subline { + subline: &word[start_index..end_index], + width: current_width, + }); + + start_index = c.0; + current_width = width; + } else { + current_width += width; + } + } + } + + if start_index != word.len() { + output.push(Subline { + subline: &word[start_index..], + width: current_width, + }); + } + + output +} + +pub fn wrap<'a>( + cell_width: usize, + mut input: impl Iterator>, + style: TextStyle, +) -> WrappedCell { + let mut lines = vec![]; + let mut current_line: Vec = vec![]; + let mut current_width = 0; + let mut first = true; + let mut max_width = 0; + loop { + // println!("{:?}", current_line); + match input.next() { + Some(item) => { + if !first { + current_width += 1; + } else { + first = false; + } + + if item.width + current_width > cell_width { + // If this is a really long single word, we need to split the word + if current_line.len() == 1 && current_width > cell_width { + max_width = cell_width; + let sublines = split_word(cell_width, ¤t_line[0].subline); + for subline in sublines { + let width = subline.width; + lines.push(Line { + sublines: vec![subline], + width, + }); + } + + first = true; + + current_width = item.width; + current_line = vec![item]; + } else { + if !current_line.is_empty() { + lines.push(Line { + sublines: current_line, + width: current_width, + }); + } + + first = true; + + current_width = item.width; + current_line = vec![item]; + max_width = std::cmp::max(max_width, current_width); + } + } else { + current_width += item.width; + current_line.push(item); + } + } + None => { + if current_width > cell_width { + // We need to break up the last word + let sublines = split_word(cell_width, ¤t_line[0].subline); + for subline in sublines { + let width = subline.width; + lines.push(Line { + sublines: vec![subline], + width, + }); + } + } else if current_width > 0 { + lines.push(Line { + sublines: current_line, + width: current_width, + }); + } + break; + } + } + } + + let mut current_max = 0; + let mut output = vec![]; + + for line in lines { + let mut current_line_width = 0; + let mut first = true; + let mut current_line = String::new(); + + for subline in line.sublines { + if !first { + current_line_width += 1 + subline.width; + current_line.push(' '); + current_line.push_str(subline.subline); + } else { + first = false; + current_line_width = subline.width; + current_line.push_str(subline.subline); + } + } + + if current_line_width > current_max { + current_max = current_line_width; + } + + output.push(WrappedLine { + line: current_line, + width: current_line_width, + }); + } + + WrappedCell { + lines: output, + max_width: current_max, + style, + } +} From b0c30098e423fcfdbdcb1f5f987fe2a1b8f76ef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20N=2E=20Robalino?= Date: Fri, 19 Jun 2020 23:34:36 -0500 Subject: [PATCH 18/27] Sort primitives explictly. (#2016) * Sort primitives explictly. * Write backing up test. --- crates/nu-cli/src/commands/sort_by.rs | 3 ++- crates/nu-cli/tests/commands/math/median.rs | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/crates/nu-cli/src/commands/sort_by.rs b/crates/nu-cli/src/commands/sort_by.rs index bf273d1152..c01c003767 100644 --- a/crates/nu-cli/src/commands/sort_by.rs +++ b/crates/nu-cli/src/commands/sort_by.rs @@ -1,4 +1,5 @@ use crate::commands::WholeStreamCommand; +use crate::data::base::coerce_compare; use crate::prelude::*; use nu_errors::ShellError; use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value}; @@ -112,7 +113,7 @@ pub fn sort( value: UntaggedValue::Primitive(_), .. } => { - vec.sort(); + vec.sort_by(|a, b| coerce_compare(a, b).expect("Unimplemented BUG: What about primitives that don't have an order defined?").compare()); } _ => { let calc_key = |item: &Value| { diff --git a/crates/nu-cli/tests/commands/math/median.rs b/crates/nu-cli/tests/commands/math/median.rs index 79f4da6d4a..90070859c1 100644 --- a/crates/nu-cli/tests/commands/math/median.rs +++ b/crates/nu-cli/tests/commands/math/median.rs @@ -27,3 +27,17 @@ fn median_numbers_with_odd_rows() { assert_eq!(actual.out, "10.5") } + +#[test] +fn median_mixed_numbers() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo [-11.5 -13.5 10] + | math median + | echo $it + "# + )); + + assert_eq!(actual.out, "-11.5") +} From 853d7e712049c6a2fef98da06a9f274fcc84961c Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Sat, 20 Jun 2020 01:28:03 -0400 Subject: [PATCH 19/27] Math median tests and documentation additions (#2018) * Add math median example and unit tests * Update output of other all math ls command examples to keep consistent with math median output * Fix output of math max example * Update output of other math commands using pwd examples to keep data consistent --- crates/nu-cli/src/commands/math/command.rs | 38 +++++++++++++---- docs/commands/math.md | 47 +++++++++++++++------- 2 files changed, 63 insertions(+), 22 deletions(-) diff --git a/crates/nu-cli/src/commands/math/command.rs b/crates/nu-cli/src/commands/math/command.rs index 266b560203..5e325523f6 100644 --- a/crates/nu-cli/src/commands/math/command.rs +++ b/crates/nu-cli/src/commands/math/command.rs @@ -35,7 +35,8 @@ impl WholeStreamCommand for Command { mod tests { use super::*; use crate::commands::math::{ - avg::average, max::maximum, min::minimum, sum::summation, utils::MathFunction, + avg::average, max::maximum, median::median, min::minimum, sum::summation, + utils::MathFunction, }; use nu_plugin::test_helpers::value::{decimal, int}; use nu_protocol::Value; @@ -67,13 +68,25 @@ mod tests { description: "Single value", values: vec![int(10)], expected_err: None, - expected_res: vec![Ok(decimal(10)), Ok(int(10)), Ok(int(10)), Ok(int(10))], + expected_res: vec![ + Ok(decimal(10)), + Ok(int(10)), + Ok(int(10)), + Ok(int(10)), + Ok(int(10)), + ], }, TestCase { description: "Multiple Values", - values: vec![int(10), int(30), int(20)], + values: vec![int(10), int(20), int(30)], expected_err: None, - expected_res: vec![Ok(decimal(20)), Ok(int(10)), Ok(int(30)), Ok(int(60))], + expected_res: vec![ + Ok(decimal(20)), + Ok(int(10)), + Ok(int(30)), + Ok(int(20)), + Ok(int(60)), + ], }, TestCase { description: "Mixed Values", @@ -83,23 +96,31 @@ mod tests { Ok(decimal(21)), Ok(int(10)), Ok(decimal(26.5)), + Ok(decimal(26.5)), Ok(decimal(63)), ], }, TestCase { description: "Negative Values", - values: vec![int(10), int(-11), int(-14)], + values: vec![int(-14), int(-11), int(10)], expected_err: None, - expected_res: vec![Ok(decimal(-5)), Ok(int(-14)), Ok(int(10)), Ok(int(-15))], + expected_res: vec![ + Ok(decimal(-5)), + Ok(int(-14)), + Ok(int(10)), + Ok(int(-11)), + Ok(int(-15)), + ], }, TestCase { description: "Mixed Negative Values", - values: vec![int(10), decimal(-11.5), decimal(-13.5)], + values: vec![decimal(-13.5), decimal(-11.5), int(10)], expected_err: None, expected_res: vec![ Ok(decimal(-5)), Ok(decimal(-13.5)), Ok(int(10)), + Ok(decimal(-11.5)), Ok(decimal(-15)), ], }, @@ -126,7 +147,8 @@ mod tests { for tc in tt.iter() { let tc: &TestCase = tc; // Just for type annotations - let math_functions: Vec = vec![average, minimum, maximum, summation]; + let math_functions: Vec = + vec![average, minimum, maximum, median, summation]; let results = math_functions .iter() .map(|mf| mf(&tc.values, &test_tag)) diff --git a/docs/commands/math.md b/docs/commands/math.md index 4c2528000f..7f7a216f23 100644 --- a/docs/commands/math.md +++ b/docs/commands/math.md @@ -6,6 +6,7 @@ Currently the following functions are implemented: * `math avg`: Finds the average of a list of numbers or tables * `math min`: Finds the minimum within a list of numbers or tables * `math max`: Finds the maximum within a list of numbers or tables +* `math median`: Finds the median of a list of numbers or tables * `math sum`: Finds the sum of a list of numbers or tables However, the mathematical functions like `min` and `max` are more permissive and also work on `Dates`. @@ -46,6 +47,8 @@ To get the average of the file sizes in a directory, simply pipe the size column ```shell > ls | get size | math avg ───┬──────── + # │ +───┼──────── 0 │ 7.2 KB ───┴──────── ``` @@ -53,21 +56,35 @@ To get the average of the file sizes in a directory, simply pipe the size column ```shell > ls | get size | math min ───┬───── + # │ +───┼───── 0 │ 0 B ───┴───── ``` ```shell -> ls | get size | math max ───┬────────── - 0 │ 113.5 KB + # │ +───┼────────── + 0 │ 113.6 KB ───┴────────── ``` +```shell +> ls | get size | math median +───┬─────── + # │ +───┼─────── + 0 │ 320 B +───┴─────── +``` + ```shell > ls | get size | math sum ───┬────────── - 0 │ 143.4 KB + # │ +───┼────────── + 0 │ 143.6 KB ───┴────────── ``` @@ -91,29 +108,31 @@ To get the average of the file sizes in a directory, simply pipe the size column # │ lines │ words │ chars │ max length ───┼───────┼───────┼───────┼──────────── 0 │ 0 │ 1 │ 5 │ 5 - 1 │ 0 │ 1 │ 7 │ 7 - 2 │ 0 │ 1 │ 9 │ 9 - 3 │ 0 │ 1 │ 7 │ 7 + 1 │ 0 │ 1 │ 11 │ 11 + 2 │ 0 │ 1 │ 11 │ 11 + 3 │ 0 │ 1 │ 4 │ 4 + 4 │ 0 │ 2 │ 12 │ 12 + 5 │ 0 │ 1 │ 7 │ 7 ───┴───────┴───────┴───────┴──────────── ``` ```shell > pwd | split row / | size | math max -───────────┬─── +────────────┬──── lines │ 0 - words │ 1 - chars │ 9 - max length │ 9 -────────────┴─── + words │ 2 + chars │ 12 + max length │ 12 +────────────┴──── ``` ```shell > pwd | split row / | size | math avg ────────────┬──────── lines │ 0.0000 - words │ 1.0000 - chars │ 7.0000 - max length │ 7.0000 + words │ 1.1666 + chars │ 8.3333 + max length │ 8.3333 ────────────┴──────── ``` From de6d8738c49f83548fe46a53d5a962aa6b66dba6 Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Sat, 20 Jun 2020 12:16:36 -0500 Subject: [PATCH 20/27] Simplify textview match (#2024) * simplify textview match code * Math median tests and documentation additions (#2018) * Add math median example and unit tests * Update output of other all math ls command examples to keep consistent with math median output * Fix output of math max example * Update output of other math commands using pwd examples to keep data consistent --- crates/nu_plugin_textview/src/textview.rs | 34 +++++++++++------------ 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/crates/nu_plugin_textview/src/textview.rs b/crates/nu_plugin_textview/src/textview.rs index 5c8aec6471..c050ff189c 100644 --- a/crates/nu_plugin_textview/src/textview.rs +++ b/crates/nu_plugin_textview/src/textview.rs @@ -35,62 +35,62 @@ pub fn view_text_value(value: &Value) { if let Ok(config) = nu_cli::data::config::config(Tag::unknown()) { if let Some(batvars) = config.get("textview") { for (idx, value) in batvars.row_entries() { - match idx { - x if x == "term_width" => { + match idx.as_ref() { + "term_width" => { term_width = match value.as_u64() { Ok(n) => n, _ => textwrap::termwidth() as u64, } } - x if x == "tab_width" => { + "tab_width" => { tab_width = match value.as_u64() { Ok(n) => n, _ => 4u64, } } - x if x == "colored_output" => { + "colored_output" => { colored_output = match value.as_bool() { Ok(b) => b, _ => true, } } - x if x == "true_color" => { + "true_color" => { true_color = match value.as_bool() { Ok(b) => b, _ => true, } } - x if x == "header" => { + "header" => { header = match value.as_bool() { Ok(b) => b, _ => true, } } - x if x == "line_numbers" => { + "line_numbers" => { line_numbers = match value.as_bool() { Ok(b) => b, _ => true, } } - x if x == "grid" => { + "grid" => { grid = match value.as_bool() { Ok(b) => b, _ => true, } } - x if x == "vcs_modification_markers" => { + "vcs_modification_markers" => { vcs_modification_markers = match value.as_bool() { Ok(b) => b, _ => true, } } - x if x == "snip" => { + "snip" => { snip = match value.as_bool() { Ok(b) => b, _ => true, } } - x if x == "wrapping_mode" => { + "wrapping_mode" => { wrapping_mode = match value.as_string() { Ok(s) if s.to_lowercase() == "nowrapping" => { bat::WrappingMode::NoWrapping @@ -101,13 +101,13 @@ pub fn view_text_value(value: &Value) { _ => bat::WrappingMode::NoWrapping, } } - x if x == "use_italics" => { + "use_italics" => { use_italics = match value.as_bool() { Ok(b) => b, _ => true, } } - x if x == "paging_mode" => { + "paging_mode" => { paging_mode = match value.as_string() { Ok(s) if s.to_lowercase() == "always" => bat::PagingMode::Always, Ok(s) if s.to_lowercase() == "never" => bat::PagingMode::Never, @@ -117,15 +117,15 @@ pub fn view_text_value(value: &Value) { _ => bat::PagingMode::QuitIfOneScreen, } } - x if x == "pager" => { + "pager" => { pager = match value.as_string() { Ok(s) => s, _ => "less".to_string(), } } - x if x == "line_ranges" => line_ranges = bat::line_range::LineRanges::all(), // not real sure what to do with this - x if x == "highlight_range" => _highlight_range = "0,0", //ignore config value for now - x if x == "theme" => { + "line_ranges" => line_ranges = bat::line_range::LineRanges::all(), // not real sure what to do with this + "highlight_range" => _highlight_range = "0,0", //ignore config value for now + "theme" => { theme = match value.as_string() { Ok(s) => s, _ => "OneDarkHalf".to_string(), From 4e83363dd3b1de52a5c24fa9885e00689dd530e2 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Sat, 20 Jun 2020 14:55:16 -0400 Subject: [PATCH 21/27] Upgrade heim to 0.1.0-beta.3 (#2019) --- Cargo.lock | 12 ++++++------ crates/nu_plugin_ps/Cargo.toml | 2 +- crates/nu_plugin_sys/Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 87e87af30b..95e3286337 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1493,9 +1493,9 @@ dependencies = [ [[package]] name = "heim" -version = "0.1.0-beta.2" +version = "0.1.0-beta.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea9164f267a5f4325020b8a989c4b0ab06acc0685ccdb22551f59257fdf296ab" +checksum = "d1014732324a9baf5a691525faabb33909bf6f40dcc2b03c8f2fb07bb01e7e3f" dependencies = [ "heim-common", "heim-cpu", @@ -1529,9 +1529,9 @@ dependencies = [ [[package]] name = "heim-cpu" -version = "0.1.0-beta.2" +version = "0.1.0-beta.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b088c42ce30cf60b485df484e0aa19c31ad8663bb939180ef64ca340d15eca6" +checksum = "73b1442359831aa671aa931f0a084aab210e77b1330ded78f1e60cc305abc4bb" dependencies = [ "cfg-if", "futures 0.3.5", @@ -1613,9 +1613,9 @@ dependencies = [ [[package]] name = "heim-process" -version = "0.1.1-beta.2" +version = "0.1.1-beta.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190f1085293c8d54060dd77c943da0d5bd1729aa00d2ac68188e26446dc0170d" +checksum = "fd969deb2a89a488b6a9bf18a65923ae4cdef6b128fa2dedb74ef5c694deb5ae" dependencies = [ "async-trait", "cfg-if", diff --git a/crates/nu_plugin_ps/Cargo.toml b/crates/nu_plugin_ps/Cargo.toml index 27195d713d..1745f3da08 100644 --- a/crates/nu_plugin_ps/Cargo.toml +++ b/crates/nu_plugin_ps/Cargo.toml @@ -19,7 +19,7 @@ futures = { version = "0.3", features = ["compat", "io-compat"] } futures-timer = "3.0.2" [dependencies.heim] -version = "0.1.0-beta.2" +version = "0.1.0-beta.3" default-features = false features = ["process"] diff --git a/crates/nu_plugin_sys/Cargo.toml b/crates/nu_plugin_sys/Cargo.toml index e651b33fda..4454d33bc1 100644 --- a/crates/nu_plugin_sys/Cargo.toml +++ b/crates/nu_plugin_sys/Cargo.toml @@ -20,7 +20,7 @@ battery = "0.7.5" futures-util = "0.3.5" [dependencies.heim] -version = "0.1.0-beta.2" +version = "0.1.0-beta.3" default-features = false features = ["host", "cpu", "memory", "disk", "net", "sensors"] From 89c737f4564479e31bb1b7c2b816085e1f07fe28 Mon Sep 17 00:00:00 2001 From: Jonathan Turner Date: Sat, 20 Jun 2020 12:25:07 -0700 Subject: [PATCH 22/27] Finish move to nu-table (#2025) --- Cargo.lock | 23 +--- crates/nu-cli/Cargo.toml | 1 - crates/nu-cli/src/commands/autoview.rs | 101 +++-------------- crates/nu-cli/src/commands/table.rs | 15 ++- crates/nu-table/src/table.rs | 128 +++++++++++++--------- crates/nu_plugin_textview/src/textview.rs | 4 - 6 files changed, 111 insertions(+), 161 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 95e3286337..5a44529ad8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1899,9 +1899,9 @@ dependencies = [ [[package]] name = "kstring" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6382df53100fd22e149030b6634720c94a151076db8d727b3274d7070975d609" +checksum = "fbbc30beb80d56ddf6346e935c7abcba96329ee5c5a4cde8984a4e6b6f18b58e" dependencies = [ "serde 1.0.110", ] @@ -2038,9 +2038,9 @@ dependencies = [ [[package]] name = "liquid-core" -version = "0.20.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea59709d9530bab4e9cc0ece12f20fe4999fdef90f7d89391b0fc9ff563b62b" +checksum = "4dc58422728185d54cd044bba4d45a2ef2a7111a421f84d344f65629949de4f1" dependencies = [ "anymap", "chrono", @@ -2481,7 +2481,6 @@ dependencies = [ "pin-utils", "pretty-hex", "pretty_env_logger", - "prettytable-rs", "ptree", "query_interface", "quickcheck", @@ -3235,20 +3234,6 @@ dependencies = [ "log", ] -[[package]] -name = "prettytable-rs" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fd04b170004fa2daccf418a7f8253aaf033c27760b5f225889024cf66d7ac2e" -dependencies = [ - "atty", - "csv", - "encode_unicode", - "lazy_static 1.4.0", - "term", - "unicode-width", -] - [[package]] name = "proc-macro-error" version = "1.0.2" diff --git a/crates/nu-cli/Cargo.toml b/crates/nu-cli/Cargo.toml index d9c8c36383..070e8f04bb 100644 --- a/crates/nu-cli/Cargo.toml +++ b/crates/nu-cli/Cargo.toml @@ -62,7 +62,6 @@ parking_lot = "0.10.2" pin-utils = "0.1.0" pretty-hex = "0.1.1" pretty_env_logger = "0.4.0" -prettytable-rs = "0.8.0" ptree = {version = "0.2" } query_interface = "0.3.5" rand = "0.7" diff --git a/crates/nu-cli/src/commands/autoview.rs b/crates/nu-cli/src/commands/autoview.rs index c587d603b3..eac2317a67 100644 --- a/crates/nu-cli/src/commands/autoview.rs +++ b/crates/nu-cli/src/commands/autoview.rs @@ -6,10 +6,7 @@ use nu_errors::ShellError; use nu_protocol::{hir, hir::Expression, hir::Literal, hir::SpannedExpression}; use nu_protocol::{Primitive, Scope, Signature, UntaggedValue, Value}; use parking_lot::Mutex; -use prettytable::format::{FormatBuilder, LinePosition, LineSeparator}; -use prettytable::{color, Attr, Cell, Row, Table}; use std::sync::atomic::AtomicBool; -use textwrap::fill; pub struct Autoview; @@ -268,90 +265,28 @@ pub async fn autoview(context: RunnableContext) -> Result textwrap::termwidth()) => { - let termwidth = std::cmp::max(textwrap::termwidth(), 20); - - enum TableMode { - Light, - Normal, - } - - let mut table = Table::new(); - let table_mode = crate::data::config::config(Tag::unknown()); - - let table_mode = if let Some(s) = table_mode?.get("table_mode") { - match s.as_string() { - Ok(typ) if typ == "light" => TableMode::Light, - _ => TableMode::Normal, - } - } else { - TableMode::Normal - }; - - match table_mode { - TableMode::Light => { - table.set_format( - FormatBuilder::new() - .separator( - LinePosition::Title, - LineSeparator::new('─', '─', ' ', ' '), - ) - .separator( - LinePosition::Bottom, - LineSeparator::new(' ', ' ', ' ', ' '), - ) - .padding(1, 1) - .build(), - ); - } - _ => { - table.set_format( - FormatBuilder::new() - .column_separator('│') - .separator( - LinePosition::Top, - LineSeparator::new('─', '┬', ' ', ' '), - ) - .separator( - LinePosition::Title, - LineSeparator::new('─', '┼', ' ', ' '), - ) - .separator( - LinePosition::Bottom, - LineSeparator::new('─', '┴', ' ', ' '), - ) - .padding(1, 1) - .build(), - ); - } - } - - let mut max_key_len = 0; - for (key, _) in row.entries.iter() { - max_key_len = std::cmp::max(max_key_len, key.chars().count()); - } - - if max_key_len > (termwidth / 2 - 1) { - max_key_len = termwidth / 2 - 1; - } - - let max_val_len = termwidth - max_key_len - 5; - + let mut entries = vec![]; for (key, value) in row.entries.iter() { - table.add_row(Row::new(vec![ - Cell::new(&fill(&key, max_key_len)) - .with_style(Attr::ForegroundColor(color::GREEN)) - .with_style(Attr::Bold), - Cell::new(&fill( - &format_leaf(value).plain_string(100_000), - max_val_len, - )), - ])); + entries.push(vec![ + nu_table::StyledString::new( + key.to_string(), + nu_table::TextStyle { + alignment: nu_table::Alignment::Left, + color: Some(ansi_term::Color::Green), + is_bold: true, + }, + ), + nu_table::StyledString::new( + format_leaf(value).plain_string(100_000), + nu_table::TextStyle::basic(), + ), + ]); } - table.printstd(); + let table = + nu_table::Table::new(vec![], entries, nu_table::Theme::compact()); - // table.print_term(&mut *context.host.lock().out_terminal().ok_or_else(|| ShellError::untagged_runtime_error("Could not open terminal for output"))?) - // .map_err(|_| ShellError::untagged_runtime_error("Internal error: could not print to terminal (for unix systems check to make sure TERM is set)"))?; + nu_table::draw_table(&table, textwrap::termwidth()); } Value { diff --git a/crates/nu-cli/src/commands/table.rs b/crates/nu-cli/src/commands/table.rs index 9953dbda93..ea63864285 100644 --- a/crates/nu-cli/src/commands/table.rs +++ b/crates/nu-cli/src/commands/table.rs @@ -56,7 +56,7 @@ fn str_to_color(s: String) -> Option { pub fn from_list(values: &[Value], starting_idx: usize) -> nu_table::Table { let config = crate::data::config::config(Tag::unknown()); - let header_style = if let Ok(config) = config { + let header_style = if let Ok(config) = &config { let header_align = config.get("header_align").map_or(Alignment::Left, |a| { a.as_string() .map_or(Alignment::Center, |a| match a.to_lowercase().as_str() { @@ -97,6 +97,19 @@ pub fn from_list(values: &[Value], starting_idx: usize) -> nu_table::Table { .collect(); let entries = values_to_entries(values, &mut headers, starting_idx); + if let Ok(config) = config { + if let Some(style) = config.get("table_mode") { + if let Ok(table_mode) = style.as_string() { + if table_mode == "light" { + return nu_table::Table { + headers, + data: entries, + theme: Theme::light(), + }; + } + } + } + } nu_table::Table { headers, data: entries, diff --git a/crates/nu-table/src/table.rs b/crates/nu-table/src/table.rs index 7ad91e9085..5bd8877bdd 100644 --- a/crates/nu-table/src/table.rs +++ b/crates/nu-table/src/table.rs @@ -81,6 +81,8 @@ pub struct Theme { pub print_left_border: bool, pub print_right_border: bool, + pub print_top_border: bool, + pub print_bottom_border: bool, } impl Theme { @@ -108,6 +110,8 @@ impl Theme { print_left_border: true, print_right_border: true, + print_top_border: true, + print_bottom_border: true, } } #[allow(unused)] @@ -136,6 +140,8 @@ impl Theme { print_left_border: true, print_right_border: true, + print_top_border: true, + print_bottom_border: true, } } #[allow(unused)] @@ -160,10 +166,12 @@ impl Theme { right_vertical: ' ', separate_header: true, - separate_rows: true, + separate_rows: false, print_left_border: true, print_right_border: true, + print_top_border: false, + print_bottom_border: true, } } #[allow(unused)] @@ -191,6 +199,8 @@ impl Theme { print_left_border: false, print_right_border: false, + print_top_border: true, + print_bottom_border: true, } } } @@ -227,84 +237,77 @@ pub struct WrappedTable { } impl WrappedTable { - //TODO: optimize this fn print_separator(&self, separator_position: SeparatorPosition) { let column_count = self.column_widths.len(); + let mut output = String::new(); match separator_position { SeparatorPosition::Top => { for column in self.column_widths.iter().enumerate() { if column.0 == 0 && self.theme.print_left_border { - print!("{}", self.theme.top_left); + output.push(self.theme.top_left); } - print!( - "{}", - std::iter::repeat(self.theme.top_horizontal) - .take(*column.1) - .collect::() - ); - print!("{}{}", self.theme.top_horizontal, self.theme.top_horizontal); + for _ in 0..*column.1 { + output.push(self.theme.top_horizontal); + } + + output.push(self.theme.top_horizontal); + output.push(self.theme.top_horizontal); if column.0 == column_count - 1 { if self.theme.print_right_border { - print!("{}", self.theme.top_right); + output.push(self.theme.top_right); } } else { - print!("{}", self.theme.top_center); + output.push(self.theme.top_center); } } } SeparatorPosition::Middle => { for column in self.column_widths.iter().enumerate() { if column.0 == 0 && self.theme.print_left_border { - print!("{}", self.theme.middle_left); + output.push(self.theme.middle_left); } - print!( - "{}", - std::iter::repeat(self.theme.middle_horizontal) - .take(*column.1) - .collect::() - ); - print!( - "{}{}", - self.theme.middle_horizontal, self.theme.middle_horizontal - ); + for _ in 0..*column.1 { + output.push(self.theme.middle_horizontal); + } + + output.push(self.theme.middle_horizontal); + output.push(self.theme.middle_horizontal); + if column.0 == column_count - 1 { if self.theme.print_right_border { - print!("{}", self.theme.middle_right); + output.push(self.theme.middle_right); } } else { - print!("{}", self.theme.center); + output.push(self.theme.center); } } } SeparatorPosition::Bottom => { for column in self.column_widths.iter().enumerate() { if column.0 == 0 && self.theme.print_left_border { - print!("{}", self.theme.bottom_left); + output.push(self.theme.bottom_left); } - print!( - "{}", - std::iter::repeat(self.theme.bottom_horizontal) - .take(*column.1) - .collect::() - ); + for _ in 0..*column.1 { + output.push(self.theme.bottom_horizontal); + } + output.push(self.theme.bottom_horizontal); + output.push(self.theme.bottom_horizontal); - print!( - "{}{}", - self.theme.bottom_horizontal, self.theme.bottom_horizontal - ); if column.0 == column_count - 1 { if self.theme.print_right_border { - print!("{}", self.theme.bottom_right); + output.push(self.theme.bottom_right); } } else { - print!("{}", self.theme.bottom_center); + output.push(self.theme.bottom_center); } } } } + + println!("{}", output); } fn print_cell_contents(&self, cells: &[WrappedCell]) { @@ -399,10 +402,13 @@ impl WrappedTable { return; } - self.print_separator(SeparatorPosition::Top); - println!(); + if self.theme.print_top_border { + self.print_separator(SeparatorPosition::Top); + } - self.print_cell_contents(&self.headers); + if !self.headers.is_empty() { + self.print_cell_contents(&self.headers); + } let mut first_row = true; @@ -410,21 +416,21 @@ impl WrappedTable { if !first_row { if self.theme.separate_rows { self.print_separator(SeparatorPosition::Middle); - println!(); } } else { first_row = false; - if self.theme.separate_header { + if self.theme.separate_header && !self.headers.is_empty() { self.print_separator(SeparatorPosition::Middle); - println!(); } } self.print_cell_contents(row); } - self.print_separator(SeparatorPosition::Bottom); - println!(); + + if self.theme.print_bottom_border { + self.print_separator(SeparatorPosition::Bottom); + } } } @@ -499,6 +505,17 @@ pub fn draw_table(table: &Table, termwidth: usize) { // maybe_truncate_columns(&mut headers, &mut entries, termwidth); let headers_len = table.headers.len(); + // fix the length of the table if there are no headers: + let headers_len = if headers_len == 0 { + if !table.data.is_empty() && !table.data[0].is_empty() { + table.data[0].len() + } else { + return; + } + } else { + headers_len + }; + // Measure how big our columns need to be (accounting for separators also) let max_naive_column_width = (termwidth - 3 * (headers_len - 1)) / headers_len; @@ -518,18 +535,23 @@ pub fn draw_table(table: &Table, termwidth: usize) { // This should give us the final max column width let max_column_width = column_space.max_width(termwidth); - let wrapped_table = wrap_cells( - processed_table, - // max_per_column, - // max_naive_column_width, - max_column_width, - ); + let wrapped_table = wrap_cells(processed_table, max_column_width); wrapped_table.new_print_table(); } fn wrap_cells(processed_table: ProcessedTable, max_column_width: usize) -> WrappedTable { - let mut column_widths = vec![0; processed_table.headers.len()]; + let mut column_widths = vec![ + 0; + std::cmp::max( + processed_table.headers.len(), + if !processed_table.data.is_empty() { + processed_table.data[0].len() + } else { + 0 + } + ) + ]; let mut output_headers = vec![]; for header in processed_table.headers.into_iter().enumerate() { let wrapped = wrap( diff --git a/crates/nu_plugin_textview/src/textview.rs b/crates/nu_plugin_textview/src/textview.rs index c050ff189c..a1a5dedd0f 100644 --- a/crates/nu_plugin_textview/src/textview.rs +++ b/crates/nu_plugin_textview/src/textview.rs @@ -134,11 +134,7 @@ pub fn view_text_value(value: &Value) { _ => (), } } - } else { - println!("Couldn't find bat section in config"); } - } else { - println!("Error reading config!"); } let value_anchor = value.anchor(); From 480600c4659740d223323d4210f373d5f537056a Mon Sep 17 00:00:00 2001 From: Jonathan Turner Date: Sat, 20 Jun 2020 14:33:58 -0700 Subject: [PATCH 23/27] Fix wrap of carriage returns in cells (#2027) * Fix carriage returns in cells * Fix carriage returns in cells --- crates/nu-table/src/main.rs | 6 +++- crates/nu-table/src/table.rs | 45 +++++++++++++++++++---------- crates/nu-table/src/wrap.rs | 56 +++++++++++++++++++++--------------- 3 files changed, 68 insertions(+), 39 deletions(-) diff --git a/crates/nu-table/src/main.rs b/crates/nu-table/src/main.rs index f426a9d02d..19a8ecf6a4 100644 --- a/crates/nu-table/src/main.rs +++ b/crates/nu-table/src/main.rs @@ -12,7 +12,11 @@ fn main() { let t = Table::new( vec![ StyledString::new("Test me".to_owned(), TextStyle::default_header()), - StyledString::new("Long column name".to_owned(), TextStyle::default_header()), + StyledString::new( + "Long column \n name with carriage returns and a lot of text\n check it out" + .to_owned(), + TextStyle::default_header(), + ), StyledString::new("Another".to_owned(), TextStyle::default_header()), ], vec![msg; 2], diff --git a/crates/nu-table/src/table.rs b/crates/nu-table/src/table.rs index 5bd8877bdd..ebd7edb02f 100644 --- a/crates/nu-table/src/table.rs +++ b/crates/nu-table/src/table.rs @@ -224,7 +224,7 @@ pub struct ProcessedTable<'a> { #[derive(Debug)] pub struct ProcessedCell<'a> { - pub contents: Vec>, + pub contents: Vec>>, pub style: TextStyle, } @@ -440,7 +440,7 @@ fn process_table(table: &Table) -> ProcessedTable { let mut out_row = vec![]; for column in row { out_row.push(ProcessedCell { - contents: split_sublines(&column.contents).collect::>(), + contents: split_sublines(&column.contents), style: column.style.clone(), }); } @@ -450,7 +450,7 @@ fn process_table(table: &Table) -> ProcessedTable { let mut processed_headers = vec![]; for header in &table.headers { processed_headers.push(ProcessedCell { - contents: split_sublines(&header.contents).collect::>(), + contents: split_sublines(&header.contents), style: header.style.clone(), }); } @@ -476,12 +476,12 @@ fn get_max_column_widths(processed_table: &ProcessedTable) -> Vec { let mut output = vec![0; max_num_columns]; for column in processed_table.headers.iter().enumerate() { - output[column.0] = max(output[column.0], column_width(column.1.contents.iter())); + output[column.0] = max(output[column.0], column_width(&column.1.contents)); } for row in &processed_table.data { for column in row.iter().enumerate() { - output[column.0] = max(output[column.0], column_width(column.1.contents.iter())); + output[column.0] = max(output[column.0], column_width(&column.1.contents)); } } @@ -554,11 +554,19 @@ fn wrap_cells(processed_table: ProcessedTable, max_column_width: usize) -> Wrapp ]; let mut output_headers = vec![]; for header in processed_table.headers.into_iter().enumerate() { - let wrapped = wrap( - max_column_width, - header.1.contents.into_iter(), - header.1.style, - ); + let mut wrapped = WrappedCell { + lines: vec![], + max_width: 0, + style: header.1.style, + }; + + for contents in header.1.contents.into_iter() { + let (mut lines, inner_max_width) = wrap(max_column_width, contents.into_iter()); + wrapped.lines.append(&mut lines); + if inner_max_width > wrapped.max_width { + wrapped.max_width = inner_max_width; + } + } if column_widths[header.0] < wrapped.max_width { column_widths[header.0] = wrapped.max_width; } @@ -569,11 +577,18 @@ fn wrap_cells(processed_table: ProcessedTable, max_column_width: usize) -> Wrapp for row in processed_table.data.into_iter() { let mut output_row = vec![]; for column in row.into_iter().enumerate() { - let wrapped = wrap( - max_column_width, - column.1.contents.into_iter(), - column.1.style, - ); + let mut wrapped = WrappedCell { + lines: vec![], + max_width: 0, + style: column.1.style, + }; + for contents in column.1.contents.into_iter() { + let (mut lines, inner_max_width) = wrap(max_column_width, contents.into_iter()); + wrapped.lines.append(&mut lines); + if inner_max_width > wrapped.max_width { + wrapped.max_width = inner_max_width; + } + } if column_widths[column.0] < wrapped.max_width { column_widths[column.0] = wrapped.max_width; } diff --git a/crates/nu-table/src/wrap.rs b/crates/nu-table/src/wrap.rs index bc86eb9f3c..33baa47c30 100644 --- a/crates/nu-table/src/wrap.rs +++ b/crates/nu-table/src/wrap.rs @@ -50,29 +50,44 @@ impl<'a> Display for Line<'a> { } } -pub fn split_sublines(input: &str) -> impl Iterator { - input.split_terminator(' ').map(|x| Subline { - subline: x, - width: UnicodeWidthStr::width(x), - }) +pub fn split_sublines(input: &str) -> Vec> { + input + .split_terminator('\n') + .map(|line| { + line.split_terminator(' ') + .map(|x| Subline { + subline: x, + width: UnicodeWidthStr::width(x), + }) + .collect::>() + }) + .collect::>() } -pub fn column_width<'a>(input: impl Iterator>) -> usize { - let mut total = 0; +pub fn column_width<'a>(input: &[Vec>]) -> usize { + let mut max = 0; - let mut first = true; - for inp in input { - if !first { - // Account for the space - total += 1; - } else { - first = false; + for line in input { + let mut total = 0; + + let mut first = true; + for inp in line { + if !first { + // Account for the space + total += 1; + } else { + first = false; + } + + total += inp.width; } - total += inp.width; + if total > max { + max = total; + } } - total + max } fn split_word<'a>(cell_width: usize, word: &'a str) -> Vec> { @@ -113,8 +128,7 @@ fn split_word<'a>(cell_width: usize, word: &'a str) -> Vec> { pub fn wrap<'a>( cell_width: usize, mut input: impl Iterator>, - style: TextStyle, -) -> WrappedCell { +) -> (Vec, usize) { let mut lines = vec![]; let mut current_line: Vec = vec![]; let mut current_width = 0; @@ -218,9 +232,5 @@ pub fn wrap<'a>( }); } - WrappedCell { - lines: output, - max_width: current_max, - style, - } + (output, current_max) } From bce6f5a3e6043cc93b18b4f8687709003d4ddd1e Mon Sep 17 00:00:00 2001 From: siedentop <284732+siedentop@users.noreply.github.com> Date: Sat, 20 Jun 2020 17:22:06 -0700 Subject: [PATCH 24/27] Uniq: `--count` flag to count occurences (#2017) * uniq: Add counting option (WIP!) Usage: fetch https://raw.githubusercontent.com/timbray/topfew/master/test/data/access-1k | lines | wrap item | uniq | sort-by count | last 10 * uniq: Add first test * uniq: Re-enable the non-counting variant. * uniq: Also handle primitive lines. * uniq: Update documentation * uniq: Final comment about error handling. Let's get some feedback * uniq: Address review comments. Not happy with the way I create a TypeError. There must be a cleaner way. Anyway, good for shipping. * uniq: Use Labeled_error as suggested by jturner in chat. * uniq: Return error directly. Co-authored-by: Christoph Siedentop --- crates/nu-cli/src/commands/uniq.rs | 69 ++++++++++++++++++++++++---- crates/nu-cli/tests/commands/uniq.rs | 23 ++++++++++ docs/commands/uniq.md | 13 ++++++ 3 files changed, 95 insertions(+), 10 deletions(-) diff --git a/crates/nu-cli/src/commands/uniq.rs b/crates/nu-cli/src/commands/uniq.rs index 4f024188d1..f4c5831434 100644 --- a/crates/nu-cli/src/commands/uniq.rs +++ b/crates/nu-cli/src/commands/uniq.rs @@ -1,9 +1,9 @@ use crate::commands::WholeStreamCommand; use crate::context::CommandRegistry; use crate::prelude::*; -use indexmap::set::IndexSet; +use indexmap::map::IndexMap; use nu_errors::ShellError; -use nu_protocol::{ReturnSuccess, Signature}; +use nu_protocol::Signature; pub struct Uniq; @@ -14,7 +14,7 @@ impl WholeStreamCommand for Uniq { } fn signature(&self) -> Signature { - Signature::build("uniq") + Signature::build("uniq").switch("count", "Count the unique rows", Some('c')) } fn usage(&self) -> &str { @@ -30,17 +30,66 @@ impl WholeStreamCommand for Uniq { } } -async fn uniq(args: CommandArgs, _registry: &CommandRegistry) -> Result { +async fn uniq(args: CommandArgs, registry: &CommandRegistry) -> Result { + let args = args.evaluate_once(®istry).await?; + let should_show_count = args.has("count"); let input = args.input; - let uniq_values: IndexSet<_> = input.collect().await; + let uniq_values = { + let mut counter = IndexMap::::new(); + for line in input.into_vec().await { + *counter.entry(line).or_insert(0) += 1; + } + counter + }; let mut values_vec_deque = VecDeque::new(); - for item in uniq_values - .iter() - .map(|row| ReturnSuccess::value(row.clone())) - { - values_vec_deque.push_back(item); + if should_show_count { + for item in uniq_values { + use nu_protocol::{UntaggedValue, Value}; + let value = { + match item.0.value { + UntaggedValue::Row(mut row) => { + row.entries.insert( + "count".to_string(), + UntaggedValue::int(item.1).into_untagged_value(), + ); + Value { + value: UntaggedValue::Row(row), + tag: item.0.tag, + } + } + UntaggedValue::Primitive(p) => { + let mut map = IndexMap::::new(); + map.insert( + "value".to_string(), + UntaggedValue::Primitive(p).into_untagged_value(), + ); + map.insert( + "count".to_string(), + UntaggedValue::int(item.1).into_untagged_value(), + ); + Value { + value: UntaggedValue::row(map), + tag: item.0.tag, + } + } + UntaggedValue::Table(_) => { + return Err(ShellError::labeled_error( + "uniq -c cannot operate on tables.", + "source", + item.0.tag.span, + )) + } + UntaggedValue::Error(_) | UntaggedValue::Block(_) => item.0, + } + }; + values_vec_deque.push_back(value); + } + } else { + for item in uniq_values { + values_vec_deque.push_back(item.0); + } } Ok(futures::stream::iter(values_vec_deque).to_output_stream()) diff --git a/crates/nu-cli/tests/commands/uniq.rs b/crates/nu-cli/tests/commands/uniq.rs index 465741c292..6449918785 100644 --- a/crates/nu-cli/tests/commands/uniq.rs +++ b/crates/nu-cli/tests/commands/uniq.rs @@ -140,3 +140,26 @@ fn uniq_when_keys_out_of_order() { assert_eq!(actual.out, "1"); } + +#[test] +fn uniq_counting() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo '["A", "B", "A"]' + | from json + | wrap item + | uniq --count + "# + )); + let expected = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo '[{"item": "A", "count": 2}, {"item": "B", "count": 1}]' + | from json + "# + )); + print!("{}", actual.out); + print!("{}", expected.out); + assert_eq!(actual.out, expected.out); +} diff --git a/docs/commands/uniq.md b/docs/commands/uniq.md index ce31db1bf3..b38a1cf696 100644 --- a/docs/commands/uniq.md +++ b/docs/commands/uniq.md @@ -34,3 +34,16 @@ Yehuda,Katz,10/11/2013,A 1 │ B ━━━┷━━━━━━━━━ ``` + +### Counting +`--count` or `-c` is the flag to output a `count` column. + +``` +> `open test.csv | get type | uniq -c` +───┬───────┬─────── + # │ value │ count +───┼───────┼─────── + 0 │ A │ 3 + 1 │ B │ 2 +───┴───────┴─────── +``` \ No newline at end of file From e3e1e6f81bd18394ee9a4d5d27a42c3c334edc0a Mon Sep 17 00:00:00 2001 From: HiranmayaGundu Date: Mon, 22 Jun 2020 00:32:58 +0530 Subject: [PATCH 25/27] Adds a test case for changing drives using drive letter in Windows (#2022) * added test case to test check switching drives using drive letter in windows * modified test case to not use config, assert file exists --- crates/nu-cli/tests/commands/cd.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/crates/nu-cli/tests/commands/cd.rs b/crates/nu-cli/tests/commands/cd.rs index d9c12bb96a..1d7455c9ae 100644 --- a/crates/nu-cli/tests/commands/cd.rs +++ b/crates/nu-cli/tests/commands/cd.rs @@ -432,3 +432,27 @@ fn valuesystem_path_not_found() { assert!(actual.err.contains("No such path exists")); }) } + +#[cfg(target_os = "windows")] +#[test] +fn test_change_windows_drive() { + Playground::setup("cd_test_20", |dirs, sandbox| { + sandbox.mkdir("test_folder"); + + let _actual = nu!( + cwd: dirs.test(), + r#" + subst Z: test_folder + Z: + echo "some text" | save test_file.txt + cd ~ + subst Z: /d + "# + ); + assert!(dirs + .test() + .join("test_folder") + .join("test_file.txt") + .exists()); + }) +} From 7532618bdc238c5f662f733c612e395e012c29a2 Mon Sep 17 00:00:00 2001 From: bailey-layzer Date: Sun, 21 Jun 2020 13:50:43 -0700 Subject: [PATCH 26/27] Add error for division by zero using "=" (#2002) (#2030) * error for division by zero with = * cargo fmt * add helper zero_division_error * tests for zero division error * fix test names (zero div error) --- crates/nu-cli/src/data/value.rs | 30 +++++++++++++-- crates/nu-cli/src/evaluate/evaluator.rs | 5 ++- crates/nu-cli/src/evaluate/operator.rs | 10 ++++- crates/nu-cli/tests/commands/math/mod.rs | 48 ++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 6 deletions(-) diff --git a/crates/nu-cli/src/data/value.rs b/crates/nu-cli/src/data/value.rs index d4521bf92b..385adf9629 100644 --- a/crates/nu-cli/src/data/value.rs +++ b/crates/nu-cli/src/data/value.rs @@ -8,6 +8,7 @@ use nu_protocol::ShellTypeName; use nu_protocol::{Primitive, Type, UntaggedValue}; use nu_source::{DebugDocBuilder, PrettyDebug, Tagged}; use nu_table::TextStyle; +use num_traits::Zero; pub fn date_from_str(s: Tagged<&str>) -> Result { let date = DateTime::parse_from_rfc3339(s.item).map_err(|err| { @@ -35,6 +36,10 @@ pub fn merge_values( } } +fn zero_division_error() -> UntaggedValue { + UntaggedValue::Error(ShellError::untagged_runtime_error("division by zero")) +} + pub fn compute_values( operator: Operator, left: &UntaggedValue, @@ -55,7 +60,9 @@ pub fn compute_values( Operator::Minus => Ok(UntaggedValue::Primitive(Primitive::Int(x - y))), Operator::Multiply => Ok(UntaggedValue::Primitive(Primitive::Int(x * y))), Operator::Divide => { - if x - (y * (x / y)) == num_bigint::BigInt::from(0) { + if y.is_zero() { + Ok(zero_division_error()) + } else if x - (y * (x / y)) == num_bigint::BigInt::from(0) { Ok(UntaggedValue::Primitive(Primitive::Int(x / y))) } else { Ok(UntaggedValue::Primitive(Primitive::Decimal( @@ -71,7 +78,12 @@ pub fn compute_values( Operator::Plus => Ok(x + bigdecimal::BigDecimal::from(y.clone())), Operator::Minus => Ok(x - bigdecimal::BigDecimal::from(y.clone())), Operator::Multiply => Ok(x * bigdecimal::BigDecimal::from(y.clone())), - Operator::Divide => Ok(x / bigdecimal::BigDecimal::from(y.clone())), + Operator::Divide => { + if y.is_zero() { + return Ok(zero_division_error()); + } + Ok(x / bigdecimal::BigDecimal::from(y.clone())) + } _ => Err((left.type_name(), right.type_name())), }?; Ok(UntaggedValue::Primitive(Primitive::Decimal(result))) @@ -81,7 +93,12 @@ pub fn compute_values( Operator::Plus => Ok(bigdecimal::BigDecimal::from(x.clone()) + y), Operator::Minus => Ok(bigdecimal::BigDecimal::from(x.clone()) - y), Operator::Multiply => Ok(bigdecimal::BigDecimal::from(x.clone()) * y), - Operator::Divide => Ok(bigdecimal::BigDecimal::from(x.clone()) / y), + Operator::Divide => { + if y.is_zero() { + return Ok(zero_division_error()); + } + Ok(bigdecimal::BigDecimal::from(x.clone()) / y) + } _ => Err((left.type_name(), right.type_name())), }?; Ok(UntaggedValue::Primitive(Primitive::Decimal(result))) @@ -91,7 +108,12 @@ pub fn compute_values( Operator::Plus => Ok(x + y), Operator::Minus => Ok(x - y), Operator::Multiply => Ok(x * y), - Operator::Divide => Ok(x / y), + Operator::Divide => { + if y.is_zero() { + return Ok(zero_division_error()); + } + Ok(x / y) + } _ => Err((left.type_name(), right.type_name())), }?; Ok(UntaggedValue::Primitive(Primitive::Decimal(result))) diff --git a/crates/nu-cli/src/evaluate/evaluator.rs b/crates/nu-cli/src/evaluate/evaluator.rs index 026e7b4663..b8f84d9e01 100644 --- a/crates/nu-cli/src/evaluate/evaluator.rs +++ b/crates/nu-cli/src/evaluate/evaluator.rs @@ -47,7 +47,10 @@ pub(crate) async fn evaluate_baseline_expr( match binary.op.expr { Expression::Literal(hir::Literal::Operator(op)) => { match apply_operator(op, &left, &right) { - Ok(result) => Ok(result.into_value(tag)), + Ok(result) => match result { + UntaggedValue::Error(shell_err) => Err(shell_err), + _ => Ok(result.into_value(tag)), + }, Err((left_type, right_type)) => Err(ShellError::coerce_error( left_type.spanned(binary.left.span), right_type.spanned(binary.right.span), diff --git a/crates/nu-cli/src/evaluate/operator.rs b/crates/nu-cli/src/evaluate/operator.rs index 291c40b4f8..9350172b51 100644 --- a/crates/nu-cli/src/evaluate/operator.rs +++ b/crates/nu-cli/src/evaluate/operator.rs @@ -1,4 +1,5 @@ use crate::data::value; +use nu_errors::ShellError; use nu_protocol::hir::Operator; use nu_protocol::{Primitive, ShellTypeName, UntaggedValue, Value}; use std::ops::Not; @@ -24,7 +25,14 @@ pub fn apply_operator( Operator::Plus => value::compute_values(op, left, right), Operator::Minus => value::compute_values(op, left, right), Operator::Multiply => value::compute_values(op, left, right), - Operator::Divide => value::compute_values(op, left, right), + Operator::Divide => value::compute_values(op, left, right).map(|res| match res { + UntaggedValue::Error(_) => UntaggedValue::Error(ShellError::labeled_error( + "Evaluation error", + "division by zero", + &right.tag.span, + )), + _ => res, + }), Operator::In => table_contains(left, right).map(UntaggedValue::boolean), Operator::NotIn => table_contains(left, right).map(|x| UntaggedValue::boolean(!x)), Operator::And => match (left.as_bool(), right.as_bool()) { diff --git a/crates/nu-cli/tests/commands/math/mod.rs b/crates/nu-cli/tests/commands/math/mod.rs index 78c591c90a..136d568723 100644 --- a/crates/nu-cli/tests/commands/math/mod.rs +++ b/crates/nu-cli/tests/commands/math/mod.rs @@ -87,6 +87,54 @@ fn division_of_ints2() { assert_eq!(actual.out, "0.25"); } +#[test] +fn error_zero_division_int_int() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + = 1 / 0 + "# + )); + + assert!(actual.err.contains("division by zero")); +} + +#[test] +fn error_zero_division_decimal_int() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + = 1.0 / 0 + "# + )); + + assert!(actual.err.contains("division by zero")); +} + +#[test] +fn error_zero_division_int_decimal() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + = 1 / 0.0 + "# + )); + + assert!(actual.err.contains("division by zero")); +} + +#[test] +fn error_zero_division_decimal_decimal() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + = 1.0 / 0.0 + "# + )); + + assert!(actual.err.contains("division by zero")); +} + #[test] fn proper_precedence_history() { let actual = nu!( From e8476d8fbb965f24d9eea2853dfa8431efce3888 Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Mon, 22 Jun 2020 08:30:43 -0500 Subject: [PATCH 27/27] removed some comments (#2032) removed comments that i shouldn't have left in --- crates/nu-cli/src/commands/open.rs | 126 ----------------------------- 1 file changed, 126 deletions(-) diff --git a/crates/nu-cli/src/commands/open.rs b/crates/nu-cli/src/commands/open.rs index 720ffa70d4..8bdc0a59cf 100644 --- a/crates/nu-cli/src/commands/open.rs +++ b/crates/nu-cli/src/commands/open.rs @@ -305,132 +305,6 @@ pub async fn fetch( span, )) } - /* - cwd.push(Path::new(location)); - if let Ok(cwd) = dunce::canonicalize(cwd) { - match std::fs::read(&cwd) { - Ok(bytes) => match std::str::from_utf8(&bytes) { - Ok(s) => Ok(( - cwd.extension() - .map(|name| name.to_string_lossy().to_string()), - UntaggedValue::string(s), - Tag { - span, - anchor: Some(AnchorLocation::File(cwd.to_string_lossy().to_string())), - }, - )), - Err(_) => { - //Non utf8 data. - match (bytes.get(0), bytes.get(1)) { - (Some(x), Some(y)) if *x == 0xff && *y == 0xfe => { - // Possibly UTF-16 little endian - let utf16 = read_le_u16(&bytes[2..]); - - if let Some(utf16) = utf16 { - match std::string::String::from_utf16(&utf16) { - Ok(s) => Ok(( - cwd.extension() - .map(|name| name.to_string_lossy().to_string()), - UntaggedValue::string(s), - Tag { - span, - anchor: Some(AnchorLocation::File( - cwd.to_string_lossy().to_string(), - )), - }, - )), - Err(_) => Ok(( - None, - UntaggedValue::binary(bytes), - Tag { - span, - anchor: Some(AnchorLocation::File( - cwd.to_string_lossy().to_string(), - )), - }, - )), - } - } else { - Ok(( - None, - UntaggedValue::binary(bytes), - Tag { - span, - anchor: Some(AnchorLocation::File( - cwd.to_string_lossy().to_string(), - )), - }, - )) - } - } - (Some(x), Some(y)) if *x == 0xfe && *y == 0xff => { - // Possibly UTF-16 big endian - let utf16 = read_be_u16(&bytes[2..]); - - if let Some(utf16) = utf16 { - match std::string::String::from_utf16(&utf16) { - Ok(s) => Ok(( - cwd.extension() - .map(|name| name.to_string_lossy().to_string()), - UntaggedValue::string(s), - Tag { - span, - anchor: Some(AnchorLocation::File( - cwd.to_string_lossy().to_string(), - )), - }, - )), - Err(_) => Ok(( - None, - UntaggedValue::binary(bytes), - Tag { - span, - anchor: Some(AnchorLocation::File( - cwd.to_string_lossy().to_string(), - )), - }, - )), - } - } else { - Ok(( - None, - UntaggedValue::binary(bytes), - Tag { - span, - anchor: Some(AnchorLocation::File( - cwd.to_string_lossy().to_string(), - )), - }, - )) - } - } - _ => Ok(( - None, - UntaggedValue::binary(bytes), - Tag { - span, - anchor: Some(AnchorLocation::File( - cwd.to_string_lossy().to_string(), - )), - }, - )), - } - } - }, - Err(_) => Err(ShellError::labeled_error( - "File could not be opened", - "file not found", - span, - )), - } - } else { - Err(ShellError::labeled_error( - "File could not be opened", - "file not found", - span, - )) - } - */ } fn convert_via_utf8(