diff --git a/Cargo.lock b/Cargo.lock index 660f439981..5a44529ad8 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" @@ -1335,10 +1479,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] -name = "heim" -version = "0.1.0-beta.2" +name = "globset" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea9164f267a5f4325020b8a989c4b0ab06acc0685ccdb22551f59257fdf296ab" +checksum = "7ad1da430bd7281dde2576f44c84cc3f0f7b475e7202cd503042dff01a8c8120" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + +[[package]] +name = "heim" +version = "0.1.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1014732324a9baf5a691525faabb33909bf6f40dcc2b03c8f2fb07bb01e7e3f" dependencies = [ "heim-common", "heim-cpu", @@ -1372,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", @@ -1456,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", @@ -1740,6 +1897,15 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "kstring" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbbc30beb80d56ddf6346e935c7abcba96329ee5c5a4cde8984a4e6b6f18b58e" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc58422728185d54cd044bba4d45a2ef2a7111a421f84d344f65629949de4f1" +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" @@ -2248,6 +2472,7 @@ dependencies = [ "nu-plugin", "nu-protocol", "nu-source", + "nu-table", "nu-test-support", "nu-value-ext", "num-bigint", @@ -2256,7 +2481,6 @@ dependencies = [ "pin-utils", "pretty-hex", "pretty_env_logger", - "prettytable-rs", "ptree", "query_interface", "quickcheck", @@ -2387,6 +2611,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" @@ -2540,13 +2772,16 @@ name = "nu_plugin_textview" version = "0.15.1" dependencies = [ "ansi_term 0.12.1", + "bat", "crossterm", "nu-build", + "nu-cli", "nu-errors", "nu-plugin", "nu-protocol", "nu-source", "syntect", + "textwrap", "url", ] @@ -2686,6 +2921,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 +3047,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" @@ -2968,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" @@ -3029,6 +3281,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 +3851,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 +4006,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 +4096,7 @@ dependencies = [ "fnv", "lazy_static 1.4.0", "lazycell", + "onig", "plist", "regex-syntax", "serde 1.0.110", @@ -3876,6 +4165,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 +4631,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-cli/Cargo.toml b/crates/nu-cli/Cargo.toml index c55dfa655a..070e8f04bb 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" @@ -61,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/cli.rs b/crates/nu-cli/src/cli.rs index 3b866004c2..97deb47a65 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()); @@ -320,6 +321,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), @@ -349,10 +351,11 @@ pub fn create_default_context( whole_stream_command(AutoenvTrust), whole_stream_command(AutoenvUnTrust), whole_stream_command(Math), - whole_stream_command(Average), - whole_stream_command(Minimum), - whole_stream_command(Maximum), - whole_stream_command(Sum), + whole_stream_command(MathAverage), + whole_stream_command(MathMedian), + whole_stream_command(MathMinimum), + whole_stream_command(MathMaximum), + whole_stream_command(MathSummation), // File format output whole_stream_command(To), whole_stream_command(ToBSON), @@ -739,7 +742,7 @@ fn chomp_newline(s: &str) -> &str { } } -enum LineResult { +pub enum LineResult { Success(String), Error(String, ShellError), CtrlC, @@ -747,7 +750,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.rs b/crates/nu-cli/src/commands.rs index d4b3b1a09c..af2c46f75d 100644 --- a/crates/nu-cli/src/commands.rs +++ b/crates/nu-cli/src/commands.rs @@ -33,6 +33,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; @@ -104,7 +105,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; @@ -156,7 +156,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; @@ -166,6 +165,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; @@ -204,7 +204,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, MathSummation}; pub(crate) use merge::Merge; pub(crate) use mkdir::Mkdir; pub(crate) use mv::Move; @@ -241,7 +241,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/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/cal.rs b/crates/nu-cli/src/commands/cal.rs index bd80c2a18f..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, + }, ] } } @@ -112,90 +123,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,10 +237,7 @@ fn add_month_to_table( }, }; - let day_limit = month_helper.number_of_days_in_month + month_helper.day_number_month_starts_on; - let mut day_count: u32 = 1; - - let days_of_the_week = [ + let mut days_of_the_week = [ "sunday", "monday", "tuesday", @@ -281,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_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 { + while day_number <= day_limit { let mut indexmap = IndexMap::new(); if should_show_year_column { @@ -299,13 +296,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) }; @@ -315,17 +312,17 @@ 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); + (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_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 } @@ -334,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/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/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/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-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/math/average.rs b/crates/nu-cli/src/commands/math/avg.rs similarity index 88% rename from crates/nu-cli/src/commands/math/average.rs rename to crates/nu-cli/src/commands/math/avg.rs index 627a072d1d..23d2357d51 100644 --- a/crates/nu-cli/src/commands/math/average.rs +++ b/crates/nu-cli/src/commands/math/avg.rs @@ -14,15 +14,15 @@ 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 { - "Gets the average of a list of numbers" + "Finds the average of a list of numbers or tables" } async fn run( @@ -49,16 +49,22 @@ 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()]), }] } } 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()).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/command.rs b/crates/nu-cli/src/commands/math/command.rs index 253220665d..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::{ - average::average, max::maximum, min::minimum, 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,31 +68,61 @@ 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)), + 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))], + expected_res: vec![ + Ok(decimal(20)), + Ok(int(10)), + Ok(int(30)), + Ok(int(20)), + 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(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))], + 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))], + expected_res: vec![ + Ok(decimal(-5)), + Ok(decimal(-13.5)), + Ok(int(10)), + Ok(decimal(-11.5)), + Ok(decimal(-15)), + ], }, // TODO-Uncomment once I figure out how to structure tables // TestCase { @@ -116,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]; + 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/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 new file mode 100644 index 0000000000..3748e0108a --- /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::Summation); + 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..f30ada16c5 100644 --- a/crates/nu-cli/src/commands/math/mod.rs +++ b/crates/nu-cli/src/commands/math/mod.rs @@ -1,10 +1,14 @@ -pub mod average; +pub mod avg; pub mod command; pub mod max; +pub mod median; pub mod min; +pub mod sum; pub mod utils; -pub use average::SubCommand as Average; +pub use avg::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; +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/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( 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/src/commands/sort_by.rs b/crates/nu-cli/src/commands/sort_by.rs index cde0c5fc87..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}; @@ -70,15 +71,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( @@ -94,11 +113,11 @@ async fn sort_by( 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| { - rest.iter() + keys.iter() .map(|f| get_data_by_key(item, f.borrow_spanned())) .collect::>>() }; @@ -106,13 +125,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/src/commands/split_by.rs b/crates/nu-cli/src/commands/split_by.rs index 6a35a178c9..d7c299c993 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() { + 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::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/commands/table.rs b/crates/nu-cli/src/commands/table.rs index ea8f3da027..ea63864285 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,188 @@ 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); + + 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, + 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 +241,8 @@ async fn table(args: CommandArgs, registry: &CommandRegistry) -> Result = VecDeque::new(); @@ -113,12 +292,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 +302,3 @@ async fn table(args: CommandArgs, registry: &CommandRegistry) -> Result 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/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/data/config/nuconfig.rs b/crates/nu-cli/src/data/config/nuconfig.rs index 3444e1f7a9..88c7d424fd 100644 --- a/crates/nu-cli/src/data/config/nuconfig.rs +++ b/crates/nu-cli/src/data/config/nuconfig.rs @@ -6,7 +6,7 @@ use parking_lot::Mutex; use std::fmt::Debug; use std::sync::Arc; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct NuConfig { pub vars: Arc>>, } 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..385adf9629 100644 --- a/crates/nu-cli/src/data/value.rs +++ b/crates/nu-cli/src/data/value.rs @@ -7,6 +7,8 @@ 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; +use num_traits::Zero; pub fn date_from_str(s: Tagged<&str>) -> Result { let date = DateTime::parse_from_rfc3339(s.item).map_err(|err| { @@ -34,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, @@ -54,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( @@ -70,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))) @@ -80,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))) @@ -90,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))) @@ -160,10 +183,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/environment.rs b/crates/nu-cli/src/env/environment.rs index bdd52e1cb3..ec24076431 100644 --- a/crates/nu-cli/src/env/environment.rs +++ b/crates/nu-cli/src/env/environment.rs @@ -71,7 +71,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); 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/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/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 cd923e2c19..a7bb6c7027 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; @@ -32,13 +32,15 @@ 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, }; 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-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-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/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()) +} diff --git a/crates/nu-cli/src/utils/data_processing.rs b/crates/nu-cli/src/utils/data_processing.rs index 82966903cf..8b94f9047e 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, @@ -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), @@ -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( @@ -641,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/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/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()); + }) +} 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/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/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/average.rs b/crates/nu-cli/tests/commands/math/avg.rs similarity index 84% rename from crates/nu-cli/tests/commands/average.rs rename to crates/nu-cli/tests/commands/math/avg.rs index 7c1e8a60b8..c139e6aba6 100644 --- a/crates/nu-cli/tests/commands/average.rs +++ b/crates/nu-cli/tests/commands/math/avg.rs @@ -7,11 +7,11 @@ fn can_average_numbers() { r#" open sgml_description.json | get glossary.GlossDiv.GlossList.GlossEntry.Sections - | math average + | math avg | echo $it "# )); - println!("{:?}", actual.err); + assert_eq!(actual.out, "101.5") } @@ -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/median.rs b/crates/nu-cli/tests/commands/math/median.rs new file mode 100644 index 0000000000..90070859c1 --- /dev/null +++ b/crates/nu-cli/tests/commands/math/median.rs @@ -0,0 +1,43 @@ +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") +} + +#[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") +} diff --git a/crates/nu-cli/tests/commands/math.rs b/crates/nu-cli/tests/commands/math/mod.rs similarity index 77% rename from crates/nu-cli/tests/commands/math.rs rename to crates/nu-cli/tests/commands/math/mod.rs index 680b1b641b..136d568723 100644 --- a/crates/nu-cli/tests/commands/math.rs +++ b/crates/nu-cli/tests/commands/math/mod.rs @@ -1,3 +1,6 @@ +mod avg; +mod median; + use nu_test_support::{nu, pipeline}; #[test] @@ -84,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!( diff --git a/crates/nu-cli/tests/commands/sum.rs b/crates/nu-cli/tests/commands/math/sum.rs similarity index 92% rename from crates/nu-cli/tests/commands/sum.rs rename to crates/nu-cli/tests/commands/math/sum.rs index 5d5e440cdf..7395ef6cf8 100644 --- a/crates/nu-cli/tests/commands/sum.rs +++ b/crates/nu-cli/tests/commands/math/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/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/mod.rs b/crates/nu-cli/tests/commands/mod.rs index b5fff2c776..b96e208217 100644 --- a/crates/nu-cli/tests/commands/mod.rs +++ b/crates/nu-cli/tests/commands/mod.rs @@ -13,6 +13,7 @@ mod default; mod drop; mod each; mod enter; +mod every; mod first; mod format; mod get; @@ -47,7 +48,6 @@ mod split_by; mod split_column; mod split_row; mod str_; -mod sum; mod touch; mod trim; mod uniq; diff --git a/crates/nu-cli/tests/commands/skip_until.rs b/crates/nu-cli/tests/commands/skip_until.rs index f901e8b655..1344f49d99 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 "#, )]); @@ -40,11 +40,11 @@ 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 "# )); - assert_eq!(actual.out, "12"); + assert_eq!(actual.out, "6"); }) } 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/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/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-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() + }} + } } 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() 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..19a8ecf6a4 --- /dev/null +++ b/crates/nu-table/src/main.rs @@ -0,0 +1,27 @@ +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 \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], + 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..ebd7edb02f --- /dev/null +++ b/crates/nu-table/src/table.rs @@ -0,0 +1,710 @@ +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, + pub print_top_border: bool, + pub print_bottom_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, + print_top_border: true, + print_bottom_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, + print_top_border: true, + print_bottom_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: false, + + print_left_border: true, + print_right_border: true, + print_top_border: false, + print_bottom_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, + print_top_border: true, + print_bottom_border: true, + } + } +} + +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 { + 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 { + output.push(self.theme.top_left); + } + + 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 { + output.push(self.theme.top_right); + } + } else { + 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 { + output.push(self.theme.middle_left); + } + + 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 { + output.push(self.theme.middle_right); + } + } else { + output.push(self.theme.center); + } + } + } + SeparatorPosition::Bottom => { + for column in self.column_widths.iter().enumerate() { + if column.0 == 0 && self.theme.print_left_border { + output.push(self.theme.bottom_left); + } + for _ in 0..*column.1 { + output.push(self.theme.bottom_horizontal); + } + output.push(self.theme.bottom_horizontal); + output.push(self.theme.bottom_horizontal); + + if column.0 == column_count - 1 { + if self.theme.print_right_border { + output.push(self.theme.bottom_right); + } + } else { + output.push(self.theme.bottom_center); + } + } + } + } + + println!("{}", output); + } + + 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; + } + + if self.theme.print_top_border { + self.print_separator(SeparatorPosition::Top); + } + + if !self.headers.is_empty() { + 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); + } + } else { + first_row = false; + + if self.theme.separate_header && !self.headers.is_empty() { + self.print_separator(SeparatorPosition::Middle); + } + } + + self.print_cell_contents(row); + } + + if self.theme.print_bottom_border { + self.print_separator(SeparatorPosition::Bottom); + } + } +} + +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), + 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), + 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)); + } + + for row in &processed_table.data { + for column in row.iter().enumerate() { + output[column.0] = max(output[column.0], column_width(&column.1.contents)); + } + } + + 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(); + + // 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; + + 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_column_width); + + wrapped_table.new_print_table(); +} + +fn wrap_cells(processed_table: ProcessedTable, max_column_width: usize) -> WrappedTable { + 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 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; + } + 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 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; + } + 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..33baa47c30 --- /dev/null +++ b/crates/nu-table/src/wrap.rs @@ -0,0 +1,236 @@ +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) -> 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: &[Vec>]) -> usize { + let mut max = 0; + + 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; + } + + if total > max { + max = total; + } + } + + max +} + +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>, +) -> (Vec, usize) { + 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, + }); + } + + (output, current_max) +} 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/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"] diff --git a/crates/nu_plugin_textview/Cargo.toml b/crates/nu_plugin_textview/Cargo.toml index da145fac8f..7e4ae79417 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..a1a5dedd0f 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 nu_source::{AnchorLocation, Tag}; use std::path::Path; -enum DrawCommand { - DrawString(Style, String), - NextLine, -} - #[derive(Default)] pub struct TextView; @@ -27,209 +11,139 @@ 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); -} - +#[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.as_ref() { + "term_width" => { + term_width = match value.as_u64() { + Ok(n) => n, + _ => textwrap::termwidth() as u64, + } + } + "tab_width" => { + tab_width = match value.as_u64() { + Ok(n) => n, + _ => 4u64, + } + } + "colored_output" => { + colored_output = match value.as_bool() { + Ok(b) => b, + _ => true, + } + } + "true_color" => { + true_color = match value.as_bool() { + Ok(b) => b, + _ => true, + } + } + "header" => { + header = match value.as_bool() { + Ok(b) => b, + _ => true, + } + } + "line_numbers" => { + line_numbers = match value.as_bool() { + Ok(b) => b, + _ => true, + } + } + "grid" => { + grid = match value.as_bool() { + Ok(b) => b, + _ => true, + } + } + "vcs_modification_markers" => { + vcs_modification_markers = match value.as_bool() { + Ok(b) => b, + _ => true, + } + } + "snip" => { + snip = match value.as_bool() { + Ok(b) => b, + _ => true, + } + } + "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, + } + } + "use_italics" => { + use_italics = match value.as_bool() { + Ok(b) => b, + _ => true, + } + } + "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, + } + } + "pager" => { + pager = match value.as_string() { + Ok(s) => s, + _ => "less".to_string(), + } + } + "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(), + } + } + _ => (), + } + } + } + } + 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 +151,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 +166,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(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"); } _ => { - scroll_view(s); + bat::PrettyPrinter::new() + .input_from_bytes(s.as_bytes()) + .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"); } } } else { - scroll_view(s); + bat::PrettyPrinter::new() + .input_from_bytes(s.as_bytes()) + .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"); } } } 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 │ │ │ │ │ +───┴──────┴─────────┴───────┴────────┴─────────┴───────────┴──────────┴────────┴──────────┴──────── +``` 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 +───┴─────────┴──────┴───────────────── +``` diff --git a/docs/commands/math.md b/docs/commands/math.md index a9eba76aa2..7f7a216f23 100644 --- a/docs/commands/math.md +++ b/docs/commands/math.md @@ -1,97 +1,154 @@ # 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 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`. ## 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 +``` -> ls | get size | math average +```shell +> ls | get size | math avg ───┬──────── - 0 │ 6.5 KB + # │ +───┼──────── + 0 │ 7.2 KB ───┴──────── +``` +```shell > ls | get size | math min ───┬───── - 0 │ 0 B + # │ +───┼───── + 0 │ 0 B ───┴───── -> ls | get size | math max -───┬────────── - 0 │ 106.3 KB -───┴────────── +``` -# Dates +```shell +───┬────────── + # │ +───┼────────── + 0 │ 113.6 KB +───┴────────── +``` + +```shell +> ls | get size | math median +───┬─────── + # │ +───┼─────── + 0 │ 320 B +───┴─────── +``` + +```shell +> ls | get size | math sum +───┬────────── + # │ +───┼────────── + 0 │ 143.6 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 +> pwd | split row / | size ───┬───────┬───────┬───────┬──────────── - # │ lines │ words │ chars │ max length + # │ 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 + 0 │ 0 │ 1 │ 5 │ 5 + 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 -────────────┴─── +────────────┬──── + lines │ 0 + words │ 2 + chars │ 12 + max length │ 12 +────────────┴──── +``` -> pwd | split row / | size | math average +```shell +> pwd | split row / | size | math avg ────────────┬──────── - lines │ 0.0000 - words │ 1.0000 - chars │ 7.0000 - max length │ 7.0000 + lines │ 0.0000 + words │ 1.1666 + chars │ 8.3333 + max length │ 8.3333 ────────────┴──────── ``` -## Errors -`math` functions are aggregation functions so empty lists are invalid +To get the sum of the characters that make up your present working directory. + ```shell -> echo [] | math average +> pwd | split row / | size | get chars | math sum +50 +``` + +## Errors + +`math` functions are aggregation functions so empty lists are invalid + +```shell +> echo [] | math avg error: Error: Unexpected: Cannot perform aggregate math operation on empty data ``` @@ -99,10 +156,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 ``` - - - - 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 deleted file mode 100644 index e87dc65afb..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 | 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 | 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 | sum -error: Unrecognized type in stream: Primitive(String("2509000000")) -- shell:1:0 -1 | open tests/fixtures/formats/caco3_plastics.csv | get tariff_item | sum - | ^^^^ source -``` - -```shell -> open tests/fixtures/formats/caco3_plastics.csv | get tariff_item | str --to-int | sum -━━━━━━━━━━━━━ - -───────────── - 29154639996 -━━━━━━━━━━━━━ -``` diff --git a/docs/commands/textview_config.md b/docs/commands/textview_config.md new file mode 100644 index 0000000000..8d60ffbbdf --- /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 +For a more detailed description of the configuration points that textview uses, please visit the `bat` repo at https://github.com/sharkdp/bat 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 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 "# );