diff --git a/.azure/azure-pipelines.yml b/.azureold/azure-pipelines.yml similarity index 100% rename from .azure/azure-pipelines.yml rename to .azureold/azure-pipelines.yml diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index c5d100a733..0000000000 --- a/.editorconfig +++ /dev/null @@ -1,14 +0,0 @@ -root = true - -[*] -indent_style = space -indent_size = 4 -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = false -end_of_line = lf - -[*.{yml,yaml}] -indent_size = 2 -charset = utf-8 -insert_final_newline = true \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..633198fe0a --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,11 @@ +# Description + +(description of your pull request here) + +# Tests + +Make sure you've run and fixed any issues with these commands: + +- [ ] `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) +- [ ] `cargo clippy --all --all-features -- -D warnings -D clippy::unwrap_used -A clippy::needless_collect` to check that you're using the standard code style +- [ ] `cargo build; cargo test --all --all-features` to check that all the tests pass diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..9b3a066fc4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +on: [pull_request] + +name: Continuous integration + +jobs: + ci: + strategy: + matrix: + platform: [ubuntu-latest, macos-latest, windows-latest] + rust: + - stable + + runs-on: ${{ matrix.platform }} + + steps: + - uses: actions/checkout@v2 + + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + override: true + components: rustfmt, clippy + + - uses: actions-rs/cargo@v1 + with: + command: build + + - uses: actions-rs/cargo@v1 + with: + command: test + args: --all --all-features + + - uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + - uses: actions-rs/cargo@v1 + with: + command: clippy + args: --all --all-features -- -D warnings -D clippy::unwrap_used -A clippy::needless_collect diff --git a/Cargo.lock b/Cargo.lock index 0477555f5a..46de166775 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + [[package]] name = "addr2line" version = "0.17.0" @@ -17,21 +27,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "adler32" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" - [[package]] name = "ahash" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom 0.2.3", + "getrandom 0.2.4", "once_cell", - "version_check", + "version_check 0.9.4", ] [[package]] @@ -59,12 +63,22 @@ dependencies = [ ] [[package]] -name = "ansi_colours" -version = "1.0.4" +name = "ansi-cut" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60e2fb6138a49ad9f1cb3c6d8f8ccbdd5e62b4dab317c1b435a47ecd7da1d28f" +checksum = "ffe8d2994390ae20a3eb52a909f9518a89f8fd7e6990f3d25d38e51021b2c8ce" dependencies = [ - "cc", + "ansi-parser", +] + +[[package]] +name = "ansi-parser" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcb2392079bf27198570d6af79ecbd9ec7d8f16d3ec6b60933922fdb66287127" +dependencies = [ + "heapless 0.5.6", + "nom 4.2.3", ] [[package]] @@ -73,14 +87,20 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] name = "anyhow" -version = "1.0.51" +version = "1.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203" +checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0" + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" [[package]] name = "arrayvec" @@ -108,16 +128,15 @@ dependencies = [ [[package]] name = "arrow2" -version = "0.7.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d873e2775c3d87a4e8d77aa544cbd43f34a0779d5164c59e7c6a1dd0678eb395" +checksum = "d3452b2ae9727464a31a726c07ffec0c0da3b87831610d9ac99fc691c78b3a44" dependencies = [ - "ahash", "arrow-format", "base64", "chrono", "csv", - "futures 0.3.18", + "futures", "hash_hasher", "indexmap", "lexical-core", @@ -131,6 +150,32 @@ dependencies = [ "strength_reduce", ] +[[package]] +name = "as-slice" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45403b49e3954a4b8428a0ac21a4b7afadccf92bfd96273f1a58cd4812496ae0" +dependencies = [ + "generic-array 0.12.4", + "generic-array 0.13.3", + "generic-array 0.14.5", + "stable_deref_trait", +] + +[[package]] +name = "assert_cmd" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ae1ddd39efd67689deb1979d80bad3bf7f2b09c6e6117c8d1f2443b5e2f83e" +dependencies = [ + "bstr", + "doc-comment", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + [[package]] name = "async-stream" version = "0.3.2" @@ -154,9 +199,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.51" +version = "0.1.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e" +checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3" dependencies = [ "proc-macro2", "quote", @@ -171,26 +216,26 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi", "libc", - "winapi 0.3.9", + "winapi", ] [[package]] name = "autocfg" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.63" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321629d8ba6513061f26707241fa9bc89524ff1cd7a915a97ef0c62c666ce1b6" +checksum = "5e121dee8023ce33ab248d9ce1493df03c3b38a659b240096fcbd7048ff9c31f" dependencies = [ "addr2line", "cc", - "cfg-if 1.0.0", + "cfg-if", "libc", - "miniz_oxide 0.4.4", + "miniz_oxide", "object", "rustc-demangle", ] @@ -201,32 +246,6 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" -[[package]] -name = "bat" -version = "0.18.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a069bad29696ecaa51ac79d3eb87abe3b65c7808ab2b3836afd9bd6c4009362" -dependencies = [ - "ansi_colours", - "ansi_term", - "bugreport", - "clircle", - "console", - "content_inspector", - "encoding", - "error-chain", - "git2", - "globset", - "grep-cli", - "path_abs", - "semver 0.11.0", - "serde", - "serde_yaml", - "shell-words", - "syntect", - "unicode-width", -] - [[package]] name = "bigdecimal" version = "0.3.0" @@ -240,19 +259,19 @@ dependencies = [ ] [[package]] -name = "bincode" -version = "1.3.3" +name = "bitflags" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] -name = "bitflags" -version = "1.2.1" +name = "bitmaps" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] [[package]] name = "bitpacking" @@ -263,20 +282,40 @@ dependencies = [ "crunchy", ] +[[package]] +name = "blake2b_simd" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" +dependencies = [ + "arrayref", + "arrayvec 0.5.2", + "constant_time_eq", +] + [[package]] name = "block-buffer" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array", + "generic-array 0.14.5", +] + +[[package]] +name = "block-buffer" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03588e54c62ae6d763e2a80090d50353b785795361b4ff5b3bf0a5097fc31c0b" +dependencies = [ + "generic-array 0.14.5", ] [[package]] name = "brotli" -version = "3.3.2" +version = "3.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71cb90ade945043d3d53597b2fc359bb063db8ade2bcffe7997351d0756e9d50" +checksum = "f838e47a451d5a8fa552371f80024dd6ace9b7acdf25c4c3d0f9bc6816fb1c39" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -293,25 +332,6 @@ dependencies = [ "alloc-stdlib", ] -[[package]] -name = "bson" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff58d466782b57e0001c8e97c6a70c01c2359d7e13e257a83654c0b783ecc139" -dependencies = [ - "ahash", - "base64", - "chrono", - "hex", - "indexmap", - "lazy_static", - "rand 0.8.4", - "serde", - "serde_bytes", - "serde_json", - "uuid", -] - [[package]] name = "bstr" version = "0.2.17" @@ -324,22 +344,11 @@ dependencies = [ "serde", ] -[[package]] -name = "bugreport" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0014b4b2b4f63bfe69c3838470121290cc437fdc79785d408a761a21e8b2404c" -dependencies = [ - "git-version", - "shell-escape", - "sys-info", -] - [[package]] name = "bumpalo" -version = "3.8.0" +version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" [[package]] name = "byte-unit" @@ -350,40 +359,24 @@ dependencies = [ "utf8-width", ] -[[package]] -name = "bytemuck" -version = "1.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72957246c41db82b8ef88a5486143830adeb8227ef9837740bdec67724cf2c5b" - [[package]] name = "byteorder" version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" -[[package]] -name = "bytes" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" -dependencies = [ - "byteorder", - "iovec", -] - -[[package]] -name = "bytes" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" - [[package]] name = "bytes" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +[[package]] +name = "bytesize" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c58ec36aac5066d5ca17df51b3e70279f5670a72102f5752cb7e7c856adfc70" + [[package]] name = "bzip2" version = "0.4.3" @@ -421,10 +414,10 @@ dependencies = [ ] [[package]] -name = "cassowary" -version = "0.3.0" +name = "capnp" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" +checksum = "16c262726f68118392269a3f7a5546baf51dcfe5cb3c3f0957b502106bf1a065" [[package]] name = "cc" @@ -435,12 +428,6 @@ dependencies = [ "jobserver", ] -[[package]] -name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" - [[package]] name = "cfg-if" version = "1.0.0" @@ -457,8 +444,8 @@ dependencies = [ "num-integer", "num-traits", "serde", - "time 0.1.44", - "winapi 0.3.9", + "time", + "winapi", ] [[package]] @@ -481,26 +468,25 @@ dependencies = [ ] [[package]] -name = "clipboard-win" -version = "4.2.2" +name = "chrono-tz" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db8340083d28acb43451166543b98c838299b7e0863621be53a338adceea0ed" +checksum = "58549f1842da3080ce63002102d5bc954c7bc843d4f47818e642abdc36253552" dependencies = [ - "error-code", - "str-buf", - "winapi 0.3.9", + "chrono", + "chrono-tz-build", + "phf 0.10.1", ] [[package]] -name = "clircle" -version = "0.3.0" +name = "chrono-tz-build" +version = "0.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e68bbd985a63de680ab4d1ad77b6306611a8f961b282c8b5ab513e6de934e396" +checksum = "db058d493fb2f65f41861bfed7e3fe6335264a9f0f92710cab5bdf01fef09069" dependencies = [ - "cfg-if 1.0.0", - "libc", - "serde", - "winapi 0.3.9", + "parse-zoneinfo", + "phf 0.10.1", + "phf_codegen 0.10.0", ] [[package]] @@ -512,53 +498,19 @@ dependencies = [ "encoding_rs", ] -[[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "serde", - "termcolor", - "unicode-width", -] - -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - -[[package]] -name = "comfy-table" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a488ea8a8e295a53c7a4514b78a2e54bcff33adf99c15aced97b2a2062d4f8" -dependencies = [ - "crossterm", - "strum", - "strum_macros", -] - -[[package]] -name = "common-path" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2382f75942f4b3be3690fe4f86365e9c853c1587d6ee58212cebf6e2a9ccd101" - [[package]] name = "console" -version = "0.14.1" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3993e6445baa160675931ec041a5e03ca84b9c6e32a056150d3aa2bdda0a1f45" +checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31" dependencies = [ "encode_unicode", - "lazy_static", "libc", + "once_cell", "regex", "terminal_size", "unicode-width", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -568,13 +520,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb58b6451e8c2a812ad979ed1d83378caa5e927eef2622017a45f251457c2c9d" [[package]] -name = "content_inspector" -version = "0.2.4" +name = "constant_time_eq" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38" -dependencies = [ - "memchr", -] +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" [[package]] name = "convert_case" @@ -584,9 +533,9 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" [[package]] name = "core-foundation" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" dependencies = [ "core-foundation-sys", "libc", @@ -609,20 +558,20 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.3.0" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "738c290dfaea84fc1ca15ad9c168d083b05a714e1efddd8edaab678dc28d2836" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" +checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "crossbeam-utils", ] @@ -632,18 +581,18 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.5" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec02e091aa634e2c3ada4a392989e7c3116673ef0ac5b72232439094d73b7fd" +checksum = "c00d6d2ea26e8b151d99093005cb442fb9a37aeaca582a03ec70946f49ab5ed9" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "crossbeam-utils", "lazy_static", "memoffset", @@ -652,37 +601,38 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.5" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" +checksum = "b5e5bed1f1c269533fa816a0a5492b3545209a205ca1a54842be180eb63a16a6" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "lazy_static", ] [[package]] name = "crossterm" -version = "0.19.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c36c10130df424b2f3552fcc2ddcd9b28a27b1e54b358b45874f88d1ca6888c" +checksum = "c85525306c4291d1b73ce93c8acf9c339f9b213aef6c1d85c3830cbf1c16325c" dependencies = [ "bitflags", "crossterm_winapi", - "lazy_static", "libc", "mio", "parking_lot", + "serde", "signal-hook", - "winapi 0.3.9", + "signal-hook-mio", + "winapi", ] [[package]] name = "crossterm_winapi" -version = "0.7.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0da8964ace4d3e4a044fd027919b2237000b24315a37c916f61809f1ff2140b9" +checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] @@ -692,13 +642,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] -name = "crypto-mac" -version = "0.11.1" +name = "crypto-common" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +checksum = "683d6b536309245c849479fba3da410962a43ed8e51c26b729208ec0ac2798d0" dependencies = [ - "generic-array", - "subtle", + "generic-array 0.14.5", ] [[package]] @@ -709,9 +658,9 @@ checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" dependencies = [ "cssparser-macros", "dtoa-short", - "itoa", + "itoa 0.4.8", "matches", - "phf", + "phf 0.8.0", "proc-macro2", "quote", "smallvec", @@ -730,9 +679,9 @@ dependencies = [ [[package]] name = "cstr_core" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ba9efe9e1e736671d5a03f006afc4e7e3f32503e2077e0bcaf519c0c8c1d3" +checksum = "644828c273c063ab0d39486ba42a5d1f3a499d35529c759e763a9c6cb8a0fb08" dependencies = [ "cty", "memchr", @@ -746,7 +695,7 @@ checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" dependencies = [ "bstr", "csv-core", - "itoa", + "itoa 0.4.8", "ryu", "serde", ] @@ -761,13 +710,23 @@ dependencies = [ ] [[package]] -name = "ctrlc" -version = "3.2.0" +name = "ctor" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "377c9b002a72a0b2c1a18c62e2f3864bdfea4a015e3683a96e24aa45dd6c02d1" +checksum = "ccc0a48a9b826acdf4028595adc9db92caea352f7af011a3034acd172a52a0aa" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "ctrlc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19c6cedffdc8c03a3346d723eb20bd85a13362bb96dc2ac000842c6381ec7bf" dependencies = [ "nix", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -776,27 +735,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" -[[package]] -name = "deflate" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" -dependencies = [ - "adler32", - "byteorder", -] - -[[package]] -name = "derive-new" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "derive_more" version = "0.99.17" @@ -806,34 +744,70 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "rustc_version 0.4.0", + "rustc_version", "syn", ] +[[package]] +name = "dialoguer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61579ada4ec0c6031cfac3f86fdba0d195a7ebeb5e36693bd53cb5999a25beeb" +dependencies = [ + "console", + "lazy_static", + "tempfile", + "zeroize", +] + +[[package]] +name = "diff" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "digest" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array", + "generic-array 0.14.5", ] [[package]] -name = "directories-next" -version = "2.0.0" +name = "digest" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +checksum = "b697d66081d42af4fba142d56918a3cb21dc8eb63372c6b85d14f44fb9c5979b" dependencies = [ - "cfg-if 1.0.0", - "dirs-sys-next", + "block-buffer 0.10.1", + "crypto-common", + "generic-array 0.14.5", ] [[package]] name = "dirs" -version = "3.0.2" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" +checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" +dependencies = [ + "libc", + "redox_users 0.3.5", + "winapi", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" dependencies = [ "dirs-sys", ] @@ -844,7 +818,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "dirs-sys-next", ] @@ -855,8 +829,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" dependencies = [ "libc", - "redox_users", - "winapi 0.3.9", + "redox_users 0.4.0", + "winapi", ] [[package]] @@ -866,8 +840,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", - "redox_users", - "winapi 0.3.9", + "redox_users 0.4.0", + "winapi", ] [[package]] @@ -898,7 +872,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13276c5dbd7f365e00efe6631242772fe6615e1899df84d1f6ce3ae7b48209f6" dependencies = [ "chrono", - "chrono-tz", + "chrono-tz 0.5.3", "lazy_static", "num-traits", "rust_decimal", @@ -910,12 +884,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453440c271cf5577fd2a40e4942540cb7d0d2f85e27c8d07dd0023c925a67541" -[[package]] -name = "dyn-clone" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf" - [[package]] name = "ego-tree" version = "0.6.2" @@ -943,85 +911,15 @@ 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.29" +version = "0.8.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746" +checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] -[[package]] -name = "endian-type" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" - [[package]] name = "env_logger" version = "0.7.1" @@ -1046,52 +944,35 @@ dependencies = [ ] [[package]] -name = "error-chain" -version = "0.12.4" +name = "erased-serde" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +checksum = "56047058e1ab118075ca22f9ecd737bcc961aa3566a3019cb71388afa280bd8a" dependencies = [ - "version_check", + "serde", ] [[package]] -name = "error-code" -version = "2.3.0" +name = "errno" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5115567ac25674e0043e472be13d14e537f37ea8aa4bdc4aef0c89add1db1ff" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" dependencies = [ + "errno-dragonfly", "libc", - "str-buf", + "winapi", ] [[package]] -name = "failure" -version = "0.1.8" +name = "errno-dragonfly" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" dependencies = [ - "backtrace", - "failure_derive", + "cc", + "libc", ] -[[package]] -name = "failure_derive" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "fallible-iterator" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" - [[package]] name = "fallible-streaming-iterator" version = "0.1.9" @@ -1099,13 +980,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] -name = "fd-lock" -version = "3.0.1" +name = "fastrand" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfc110fe50727d46a428eed832df40affe9bf74d077cac1bf3f2718e823f14c5" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" dependencies = [ - "cfg-if 1.0.0", - "libc", + "instant", +] + +[[package]] +name = "fd-lock" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcef756dea9cf3db5ce73759cf0467330427a786b47711b8d6c97620d718ceb9" +dependencies = [ + "cfg-if", + "rustix", "windows-sys", ] @@ -1115,7 +1005,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12d741e2415d4e2e5bd1c1d00409d1a8865a57892c2d689b504365655d237d43" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] @@ -1135,10 +1025,10 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "crc32fast", "libc", - "miniz_oxide 0.4.4", + "miniz_oxide", ] [[package]] @@ -1172,17 +1062,11 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs_extra" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" - [[package]] name = "futf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c9c1ce3fa9336301af935ab852c437817d14cd33690446569392e65170aac3b" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" dependencies = [ "mac", "new_debug_unreachable", @@ -1190,15 +1074,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.1.31" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" - -[[package]] -name = "futures" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd0210d8c325c245ff06fd95a3b13689a1a276ac8cfa8e8720cb840bfb84b9e" +checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" dependencies = [ "futures-channel", "futures-core", @@ -1211,9 +1089,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.18" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fc8cd39e3dbf865f7340dce6a2d401d24fd37c6fe6c4f0ee0de8bfca2252d27" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" dependencies = [ "futures-core", "futures-sink", @@ -1221,15 +1099,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.18" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" [[package]] name = "futures-executor" -version = "0.3.18" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" dependencies = [ "futures-core", "futures-task", @@ -1238,15 +1116,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.18" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e481354db6b5c353246ccf6a728b0c5511d752c08da7260546fc0933869daa11" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" [[package]] name = "futures-macro" -version = "0.3.18" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89f17b21645bc4ed773c69af9c9a0effd4a3f1a3876eadd453469f8854e7fdd" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" dependencies = [ "proc-macro2", "quote", @@ -1255,23 +1133,22 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.18" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "996c6442437b62d21a32cd9906f9c41e7dc1e19a9579843fad948696769305af" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" [[package]] name = "futures-task" -version = "0.3.18" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12" +checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" [[package]] name = "futures-util" -version = "0.3.18" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" dependencies = [ - "futures 0.1.31", "futures-channel", "futures-core", "futures-io", @@ -1282,7 +1159,6 @@ dependencies = [ "pin-project-lite", "pin-utils", "slab", - "tokio-io", ] [[package]] @@ -1296,12 +1172,30 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.4" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" dependencies = [ "typenum", - "version_check", +] + +[[package]] +name = "generic-array" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f797e67af32588215eaaab8327027ee8e71b9dd0b2b26996aedf20c030fce309" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +dependencies = [ + "typenum", + "version_check 0.9.4", ] [[package]] @@ -1319,18 +1213,18 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "wasi 0.9.0+wasi-snapshot-preview1", ] [[package]] name = "getrandom" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "wasi 0.10.0+wasi-snapshot-preview1", ] @@ -1348,33 +1242,22 @@ dependencies = [ ] [[package]] -name = "gimli" -version = "0.26.1" +name = "ghost" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" - -[[package]] -name = "git-version" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6b0decc02f4636b9ccad390dcbe77b722a77efedfa393caf8379a51d5c61899" +checksum = "1a5bcf1bbeab73aa4cf2fde60a846858dc036163c7c33bec309f8d17de785479" dependencies = [ - "git-version-macro", - "proc-macro-hack", -] - -[[package]] -name = "git-version-macro" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe69f1cbdb6e28af2bac214e943b99ce8a0a06b447d15d3e61161b0423139f3f" -dependencies = [ - "proc-macro-hack", "proc-macro2", "quote", "syn", ] +[[package]] +name = "gimli" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" + [[package]] name = "git2" version = "0.13.25" @@ -1385,6 +1268,8 @@ dependencies = [ "libc", "libgit2-sys", "log", + "openssl-probe", + "openssl-sys", "url", ] @@ -1400,43 +1285,13 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" -[[package]] -name = "globset" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd" -dependencies = [ - "aho-corasick", - "bstr", - "fnv", - "log", - "regex", -] - -[[package]] -name = "grep-cli" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dd110c34bb4460d0de5062413b773e385cbf8a85a63fc535590110a09e79e8a" -dependencies = [ - "atty", - "bstr", - "globset", - "lazy_static", - "log", - "regex", - "same-file", - "termcolor", - "winapi-util", -] - [[package]] name = "h2" -version = "0.3.7" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fd819562fcebdac5afc5c113c3ec36f902840b70fd4fc458799c8ce4607ae55" +checksum = "d9f1f717ddc7b2ba36df7e871fd88db79326551d3d6f1fc406fbfd28b582ff8e" dependencies = [ - "bytes 1.1.0", + "bytes", "fnv", "futures-core", "futures-sink", @@ -1459,6 +1314,15 @@ dependencies = [ "regex", ] +[[package]] +name = "hash32" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4041af86e63ac4298ce40e5cca669066e75b6f1aa3390fe2561ffa5e1d9f4cc" +dependencies = [ + "byteorder", +] + [[package]] name = "hash32" version = "0.2.1" @@ -1485,21 +1349,24 @@ dependencies = [ ] [[package]] -name = "hashlink" -version = "0.7.0" +name = "heapless" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +checksum = "74911a68a1658cfcfb61bc0ccfbd536e3b6e906f8c2f7883ee50157e3e2184f1" dependencies = [ - "hashbrown", + "as-slice", + "generic-array 0.13.3", + "hash32 0.1.1", + "stable_deref_trait", ] [[package]] name = "heapless" -version = "0.7.8" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c1ad878e07405df82b695089e63d278244344f80e764074d0bdfe99b89460f3" +checksum = "d076121838e03f862871315477528debffdb7462fb229216ecef91b1a3eb31eb" dependencies = [ - "hash32", + "hash32 0.2.1", "spin", "stable_deref_trait", ] @@ -1513,12 +1380,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "heck" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" - [[package]] name = "hermit-abi" version = "0.1.19" @@ -1534,25 +1395,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hmac" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" -dependencies = [ - "crypto-mac", - "digest", -] - -[[package]] -name = "hmac-sha1" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1333fad8d94b82cab989da428b0b36a3435db3870d85e971a1d6dc0a8576722" -dependencies = [ - "sha1", -] - [[package]] name = "html5ever" version = "0.25.1" @@ -1575,13 +1417,13 @@ checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" [[package]] name = "http" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b" +checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" dependencies = [ - "bytes 1.1.0", + "bytes", "fnv", - "itoa", + "itoa 1.0.1", ] [[package]] @@ -1590,7 +1432,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" dependencies = [ - "bytes 1.1.0", + "bytes", "http", "pin-project-lite", ] @@ -1618,11 +1460,11 @@ dependencies = [ [[package]] name = "hyper" -version = "0.14.15" +version = "0.14.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436ec0091e4f20e655156a30a0df3770fe2900aa301e548e08446ec794b6953c" +checksum = "b7ec3e62bdc98a2f0393a5048e4c30ef659440ea6e0e572965103e72bd836f55" dependencies = [ - "bytes 1.1.0", + "bytes", "futures-channel", "futures-core", "futures-util", @@ -1631,7 +1473,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa", + "itoa 0.4.8", "pin-project-lite", "socket2", "tokio", @@ -1646,7 +1488,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ - "bytes 1.1.0", + "bytes", "hyper", "native-tls", "tokio", @@ -1674,54 +1516,37 @@ dependencies = [ ] [[package]] -name = "image" -version = "0.23.14" +name = "im" +version = "15.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" +checksum = "111c1983f3c5bb72732df25cddacee9b546d08325fb584b5ebd38148be7b0246" dependencies = [ - "bytemuck", - "byteorder", - "color_quant", - "jpeg-decoder", - "num-iter", - "num-rational 0.3.2", - "num-traits", - "png", + "bitmaps", + "rand_core 0.5.1", + "rand_xoshiro", + "sized-chunks", + "typenum", + "version_check 0.9.4", ] [[package]] name = "indexmap" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" dependencies = [ "autocfg", "hashbrown", "serde", ] -[[package]] -name = "insta" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15226a375927344c78d39dc6b49e2d5562a5b0705e26a589093c6792e52eed8e" -dependencies = [ - "console", - "lazy_static", - "serde", - "serde_json", - "serde_yaml", - "similar", - "uuid", -] - [[package]] name = "instant" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -1735,12 +1560,22 @@ dependencies = [ ] [[package]] -name = "iovec" -version = "0.1.4" +name = "inventory" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +checksum = "ce6b5d8c669bfbad811d95ddd7a1c6cf9cfdbf2777e59928b6f3fa8ff54f72a0" dependencies = [ - "libc", + "ctor", + "ghost", +] + +[[package]] +name = "io-lifetimes" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ef6787e7f0faedc040f95716bdd0e62bcfcf4ba93da053b62dea2691c13864" +dependencies = [ + "winapi", ] [[package]] @@ -1749,6 +1584,12 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" +[[package]] +name = "is_ci" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" + [[package]] name = "is_debug" version = "1.0.1" @@ -1761,14 +1602,14 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa9acdc6d67b75e626ad644734e8bc6df893d9cd2a834129065d3dd6158ea9c8" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] name = "itertools" -version = "0.10.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" +checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" dependencies = [ "either", ] @@ -1779,6 +1620,12 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + [[package]] name = "jobserver" version = "0.1.24" @@ -1788,17 +1635,11 @@ dependencies = [ "libc", ] -[[package]] -name = "jpeg-decoder" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" - [[package]] name = "js-sys" -version = "0.3.55" +version = "0.3.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04" dependencies = [ "wasm-bindgen", ] @@ -1809,12 +1650,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "lexical" version = "6.0.1" @@ -1890,9 +1725,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.112" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" +checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c" [[package]] name = "libgit2-sys" @@ -1902,23 +1737,38 @@ checksum = "19e1c899248e606fbfe68dcb31d8b0176ebab833b103824af31bddf4b7457494" dependencies = [ "cc", "libc", + "libssh2-sys", "libz-sys", + "openssl-sys", "pkg-config", ] [[package]] name = "libm" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" +checksum = "33a33a362ce288760ec6a508b94caaec573ae7d3bbbd91b87aa0bad4456839db" [[package]] -name = "libsqlite3-sys" -version = "0.23.1" +name = "libproc" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abd5850c449b40bacb498b2bbdfaff648b1b055630073ba8db499caf2d0ea9f2" +checksum = "6466fc1f834276563fbbd4be1c24236ef92bb9efdbd4691e07f1cf85a0b407f0" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "libssh2-sys" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b094a36eb4b8b8c8a7b4b8ae43b2944502be3e59cd87687595cf6b0a71b3f4ca" dependencies = [ "cc", + "libc", + "libz-sys", + "openssl-sys", "pkg-config", "vcpkg", ] @@ -1935,15 +1785,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "line-wrap" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" -dependencies = [ - "safemem", -] - [[package]] name = "linked-hash-map" version = "0.5.4" @@ -1955,10 +1796,16 @@ dependencies = [ ] [[package]] -name = "lock_api" -version = "0.4.5" +name = "linux-raw-sys" +version = "0.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +checksum = "95f5690fef754d905294c56f7ac815836f2513af966aa47f2e07ac79be07827f" + +[[package]] +name = "lock_api" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" dependencies = [ "scopeguard", ] @@ -1969,7 +1816,17 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", +] + +[[package]] +name = "lscolors" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd58d8727f3035fa6d5272f16b519741fd4875936b99d8a7cde21291b7d9174" +dependencies = [ + "ansi_term", + "crossterm", ] [[package]] @@ -2014,8 +1871,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd" dependencies = [ "log", - "phf", - "phf_codegen", + "phf 0.8.0", + "phf_codegen 0.8.0", "string_cache", "string_cache_codegen", "tendril", @@ -2029,21 +1886,13 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "md-5" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" +checksum = "e6a38fc55c8bbc10058782919516f88826e70320db6d206aebc49611d24216ae" dependencies = [ - "block-buffer", - "digest", - "opaque-debug", + "digest 0.10.1", ] -[[package]] -name = "md5" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" - [[package]] name = "memchr" version = "2.4.1" @@ -2052,18 +1901,18 @@ checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" [[package]] name = "memmap2" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4647a11b578fead29cdbb34d4adef8dd3dc35b876c9c6d5240d83f205abfe96e" +checksum = "fe3179b85e1fd8b14447cbebadb75e45a1002f541b925f0bfec366d56a81c56d" dependencies = [ "libc", ] [[package]] name = "memoffset" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" dependencies = [ "autocfg", ] @@ -2075,7 +1924,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f79496a5651c8d57cd033c5add8ca7ee4e3d5f7587a4777484640d9cb60392d9" dependencies = [ "fnv", - "nom", + "nom 1.2.4", +] + +[[package]] +name = "miette" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd2adcfcced5d625bf90a958a82ae5b93231f57f3df1383fee28c9b5096d35ed" +dependencies = [ + "atty", + "backtrace", + "miette-derive", + "once_cell", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "thiserror", +] + +[[package]] +name = "miette-derive" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c01a8b61312d367ce87956bb686731f87e4c6dd5dbc550e8f06e3c24fb1f67f" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2084,25 +1963,6 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" -[[package]] -name = "mime_guess" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" -dependencies = [ - "mime", - "unicase", -] - -[[package]] -name = "miniz_oxide" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" -dependencies = [ - "adler32", -] - [[package]] name = "miniz_oxide" version = "0.4.4" @@ -2123,7 +1983,7 @@ dependencies = [ "log", "miow", "ntapi", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -2132,21 +1992,7 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" dependencies = [ - "winapi 0.3.9", -] - -[[package]] -name = "mp4" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85660d4d88b9318d95396943adc4a254b3ed8bf1de917e6f093abda1ccf0bec0" -dependencies = [ - "byteorder", - "bytes 0.5.6", - "num-rational 0.3.2", - "serde", - "serde_json", - "thiserror", + "winapi", ] [[package]] @@ -2187,44 +2033,21 @@ dependencies = [ "tempfile", ] -[[package]] -name = "neso" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b3c31defbcb081163db18437fd88c2a267cb3e26f7bd5e4b186e4b1b38fe8c8" -dependencies = [ - "bincode", - "cfg-if 0.1.10", - "log", - "serde", - "serde_derive", - "wasm-bindgen", -] - [[package]] name = "new_debug_unreachable" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" -[[package]] -name = "nibble_vec" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" -dependencies = [ - "smallvec", -] - [[package]] name = "nix" -version = "0.22.2" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3bb9a13fa32bc5aeb64150cd3f32d6cf4c748f8f8a417cce5d2eb976a8370ba" +checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" dependencies = [ "bitflags", "cc", - "cfg-if 1.0.0", + "cfg-if", "libc", "memoffset", ] @@ -2241,170 +2064,170 @@ version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce" +[[package]] +name = "nom" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" +dependencies = [ + "memchr", + "version_check 0.1.5", +] + [[package]] name = "ntapi" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] name = "nu" -version = "0.44.0" +version = "0.59.0" dependencies = [ - "ctrlc", - "futures 0.3.18", - "hamcrest2", - "itertools", - "nu-cli", - "nu-command", - "nu-completion", - "nu-data", - "nu-engine", - "nu-errors", - "nu-parser", - "nu-path", - "nu-plugin", - "nu-protocol", - "nu-source", - "nu-test-support", - "nu-value-ext", - "nu_plugin_binaryview", - "nu_plugin_chart", - "nu_plugin_from_bson", - "nu_plugin_from_sqlite", - "nu_plugin_inc", - "nu_plugin_match", - "nu_plugin_query_json", - "nu_plugin_s3", - "nu_plugin_selector", - "nu_plugin_start", - "nu_plugin_textview", - "nu_plugin_to_bson", - "nu_plugin_to_sqlite", - "nu_plugin_tree", - "nu_plugin_xpath", - "rstest", - "serial_test", -] - -[[package]] -name = "nu-ansi-term" -version = "0.44.0" -dependencies = [ - "doc-comment", - "overload", - "regex", - "serde", - "serde_json", - "winapi 0.3.9", -] - -[[package]] -name = "nu-cli" -version = "0.44.0" -dependencies = [ - "ctrlc", - "indexmap", - "lazy_static", - "log", - "nu-ansi-term", - "nu-command", - "nu-completion", - "nu-data", - "nu-engine", - "nu-errors", - "nu-parser", - "nu-path", - "nu-protocol", - "nu-source", - "nu-stream", - "pretty_env_logger", - "rustyline", - "serde", - "serde_yaml", - "shadow-rs", - "strip-ansi-escapes", -] - -[[package]] -name = "nu-command" -version = "0.44.0" -dependencies = [ - "base64", - "bigdecimal", - "calamine", - "chrono", - "chrono-tz", + "assert_cmd", "crossterm", - "csv", + "crossterm_winapi", "ctrlc", - "derive-new", - "digest", - "dirs-next", - "dtparse", - "eml-parser", - "encoding_rs", - "filesize", - "futures 0.3.18", - "glob", "hamcrest2", - "heck 0.4.0", - "htmlescape", - "ical", - "indexmap", "itertools", - "lazy_static", "log", - "md-5", - "meval", - "mime", + "miette", "nu-ansi-term", - "nu-data", + "nu-cli", + "nu-color-config", + "nu-command", "nu-engine", - "nu-errors", "nu-json", "nu-parser", "nu-path", "nu-plugin", "nu-pretty-hex", "nu-protocol", - "nu-serde", - "nu-source", - "nu-stream", + "nu-system", "nu-table", + "nu-term-grid", "nu-test-support", - "nu-value-ext", - "num-bigint 0.4.3", - "num-format", - "num-traits", - "parking_lot", + "nu_plugin_example", + "nu_plugin_gstat", + "nu_plugin_inc", + "nu_plugin_query", + "pretty_assertions", + "pretty_env_logger", + "reedline", + "rstest", + "serial_test", + "tempfile", +] + +[[package]] +name = "nu-ansi-term" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8afa9b5ba9e7ea9898e119244372cac911bea31ee7a5de42f51bbc36dc66318" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "nu-cli" +version = "0.59.0" +dependencies = [ + "is_executable", + "log", + "miette", + "nu-ansi-term", + "nu-color-config", + "nu-engine", + "nu-parser", + "nu-path", + "nu-protocol", + "reedline", + "thiserror", +] + +[[package]] +name = "nu-color-config" +version = "0.59.0" +dependencies = [ + "nu-ansi-term", + "nu-json", + "nu-protocol", + "nu-table", + "serde", +] + +[[package]] +name = "nu-command" +version = "0.59.0" +dependencies = [ + "Inflector", + "base64", + "bytesize", + "calamine", + "chrono", + "chrono-humanize", + "chrono-tz 0.6.1", + "crossterm", + "csv", + "dialoguer", + "digest 0.10.1", + "dirs-next", + "dtparse", + "eml-parser", + "encoding_rs", + "filesize", + "glob", + "hamcrest2", + "htmlescape", + "ical", + "indexmap", + "itertools", + "lazy_static", + "log", + "lscolors", + "md-5", + "meval", + "mime", + "nu-ansi-term", + "nu-color-config", + "nu-engine", + "nu-json", + "nu-parser", + "nu-path", + "nu-pretty-hex", + "nu-protocol", + "nu-system", + "nu-table", + "nu-term-grid", + "nu-test-support", + "num 0.4.0", + "pathdiff", "polars", "quick-xml 0.22.0", "quickcheck", "quickcheck_macros", "rand 0.8.4", + "rayon", + "reedline", "regex", "reqwest", "roxmltree", "rust-embed", - "rustyline", "serde", "serde_ini", - "serde_json", "serde_urlencoded", "serde_yaml", - "sha2", + "sha2 0.10.1", "shadow-rs", "strip-ansi-escapes", "sysinfo", - "term", - "term_size", + "terminal_size", "thiserror", "titlecase", - "tokio", "toml", "trash", "umask", @@ -2416,124 +2239,24 @@ dependencies = [ "zip", ] -[[package]] -name = "nu-completion" -version = "0.44.0" -dependencies = [ - "indexmap", - "is_executable", - "nu-data", - "nu-engine", - "nu-parser", - "nu-path", - "nu-protocol", - "nu-source", - "nu-test-support", - "parking_lot", -] - -[[package]] -name = "nu-data" -version = "0.44.0" -dependencies = [ - "bigdecimal", - "byte-unit", - "chrono", - "common-path", - "derive-new", - "directories-next", - "getset", - "indexmap", - "log", - "nu-ansi-term", - "nu-errors", - "nu-path", - "nu-protocol", - "nu-source", - "nu-table", - "nu-test-support", - "nu-value-ext", - "num-bigint 0.4.3", - "num-format", - "num-traits", - "serde", - "sha2", - "sys-locale", - "toml", -] - [[package]] name = "nu-engine" -version = "0.44.0" +version = "0.59.0" dependencies = [ - "bigdecimal", - "bytes 1.1.0", "chrono", - "codespan-reporting", - "derive-new", - "dirs-next", - "encoding_rs", - "filesize", - "fs_extra", - "getset", "glob", - "hamcrest2", - "indexmap", "itertools", - "lazy_static", - "log", - "nu-ansi-term", - "nu-data", - "nu-errors", - "nu-parser", "nu-path", - "nu-plugin", "nu-protocol", - "nu-source", - "nu-stream", - "nu-test-support", - "nu-value-ext", - "num-bigint 0.4.3", - "parking_lot", - "rayon", - "serde", - "serde_json", - "tempfile", - "term_size", - "termcolor", - "trash", - "umask", - "users", - "which", -] - -[[package]] -name = "nu-errors" -version = "0.44.0" -dependencies = [ - "bigdecimal", - "codespan-reporting", - "derive-new", - "getset", - "glob", - "nu-ansi-term", - "nu-source", - "num-bigint 0.4.3", - "num-traits", - "serde", - "serde_json", - "serde_yaml", - "toml", ] [[package]] name = "nu-json" -version = "0.44.0" +version = "0.59.0" dependencies = [ "lazy_static", "linked-hash-map", "nu-path", - "nu-test-support", "num-traits", "regex", "serde", @@ -2542,26 +2265,20 @@ dependencies = [ [[package]] name = "nu-parser" -version = "0.44.0" +version = "0.59.0" dependencies = [ - "bigdecimal", - "derive-new", - "indexmap", - "itertools", "log", - "nu-data", - "nu-errors", + "miette", "nu-path", + "nu-plugin", "nu-protocol", - "nu-source", - "nu-test-support", - "num-bigint 0.4.3", - "smart-default", + "serde_json", + "thiserror", ] [[package]] name = "nu-path" -version = "0.44.0" +version = "0.59.0" dependencies = [ "dirs-next", "dunce", @@ -2569,95 +2286,83 @@ dependencies = [ [[package]] name = "nu-plugin" -version = "0.44.0" +version = "0.59.0" dependencies = [ - "indexmap", - "nu-errors", + "capnp", + "nu-engine", "nu-protocol", - "nu-source", - "nu-test-support", - "nu-value-ext", "serde", "serde_json", ] [[package]] name = "nu-pretty-hex" -version = "0.44.0" +version = "0.59.0" dependencies = [ - "heapless", + "heapless 0.7.10", "nu-ansi-term", "rand 0.8.4", ] [[package]] name = "nu-protocol" -version = "0.44.0" +version = "0.59.0" dependencies = [ - "bigdecimal", "byte-unit", "chrono", "chrono-humanize", - "derive-new", - "getset", + "im", "indexmap", - "log", - "nu-errors", - "nu-source", - "num-bigint 0.4.3", - "num-integer", - "num-traits", - "polars", - "serde", - "serde_bytes", -] - -[[package]] -name = "nu-serde" -version = "0.44.0" -dependencies = [ - "bigdecimal", - "insta", - "nu-protocol", - "nu-source", + "miette", + "nu-json", + "num-format", "serde", + "serde_json", + "sys-locale", "thiserror", + "typetag", ] [[package]] -name = "nu-source" -version = "0.44.0" +name = "nu-system" +version = "0.59.0" dependencies = [ - "derive-new", - "getset", - "pretty", - "serde", - "termcolor", -] - -[[package]] -name = "nu-stream" -version = "0.44.0" -dependencies = [ - "nu-errors", - "nu-protocol", - "nu-source", + "chrono", + "errno", + "libc", + "libproc", + "ntapi", + "once_cell", + "procfs", + "users", + "which", + "winapi", ] [[package]] name = "nu-table" -version = "0.44.0" +version = "0.59.0" dependencies = [ + "ansi-cut", "atty", "nu-ansi-term", + "nu-protocol", "regex", "strip-ansi-escapes", "unicode-width", ] +[[package]] +name = "nu-term-grid" +version = "0.59.0" +dependencies = [ + "strip-ansi-escapes", + "unicode-width", +] + [[package]] name = "nu-test-support" -version = "0.44.0" +version = "0.59.0" dependencies = [ "bigdecimal", "chrono", @@ -2665,226 +2370,48 @@ dependencies = [ "glob", "hamcrest2", "indexmap", - "nu-errors", "nu-path", "nu-protocol", - "nu-source", "num-bigint 0.4.3", "tempfile", ] [[package]] -name = "nu-value-ext" -version = "0.44.0" +name = "nu_plugin_example" +version = "0.59.0" dependencies = [ - "indexmap", - "itertools", - "nu-errors", + "nu-plugin", "nu-protocol", - "nu-source", - "num-traits", ] [[package]] -name = "nu_plugin_binaryview" -version = "0.44.0" +name = "nu_plugin_gstat" +version = "0.59.0" dependencies = [ - "crossterm", - "image", - "neso", - "nu-ansi-term", - "nu-errors", - "nu-plugin", - "nu-pretty-hex", - "nu-protocol", - "nu-source", - "rawkey", -] - -[[package]] -name = "nu_plugin_chart" -version = "0.44.0" -dependencies = [ - "crossterm", - "nu-data", - "nu-errors", + "git2", + "nu-engine", "nu-plugin", "nu-protocol", - "nu-source", - "nu-value-ext", - "tui", -] - -[[package]] -name = "nu_plugin_from_bson" -version = "0.44.0" -dependencies = [ - "bigdecimal", - "bson", - "nu-errors", - "nu-plugin", - "nu-protocol", - "nu-source", -] - -[[package]] -name = "nu_plugin_from_mp4" -version = "0.44.0" -dependencies = [ - "mp4", - "nu-errors", - "nu-plugin", - "nu-protocol", - "nu-source", - "tempfile", -] - -[[package]] -name = "nu_plugin_from_sqlite" -version = "0.44.0" -dependencies = [ - "bigdecimal", - "nu-errors", - "nu-plugin", - "nu-protocol", - "nu-source", - "rusqlite", - "tempfile", ] [[package]] name = "nu_plugin_inc" -version = "0.44.0" +version = "0.59.0" dependencies = [ - "nu-errors", "nu-plugin", "nu-protocol", - "nu-source", - "nu-test-support", - "nu-value-ext", "semver 0.11.0", ] [[package]] -name = "nu_plugin_match" -version = "0.44.0" -dependencies = [ - "nu-errors", - "nu-plugin", - "nu-protocol", - "regex", -] - -[[package]] -name = "nu_plugin_query_json" -version = "0.44.0" +name = "nu_plugin_query" +version = "0.59.0" dependencies = [ "gjson", - "nu-errors", + "nu-engine", "nu-plugin", "nu-protocol", - "nu-source", -] - -[[package]] -name = "nu_plugin_s3" -version = "0.44.0" -dependencies = [ - "futures 0.3.18", - "nu-errors", - "nu-plugin", - "nu-protocol", - "nu-source", - "s3handler", -] - -[[package]] -name = "nu_plugin_selector" -version = "0.44.0" -dependencies = [ - "indexmap", - "nu-errors", - "nu-plugin", - "nu-protocol", - "nu-source", "scraper", -] - -[[package]] -name = "nu_plugin_start" -version = "0.44.0" -dependencies = [ - "glob", - "nu-errors", - "nu-plugin", - "nu-protocol", - "nu-source", - "open", - "url", - "webbrowser", -] - -[[package]] -name = "nu_plugin_textview" -version = "0.44.0" -dependencies = [ - "bat", - "nu-data", - "nu-errors", - "nu-plugin", - "nu-protocol", - "nu-source", - "term_size", - "url", -] - -[[package]] -name = "nu_plugin_to_bson" -version = "0.44.0" -dependencies = [ - "bson", - "nu-errors", - "nu-plugin", - "nu-protocol", - "nu-source", - "num-traits", -] - -[[package]] -name = "nu_plugin_to_sqlite" -version = "0.44.0" -dependencies = [ - "hex", - "nu-errors", - "nu-plugin", - "nu-protocol", - "nu-source", - "rusqlite", - "tempfile", -] - -[[package]] -name = "nu_plugin_tree" -version = "0.44.0" -dependencies = [ - "derive-new", - "nu-errors", - "nu-plugin", - "nu-protocol", - "ptree", -] - -[[package]] -name = "nu_plugin_xpath" -version = "0.44.0" -dependencies = [ - "bigdecimal", - "indexmap", - "nu-errors", - "nu-plugin", - "nu-protocol", - "nu-source", - "nu-test-support", "sxd-document", "sxd-xpath", ] @@ -2928,17 +2455,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-bigint" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6f7833f2cbf2360a6cfd58cd41a53aa7a90bd4c202f5b1c7dd2ed73c57b2c3" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-bigint" version = "0.4.3" @@ -2977,8 +2493,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bafe4179722c2894288ee77a9f044f02811c86af699344c498b0840c698a2465" dependencies = [ "arrayvec 0.4.12", - "itoa", - "num-bigint 0.2.6", + "itoa 0.4.8", ] [[package]] @@ -3014,19 +2529,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-rational" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" -dependencies = [ - "autocfg", - "num-bigint 0.3.3", - "num-integer", - "num-traits", - "serde", -] - [[package]] name = "num-rational" version = "0.4.0" @@ -3051,9 +2553,9 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" dependencies = [ "hermit-abi", "libc", @@ -3079,31 +2581,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" - -[[package]] -name = "onig" -version = "6.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ddfe2c93bb389eea6e6d713306880c7f6dcc99a75b659ce145d962c861b225" -dependencies = [ - "bitflags", - "lazy_static", - "libc", - "onig_sys", -] - -[[package]] -name = "onig_sys" -version = "69.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd3eee045c84695b53b20255bb7317063df090b68e18bfac0abb6c39cf7f33e" -dependencies = [ - "cc", - "pkg-config", -] +checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" [[package]] name = "opaque-debug" @@ -3111,16 +2591,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" -[[package]] -name = "open" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcea7a30d6b81a2423cc59c43554880feff7b57d12916f231a79f8d6d9470201" -dependencies = [ - "pathdiff", - "winapi 0.3.9", -] - [[package]] name = "openssl" version = "0.10.38" @@ -3128,7 +2598,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95" dependencies = [ "bitflags", - "cfg-if 1.0.0", + "cfg-if", "foreign-types", "libc", "once_cell", @@ -3137,15 +2607,15 @@ dependencies = [ [[package]] name = "openssl-probe" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.71" +version = "0.9.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df13d165e607909b363a4757a6f133f8a818a74e9d3a98d09c6128e15fa4c73" +checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" dependencies = [ "autocfg", "cc", @@ -3163,12 +2633,27 @@ dependencies = [ "num-traits", ] +[[package]] +name = "output_vt100" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9" +dependencies = [ + "winapi", +] + [[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "owo-colors" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20448fd678ec04e6ea15bbe0476874af65e98a01515d667aa49f1434dc44ebf4" + [[package]] name = "parking_lot" version = "0.11.2" @@ -3186,12 +2671,12 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "instant", "libc", - "redox_syscall", + "redox_syscall 0.2.10", "smallvec", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -3202,22 +2687,22 @@ checksum = "03abc2f9c83fe9ceec83f47c76cc071bfd56caba33794340330f35623ab1f544" dependencies = [ "async-trait", "byteorder", - "futures 0.3.18", + "futures", "integer-encoding", "ordered-float", ] [[package]] name = "parquet2" -version = "0.6.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db82df54cdd88931d29b850190915b9069bb93fba8e1aefc0d59d8ca81603d6d" +checksum = "57e98d7da0076cead49c49580cc5771dfe0ba8a93cadff9b47c1681a4a78e1f9" dependencies = [ "async-stream", "bitpacking", "brotli", "flate2", - "futures 0.3.18", + "futures", "lz4", "parquet-format-async-temp", "snap", @@ -3234,15 +2719,6 @@ dependencies = [ "regex", ] -[[package]] -name = "path_abs" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ef02f6342ac01d8a93b65f96db53fe68a92a15f41144f97fb00a9e669633c3" -dependencies = [ - "std_prelude", -] - [[package]] name = "pathdiff" version = "0.2.1" @@ -3277,18 +2753,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ "phf_macros", - "phf_shared", + "phf_shared 0.8.0", "proc-macro-hack", ] +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_shared 0.10.0", +] + [[package]] name = "phf_codegen" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", ] [[package]] @@ -3297,18 +2792,28 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" dependencies = [ - "phf_shared", + "phf_shared 0.8.0", "rand 0.7.3", ] +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.4", +] + [[package]] name = "phf_macros" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.8.0", + "phf_shared 0.8.0", "proc-macro-hack", "proc-macro2", "quote", @@ -3325,10 +2830,20 @@ dependencies = [ ] [[package]] -name = "pin-project-lite" -version = "0.2.7" +name = "phf_shared" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", + "uncased", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" [[package]] name = "pin-utils" @@ -3338,41 +2853,15 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.22" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f" - -[[package]] -name = "plist" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd39bc6cdc9355ad1dc5eeedefee696bb35c34caf21768741e81826c0bbd7225" -dependencies = [ - "base64", - "indexmap", - "line-wrap", - "serde", - "time 0.3.5", - "xml-rs", -] - -[[package]] -name = "png" -version = "0.16.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" -dependencies = [ - "bitflags", - "crc32fast", - "deflate", - "miniz_oxide 0.3.7", -] +checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" [[package]] name = "polars" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c94a25d46e93b64eac7848c028a545dc08fa01e148e4942c5442b3843c3a598" +checksum = "3e9211d1bb8d2d81541e4ab80ce9148a8e2a987d6412c2a48017fbbe24231ea1" dependencies = [ "polars-core", "polars-io", @@ -3381,9 +2870,9 @@ dependencies = [ [[package]] name = "polars-arrow" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cc4488d2f2d6b901bb6e5728e58966013a272cae48861070b676215a79b4a99" +checksum = "fa5ee9c385bf6643893f98efa80ff5a07169b50f65962c7843c0a13e12f0b0cf" dependencies = [ "arrow2", "num 0.4.0", @@ -3392,22 +2881,22 @@ dependencies = [ [[package]] name = "polars-core" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6771524063d742a08163d96875ca5df71dff7113f27da58db5ec5fa164165bf6" +checksum = "3cb1de44e479ce2764a7a3ad057e16f434efa334feb993284e1a48bb8888c6d1" dependencies = [ "ahash", "anyhow", "arrow2", "chrono", - "comfy-table", "hashbrown", "itertools", "lazy_static", "num 0.4.0", "num_cpus", "polars-arrow", - "rand 0.7.3", + "prettytable-rs", + "rand 0.8.4", "rand_distr", "rayon", "regex", @@ -3419,15 +2908,15 @@ dependencies = [ [[package]] name = "polars-io" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11a5f5f51525043ee7befd49e586e6919345237826a5f17b53956f8242100957" +checksum = "8bcb74f52ee9ff84863ae01de6ba25db092a9880302db4bf8f351f65b3ff0d12" dependencies = [ "ahash", "anyhow", "arrow2", "csv-core", - "dirs", + "dirs 4.0.0", "lazy_static", "lexical", "memchr", @@ -3443,9 +2932,9 @@ dependencies = [ [[package]] name = "polars-lazy" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3ea647e2fa59d1bbbf90929c5d10ef6a9018aac256d1c6d0e8248211804b61" +checksum = "43f91022ba6463df71ad6eb80ac2307884578d9959e85e1fe9dac18988291d46" dependencies = [ "ahash", "itertools", @@ -3457,9 +2946,9 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" [[package]] name = "precomputed-hash" @@ -3468,12 +2957,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] -name = "pretty" -version = "0.5.2" +name = "predicates" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60c0d9f6fc88ecdd245d90c1920ff76a430ab34303fc778d33b1d0a4c3bf6d3" +checksum = "a5aab5be6e4732b473071984b3164dbbfb7a3674d30ea5ff44410b6bcd960c3c" dependencies = [ - "typed-arena", + "difflib", + "itertools", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da1c2388b1513e1b605fcec39a95e0a9e8ef088f71443ef37099fa9ae6673fcb" + +[[package]] +name = "predicates-tree" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d86de6de25020a36c6d3643a86d9a6a9f552107c0559c60ea03551b5e16c032" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "pretty_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d5b548b725018ab5496482b45cb8bef21e9fed1858a6d674e3a8a0f0bb5d50" +dependencies = [ + "ansi_term", + "ctor", + "diff", + "output_vt100", ] [[package]] @@ -3486,6 +3005,20 @@ 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", + "term", + "unicode-width", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -3496,7 +3029,7 @@ dependencies = [ "proc-macro2", "quote", "syn", - "version_check", + "version_check 0.9.4", ] [[package]] @@ -3507,7 +3040,7 @@ checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ "proc-macro2", "quote", - "version_check", + "version_check 0.9.4", ] [[package]] @@ -3518,20 +3051,26 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" [[package]] name = "proc-macro2" -version = "1.0.32" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" dependencies = [ "unicode-xid", ] [[package]] -name = "ptree" -version = "0.4.0" +name = "procfs" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0de80796b316aec75344095a6d2ef68ec9b8f573b9e7adc821149ba3598e270" +checksum = "0941606b9934e2d98a3677759a971756eb821f75764d0e0d26946d08e74d9104" dependencies = [ - "serde", + "bitflags", + "byteorder", + "chrono", + "flate2", + "hex", + "lazy_static", + "libc", ] [[package]] @@ -3583,23 +3122,13 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.10" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" dependencies = [ "proc-macro2", ] -[[package]] -name = "radix_trie" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" -dependencies = [ - "endian-type", - "nibble_vec", -] - [[package]] name = "rand" version = "0.7.3" @@ -3661,17 +3190,17 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ - "getrandom 0.2.3", + "getrandom 0.2.4", ] [[package]] name = "rand_distr" -version = "0.3.0" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9532ada3929fb8b2e9dbe28d1e06c9b2cc65813f074fcb6bd5fbefeff9d56" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" dependencies = [ "num-traits", - "rand 0.7.3", + "rand 0.8.4", ] [[package]] @@ -3702,15 +3231,12 @@ dependencies = [ ] [[package]] -name = "rawkey" -version = "0.1.3" +name = "rand_xoshiro" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ad6efac35ef044d565b23f0d111d76aa21ab2e86934b1225f7071d42e58ebad" +checksum = "a9fcdd2e881d02f1d9390ae47ad8e5696a9e4be7b547a1da2afbc61973217004" dependencies = [ - "readkey", - "user32-sys", - "winapi 0.3.9", - "x11", + "rand_core 0.5.1", ] [[package]] @@ -3739,10 +3265,10 @@ dependencies = [ ] [[package]] -name = "readkey" -version = "0.1.7" +name = "redox_syscall" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86d401b6d6a1725a59f1b4e813275d289dff3ad09c72b373a10a7a8217ba3146" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" [[package]] name = "redox_syscall" @@ -3753,14 +3279,42 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" +dependencies = [ + "getrandom 0.1.16", + "redox_syscall 0.1.57", + "rust-argon2", +] + [[package]] name = "redox_users" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" dependencies = [ - "getrandom 0.2.3", - "redox_syscall", + "getrandom 0.2.4", + "redox_syscall 0.2.10", +] + +[[package]] +name = "reedline" +version = "0.2.0" +source = "git+https://github.com/nushell/reedline?branch=main#da21703840e745e30016305d9fadb70403027be3" +dependencies = [ + "chrono", + "crossterm", + "fd-lock", + "nu-ansi-term", + "serde", + "strip-ansi-escapes", + "strum", + "strum_macros", + "unicode-segmentation", + "unicode-width", ] [[package]] @@ -3792,20 +3346,21 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] name = "reqwest" -version = "0.11.7" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bea77bc708afa10e59905c3d4af7c8fd43c9214251673095ff8b14345fcbc5" +checksum = "87f242f1488a539a79bac6dbe7c8609ae43b7914b7736210f239a37cccb32525" dependencies = [ "base64", - "bytes 1.1.0", + "bytes", "encoding_rs", "futures-core", "futures-util", + "h2", "http", "http-body", "hyper", @@ -3847,37 +3402,34 @@ dependencies = [ [[package]] name = "rstest" -version = "0.10.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "041bb0202c14f6a158bbbf086afb03d0c6e975c2dec7d4912f8061ed44f290af" +checksum = "d912f35156a3f99a66ee3e11ac2e0b3f34ac85a07e05263d05a7e2c8810d616f" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "proc-macro2", "quote", - "rustc_version 0.3.3", + "rustc_version", "syn", ] [[package]] -name = "rusqlite" -version = "0.26.1" +name = "rust-argon2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a82b0b91fad72160c56bf8da7a549b25d7c31109f52cc1437eac4c0ad2550a7" +checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" dependencies = [ - "bitflags", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "memchr", - "smallvec", + "base64", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils", ] [[package]] name = "rust-embed" -version = "5.9.0" +version = "6.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fe1fe6aac5d6bb9e1ffd81002340363272a7648234ec7bdfac5ee202cb65523" +checksum = "d40377bff8cceee81e28ddb73ac97f5c2856ce5522f0b260b763f434cdfae602" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -3886,9 +3438,9 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "5.9.0" +version = "6.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed91c41c42ef7bf687384439c312e75e0da9c149b0390889b94de3c7d9d9e66" +checksum = "94e763e24ba2bf0c72bc6be883f967f794a019fafd1b86ba1daff9c91a7edd30" dependencies = [ "proc-macro2", "quote", @@ -3899,10 +3451,11 @@ dependencies = [ [[package]] name = "rust-embed-utils" -version = "5.1.0" +version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a512219132473ab0a77b52077059f1c47ce4af7fbdc94503e9862a34422876d" +checksum = "ad22c7226e4829104deab21df575e995bfbc4adfad13a595e387477f238c1aec" dependencies = [ + "sha2 0.9.9", "walkdir", ] @@ -3924,103 +3477,40 @@ version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" -[[package]] -name = "rustc-serialize" -version = "0.3.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" - -[[package]] -name = "rustc_version" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" -dependencies = [ - "semver 0.11.0", -] - [[package]] name = "rustc_version" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.4", + "semver 1.0.5", +] + +[[package]] +name = "rustix" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cee647393af53c750e15dcbf7781cdd2e550b246bde76e46c326e7ea3c73773" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "winapi", ] [[package]] name = "rustversion" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" - -[[package]] -name = "rustyline" -version = "9.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790487c3881a63489ae77126f57048b42d62d3b2bafbf37453ea19eedb6340d6" -dependencies = [ - "bitflags", - "cfg-if 1.0.0", - "clipboard-win", - "dirs-next", - "fd-lock", - "libc", - "log", - "memchr", - "nix", - "radix_trie", - "scopeguard", - "smallvec", - "unicode-segmentation", - "unicode-width", - "utf8parse", - "winapi 0.3.9", -] +checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" [[package]] name = "ryu" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c9613b5a66ab9ba26415184cfc41156594925a9cf3a2057e57f31ff145f6568" - -[[package]] -name = "s3handler" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b9590f1cae0b8b38aceabab05885c754416b6d33089c244a25441ae997fdb1" -dependencies = [ - "async-trait", - "base64", - "bytes 1.1.0", - "chrono", - "dyn-clone", - "failure", - "failure_derive", - "futures 0.3.18", - "hex", - "hmac", - "hmac-sha1", - "log", - "md5", - "mime_guess", - "quick-xml 0.22.0", - "regex", - "reqwest", - "rustc-serialize", - "serde", - "serde_derive", - "serde_json", - "sha2", - "tokio", - "url", -] - -[[package]] -name = "safemem" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" [[package]] name = "same-file" @@ -4038,7 +3528,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" dependencies = [ "lazy_static", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -4065,9 +3555,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.3.1" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467" +checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" dependencies = [ "bitflags", "core-foundation", @@ -4078,9 +3568,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.4.2" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" dependencies = [ "core-foundation-sys", "libc", @@ -4098,8 +3588,8 @@ dependencies = [ "fxhash", "log", "matches", - "phf", - "phf_codegen", + "phf 0.8.0", + "phf_codegen 0.8.0", "precomputed-hash", "servo_arc", "smallvec", @@ -4117,9 +3607,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" +checksum = "0486718e92ec9a68fbed73bb5ef687d71103b142595b406835649bebd33f72c7" [[package]] name = "semver-parser" @@ -4132,27 +3622,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.130" +version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" dependencies = [ "serde_derive", ] -[[package]] -name = "serde_bytes" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16ae07dd2f88a366f15bd0632ba725227018c69a1c8550a927324f8eb8368bb9" -dependencies = [ - "serde", -] - [[package]] name = "serde_derive" -version = "1.0.130" +version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" dependencies = [ "proc-macro2", "quote", @@ -4172,45 +3653,45 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.72" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527" +checksum = "d23c1ba4cf0efd44be32017709280b32d1cea5c3f1275c3b6d9e8bc54f758085" dependencies = [ "indexmap", - "itoa", + "itoa 1.0.1", "ryu", "serde", ] [[package]] name = "serde_test" -version = "1.0.130" +version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d82178225dbdeae2d5d190e8649287db6a3a32c6d24da22ae3146325aa353e4c" +checksum = "21675ba6f9d97711cc00eee79d8dd7d0a31e571c350fb4d8a7c78f70c0e7b0e9" dependencies = [ "serde", ] [[package]] name = "serde_urlencoded" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa", + "itoa 1.0.1", "ryu", "serde", ] [[package]] name = "serde_yaml" -version = "0.8.21" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8c608a35705a5d3cdc9fbe403147647ff34b921f8e833e49306df898f9b20af" +checksum = "a4a521f2940385c165a24ee286aa8599633d162077a54bdcae2a6fd5a7bfa7a0" dependencies = [ - "dtoa", "indexmap", + "ryu", "serde", "yaml-rust", ] @@ -4248,22 +3729,27 @@ dependencies = [ ] [[package]] -name = "sha1" -version = "0.2.0" +name = "sha2" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc30b1e1e8c40c121ca33b86c23308a090d19974ef001b4bf6e61fd1a0fb095c" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] [[package]] name = "sha2" -version = "0.9.8" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa" +checksum = "99c3bd8169c58782adad9290a9af5939994036b76187f7b4f0e6de91dbbfc0ec" dependencies = [ - "block-buffer", - "cfg-if 1.0.0", + "cfg-if", "cpufeatures", - "digest", - "opaque-debug", + "digest 0.10.1", ] [[package]] @@ -4277,27 +3763,25 @@ dependencies = [ "is_debug", ] -[[package]] -name = "shell-escape" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f" - -[[package]] -name = "shell-words" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fa3938c99da4914afedd13bf3d79bcb6c277d1b2c398d23257a304d9e1b074" - [[package]] name = "signal-hook" -version = "0.1.17" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e31d442c16f047a671b5a71e2161d6e68814012b7f5379d269ebd915fac2729" +checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fd5867f1c4f2c5be079aee7a2adf1152ebb04a4bc4d341f504b7dece607ed4" dependencies = [ "libc", "mio", - "signal-hook-registry", + "signal-hook", ] [[package]] @@ -4316,16 +3800,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c970da16e7c682fa90a261cf0724dee241c9f7831635ecc4e988ae8f3b505559" [[package]] -name = "similar" -version = "1.3.0" +name = "siphasher" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec" +checksum = "a86232ab60fa71287d7f2ddae4a7073f6b7aac33631c3015abb556f08c6d0a3e" [[package]] -name = "siphasher" -version = "0.3.7" +name = "sized-chunks" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] [[package]] name = "slab" @@ -4335,20 +3823,15 @@ checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" [[package]] name = "smallvec" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" [[package]] -name = "smart-default" -version = "0.6.0" +name = "smawk" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "133659a15339456eeeb07572eb02a91c91e9815e9cbc89566944d2c8d3efdbf6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" [[package]] name = "snap" @@ -4358,12 +3841,12 @@ checksum = "45456094d1983e2ee2a18fdfebce3189fa451699d0502cb8e3b49dba5ba41451" [[package]] name = "socket2" -version = "0.4.2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" dependencies = [ "libc", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -4387,18 +3870,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "std_prelude" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8207e78455ffdf55661170876f88daf85356e4edd54e0a3dbc79586ca1e50cbe" - -[[package]] -name = "str-buf" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a" - [[package]] name = "streaming-decompression" version = "0.1.0" @@ -4422,14 +3893,14 @@ checksum = "a3ff2f71c82567c565ba4b3009a9350a96a7269eaa4001ebedae926230bc2254" [[package]] name = "string_cache" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "923f0f39b6267d37d23ce71ae7235602134b250ace715dd2c90421998ddac0c6" +checksum = "33994d0838dc2d152d17a62adf608a869b5e846b65b389af7f3dbc1de45c5b26" dependencies = [ "lazy_static", "new_debug_unreachable", "parking_lot", - "phf_shared", + "phf_shared 0.10.0", "precomputed-hash", "serde", ] @@ -4440,8 +3911,8 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f24c8e5e19d22a726626f1a5e16fe15b132dcf21d10177fa5a45ce7962996b97" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.8.0", + "phf_shared 0.8.0", "proc-macro2", "quote", ] @@ -4457,27 +3928,50 @@ dependencies = [ [[package]] name = "strum" -version = "0.20.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7318c509b5ba57f18533982607f24070a55d353e90d4cae30c467cdb2ad5ac5c" +checksum = "cae14b91c7d11c9a851d3fbc80a963198998c2a64eec840477fa92d8ce9b70bb" [[package]] name = "strum_macros" -version = "0.20.1" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee8bc6b87a5112aeeab1f4a9f7ab634fe6cbefc4850006df31267f4cfb9e3149" +checksum = "5bb0dc7ee9c15cea6199cde9a127fa16a4c5819af85395457ad72d68edc85a38" dependencies = [ - "heck 0.3.3", + "heck", "proc-macro2", "quote", + "rustversion", "syn", ] [[package]] -name = "subtle" -version = "2.4.1" +name = "supports-color" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "4872ced36b91d47bae8a214a683fe54e7078875b399dfa251df346c9b547d1f9" +dependencies = [ + "atty", + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "590b34f7c5f01ecc9d78dba4b3f445f31df750a67621cf31626f3b7441ce6406" +dependencies = [ + "atty", +] + +[[package]] +name = "supports-unicode" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8b945e45b417b125a8ec51f1b7df2f8df7920367700d1f98aedd21e5735f8b2" +dependencies = [ + "atty", +] [[package]] name = "sxd-document" @@ -4502,59 +3996,15 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.82" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" +checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" dependencies = [ "proc-macro2", "quote", "unicode-xid", ] -[[package]] -name = "synstructure" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "unicode-xid", -] - -[[package]] -name = "syntect" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b20815bbe80ee0be06e6957450a841185fcf690fe0178f14d77a05ce2caa031" -dependencies = [ - "bincode", - "bitflags", - "flate2", - "fnv", - "lazy_static", - "lazycell", - "onig", - "plist", - "regex-syntax", - "serde", - "serde_derive", - "serde_json", - "walkdir", - "yaml-rust", -] - -[[package]] -name = "sys-info" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b3a0d0aba8bf96a0e1ddfdc352fc53b3df7f39318c71854910c3c4b024ae52c" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "sys-locale" version = "0.1.0" @@ -4565,36 +4015,36 @@ dependencies = [ "cstr_core", "libc", "web-sys", - "winapi 0.3.9", + "winapi", ] [[package]] name = "sysinfo" -version = "0.23.0" +version = "0.22.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e757000a4bed2b1be9be65a3f418b9696adf30bb419214c73997422de73a591" +checksum = "7f1bfab07306a27332451a662ca9c8156e3a9986f82660ba9c8e744fe8455d43" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "core-foundation-sys", "libc", "ntapi", "once_cell", "rayon", - "winapi 0.3.9", + "winapi", ] [[package]] name = "tempfile" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", + "fastrand", "libc", - "rand 0.8.4", - "redox_syscall", + "redox_syscall 0.2.10", "remove_dir_all", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -4610,23 +4060,13 @@ dependencies = [ [[package]] name = "term" -version = "0.7.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +checksum = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42" dependencies = [ - "dirs-next", - "rustversion", - "winapi 0.3.9", -] - -[[package]] -name = "term_size" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" -dependencies = [ - "libc", - "winapi 0.3.9", + "byteorder", + "dirs 1.0.5", + "winapi", ] [[package]] @@ -4645,7 +4085,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" dependencies = [ "libc", - "winapi 0.3.9", + "winapi", +] + +[[package]] +name = "termtree" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b" + +[[package]] +name = "textwrap" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", ] [[package]] @@ -4682,17 +4139,7 @@ checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ "libc", "wasi 0.10.0+wasi-snapshot-preview1", - "winapi 0.3.9", -] - -[[package]] -name = "time" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41effe7cfa8af36f439fac33861b66b049edc6f9a32331e2312660529c1c24ad" -dependencies = [ - "itoa", - "libc", + "winapi", ] [[package]] @@ -4722,41 +4169,17 @@ dependencies = [ [[package]] name = "tokio" -version = "1.14.0" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144" +checksum = "0c27a64b625de6d309e8c57716ba93021dccf1b3b5c97edd6d3dd2d2135afc0a" dependencies = [ - "autocfg", - "bytes 1.1.0", + "bytes", "libc", "memchr", "mio", "num_cpus", "pin-project-lite", - "tokio-macros", - "winapi 0.3.9", -] - -[[package]] -name = "tokio-io" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57fc868aae093479e3131e3d165c93b1c7474109d13c90ec0dda2a1bbfff0674" -dependencies = [ - "bytes 0.4.12", - "futures 0.1.31", - "log", -] - -[[package]] -name = "tokio-macros" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9efc1aba077437943f7515666aa2b882dfabfbfdf89c819ea75a8d6e9eaba5e" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "winapi", ] [[package]] @@ -4775,7 +4198,7 @@ version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" dependencies = [ - "bytes 1.1.0", + "bytes", "futures-core", "futures-sink", "log", @@ -4800,34 +4223,35 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" +checksum = "2d8d93354fe2a8e50d5953f5ae2e47a3fc2ef03292e7ea46e3cc38f549525fb9" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" +checksum = "03cfcb51380632a72d3111cb8d3447a8d908e577d31beeac006f836383d29a23" dependencies = [ "lazy_static", ] [[package]] name = "trash" -version = "2.0.2" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3ebb6cb2db7947ab9f65dec9f7c5dbe01042b708f564242dcfb6d5cb2957cbc" +checksum = "9a2ed4369f59214865022230fb397ad71353101fe87bfef0f0cf887c43eaa094" dependencies = [ "chrono", "libc", "log", "objc", + "once_cell", "scopeguard", "url", "windows", @@ -4839,19 +4263,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" -[[package]] -name = "tui" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "861d8f3ad314ede6219bcb2ab844054b1de279ee37a9bc38e3d606f9d3fb2a71" -dependencies = [ - "bitflags", - "cassowary", - "crossterm", - "unicode-segmentation", - "unicode-width", -] - [[package]] name = "typed-arena" version = "1.7.0" @@ -4860,9 +4271,33 @@ checksum = "a9b2228007eba4120145f785df0f6c92ea538f5a3635a612ecf4e334c8c1446d" [[package]] name = "typenum" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "typetag" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4080564c5b2241b5bff53ab610082234e0c57b0417f4bd10596f183001505b8a" +dependencies = [ + "erased-serde", + "inventory", + "once_cell", + "serde", + "typetag-impl", +] + +[[package]] +name = "typetag-impl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e60147782cc30833c05fba3bab1d9b5771b2685a2557672ac96fa5d154099c0e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "ucd-trie" @@ -4872,17 +4307,17 @@ checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" [[package]] name = "umask" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "982efbf70ec4d28f7862062c03dd1a4def601a5079e0faf1edc55f2ad0f6fe46" +checksum = "efb3f38a494193b563eb215c43cb635a4fda1dfcd885fe3906b215bc6a9fb6b8" [[package]] -name = "unicase" -version = "2.6.0" +name = "uncased" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +checksum = "5baeed7327e25054889b9bd4f975f32e5f4c5d434042d59ab6cd4142c0a76ed0" dependencies = [ - "version_check", + "version_check 0.9.4", ] [[package]] @@ -4891,6 +4326,15 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" +[[package]] +name = "unicode-linebreak" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f" +dependencies = [ + "regex", +] + [[package]] name = "unicode-normalization" version = "0.1.19" @@ -4902,9 +4346,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" [[package]] name = "unicode-width" @@ -4936,16 +4380,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "user32-sys" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ef4711d107b21b410a3a974b1204d9accc8b10dad75d8324b5d755de1617d47" -dependencies = [ - "winapi 0.2.8", - "winapi-build", -] - [[package]] name = "users" version = "0.11.0" @@ -4980,7 +4414,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom 0.2.3", + "getrandom 0.2.4", ] [[package]] @@ -4991,9 +4425,15 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "void" @@ -5022,6 +4462,15 @@ dependencies = [ "quote", ] +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.3.2" @@ -5029,7 +4478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" dependencies = [ "same-file", - "winapi 0.3.9", + "winapi", "winapi-util", ] @@ -5057,19 +4506,19 @@ checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[package]] name = "wasm-bindgen" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca" dependencies = [ "bumpalo", "lazy_static", @@ -5082,11 +4531,11 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39" +checksum = "2eb6ec270a31b1d3c7e266b999739109abce8b6c87e4b31fcfcd788b65267395" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "js-sys", "wasm-bindgen", "web-sys", @@ -5094,9 +4543,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5104,9 +4553,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" dependencies = [ "proc-macro2", "quote", @@ -5117,54 +4566,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" +checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2" [[package]] name = "web-sys" -version = "0.3.55" +version = "0.3.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb" dependencies = [ "js-sys", "wasm-bindgen", ] -[[package]] -name = "webbrowser" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecad156490d6b620308ed411cfee90d280b3cbd13e189ea0d3fada8acc89158a" -dependencies = [ - "web-sys", - "widestring", - "winapi 0.3.9", -] - [[package]] name = "which" -version = "4.2.2" +version = "4.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea187a8ef279bc014ec368c27a920da2024d2a711109bfbe3440585d5cf27ad9" +checksum = "2a5a7e487e921cf220206864a94a89b6c6905bfc19f1057fa26a4cb360e5c1d2" dependencies = [ "either", "lazy_static", "libc", ] -[[package]] -name = "widestring" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" - -[[package]] -name = "winapi" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" - [[package]] name = "winapi" version = "0.3.9" @@ -5175,12 +4601,6 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] -[[package]] -name = "winapi-build" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" - [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -5193,7 +4613,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] @@ -5215,9 +4635,9 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.28.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82ca39602d5cbfa692c4b67e3bcbb2751477355141c1ed434c94da4186836ff6" +checksum = "030b7ff91626e57a05ca64a07c481973cbb2db774e4852c9c7ca342408c6a99a" dependencies = [ "windows_aarch64_msvc", "windows_i686_gnu", @@ -5228,9 +4648,9 @@ dependencies = [ [[package]] name = "windows_aarch64_msvc" -version = "0.28.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52695a41e536859d5308cc613b4a022261a274390b25bd29dfff4bf08505f3c2" +checksum = "29277a4435d642f775f63c7d1faeb927adba532886ce0287bd985bffb16b6bca" [[package]] name = "windows_gen" @@ -5243,15 +4663,15 @@ dependencies = [ [[package]] name = "windows_i686_gnu" -version = "0.28.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f54725ac23affef038fecb177de6c9bf065787c2f432f79e3c373da92f3e1d8a" +checksum = "1145e1989da93956c68d1864f32fb97c8f561a8f89a5125f6a2b7ea75524e4b8" [[package]] name = "windows_i686_msvc" -version = "0.28.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d5158a43cc43623c0729d1ad6647e62fa384a3d135fd15108d37c683461f64" +checksum = "d4a09e3a0d4753b73019db171c1339cd4362c8c44baf1bcea336235e955954a6" [[package]] name = "windows_macros" @@ -5265,15 +4685,15 @@ dependencies = [ [[package]] name = "windows_x86_64_gnu" -version = "0.28.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc31f409f565611535130cfe7ee8e6655d3fa99c1c61013981e491921b5ce954" +checksum = "8ca64fcb0220d58db4c119e050e7af03c69e6f4f415ef69ec1773d9aab422d5a" [[package]] name = "windows_x86_64_msvc" -version = "0.28.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f2b8c7cbd3bfdddd9ab98769f9746a7fad1bca236554cd032b78d768bc0e89f" +checksum = "08cabc9f0066848fef4bc6a1c1668e6efce38b661d2aeec75d18d8617eebb5f1" [[package]] name = "winreg" @@ -5281,25 +4701,9 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" dependencies = [ - "winapi 0.3.9", + "winapi", ] -[[package]] -name = "x11" -version = "2.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dd0565fa8bfba8c5efe02725b14dff114c866724eff2cfd44d76cea74bcd87a" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "xml-rs" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" - [[package]] name = "xmlparser" version = "0.13.3" @@ -5315,6 +4719,12 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "zeroize" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c88870063c39ee00ec285a2f8d6a966e5b6fb2becc4e8dac77ed0d370ed6006" + [[package]] name = "zip" version = "0.5.13" @@ -5326,23 +4736,23 @@ dependencies = [ "crc32fast", "flate2", "thiserror", - "time 0.1.44", + "time", ] [[package]] name = "zstd" -version = "0.9.0+zstd.1.5.0" +version = "0.9.2+zstd.1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07749a5dc2cb6b36661290245e350f15ec3bbb304e493db54a1d354480522ccd" +checksum = "2390ea1bf6c038c39674f22d95f0564725fc06034a47129179810b2fc58caa54" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "4.1.1+zstd.1.5.0" +version = "4.1.3+zstd.1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91c90f2c593b003603e5e0493c837088df4469da25aafff8bce42ba48caf079" +checksum = "e99d81b99fb3c2c2c794e3fe56c305c63d5173a16a46b5850b07c935ffc7db79" dependencies = [ "libc", "zstd-sys", @@ -5350,9 +4760,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "1.6.1+zstd.1.5.0" +version = "1.6.2+zstd.1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "615120c7a2431d16cf1cf979e7fc31ba7a5b5e5707b29c8a99e5dbf8a8392a33" +checksum = "2daf2f248d9ea44454bfcb2516534e8b8ad2fc91bf818a1885495fc42bc8ac9f" dependencies = [ "cc", "libc", diff --git a/Cargo.toml b/Cargo.toml index 5f55694a04..1b1c085ead 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,218 +10,127 @@ license = "MIT" name = "nu" readme = "README.md" repository = "https://github.com/nushell/nushell" -version = "0.44.0" - -[workspace] -members = ["crates/*/"] +version = "0.59.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[workspace] +members = [ + "crates/nu-cli", + "crates/nu-engine", + "crates/nu-parser", + "crates/nu-system", + "crates/nu-command", + "crates/nu-protocol", + "crates/nu-plugin", + "crates/nu_plugin_inc", + "crates/nu_plugin_gstat", + "crates/nu_plugin_example", + "crates/nu_plugin_query", +] + [dependencies] -nu-cli = { version = "0.44.0", path="./crates/nu-cli", default-features=false } -nu-command = { version = "0.44.0", path="./crates/nu-command" } -nu-completion = { version = "0.44.0", path="./crates/nu-completion" } -nu-data = { version = "0.44.0", path="./crates/nu-data" } -nu-engine = { version = "0.44.0", path="./crates/nu-engine" } -nu-errors = { version = "0.44.0", path="./crates/nu-errors" } -nu-parser = { version = "0.44.0", path="./crates/nu-parser" } -nu-path = { version = "0.44.0", path="./crates/nu-path" } -nu-plugin = { version = "0.44.0", path="./crates/nu-plugin" } -nu-protocol = { version = "0.44.0", path="./crates/nu-protocol" } -nu-source = { version = "0.44.0", path="./crates/nu-source" } -nu-value-ext = { version = "0.44.0", path="./crates/nu-value-ext" } +reedline = { git = "https://github.com/nushell/reedline", branch = "main" } -nu_plugin_binaryview = { version = "0.44.0", path="./crates/nu_plugin_binaryview", optional=true } -nu_plugin_chart = { version = "0.44.0", path="./crates/nu_plugin_chart", optional=true } -nu_plugin_from_bson = { version = "0.44.0", path="./crates/nu_plugin_from_bson", optional=true } -nu_plugin_from_sqlite = { version = "0.44.0", path="./crates/nu_plugin_from_sqlite", optional=true } -nu_plugin_inc = { version = "0.44.0", path="./crates/nu_plugin_inc", optional=true } -nu_plugin_match = { version = "0.44.0", path="./crates/nu_plugin_match", optional=true } -nu_plugin_query_json = { version = "0.44.0", path="./crates/nu_plugin_query_json", optional=true } -nu_plugin_s3 = { version = "0.44.0", path="./crates/nu_plugin_s3", optional=true } -nu_plugin_selector = { version = "0.44.0", path="./crates/nu_plugin_selector", optional=true } -nu_plugin_start = { version = "0.44.0", path="./crates/nu_plugin_start", optional=true } -nu_plugin_textview = { version = "0.44.0", path="./crates/nu_plugin_textview", optional=true } -nu_plugin_to_bson = { version = "0.44.0", path="./crates/nu_plugin_to_bson", optional=true } -nu_plugin_to_sqlite = { version = "0.44.0", path="./crates/nu_plugin_to_sqlite", optional=true } -nu_plugin_tree = { version = "0.44.0", path="./crates/nu_plugin_tree", optional=true } -nu_plugin_xpath = { version = "0.44.0", path="./crates/nu_plugin_xpath", optional=true } +crossterm = "0.22.*" +nu-cli = { path="./crates/nu-cli", version = "0.59.0" } +nu-command = { path="./crates/nu-command", version = "0.59.0" } +nu-engine = { path="./crates/nu-engine", version = "0.59.0" } +nu-json = { path="./crates/nu-json", version = "0.59.0" } +nu-parser = { path="./crates/nu-parser", version = "0.59.0" } +nu-path = { path="./crates/nu-path", version = "0.59.0" } +nu-pretty-hex = { path = "./crates/nu-pretty-hex", version = "0.59.0" } +nu-protocol = { path = "./crates/nu-protocol", version = "0.59.0" } +nu-plugin = { path = "./crates/nu-plugin", optional = true, version = "0.59.0" } +nu-system = { path = "./crates/nu-system", version = "0.59.0" } +nu-table = { path = "./crates/nu-table", version = "0.59.0" } +nu-term-grid = { path = "./crates/nu-term-grid", version = "0.59.0" } +# nu-ansi-term = { path = "./crates/nu-ansi-term", version = "0.59.0" } +nu-ansi-term = "0.42.0" -# Required to bootstrap the main binary -ctrlc = { version="3.1.7", optional=true } -futures = { version="0.3.12", features=["compat", "io-compat"] } -itertools = "0.10.0" +nu-color-config = { path = "./crates/nu-color-config", version = "0.59.0" } + +miette = "3.0.0" +ctrlc = "3.2.1" +crossterm_winapi = "0.9.0" +log = "0.4" +pretty_env_logger = "0.4.0" +# mimalloc = { version = "*", default-features = false } + + +nu_plugin_inc = { version = "0.59.0", path = "./crates/nu_plugin_inc", optional = true } +nu_plugin_example = { version = "0.59.0", path = "./crates/nu_plugin_example", optional = true } +nu_plugin_gstat = { version = "0.59.0", path = "./crates/nu_plugin_gstat", optional = true } +nu_plugin_query = { version = "0.59.0", path = "./crates/nu_plugin_query", optional = true } [dev-dependencies] -nu-test-support = { version = "0.44.0", path="./crates/nu-test-support" } +nu-test-support = { path="./crates/nu-test-support" } +tempfile = "3.2.0" +assert_cmd = "2.0.2" +pretty_assertions = "1.0.0" serial_test = "0.5.1" hamcrest2 = "0.3.0" -rstest = "0.10.0" - -[build-dependencies] +rstest = "0.12.0" +itertools = "0.10.3" [features] -fetch-support = ["nu-command/fetch", "nu-command/post"] -sys-support = ["nu-command/sys", "nu-command/ps"] -ctrlc-support = ["nu-cli/ctrlc", "nu-command/ctrlc"] -rustyline-support = ["nu-cli/rustyline-support", "nu-command/rustyline-support"] -term-support = ["nu-command/term"] -uuid-support = ["nu-command/uuid_crate"] -which-support = ["nu-command/which", "nu-engine/which"] - +plugin = ["nu-plugin", "nu-parser/plugin", "nu-command/plugin", "nu-protocol/plugin", "nu-engine/plugin"] default = [ - "nu-cli/shadow-rs", - "sys-support", - "ctrlc-support", - "which-support", - "term-support", - "rustyline-support", - "match", - "fetch-support", - "zip-support", - "dataframe", -] + "plugin", + "inc", + "example", + "which" + ] stable = ["default"] + extra = [ - "default", - "binaryview", - "inc", - "tree", - "textview", - "trash-support", - "uuid-support", - "start", - "bson", - "sqlite", - "s3", - "chart", - "xpath", - "selector", - "query-json", + "default", + "dataframe", + "gstat", + "zip-support", + "query", ] -wasi = ["inc", "match", "match", "tree", "rustyline-support"] +wasi = ["inc"] # Stable (Default) inc = ["nu_plugin_inc"] -match = ["nu_plugin_match"] -textview = ["nu_plugin_textview"] +example = ["nu_plugin_example"] +which = ["nu-command/which"] # Extra -binaryview = ["nu_plugin_binaryview"] -bson = ["nu_plugin_from_bson", "nu_plugin_to_bson"] -chart = ["nu_plugin_chart"] -query-json = ["nu_plugin_query_json"] -s3 = ["nu_plugin_s3"] -selector = ["nu_plugin_selector"] -sqlite = ["nu_plugin_from_sqlite", "nu_plugin_to_sqlite"] -start = ["nu_plugin_start"] -trash-support = [ - "nu-command/trash-support", - "nu-engine/trash-support", -] -tree = ["nu_plugin_tree"] -xpath = ["nu_plugin_xpath"] +gstat = ["nu_plugin_gstat"] zip-support = ["nu-command/zip"] +query = ["nu_plugin_query"] -#dataframe feature for nushell -dataframe = [ - "nu-engine/dataframe", - "nu-protocol/dataframe", - "nu-command/dataframe", - "nu-value-ext/dataframe", - "nu-data/dataframe", - "nu_plugin_to_bson/dataframe", -] +# Dataframe feature for nushell +dataframe = ["nu-command/dataframe"] [profile.release] -opt-level = "s" # Optimize for size. +opt-level = "s" # Optimize for size -# Core plugins that ship with `cargo install nu` by default -# Currently, Cargo limits us to installing only one binary -# unless we use [[bin]], so we use this as a workaround +# Build plugins [[bin]] -name = "nu_plugin_core_textview" -path = "src/plugins/nu_plugin_core_textview.rs" -required-features = ["textview"] - -[[bin]] -name = "nu_plugin_core_inc" +name = "nu_plugin_inc" path = "src/plugins/nu_plugin_core_inc.rs" required-features = ["inc"] [[bin]] -name = "nu_plugin_core_match" -path = "src/plugins/nu_plugin_core_match.rs" -required-features = ["match"] +name = "nu_plugin_example" +path = "src/plugins/nu_plugin_core_example.rs" +required-features = ["example"] # Extra plugins +[[bin]] +name = "nu_plugin_gstat" +path = "src/plugins/nu_plugin_extra_gstat.rs" +required-features = ["gstat"] [[bin]] -name = "nu_plugin_extra_binaryview" -path = "src/plugins/nu_plugin_extra_binaryview.rs" -required-features = ["binaryview"] - -[[bin]] -name = "nu_plugin_extra_tree" -path = "src/plugins/nu_plugin_extra_tree.rs" -required-features = ["tree"] - -[[bin]] -name = "nu_plugin_extra_query_json" -path = "src/plugins/nu_plugin_extra_query_json.rs" -required-features = ["query-json"] - -[[bin]] -name = "nu_plugin_extra_start" -path = "src/plugins/nu_plugin_extra_start.rs" -required-features = ["start"] - -[[bin]] -name = "nu_plugin_extra_s3" -path = "src/plugins/nu_plugin_extra_s3.rs" -required-features = ["s3"] - -[[bin]] -name = "nu_plugin_extra_chart_bar" -path = "src/plugins/nu_plugin_extra_chart_bar.rs" -required-features = ["chart"] - -[[bin]] -name = "nu_plugin_extra_chart_line" -path = "src/plugins/nu_plugin_extra_chart_line.rs" -required-features = ["chart"] - -[[bin]] -name = "nu_plugin_extra_xpath" -path = "src/plugins/nu_plugin_extra_xpath.rs" -required-features = ["xpath"] - -[[bin]] -name = "nu_plugin_extra_selector" -path = "src/plugins/nu_plugin_extra_selector.rs" -required-features = ["selector"] - -[[bin]] -name = "nu_plugin_extra_from_bson" -path = "src/plugins/nu_plugin_extra_from_bson.rs" -required-features = ["bson"] - -[[bin]] -name = "nu_plugin_extra_to_bson" -path = "src/plugins/nu_plugin_extra_to_bson.rs" -required-features = ["bson"] - -[[bin]] -name = "nu_plugin_extra_from_sqlite" -path = "src/plugins/nu_plugin_extra_from_sqlite.rs" -required-features = ["sqlite"] - -[[bin]] -name = "nu_plugin_extra_to_sqlite" -path = "src/plugins/nu_plugin_extra_to_sqlite.rs" -required-features = ["sqlite"] +name = "nu_plugin_query" +path = "src/plugins/nu_plugin_extra_query.rs" +required-features = ["query"] # Main nu binary [[bin]] diff --git a/Cargo.toml.old b/Cargo.toml.old new file mode 100644 index 0000000000..e20ef867eb --- /dev/null +++ b/Cargo.toml.old @@ -0,0 +1,229 @@ +[package] +authors = ["The Nu Project Contributors"] +default-run = "nu" +description = "A new type of shell" +documentation = "https://www.nushell.sh/book/" +edition = "2018" +exclude = ["images"] +homepage = "https://www.nushell.sh" +license = "MIT" +name = "nu" +readme = "README.md" +repository = "https://github.com/nushell/nushell" +version = "0.59.0" + +[workspace] +members = ["crates/*/"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +nu-cli = { version = "0.59.0", path="./crates/nu-cli", default-features=false } +nu-command = { version = "0.59.0", path="./crates/nu-command" } +nu-completion = { version = "0.59.0", path="./crates/nu-completion" } +nu-data = { version = "0.59.0", path="./crates/nu-data" } +nu-engine = { version = "0.59.0", path="./crates/nu-engine" } +nu-errors = { version = "0.59.0", path="./crates/nu-errors" } +nu-parser = { version = "0.59.0", path="./crates/nu-parser" } +nu-path = { version = "0.59.0", path="./crates/nu-path" } +nu-plugin = { version = "0.59.0", path="./crates/nu-plugin" } +nu-protocol = { version = "0.59.0", path="./crates/nu-protocol" } +nu-source = { version = "0.59.0", path="./crates/nu-source" } +nu-value-ext = { version = "0.59.0", path="./crates/nu-value-ext" } + +# nu_plugin_binaryview = { version = "0.59.0", path="./crates/nu_plugin_binaryview", optional=true } +# nu_plugin_chart = { version = "0.59.0", path="./crates/nu_plugin_chart", optional=true } +# nu_plugin_from_bson = { version = "0.59.0", path="./crates/nu_plugin_from_bson", optional=true } +# nu_plugin_from_sqlite = { version = "0.59.0", path="./crates/nu_plugin_from_sqlite", optional=true } +# nu_plugin_inc = { version = "0.59.0", path="./crates/nu_plugin_inc", optional=true } +# nu_plugin_match = { version = "0.59.0", path="./crates/nu_plugin_match", optional=true } +# nu_plugin_query_json = { version = "0.59.0", path="./crates/nu_plugin_query_json", optional=true } +# nu_plugin_s3 = { version = "0.59.0", path="./crates/nu_plugin_s3", optional=true } +# nu_plugin_selector = { version = "0.59.0", path="./crates/nu_plugin_selector", optional=true } +# nu_plugin_start = { version = "0.59.0", path="./crates/nu_plugin_start", optional=true } +# nu_plugin_textview = { version = "0.59.0", path="./crates/nu_plugin_textview", optional=true } +# nu_plugin_to_bson = { version = "0.59.0", path="./crates/nu_plugin_to_bson", optional=true } +# nu_plugin_to_sqlite = { version = "0.59.0", path="./crates/nu_plugin_to_sqlite", optional=true } +# nu_plugin_tree = { version = "0.59.0", path="./crates/nu_plugin_tree", optional=true } +# nu_plugin_xpath = { version = "0.59.0", path="./crates/nu_plugin_xpath", optional=true } + +# Required to bootstrap the main binary +ctrlc = { version="3.1.7", optional=true } +futures = { version="0.3.12", features=["compat", "io-compat"] } +itertools = "0.10.0" + +[dev-dependencies] +nu-test-support = { version = "0.59.0", path="./crates/nu-test-support" } +serial_test = "0.5.1" +hamcrest2 = "0.3.0" +rstest = "0.10.0" + +[build-dependencies] + +[features] +fetch-support = ["nu-command/fetch", "nu-command/post"] +sys-support = ["nu-command/sys", "nu-command/ps"] +ctrlc-support = ["nu-cli/ctrlc", "nu-command/ctrlc"] +rustyline-support = ["nu-cli/rustyline-support", "nu-command/rustyline-support"] +term-support = ["nu-command/term"] +uuid-support = ["nu-command/uuid_crate"] +which-support = ["nu-command/which", "nu-engine/which"] + +default = [ + "nu-cli/shadow-rs", + "sys-support", + "ctrlc-support", + "which-support", + "term-support", + "rustyline-support", + # "match", + "fetch-support", + "zip-support", + "dataframe", +] + +stable = ["default"] +extra = [ + "default", + # "binaryview", + # "inc", + # "tree", + # "textview", + "trash-support", + "uuid-support", + # "start", + # "bson", + # "sqlite", + # "s3", + # "chart", + # "xpath", + # "selector", + # "query-json", +] + +# wasi = ["inc", "match", "match", "tree", "rustyline-support"] + +# Stable (Default) +# inc = ["nu_plugin_inc"] +# match = ["nu_plugin_match"] +# textview = ["nu_plugin_textview"] + +# Extra +# binaryview = ["nu_plugin_binaryview"] +# bson = ["nu_plugin_from_bson", "nu_plugin_to_bson"] +# chart = ["nu_plugin_chart"] +# query-json = ["nu_plugin_query_json"] +# s3 = ["nu_plugin_s3"] +# selector = ["nu_plugin_selector"] +# sqlite = ["nu_plugin_from_sqlite", "nu_plugin_to_sqlite"] +# start = ["nu_plugin_start"] +trash-support = [ + "nu-command/trash-support", + "nu-engine/trash-support", +] +# tree = ["nu_plugin_tree"] +# xpath = ["nu_plugin_xpath"] +zip-support = ["nu-command/zip"] + +#dataframe feature for nushell +dataframe = [ + "nu-engine/dataframe", + "nu-protocol/dataframe", + "nu-command/dataframe", + "nu-value-ext/dataframe", + "nu-data/dataframe", + # "nu_plugin_to_bson/dataframe", +] + +[profile.release] +opt-level = "s" # Optimize for size. + +# Core plugins that ship with `cargo install nu` by default +# Currently, Cargo limits us to installing only one binary +# unless we use [[bin]], so we use this as a workaround +# [[bin]] +# name = "nu_plugin_core_textview" +# path = "src/plugins/nu_plugin_core_textview.rs" +# required-features = ["textview"] +# +# [[bin]] +# name = "nu_plugin_core_inc" +# path = "src/plugins/nu_plugin_core_inc.rs" +# required-features = ["inc"] +# +# [[bin]] +# name = "nu_plugin_core_match" +# path = "src/plugins/nu_plugin_core_match.rs" +# required-features = ["match"] +# +# # Extra plugins +# +# [[bin]] +# name = "nu_plugin_extra_binaryview" +# path = "src/plugins/nu_plugin_extra_binaryview.rs" +# required-features = ["binaryview"] +# +# [[bin]] +# name = "nu_plugin_extra_tree" +# path = "src/plugins/nu_plugin_extra_tree.rs" +# required-features = ["tree"] +# +# [[bin]] +# name = "nu_plugin_extra_query_json" +# path = "src/plugins/nu_plugin_extra_query_json.rs" +# required-features = ["query-json"] +# +# [[bin]] +# name = "nu_plugin_extra_start" +# path = "src/plugins/nu_plugin_extra_start.rs" +# required-features = ["start"] +# +# [[bin]] +# name = "nu_plugin_extra_s3" +# path = "src/plugins/nu_plugin_extra_s3.rs" +# required-features = ["s3"] +# +# [[bin]] +# name = "nu_plugin_extra_chart_bar" +# path = "src/plugins/nu_plugin_extra_chart_bar.rs" +# required-features = ["chart"] +# +# [[bin]] +# name = "nu_plugin_extra_chart_line" +# path = "src/plugins/nu_plugin_extra_chart_line.rs" +# required-features = ["chart"] +# +# [[bin]] +# name = "nu_plugin_extra_xpath" +# path = "src/plugins/nu_plugin_extra_xpath.rs" +# required-features = ["xpath"] +# +# [[bin]] +# name = "nu_plugin_extra_selector" +# path = "src/plugins/nu_plugin_extra_selector.rs" +# required-features = ["selector"] +# +# [[bin]] +# name = "nu_plugin_extra_from_bson" +# path = "src/plugins/nu_plugin_extra_from_bson.rs" +# required-features = ["bson"] +# +# [[bin]] +# name = "nu_plugin_extra_to_bson" +# path = "src/plugins/nu_plugin_extra_to_bson.rs" +# required-features = ["bson"] +# +# [[bin]] +# name = "nu_plugin_extra_from_sqlite" +# path = "src/plugins/nu_plugin_extra_from_sqlite.rs" +# required-features = ["sqlite"] +# +# [[bin]] +# name = "nu_plugin_extra_to_sqlite" +# path = "src/plugins/nu_plugin_extra_to_sqlite.rs" +# required-features = ["sqlite"] +# +# Main nu binary +[[bin]] +name = "nu" +path = "src/main.rs" diff --git a/crates/nu-ansi-term/Cargo.toml b/crates/nu-ansi-term/Cargo.toml index 808e7ce9b6..48ac4dc3c5 100644 --- a/crates/nu-ansi-term/Cargo.toml +++ b/crates/nu-ansi-term/Cargo.toml @@ -9,7 +9,7 @@ description = "Library for ANSI terminal colors and styles (bold, underline)" edition = "2018" license = "MIT" name = "nu-ansi-term" -version = "0.44.0" +version = "0.42.0" [lib] doctest = false diff --git a/crates/nu-cli/Cargo.toml b/crates/nu-cli/Cargo.toml index 7af081a40e..f5b596e0fe 100644 --- a/crates/nu-cli/Cargo.toml +++ b/crates/nu-cli/Cargo.toml @@ -1,43 +1,21 @@ [package] -authors = ["The Nu Project Contributors"] -description = "CLI for nushell" -edition = "2018" -license = "MIT" name = "nu-cli" -version = "0.44.0" -build = "build.rs" - -[lib] -doctest = false +version = "0.59.0" +edition = "2021" [dependencies] -nu-completion = { version = "0.44.0", path="../nu-completion" } -nu-command = { version = "0.44.0", path="../nu-command" } -nu-data = { version = "0.44.0", path="../nu-data" } -nu-engine = { version = "0.44.0", path="../nu-engine" } -nu-errors = { version = "0.44.0", path="../nu-errors" } -nu-parser = { version = "0.44.0", path="../nu-parser" } -nu-protocol = { version = "0.44.0", path="../nu-protocol" } -nu-source = { version = "0.44.0", path="../nu-source" } -nu-stream = { version = "0.44.0", path="../nu-stream" } -nu-ansi-term = { version = "0.44.0", path="../nu-ansi-term" } -nu-path = { version = "0.44.0", path="../nu-path" } +nu-engine = { path = "../nu-engine", version = "0.59.0" } +nu-path = { path = "../nu-path", version = "0.59.0" } +nu-parser = { path = "../nu-parser", version = "0.59.0" } +nu-protocol = { path = "../nu-protocol", version = "0.59.0" } +# nu-ansi-term = { path = "../nu-ansi-term", version = "0.59.0" } +nu-ansi-term = "0.42.0" -indexmap ="1.6.1" -log = "0.4.14" -pretty_env_logger = "0.4.0" -strip-ansi-escapes = "0.1.0" -rustyline = { version="9.0.0", optional=true } -ctrlc = { version="3.1.7", optional=true } -shadow-rs = { version = "0.8.1", default-features = false, optional = true } -serde = { version="1.0.123", features=["derive"] } -serde_yaml = "0.8.16" -lazy_static = "1.4.0" +nu-color-config = { path = "../nu-color-config" } -[build-dependencies] -shadow-rs = "0.8.1" +miette = { version = "3.0.0", features = ["fancy"] } +thiserror = "1.0.29" +reedline = { git = "https://github.com/nushell/reedline", branch = "main" } -[features] -default = ["shadow-rs"] -rustyline-support = ["rustyline", "nu-engine/rustyline-support"] -stable = [] +log = "0.4" +is_executable = "1.0.1" diff --git a/crates/nu-cli/README.md b/crates/nu-cli/README.md deleted file mode 100644 index ae7eeaed70..0000000000 --- a/crates/nu-cli/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# nu-cli - -This crate provides the fundamental needs when creating the Nushell interactive REPL. In it, you'll find features for interacting with the line editor (the piece which writes the prompt and takes input from the user), keybindings, handlers for the commandline arguments passed to the REPL as it starts up, and more. - diff --git a/crates/nu-cli/build.rs b/crates/nu-cli/build.rs deleted file mode 100644 index 4a0dfc4591..0000000000 --- a/crates/nu-cli/build.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() -> shadow_rs::SdResult<()> { - shadow_rs::new() -} diff --git a/crates/nu-cli/src/app.rs b/crates/nu-cli/src/app.rs deleted file mode 100644 index 51cd6be0f0..0000000000 --- a/crates/nu-cli/src/app.rs +++ /dev/null @@ -1,638 +0,0 @@ -mod logger; -mod options; -mod options_parser; -pub mod stopwatch; - -use self::stopwatch::Stopwatch; -use lazy_static::lazy_static; -use nu_command::{commands::NuSignature as Nu, utils::test_bins as binaries}; -use nu_engine::{get_full_help, EvaluationContext}; -use nu_errors::ShellError; -use nu_protocol::hir::{Call, Expression, SpannedExpression, Synthetic}; -use nu_protocol::{Primitive, UntaggedValue}; -use nu_source::{Span, Tag}; -use nu_stream::InputStream; -pub use options::{CliOptions, NuScript, Options}; -use options_parser::{NuParser, OptionsParser}; -use std::sync::Mutex; - -lazy_static! { - pub static ref STOPWATCH: Mutex = { - let mut sw = Stopwatch::default(); - sw.start(); - sw.stop(); - Mutex::new(sw) - }; -} - -pub struct App { - parser: Box, - pub options: Options, -} - -impl App { - pub fn new(parser: Box, options: Options) -> Self { - Self { parser, options } - } - - pub fn run(args: &[String]) -> Result<(), ShellError> { - let nu = Box::new(NuParser::new()); - let options = Options::default(); - let ui = App::new(nu, options); - - ui.main(args) - } - - pub fn main(&self, argv: &[String]) -> Result<(), ShellError> { - if self.perf() { - // start the stopwatch running - STOPWATCH - .lock() - .expect("unable to lock the stopwatch") - .start(); - } - - let argv = quote_positionals(argv).join(" "); - - if let Err(cause) = self.parse(&argv) { - self.parser - .context() - .host() - .lock() - .print_err(cause, &nu_source::Text::from(argv)); - std::process::exit(1); - } - - if self.help() { - let context = self.parser.context(); - let stream = nu_stream::OutputStream::one( - UntaggedValue::string(get_full_help(&Nu, &context.scope)) - .into_value(nu_source::Tag::unknown()), - ); - - consume(context, stream)?; - - if self.perf() { - // stop the stopwatch since we're exiting - STOPWATCH - .lock() - .expect("unable to lock the stopwatch") - .stop(); - eprintln!( - "help {:?}", - STOPWATCH - .lock() - .expect("unable to lock the stopwatch") - .elapsed() - ); - } - - std::process::exit(0); - } - - if self.version() { - let context = self.parser.context(); - - let stream = nu_command::commands::version(nu_engine::CommandArgs { - context: context.clone(), - call_info: nu_engine::UnevaluatedCallInfo { - args: Call::new( - Box::new(SpannedExpression::new( - Expression::Synthetic(Synthetic::String("version".to_string())), - Span::unknown(), - )), - Span::unknown(), - ), - name_tag: Tag::unknown(), - }, - input: InputStream::empty(), - })?; - - let stream = { - let command = context - .get_command("pivot") - .expect("could not find version command"); - - context.run_command( - command, - Tag::unknown(), - Call::new( - Box::new(SpannedExpression::new( - Expression::Synthetic(Synthetic::String("pivot".to_string())), - Span::unknown(), - )), - Span::unknown(), - ), - stream, - )? - }; - - consume(context, stream)?; - - if self.perf() { - // stop the stopwatch since we're exiting - STOPWATCH - .lock() - .expect("unable to lock the stopwatch") - .stop(); - eprintln!( - "version {:?}", - STOPWATCH - .lock() - .expect("unable to lock the stopwatch") - .elapsed() - ); - } - - std::process::exit(0); - } - - if let Some(bin) = self.testbin() { - match bin.as_deref() { - Ok("echo_env") => binaries::echo_env(), - Ok("cococo") => binaries::cococo(), - Ok("meow") => binaries::meow(), - Ok("iecho") => binaries::iecho(), - Ok("fail") => binaries::fail(), - Ok("nonu") => binaries::nonu(), - Ok("chop") => binaries::chop(), - Ok("repeater") => binaries::repeater(), - _ => unreachable!(), - } - - return Ok(()); - } - - let mut opts = CliOptions::new(); - opts.config = self.config().map(std::ffi::OsString::from); - opts.stdin = self.takes_stdin(); - opts.save_history = self.save_history(); - opts.perf = self.perf(); - - use logger::{configure, debug_filters, logger, trace_filters}; - - logger(|builder| { - configure(self, builder)?; - trace_filters(self, builder)?; - debug_filters(self, builder)?; - - Ok(()) - })?; - - if self.perf() { - // start a new split - STOPWATCH - .lock() - .expect("unable to lock the stopwatch") - .start() - } - - if let Some(commands) = self.commands() { - let commands = commands?; - let script = NuScript::code(&commands)?; - opts.scripts = vec![script]; - let context = crate::create_default_context(false)?; - return crate::run_script_file(context, opts); - } - - if self.perf() { - // start a new spit - eprintln!( - "commands using -c at launch: {:?}", - STOPWATCH - .lock() - .expect("unable to lock the stopwatch") - .elapsed_split() - ); - STOPWATCH - .lock() - .expect("unable to lock the stopwatch") - .start(); - } - - if let Some(scripts) = self.scripts() { - let mut source_files = vec![]; - for script in scripts { - let script_name = script?; - let path = std::ffi::OsString::from(&script_name); - - match NuScript::source_file(path.as_os_str()) { - Ok(file) => source_files.push(file), - Err(_) => { - eprintln!("File not found: {}", script_name); - return Ok(()); - } - } - } - - for file in source_files { - let mut opts = opts.clone(); - opts.scripts = vec![file]; - - let context = crate::create_default_context(false)?; - crate::run_script_file(context, opts)?; - } - - return Ok(()); - } - - if self.perf() { - // start a new split - eprintln!( - "script file(s) passed in: {:?}", - STOPWATCH - .lock() - .expect("unable to lock the stopwatch") - .elapsed_split() - ); - STOPWATCH - .lock() - .expect("unable to lock the stopwatch") - .start(); - } - - let context = crate::create_default_context(true)?; - - if !self.skip_plugins() { - let _ = crate::register_plugins(&context); - } - - if self.perf() { - // start a new split - eprintln!( - "plugins registered: {:?}", - STOPWATCH - .lock() - .expect("unable to lock the stopwatch") - .elapsed_split() - ); - } - - #[cfg(feature = "rustyline-support")] - { - crate::cli(context, opts)?; - } - - #[cfg(not(feature = "rustyline-support"))] - { - println!("Nushell needs the 'rustyline-support' feature for CLI support"); - } - - Ok(()) - } - - pub fn commands(&self) -> Option> { - self.options.get("commands").map(|v| match v.value { - UntaggedValue::Error(err) => Err(err), - UntaggedValue::Primitive(Primitive::String(name)) => Ok(name), - _ => Err(ShellError::untagged_runtime_error("Unsupported option")), - }) - } - - pub fn perf(&self) -> bool { - self.options - .get("perf") - .map(|v| matches!(v.as_bool(), Ok(true))) - .unwrap_or(false) - } - - pub fn help(&self) -> bool { - self.options - .get("help") - .map(|v| matches!(v.as_bool(), Ok(true))) - .unwrap_or(false) - } - - pub fn version(&self) -> bool { - self.options - .get("version") - .map(|v| matches!(v.as_bool(), Ok(true))) - .unwrap_or(false) - } - - pub fn scripts(&self) -> Option>> { - self.options.get("args").map(|v| { - v.table_entries() - .map(|v| match &v.value { - UntaggedValue::Error(err) => Err(err.clone()), - UntaggedValue::Primitive(Primitive::FilePath(path)) => { - Ok(path.display().to_string()) - } - UntaggedValue::Primitive(Primitive::String(name)) => Ok(name.clone()), - _ => Err(ShellError::untagged_runtime_error("Unsupported option")), - }) - .collect() - }) - } - - pub fn takes_stdin(&self) -> bool { - self.options - .get("stdin") - .map(|v| matches!(v.as_bool(), Ok(true))) - .unwrap_or(false) - } - - pub fn config(&self) -> Option { - self.options - .get("config-file") - .map(|v| v.as_string().expect("not a string")) - } - - pub fn develop(&self) -> Option>> { - self.options.get("develop").map(|v| { - let mut values = vec![]; - - match v.value { - UntaggedValue::Error(err) => values.push(Err(err)), - UntaggedValue::Primitive(Primitive::String(filters)) => { - values.extend(filters.split(',').map(|filter| Ok(filter.to_string()))); - } - _ => values.push(Err(ShellError::untagged_runtime_error( - "Unsupported option", - ))), - }; - - values - }) - } - - pub fn debug(&self) -> Option>> { - self.options.get("debug").map(|v| { - let mut values = vec![]; - - match v.value { - UntaggedValue::Error(err) => values.push(Err(err)), - UntaggedValue::Primitive(Primitive::String(filters)) => { - values.extend(filters.split(',').map(|filter| Ok(filter.to_string()))); - } - _ => values.push(Err(ShellError::untagged_runtime_error( - "Unsupported option", - ))), - }; - - values - }) - } - - pub fn loglevel(&self) -> Option> { - self.options.get("loglevel").map(|v| match v.value { - UntaggedValue::Error(err) => Err(err), - UntaggedValue::Primitive(Primitive::String(name)) => Ok(name), - _ => Err(ShellError::untagged_runtime_error("Unsupported option")), - }) - } - - pub fn testbin(&self) -> Option> { - self.options.get("testbin").map(|v| match v.value { - UntaggedValue::Error(err) => Err(err), - UntaggedValue::Primitive(Primitive::String(name)) => Ok(name), - _ => Err(ShellError::untagged_runtime_error("Unsupported option")), - }) - } - - pub fn skip_plugins(&self) -> bool { - self.options - .get("skip-plugins") - .map(|v| matches!(v.as_bool(), Ok(true))) - .unwrap_or(false) - } - - pub fn save_history(&self) -> bool { - self.options - .get("no-history") - .map(|v| !matches!(v.as_bool(), Ok(true))) - .unwrap_or(true) - } - - pub fn parse(&self, args: &str) -> Result<(), ShellError> { - self.parser.parse(args).map(|options| { - self.options.swap(&options); - }) - } -} - -fn quote_positionals(parameters: &[String]) -> Vec { - parameters - .iter() - .cloned() - .map(|arg| { - if arg.contains(' ') { - format!("\"{}\"", arg) - } else { - arg - } - }) - .collect::>() -} - -fn consume(context: &EvaluationContext, stream: InputStream) -> Result<(), ShellError> { - let autoview_cmd = context - .get_command("autoview") - .expect("could not find autoview command"); - - let stream = context.run_command( - autoview_cmd, - Tag::unknown(), - Call::new( - Box::new(SpannedExpression::new( - Expression::Synthetic(Synthetic::String("autoview".to_string())), - Span::unknown(), - )), - Span::unknown(), - ), - stream, - )?; - - for _ in stream {} - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn cli_app() -> App { - let parser = Box::new(NuParser::new()); - let options = Options::default(); - - App::new(parser, options) - } - - #[test] - fn default_options() -> Result<(), ShellError> { - let ui = cli_app(); - - ui.parse("nu")?; - assert!(!ui.version()); - assert!(!ui.help()); - assert!(!ui.takes_stdin()); - assert!(ui.save_history()); - assert!(!ui.skip_plugins()); - assert_eq!(ui.config(), None); - assert_eq!(ui.loglevel(), None); - assert_eq!(ui.debug(), None); - assert_eq!(ui.develop(), None); - assert_eq!(ui.testbin(), None); - assert_eq!(ui.commands(), None); - assert_eq!(ui.scripts(), None); - Ok(()) - } - - #[test] - fn reports_errors_on_unsupported_flags() -> Result<(), ShellError> { - let ui = cli_app(); - - assert!(ui.parse("nu --coonfig-file /path/to/config.toml").is_err()); - assert!(ui.config().is_none()); - Ok(()) - } - - #[test] - fn configures_debug_trace_level_with_filters() -> Result<(), ShellError> { - let ui = cli_app(); - ui.parse("nu --develop=cli,parser")?; - assert_eq!(ui.develop().unwrap()[0], Ok("cli".to_string())); - assert_eq!(ui.develop().unwrap()[1], Ok("parser".to_string())); - Ok(()) - } - - #[test] - fn configures_debug_level_with_filters() -> Result<(), ShellError> { - let ui = cli_app(); - ui.parse("nu --debug=cli,run")?; - assert_eq!(ui.debug().unwrap()[0], Ok("cli".to_string())); - assert_eq!(ui.debug().unwrap()[1], Ok("run".to_string())); - Ok(()) - } - - #[test] - fn can_use_loglevels() -> Result<(), ShellError> { - for level in ["error", "warn", "info", "debug", "trace"] { - let ui = cli_app(); - let args = format!("nu --loglevel={}", level); - ui.parse(&args)?; - assert_eq!(ui.loglevel().unwrap(), Ok(level.to_string())); - } - - let ui = cli_app(); - ui.parse("nu --loglevel=nada")?; - assert_eq!( - ui.loglevel().unwrap(), - Err(ShellError::untagged_runtime_error("nada is not supported.")) - ); - - Ok(()) - } - - #[test] - fn can_be_login() -> Result<(), ShellError> { - let ui = cli_app(); - ui.parse("nu -l")?; - - let ui = cli_app(); - ui.parse("nu --login")?; - - Ok(()) - } - - #[test] - fn can_be_passed_nu_scripts() -> Result<(), ShellError> { - let ui = cli_app(); - ui.parse("nu code.nu bootstrap.nu")?; - assert_eq!(ui.scripts().unwrap()[0], Ok("code.nu".into())); - assert_eq!(ui.scripts().unwrap()[1], Ok("bootstrap.nu".into())); - Ok(()) - } - - #[test] - fn can_use_test_binaries() -> Result<(), ShellError> { - for binarie_name in [ - "echo_env", "cococo", "iecho", "fail", "nonu", "chop", "repeater", "meow", - ] { - let ui = cli_app(); - let args = format!("nu --testbin={}", binarie_name); - ui.parse(&args)?; - assert_eq!(ui.testbin().unwrap(), Ok(binarie_name.to_string())); - } - - let ui = cli_app(); - ui.parse("nu --testbin=andres")?; - assert_eq!( - ui.testbin().unwrap(), - Err(ShellError::untagged_runtime_error( - "andres is not supported." - )) - ); - - Ok(()) - } - - #[test] - fn has_version() -> Result<(), ShellError> { - let ui = cli_app(); - - ui.parse("nu --version")?; - assert!(ui.version()); - Ok(()) - } - - #[test] - fn has_help() -> Result<(), ShellError> { - let ui = cli_app(); - - ui.parse("nu --help")?; - assert!(ui.help()); - Ok(()) - } - - #[test] - fn can_take_stdin() -> Result<(), ShellError> { - let ui = cli_app(); - - ui.parse("nu --stdin")?; - assert!(ui.takes_stdin()); - Ok(()) - } - - #[test] - fn can_opt_to_avoid_saving_history() -> Result<(), ShellError> { - let ui = cli_app(); - - ui.parse("nu --no-history")?; - assert!(!ui.save_history()); - Ok(()) - } - - #[test] - fn can_opt_to_skip_plugins() -> Result<(), ShellError> { - let ui = cli_app(); - - ui.parse("nu --skip-plugins")?; - assert!(ui.skip_plugins()); - Ok(()) - } - - #[test] - fn understands_commands_need_to_be_run() -> Result<(), ShellError> { - let ui = cli_app(); - - ui.parse("nu -c \"ls | get name\"")?; - assert_eq!(ui.commands().unwrap(), Ok(String::from("ls | get name"))); - - let ui = cli_app(); - - ui.parse("nu -c \"echo 'hola'\"")?; - assert_eq!(ui.commands().unwrap(), Ok(String::from("echo 'hola'"))); - Ok(()) - } - - #[test] - fn knows_custom_configurations() -> Result<(), ShellError> { - let ui = cli_app(); - - ui.parse("nu --config-file /path/to/config.toml")?; - assert_eq!(ui.config().unwrap(), String::from("/path/to/config.toml")); - Ok(()) - } -} diff --git a/crates/nu-cli/src/app/logger.rs b/crates/nu-cli/src/app/logger.rs deleted file mode 100644 index 02644a856b..0000000000 --- a/crates/nu-cli/src/app/logger.rs +++ /dev/null @@ -1,52 +0,0 @@ -use super::App; -use log::LevelFilter; -use nu_errors::ShellError; -use pretty_env_logger::env_logger::Builder; - -pub fn logger(f: impl FnOnce(&mut Builder) -> Result<(), ShellError>) -> Result<(), ShellError> { - let mut builder = pretty_env_logger::formatted_builder(); - f(&mut builder)?; - let _ = builder.try_init(); - Ok(()) -} - -pub fn configure(app: &App, logger: &mut Builder) -> Result<(), ShellError> { - if let Some(level) = app.loglevel() { - let level = match level.as_deref() { - Ok("error") => LevelFilter::Error, - Ok("warn") => LevelFilter::Warn, - Ok("info") => LevelFilter::Info, - Ok("debug") => LevelFilter::Debug, - Ok("trace") => LevelFilter::Trace, - Ok(_) | Err(_) => LevelFilter::Warn, - }; - - logger.filter_module("nu", level); - }; - - if let Ok(s) = std::env::var("RUST_LOG") { - logger.parse_filters(&s); - } - - Ok(()) -} - -pub fn trace_filters(app: &App, logger: &mut Builder) -> Result<(), ShellError> { - if let Some(filters) = app.develop() { - filters.into_iter().filter_map(Result::ok).for_each(|name| { - logger.filter_module(&name, LevelFilter::Trace); - }) - } - - Ok(()) -} - -pub fn debug_filters(app: &App, logger: &mut Builder) -> Result<(), ShellError> { - if let Some(filters) = app.debug() { - filters.into_iter().filter_map(Result::ok).for_each(|name| { - logger.filter_module(&name, LevelFilter::Debug); - }) - } - - Ok(()) -} diff --git a/crates/nu-cli/src/app/options.rs b/crates/nu-cli/src/app/options.rs deleted file mode 100644 index 025316578d..0000000000 --- a/crates/nu-cli/src/app/options.rs +++ /dev/null @@ -1,102 +0,0 @@ -use indexmap::IndexMap; -use nu_errors::ShellError; -use nu_protocol::{UntaggedValue, Value}; -use std::cell::RefCell; -use std::ffi::{OsStr, OsString}; - -#[derive(Debug, Clone)] -pub struct CliOptions { - pub config: Option, - pub stdin: bool, - pub scripts: Vec, - pub save_history: bool, - pub perf: bool, -} - -impl Default for CliOptions { - fn default() -> Self { - Self::new() - } -} - -impl CliOptions { - pub fn new() -> Self { - Self { - config: None, - stdin: false, - scripts: vec![], - save_history: true, - perf: false, - } - } -} - -#[derive(Debug)] -pub struct Options { - inner: RefCell>, -} - -impl Options { - pub fn default() -> Self { - Self { - inner: RefCell::new(IndexMap::default()), - } - } - - pub fn get(&self, key: &str) -> Option { - self.inner.borrow().get(key).cloned() - } - - pub fn put(&self, key: &str, value: Value) { - self.inner.borrow_mut().insert(key.into(), value); - } - - pub fn shift(&self) { - if let Some(Value { - value: UntaggedValue::Table(ref mut args), - .. - }) = self.inner.borrow_mut().get_mut("args") - { - args.remove(0); - } - } - - pub fn swap(&self, other: &Options) { - self.inner.swap(&other.inner); - } -} - -#[derive(Debug, Clone)] -pub struct NuScript { - pub filepath: Option, - pub contents: String, -} - -impl NuScript { - pub fn code(content: &str) -> Result { - Ok(Self { - filepath: None, - contents: content.to_string(), - }) - } - - pub fn get_code(&self) -> &str { - &self.contents - } - - pub fn source_file(path: &OsStr) -> Result { - use std::fs::File; - use std::io::Read; - - let path = path.to_os_string(); - let mut file = File::open(&path)?; - let mut buffer = String::new(); - - file.read_to_string(&mut buffer)?; - - Ok(Self { - filepath: Some(path), - contents: buffer, - }) - } -} diff --git a/crates/nu-cli/src/app/options_parser.rs b/crates/nu-cli/src/app/options_parser.rs deleted file mode 100644 index d6efb6b0b1..0000000000 --- a/crates/nu-cli/src/app/options_parser.rs +++ /dev/null @@ -1,132 +0,0 @@ -use super::Options; - -use nu_command::commands::{loglevels, testbins, NuSignature as Nu}; -use nu_command::commands::{Autoview, Pivot, Table, Version as NuVersion}; -use nu_engine::{whole_stream_command, EvaluationContext}; -use nu_errors::ShellError; -use nu_protocol::hir::{ClassifiedCommand, InternalCommand, NamedValue}; -use nu_protocol::UntaggedValue; -use nu_source::Tag; - -pub struct NuParser { - context: EvaluationContext, -} - -pub trait OptionsParser { - fn parse(&self, input: &str) -> Result; - fn context(&self) -> &EvaluationContext; -} - -impl NuParser { - pub fn new() -> Self { - let context = EvaluationContext::basic(); - context.add_commands(vec![ - whole_stream_command(Nu {}), - whole_stream_command(NuVersion {}), - whole_stream_command(Autoview {}), - whole_stream_command(Pivot {}), - whole_stream_command(Table {}), - ]); - - Self { context } - } -} - -impl OptionsParser for NuParser { - fn context(&self) -> &EvaluationContext { - &self.context - } - - fn parse(&self, input: &str) -> Result { - let options = Options::default(); - let (lite_result, _err) = nu_parser::lex(input, 0, nu_parser::NewlineMode::Normal); - let (lite_result, _err) = nu_parser::parse_block(lite_result); - - let (parsed, err) = nu_parser::classify_block(&lite_result, &self.context.scope); - - if let Some(reason) = err { - return Err(reason.into()); - } - - match parsed.block[0].pipelines[0].list[0] { - ClassifiedCommand::Internal(InternalCommand { ref args, .. }) => { - if let Some(ref params) = args.named { - params.iter().for_each(|(k, v)| { - let value = match v { - NamedValue::AbsentSwitch => { - Some(UntaggedValue::from(false).into_untagged_value()) - } - NamedValue::PresentSwitch(span) => { - Some(UntaggedValue::from(true).into_value(Tag::from(span))) - } - NamedValue::AbsentValue => None, - NamedValue::Value(span, exprs) => { - let value = nu_engine::evaluate_baseline_expr(exprs, &self.context) - .expect("value"); - Some(value.value.into_value(Tag::from(span))) - } - }; - - let value = value.map(|v| match k.as_ref() { - "testbin" => { - if let Ok(name) = v.as_string() { - if testbins().iter().any(|n| name == *n) { - v - } else { - UntaggedValue::Error(ShellError::untagged_runtime_error( - format!("{} is not supported.", name), - )) - .into_value(v.tag) - } - } else { - v - } - } - "loglevel" => { - if let Ok(name) = v.as_string() { - if loglevels().iter().any(|n| name == *n) { - v - } else { - UntaggedValue::Error(ShellError::untagged_runtime_error( - format!("{} is not supported.", name), - )) - .into_value(v.tag) - } - } else { - v - } - } - _ => v, - }); - - if let Some(value) = value { - options.put(k, value); - } - }); - } - - let mut positional_args = vec![]; - - if let Some(positional) = &args.positional { - for pos in positional { - let result = nu_engine::evaluate_baseline_expr(pos, &self.context)?; - positional_args.push(result); - } - } - - if !positional_args.is_empty() { - options.put( - "args", - UntaggedValue::Table(positional_args).into_untagged_value(), - ); - } - } - ClassifiedCommand::Error(ref reason) => { - return Err(reason.clone().into()); - } - _ => return Err(ShellError::untagged_runtime_error("unrecognized command")), - } - - Ok(options) - } -} diff --git a/crates/nu-cli/src/app/stopwatch.rs b/crates/nu-cli/src/app/stopwatch.rs deleted file mode 100644 index e433782aff..0000000000 --- a/crates/nu-cli/src/app/stopwatch.rs +++ /dev/null @@ -1,118 +0,0 @@ -#![allow(dead_code)] -use std::default::Default; -use std::fmt; -use std::time::{Duration, Instant}; - -#[derive(Clone, Copy)] -pub struct Stopwatch { - /// The time the stopwatch was started last, if ever. - start_time: Option, - /// The time the stopwatch was split last, if ever. - split_time: Option, - /// The time elapsed while the stopwatch was running (between start() and stop()). - elapsed: Duration, -} - -impl Default for Stopwatch { - fn default() -> Stopwatch { - Stopwatch { - start_time: None, - split_time: None, - elapsed: Duration::from_secs(0), - } - } -} - -impl fmt::Display for Stopwatch { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - return write!(f, "{}ms", self.elapsed_ms()); - } -} - -impl Stopwatch { - /// Returns a new stopwatch. - pub fn new() -> Stopwatch { - let sw: Stopwatch = Default::default(); - sw - } - - /// Returns a new stopwatch which will immediately be started. - pub fn start_new() -> Stopwatch { - let mut sw = Stopwatch::new(); - sw.start(); - sw - } - - /// Starts the stopwatch. - pub fn start(&mut self) { - self.start_time = Some(Instant::now()); - } - - /// Stops the stopwatch. - pub fn stop(&mut self) { - self.elapsed = self.elapsed(); - self.start_time = None; - self.split_time = None; - } - - /// Resets all counters and stops the stopwatch. - pub fn reset(&mut self) { - self.elapsed = Duration::from_secs(0); - self.start_time = None; - self.split_time = None; - } - - /// Resets and starts the stopwatch again. - pub fn restart(&mut self) { - self.reset(); - self.start(); - } - - /// Returns whether the stopwatch is running. - pub fn is_running(&self) -> bool { - self.start_time.is_some() - } - - /// Returns the elapsed time since the start of the stopwatch. - pub fn elapsed(&self) -> Duration { - match self.start_time { - // stopwatch is running - Some(t1) => t1.elapsed() + self.elapsed, - // stopwatch is not running - None => self.elapsed, - } - } - - /// Returns the elapsed time since the start of the stopwatch in milliseconds. - pub fn elapsed_ms(&self) -> i64 { - let dur = self.elapsed(); - (dur.as_secs() * 1000 + dur.subsec_millis() as u64) as i64 - } - - /// Returns the elapsed time since last split or start/restart. - /// - /// If the stopwatch is in stopped state this will always return a zero Duration. - pub fn elapsed_split(&mut self) -> Duration { - match self.start_time { - // stopwatch is running - Some(start) => { - let res = match self.split_time { - Some(split) => split.elapsed(), - None => start.elapsed(), - }; - self.split_time = Some(Instant::now()); - res - } - // stopwatch is not running - None => Duration::from_secs(0), - } - } - - /// Returns the elapsed time since last split or start/restart in milliseconds. - /// - /// If the stopwatch is in stopped state this will always return zero. - pub fn elapsed_split_ms(&mut self) -> i64 { - let dur = self.elapsed_split(); - (dur.as_secs() * 1000 + dur.subsec_millis() as u64) as i64 - } -} diff --git a/crates/nu-cli/src/cli.rs b/crates/nu-cli/src/cli.rs deleted file mode 100644 index 72f2f4c978..0000000000 --- a/crates/nu-cli/src/cli.rs +++ /dev/null @@ -1,538 +0,0 @@ -use crate::app::STOPWATCH; -use crate::line_editor::configure_ctrl_c; -use nu_ansi_term::Color; -use nu_engine::{maybe_print_errors, run_block, script::run_script_standalone, EvaluationContext}; - -#[allow(unused_imports)] -pub(crate) use nu_engine::script::{process_script, LineResult}; - -#[cfg(feature = "rustyline-support")] -use crate::line_editor::{ - configure_rustyline_editor, convert_rustyline_result_to_string, - default_rustyline_editor_configuration, nu_line_editor_helper, -}; - -#[allow(unused_imports)] -use nu_data::config; -use nu_source::{Tag, Text}; -use nu_stream::InputStream; -#[allow(unused_imports)] -use std::sync::atomic::Ordering; - -#[cfg(feature = "rustyline-support")] -use rustyline::{self, error::ReadlineError}; - -use nu_errors::ShellError; -use nu_parser::ParserScope; -use nu_path::expand_tilde; -use nu_protocol::{hir::ExternalRedirection, ConfigPath, UntaggedValue, Value}; - -use log::trace; -use std::error::Error; -use std::iter::Iterator; -use std::path::PathBuf; - -// Name of environment variable where the prompt could be stored -#[cfg(feature = "rustyline-support")] -const PROMPT_COMMAND: &str = "PROMPT_COMMAND"; - -pub fn search_paths() -> Vec { - use std::env; - let mut search_paths = Vec::new(); - - // Automatically add path `nu` is in as a search path - if let Ok(exe_path) = env::current_exe() { - if let Some(exe_dir) = exe_path.parent() { - search_paths.push(exe_dir.to_path_buf()); - } - } - - if let Ok(config) = nu_data::config::config(Tag::unknown()) { - if let Some(Value { - value: UntaggedValue::Table(pipelines), - .. - }) = config.get("plugin_dirs") - { - for pipeline in pipelines { - if let Ok(plugin_dir) = pipeline.as_string() { - search_paths.push(expand_tilde(plugin_dir)); - } - } - } - } - - search_paths -} - -pub fn run_script_file( - context: EvaluationContext, - options: super::app::CliOptions, -) -> Result<(), ShellError> { - if let Some(cfg) = options.config { - load_cfg_as_global_cfg(&context, PathBuf::from(cfg)); - } else { - load_global_cfg(&context); - } - - let _ = register_plugins(&context); - let _ = configure_ctrl_c(&context); - - let script = options - .scripts - .get(0) - .ok_or_else(|| ShellError::unexpected("Nu source code not available"))?; - - run_script_standalone(script.get_code().to_string(), options.stdin, &context, true)?; - - Ok(()) -} - -#[cfg(feature = "rustyline-support")] -fn default_prompt_string(cwd: &str) -> String { - format!( - "{}{}{}{}{}{}> ", - Color::Green.bold().prefix(), - cwd, - nu_ansi_term::ansi::RESET, - Color::Cyan.bold().prefix(), - current_branch(), - nu_ansi_term::ansi::RESET - ) -} - -#[cfg(feature = "rustyline-support")] -fn evaluate_prompt_string(prompt_line: &str, context: &EvaluationContext, cwd: &str) -> String { - context.scope.enter_scope(); - let (prompt_block, err) = nu_parser::parse(prompt_line, 0, &context.scope); - - if err.is_some() { - context.scope.exit_scope(); - default_prompt_string(cwd) - } else { - let run_result = run_block( - &prompt_block, - context, - InputStream::empty(), - ExternalRedirection::Stdout, - ); - context.scope.exit_scope(); - - match run_result { - Ok(result) => match result.collect_string(Tag::unknown()) { - Ok(string_result) => { - let errors = context.get_errors(); - maybe_print_errors(context, Text::from(prompt_line)); - context.clear_errors(); - - if !errors.is_empty() { - "> ".into() - } else { - string_result.item - } - } - Err(e) => { - context.host().lock().print_err(e, &Text::from(prompt_line)); - context.clear_errors(); - - "> ".into() - } - }, - Err(e) => { - context.host().lock().print_err(e, &Text::from(prompt_line)); - context.clear_errors(); - - "> ".into() - } - } - } -} - -#[cfg(feature = "rustyline-support")] -pub fn cli( - context: EvaluationContext, - options: super::app::CliOptions, -) -> Result<(), Box> { - let _ = configure_ctrl_c(&context); - - // start time for running startup scripts (this metric includes loading of the cfg, but w/e) - let startup_commands_start_time = std::time::Instant::now(); - - if let Some(cfg) = options.config { - load_cfg_as_global_cfg(&context, PathBuf::from(cfg)); - } else { - load_global_cfg(&context); - } - - // Store cmd duration in an env var - context.scope.add_env_var( - "CMD_DURATION_MS", - startup_commands_start_time - .elapsed() - .as_millis() - .to_string(), - ); - - if options.perf { - eprintln!( - "config loaded: {:?}", - STOPWATCH - .lock() - .expect("unable to lock the stopwatch") - .elapsed_split() - ); - STOPWATCH - .lock() - .expect("unable to lock the stopwatch") - .start(); - } - - //Configure rustyline - let mut rl = default_rustyline_editor_configuration(); - let history_path = if let Some(cfg) = &context.configs().lock().global_config { - let _ = configure_rustyline_editor(&mut rl, cfg); - let helper = Some(nu_line_editor_helper(&context, cfg)); - rl.set_helper(helper); - nu_data::config::path::history_path_or_default(cfg) - } else { - nu_data::config::path::default_history_path() - }; - - if options.perf { - eprintln!( - "rustyline configuration: {:?}", - STOPWATCH - .lock() - .expect("unable to lock the stopwatch") - .elapsed_split() - ); - STOPWATCH - .lock() - .expect("unable to lock the stopwatch") - .start(); - } - - // Don't load history if it's not necessary - if options.save_history { - let _ = rl.load_history(&history_path); - } - - if options.perf { - eprintln!( - "history load: {:?}", - STOPWATCH - .lock() - .expect("unable to lock the stopwatch") - .elapsed_split() - ); - STOPWATCH - .lock() - .expect("unable to lock the stopwatch") - .start(); - } - - //set vars from cfg if present - let (skip_welcome_message, prompt) = if let Some(cfg) = &context.configs().lock().global_config - { - ( - cfg.var("skip_welcome_message") - .map(|x| x.is_true()) - .unwrap_or(false), - cfg.var("prompt"), - ) - } else { - (false, None) - }; - - if options.perf { - eprintln!( - "load custom prompt: {:?}", - STOPWATCH - .lock() - .expect("unable to lock the stopwatch") - .elapsed_split() - ); - STOPWATCH - .lock() - .expect("unable to lock the stopwatch") - .start(); - } - - //Check whether dir we start in contains local cfg file and if so load it. - load_local_cfg_if_present(&context); - - // Give ourselves a scope to work in - context.scope.enter_scope(); - - let mut session_text = String::new(); - let mut line_start: usize = 0; - - if !skip_welcome_message { - println!( - "Welcome to Nushell {} (type 'help' for more info)", - nu_command::commands::core_version() - ); - } - - #[cfg(windows)] - { - let _ = nu_ansi_term::enable_ansi_support(); - } - - let mut ctrlcbreak = false; - - if options.perf { - eprintln!( - "timing stopped. starting run loop: {:?}", - STOPWATCH - .lock() - .expect("unable to lock the stopwatch") - .elapsed_split() - ); - STOPWATCH - .lock() - .expect("unable to lock the stopwatch") - .stop(); - } - - loop { - if context.ctrl_c().load(Ordering::SeqCst) { - context.ctrl_c().store(false, Ordering::SeqCst); - continue; - } - - let cwd = context.shell_manager().path(); - - // Check if the PROMPT_COMMAND env variable is set. This env variable - // contains nu code that is used to overwrite the prompt - let colored_prompt = match context.scope.get_env(PROMPT_COMMAND) { - Some(env_prompt) => evaluate_prompt_string(&env_prompt, &context, &cwd), - None => { - if let Some(prompt) = &prompt { - let prompt_line = prompt.as_string()?; - evaluate_prompt_string(&prompt_line, &context, &cwd) - } else { - default_prompt_string(&cwd) - } - } - }; - - let prompt = { - if let Ok(bytes) = strip_ansi_escapes::strip(&colored_prompt) { - String::from_utf8_lossy(&bytes).to_string() - } else { - "> ".to_string() - } - }; - - if let Some(helper) = rl.helper_mut() { - helper.colored_prompt = colored_prompt; - } - let mut initial_command = Some(String::new()); - let mut readline = Err(ReadlineError::Eof); - while let Some(ref cmd) = initial_command { - readline = rl.readline_with_initial(&prompt, (cmd, "")); - initial_command = None; - } - - if let Ok(line) = &readline { - line_start = session_text.len(); - session_text.push_str(line); - session_text.push('\n'); - } - - // start time for command duration - let cmd_start_time = std::time::Instant::now(); - - let line = match convert_rustyline_result_to_string(readline) { - LineResult::Success(_) => process_script( - &session_text[line_start..], - &context, - false, - line_start, - true, - ), - x => x, - }; - - // Store cmd duration in an env var - context.scope.add_env_var( - "CMD_DURATION_MS", - cmd_start_time.elapsed().as_millis().to_string(), - ); - - match line { - LineResult::Success(line) => { - if options.save_history && !line.trim().is_empty() { - rl.add_history_entry(&line); - let _ = rl.append_history(&history_path); - } - maybe_print_errors(&context, Text::from(session_text.clone())); - } - - LineResult::ClearHistory => { - if options.save_history { - rl.clear_history(); - std::fs::remove_file(&history_path)?; - } - } - - LineResult::Error(line, err) => { - if options.save_history && !line.trim().is_empty() { - rl.add_history_entry(&line); - let _ = rl.append_history(&history_path); - } - - context - .host() - .lock() - .print_err(err, &Text::from(session_text.clone())); - - // I am not so sure, we don't need maybe_print_errors here (as we printed an err - // above), because maybe_print_errors also clears the errors. - // TODO Analyze where above err comes from, and whether we need to clear - // context.errors here - // Or just be consistent and return errors always in context.errors... - maybe_print_errors(&context, Text::from(session_text.clone())); - } - - LineResult::CtrlC => { - let config_ctrlc_exit = context - .configs() - .lock() - .global_config - .as_ref() - .and_then(|cfg| cfg.var("ctrlc_exit")) - .map(|ctrl_c| ctrl_c.is_true()) - .unwrap_or(false); // default behavior is to allow CTRL-C spamming similar to other shells - - if !config_ctrlc_exit { - continue; - } - - if ctrlcbreak { - if options.save_history { - let _ = rl.append_history(&history_path); - } - std::process::exit(0); - } else { - context.with_host(|host| host.stdout("CTRL-C pressed (again to quit)")); - ctrlcbreak = true; - continue; - } - } - - LineResult::CtrlD => { - context.shell_manager().remove_at_current(); - if context.shell_manager().is_empty() { - break; - } - } - - LineResult::Break => { - break; - } - } - ctrlcbreak = false; - } - - // we are ok if we can not save history - if options.save_history { - let _ = rl.append_history(&history_path); - } - - Ok(()) -} - -#[cfg(feature = "rustyline-support")] -pub fn load_local_cfg_if_present(context: &EvaluationContext) { - trace!("Loading local cfg if present"); - match config::loadable_cfg_exists_in_dir(PathBuf::from(context.shell_manager().path())) { - Ok(Some(cfg_path)) => { - if let Err(err) = context.load_config(&ConfigPath::Local(cfg_path)) { - context.host().lock().print_err(err, &Text::from("")) - } - } - Err(e) => { - //Report error while checking for local cfg file - context.host().lock().print_err(e, &Text::from("")) - } - Ok(None) => { - //No local cfg file present in start dir - } - } -} - -fn load_cfg_as_global_cfg(context: &EvaluationContext, path: PathBuf) { - if let Err(err) = context.load_config(&ConfigPath::Global(path)) { - context.host().lock().print_err(err, &Text::from("")); - } -} - -pub fn load_global_cfg(context: &EvaluationContext) { - match config::default_path() { - Ok(path) => { - load_cfg_as_global_cfg(context, path); - } - Err(e) => { - context.host().lock().print_err(e, &Text::from("")); - } - } -} - -pub fn register_plugins(context: &EvaluationContext) -> Result<(), ShellError> { - if let Ok(plugins) = nu_engine::plugin::build_plugin::scan(search_paths()) { - context.add_commands( - plugins - .into_iter() - .filter(|p| !context.is_command_registered(p.name())) - .collect(), - ); - } - - Ok(()) -} - -pub fn parse_and_eval(line: &str, ctx: &EvaluationContext) -> Result { - // FIXME: do we still need this? - let line = if let Some(s) = line.strip_suffix('\n') { - s - } else { - line - }; - - // TODO ensure the command whose examples we're testing is actually in the pipeline - ctx.scope.enter_scope(); - let (classified_block, err) = nu_parser::parse(line, 0, &ctx.scope); - if let Some(err) = err { - ctx.scope.exit_scope(); - return Err(err.into()); - } - - let input_stream = InputStream::empty(); - - let result = run_block( - &classified_block, - ctx, - input_stream, - ExternalRedirection::Stdout, - ); - ctx.scope.exit_scope(); - - result?.collect_string(Tag::unknown()).map(|x| x.item) -} - -#[allow(dead_code)] -fn current_branch() -> String { - #[cfg(feature = "shadow-rs")] - { - Some(shadow_rs::branch()) - .map(|x| x.trim().to_string()) - .filter(|x| !x.is_empty()) - .map(|x| format!("({})", x)) - .unwrap_or_default() - } - #[cfg(not(feature = "shadow-rs"))] - { - "".to_string() - } -} diff --git a/crates/nu-cli/src/completions.rs b/crates/nu-cli/src/completions.rs new file mode 100644 index 0000000000..d675d44251 --- /dev/null +++ b/crates/nu-cli/src/completions.rs @@ -0,0 +1,442 @@ +use nu_engine::eval_block; +use nu_parser::{flatten_expression, parse}; +use nu_protocol::{ + ast::{Expr, Statement}, + engine::{EngineState, Stack, StateWorkingSet}, + PipelineData, Span, +}; +use reedline::Completer; + +const SEP: char = std::path::MAIN_SEPARATOR; + +#[derive(Clone)] +pub struct NuCompleter { + engine_state: EngineState, +} + +impl NuCompleter { + pub fn new(engine_state: EngineState) -> Self { + Self { engine_state } + } + + fn external_command_completion(&self, prefix: &str) -> Vec { + let mut executables = vec![]; + + let paths; + paths = self.engine_state.env_vars.get("PATH"); + + if let Some(paths) = paths { + if let Ok(paths) = paths.as_list() { + for path in paths { + let path = path.as_string().unwrap_or_default(); + + if let Ok(mut contents) = std::fs::read_dir(path) { + while let Some(Ok(item)) = contents.next() { + if !executables.contains( + &item + .path() + .file_name() + .map(|x| x.to_string_lossy().to_string()) + .unwrap_or_default(), + ) && matches!( + item.path() + .file_name() + .map(|x| x.to_string_lossy().starts_with(prefix)), + Some(true) + ) && is_executable::is_executable(&item.path()) + { + if let Ok(name) = item.file_name().into_string() { + executables.push(name); + } + } + } + } + } + } + } + + executables + } + + fn complete_variables( + &self, + working_set: &StateWorkingSet, + prefix: &[u8], + span: Span, + offset: usize, + ) -> Vec<(reedline::Span, String)> { + let mut output = vec![]; + + let builtins = ["$nu", "$scope", "$in", "$config", "$env"]; + + for builtin in builtins { + if builtin.as_bytes().starts_with(prefix) { + output.push(( + reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, + builtin.to_string(), + )); + } + } + + for scope in &working_set.delta.scope { + for v in &scope.vars { + if v.0.starts_with(prefix) { + output.push(( + reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, + String::from_utf8_lossy(v.0).to_string(), + )); + } + } + } + for scope in &self.engine_state.scope { + for v in &scope.vars { + if v.0.starts_with(prefix) { + output.push(( + reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, + String::from_utf8_lossy(v.0).to_string(), + )); + } + } + } + + output.dedup(); + + output + } + + fn complete_commands( + &self, + working_set: &StateWorkingSet, + span: Span, + offset: usize, + ) -> Vec<(reedline::Span, String)> { + let prefix = working_set.get_span_contents(span); + + let results = working_set + .find_commands_by_prefix(prefix) + .into_iter() + .map(move |x| { + ( + reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, + String::from_utf8_lossy(&x).to_string(), + ) + }); + + let prefix = working_set.get_span_contents(span); + let prefix = String::from_utf8_lossy(prefix).to_string(); + let results_external = + self.external_command_completion(&prefix) + .into_iter() + .map(move |x| { + ( + reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, + x, + ) + }); + + results + .into_iter() + .chain(results_external.into_iter()) + .collect() + } + + fn completion_helper(&self, line: &str, pos: usize) -> Vec<(reedline::Span, String)> { + let mut working_set = StateWorkingSet::new(&self.engine_state); + let offset = working_set.next_span_start(); + let pos = offset + pos; + let (output, _err) = parse(&mut working_set, Some("completer"), line.as_bytes(), false); + + for stmt in output.stmts.into_iter() { + if let Statement::Pipeline(pipeline) = stmt { + for expr in pipeline.expressions { + let flattened = flatten_expression(&working_set, &expr); + for flat in flattened { + if pos >= flat.0.start && pos <= flat.0.end { + let prefix = working_set.get_span_contents(flat.0); + + if prefix.starts_with(b"$") { + return self.complete_variables( + &working_set, + prefix, + flat.0, + offset, + ); + } + if prefix.starts_with(b"-") { + // this might be a flag, let's see + if let Expr::Call(call) = &expr.expr { + let decl = working_set.get_decl(call.decl_id); + let sig = decl.signature(); + + let mut output = vec![]; + + for named in &sig.named { + let mut named = named.long.as_bytes().to_vec(); + named.insert(0, b'-'); + named.insert(0, b'-'); + if named.starts_with(prefix) { + output.push(( + reedline::Span { + start: flat.0.start - offset, + end: flat.0.end - offset, + }, + String::from_utf8_lossy(&named).to_string(), + )); + } + } + return output; + } + } + + match &flat.1 { + nu_parser::FlatShape::Custom(custom_completion) => { + let prefix = working_set.get_span_contents(flat.0).to_vec(); + + let (block, ..) = parse( + &mut working_set, + None, + custom_completion.as_bytes(), + false, + ); + + let mut stack = Stack::default(); + let result = eval_block( + &self.engine_state, + &mut stack, + &block, + PipelineData::new(flat.0), + ); + + let v: Vec<_> = match result { + Ok(pd) => pd + .into_iter() + .map(move |x| { + let s = x.as_string().expect( + "FIXME: better error handling for custom completions", + ); + + ( + reedline::Span { + start: flat.0.start - offset, + end: flat.0.end - offset, + }, + s, + ) + }) + .filter(|x| x.1.as_bytes().starts_with(&prefix)) + .collect(), + _ => vec![], + }; + + return v; + } + nu_parser::FlatShape::External + | nu_parser::FlatShape::InternalCall + | nu_parser::FlatShape::String => { + let subcommands = self.complete_commands( + &working_set, + Span { + start: expr.span.start, + end: pos, + }, + offset, + ); + + let cwd = if let Some(d) = self.engine_state.env_vars.get("PWD") + { + match d.as_string() { + Ok(s) => s, + Err(_) => "".to_string(), + } + } else { + "".to_string() + }; + + let prefix = working_set.get_span_contents(flat.0); + let prefix = String::from_utf8_lossy(prefix).to_string(); + return file_path_completion(flat.0, &prefix, &cwd) + .into_iter() + .map(move |x| { + ( + reedline::Span { + start: x.0.start - offset, + end: x.0.end - offset, + }, + x.1, + ) + }) + .chain(subcommands.into_iter()) + .collect(); + } + nu_parser::FlatShape::Filepath + | nu_parser::FlatShape::GlobPattern + | nu_parser::FlatShape::ExternalArg => { + // Check for subcommands + let subcommands = self.complete_commands( + &working_set, + Span { + start: expr.span.start, + end: pos, + }, + offset, + ); + + // Check for args + let prefix = working_set.get_span_contents(flat.0); + let prefix = String::from_utf8_lossy(prefix).to_string(); + let cwd = if let Some(d) = self.engine_state.env_vars.get("PWD") + { + match d.as_string() { + Ok(s) => s, + Err(_) => "".to_string(), + } + } else { + "".to_string() + }; + + let results = file_path_completion(flat.0, &prefix, &cwd); + + return results + .into_iter() + .map(move |x| { + ( + reedline::Span { + start: x.0.start - offset, + end: x.0.end - offset, + }, + x.1, + ) + }) + .chain(subcommands.into_iter()) + .collect(); + } + _ => { + return self.complete_commands( + &working_set, + Span { + start: expr.span.start, + end: pos, + }, + offset, + ) + } + } + } + + // If we get here, let's just check to see if we can complete a subcommand + // Check for subcommands + let subcommands = self.complete_commands( + &working_set, + Span { + start: expr.span.start, + end: pos, + }, + offset, + ); + + if !subcommands.is_empty() { + return subcommands; + } + } + } + } + } + + vec![] + } +} + +impl Completer for NuCompleter { + fn complete(&self, line: &str, pos: usize) -> Vec<(reedline::Span, String)> { + let mut output = self.completion_helper(line, pos); + + output.sort_by(|a, b| a.1.cmp(&b.1)); + + output + } +} + +fn file_path_completion( + span: nu_protocol::Span, + partial: &str, + cwd: &str, +) -> Vec<(nu_protocol::Span, String)> { + use std::path::{is_separator, Path}; + + let partial = if let Some(s) = partial.strip_prefix('"') { + s + } else { + partial + }; + + let partial = if let Some(s) = partial.strip_suffix('"') { + s + } else { + partial + }; + + let (base_dir_name, partial) = { + // If partial is only a word we want to search in the current dir + let (base, rest) = partial.rsplit_once(is_separator).unwrap_or((".", partial)); + // On windows, this standardizes paths to use \ + let mut base = base.replace(is_separator, &SEP.to_string()); + + // rsplit_once removes the separator + base.push(SEP); + (base, rest) + }; + + let base_dir = nu_path::expand_path_with(&base_dir_name, cwd); + // This check is here as base_dir.read_dir() with base_dir == "" will open the current dir + // which we don't want in this case (if we did, base_dir would already be ".") + if base_dir == Path::new("") { + return Vec::new(); + } + + if let Ok(result) = base_dir.read_dir() { + result + .filter_map(|entry| { + entry.ok().and_then(|entry| { + let mut file_name = entry.file_name().to_string_lossy().into_owned(); + if matches(partial, &file_name) { + let mut path = format!("{}{}", base_dir_name, file_name); + if entry.path().is_dir() { + path.push(SEP); + file_name.push(SEP); + } + + if path.contains(' ') { + path = format!("\"{}\"", path); + } + + Some((span, path)) + } else { + None + } + }) + }) + .collect() + } else { + Vec::new() + } +} + +fn matches(partial: &str, from: &str) -> bool { + from.to_ascii_lowercase() + .starts_with(&partial.to_ascii_lowercase()) +} diff --git a/crates/nu-cli/src/errors.rs b/crates/nu-cli/src/errors.rs new file mode 100644 index 0000000000..88c1d3d48d --- /dev/null +++ b/crates/nu-cli/src/errors.rs @@ -0,0 +1,46 @@ +use miette::{LabeledSpan, MietteHandler, ReportHandler, Severity, SourceCode}; +use nu_protocol::engine::StateWorkingSet; +use thiserror::Error; + +/// This error exists so that we can defer SourceCode handling. It simply +/// forwards most methods, except for `.source_code()`, which we provide. +#[derive(Error)] +#[error("{0}")] +pub struct CliError<'src>( + pub &'src (dyn miette::Diagnostic + Send + Sync + 'static), + pub &'src StateWorkingSet<'src>, +); + +impl std::fmt::Debug for CliError<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + MietteHandler::default().debug(self, f)?; + Ok(()) + } +} + +impl<'src> miette::Diagnostic for CliError<'src> { + fn code<'a>(&'a self) -> Option> { + self.0.code() + } + + fn severity(&self) -> Option { + self.0.severity() + } + + fn help<'a>(&'a self) -> Option> { + self.0.help() + } + + fn url<'a>(&'a self) -> Option> { + self.0.url() + } + + fn labels<'a>(&'a self) -> Option + 'a>> { + self.0.labels() + } + + // Finally, we redirect the source_code method to our own source. + fn source_code(&self) -> Option<&dyn SourceCode> { + Some(&self.1) + } +} diff --git a/crates/nu-cli/src/keybinding.rs b/crates/nu-cli/src/keybinding.rs deleted file mode 100644 index 0b90255ede..0000000000 --- a/crates/nu-cli/src/keybinding.rs +++ /dev/null @@ -1,473 +0,0 @@ -use rustyline::{KeyCode as RustyKeyCode, Modifiers}; -use serde::{Deserialize, Serialize}; - -pub fn convert_keyevent(key_event: KeyCode, modifiers: Option) -> rustyline::KeyEvent { - match key_event { - KeyCode::UnknownEscSeq => convert_to_rl_keyevent(RustyKeyCode::UnknownEscSeq, modifiers), - KeyCode::Backspace => convert_to_rl_keyevent(RustyKeyCode::Backspace, modifiers), - KeyCode::BackTab => convert_to_rl_keyevent(RustyKeyCode::BackTab, modifiers), - KeyCode::BracketedPasteStart => { - convert_to_rl_keyevent(RustyKeyCode::BracketedPasteStart, modifiers) - } - KeyCode::BracketedPasteEnd => { - convert_to_rl_keyevent(RustyKeyCode::BracketedPasteEnd, modifiers) - } - KeyCode::Char(c) => convert_to_rl_keyevent(RustyKeyCode::Char(c), modifiers), - KeyCode::Delete => convert_to_rl_keyevent(RustyKeyCode::Delete, modifiers), - KeyCode::Down => convert_to_rl_keyevent(RustyKeyCode::Down, modifiers), - KeyCode::End => convert_to_rl_keyevent(RustyKeyCode::End, modifiers), - KeyCode::Enter => convert_to_rl_keyevent(RustyKeyCode::Enter, modifiers), - KeyCode::Esc => convert_to_rl_keyevent(RustyKeyCode::Esc, modifiers), - KeyCode::F(u) => convert_to_rl_keyevent(RustyKeyCode::F(u), modifiers), - KeyCode::Home => convert_to_rl_keyevent(RustyKeyCode::Home, modifiers), - KeyCode::Insert => convert_to_rl_keyevent(RustyKeyCode::Insert, modifiers), - KeyCode::Left => convert_to_rl_keyevent(RustyKeyCode::Left, modifiers), - KeyCode::Null => convert_to_rl_keyevent(RustyKeyCode::Null, modifiers), - KeyCode::PageDown => convert_to_rl_keyevent(RustyKeyCode::PageDown, modifiers), - KeyCode::PageUp => convert_to_rl_keyevent(RustyKeyCode::PageUp, modifiers), - KeyCode::Right => convert_to_rl_keyevent(RustyKeyCode::Right, modifiers), - KeyCode::Tab => convert_to_rl_keyevent(RustyKeyCode::Tab, modifiers), - KeyCode::Up => convert_to_rl_keyevent(RustyKeyCode::Up, modifiers), - } -} - -fn convert_to_rl_keyevent( - key_code: RustyKeyCode, - modifier: Option, -) -> rustyline::KeyEvent { - rustyline::KeyEvent { - 0: key_code, - 1: modifier.unwrap_or(Modifiers::NONE), - } -} - -fn convert_word(word: Word) -> rustyline::Word { - match word { - Word::Big => rustyline::Word::Big, - Word::Emacs => rustyline::Word::Emacs, - Word::Vi => rustyline::Word::Vi, - } -} - -fn convert_at(at: At) -> rustyline::At { - match at { - At::AfterEnd => rustyline::At::AfterEnd, - At::BeforeEnd => rustyline::At::BeforeEnd, - At::Start => rustyline::At::Start, - } -} - -fn convert_char_search(search: CharSearch) -> rustyline::CharSearch { - match search { - CharSearch::Backward(c) => rustyline::CharSearch::Backward(c), - CharSearch::BackwardAfter(c) => rustyline::CharSearch::BackwardAfter(c), - CharSearch::Forward(c) => rustyline::CharSearch::Forward(c), - CharSearch::ForwardBefore(c) => rustyline::CharSearch::ForwardBefore(c), - } -} - -fn convert_movement(movement: Movement) -> rustyline::Movement { - match movement { - Movement::BackwardChar(u) => rustyline::Movement::BackwardChar(u), - Movement::BackwardWord { repeat, word } => { - rustyline::Movement::BackwardWord(repeat, convert_word(word)) - } - Movement::BeginningOfBuffer => rustyline::Movement::BeginningOfBuffer, - Movement::BeginningOfLine => rustyline::Movement::BeginningOfLine, - Movement::EndOfBuffer => rustyline::Movement::EndOfBuffer, - Movement::EndOfLine => rustyline::Movement::EndOfLine, - Movement::ForwardChar(u) => rustyline::Movement::ForwardChar(u), - Movement::ForwardWord { repeat, at, word } => { - rustyline::Movement::ForwardWord(repeat, convert_at(at), convert_word(word)) - } - Movement::LineDown(u) => rustyline::Movement::LineDown(u), - Movement::LineUp(u) => rustyline::Movement::LineUp(u), - Movement::ViCharSearch { repeat, search } => { - rustyline::Movement::ViCharSearch(repeat, convert_char_search(search)) - } - Movement::ViFirstPrint => rustyline::Movement::ViFirstPrint, - Movement::WholeBuffer => rustyline::Movement::WholeBuffer, - Movement::WholeLine => rustyline::Movement::WholeLine, - } -} - -fn convert_anchor(anchor: Anchor) -> rustyline::Anchor { - match anchor { - Anchor::After => rustyline::Anchor::After, - Anchor::Before => rustyline::Anchor::Before, - } -} - -fn convert_cmd(cmd: Cmd) -> rustyline::Cmd { - match cmd { - Cmd::Abort => rustyline::Cmd::Abort, - Cmd::AcceptLine => rustyline::Cmd::AcceptLine, - Cmd::AcceptOrInsertLine => rustyline::Cmd::AcceptOrInsertLine { - accept_in_the_middle: false, - }, - Cmd::BeginningOfHistory => rustyline::Cmd::BeginningOfHistory, - Cmd::CapitalizeWord => rustyline::Cmd::CapitalizeWord, - Cmd::ClearScreen => rustyline::Cmd::ClearScreen, - Cmd::Complete => rustyline::Cmd::Complete, - Cmd::CompleteBackward => rustyline::Cmd::CompleteBackward, - Cmd::CompleteHint => rustyline::Cmd::CompleteHint, - Cmd::Dedent(movement) => rustyline::Cmd::Dedent(convert_movement(movement)), - Cmd::DowncaseWord => rustyline::Cmd::DowncaseWord, - Cmd::EndOfFile => rustyline::Cmd::EndOfFile, - Cmd::EndOfHistory => rustyline::Cmd::EndOfHistory, - Cmd::ForwardSearchHistory => rustyline::Cmd::ForwardSearchHistory, - Cmd::HistorySearchBackward => rustyline::Cmd::HistorySearchBackward, - Cmd::HistorySearchForward => rustyline::Cmd::HistorySearchForward, - Cmd::Indent(movement) => rustyline::Cmd::Indent(convert_movement(movement)), - Cmd::Insert { repeat, string } => rustyline::Cmd::Insert(repeat, string), - Cmd::Interrupt => rustyline::Cmd::Interrupt, - Cmd::Kill(movement) => rustyline::Cmd::Kill(convert_movement(movement)), - Cmd::LineDownOrNextHistory(u) => rustyline::Cmd::LineDownOrNextHistory(u), - Cmd::LineUpOrPreviousHistory(u) => rustyline::Cmd::LineUpOrPreviousHistory(u), - Cmd::Move(movement) => rustyline::Cmd::Move(convert_movement(movement)), - Cmd::NextHistory => rustyline::Cmd::NextHistory, - Cmd::Newline => rustyline::Cmd::Newline, - Cmd::Noop => rustyline::Cmd::Noop, - Cmd::Overwrite(c) => rustyline::Cmd::Overwrite(c), - #[cfg(windows)] - Cmd::PasteFromClipboard => rustyline::Cmd::PasteFromClipboard, - Cmd::PreviousHistory => rustyline::Cmd::PreviousHistory, - Cmd::QuotedInsert => rustyline::Cmd::QuotedInsert, - Cmd::Replace { - movement, - replacement, - } => rustyline::Cmd::Replace(convert_movement(movement), replacement), - Cmd::ReplaceChar { repeat, ch } => rustyline::Cmd::ReplaceChar(repeat, ch), - Cmd::ReverseSearchHistory => rustyline::Cmd::ReverseSearchHistory, - Cmd::SelfInsert { repeat, ch } => rustyline::Cmd::SelfInsert(repeat, ch), - Cmd::Suspend => rustyline::Cmd::Suspend, - Cmd::TransposeChars => rustyline::Cmd::TransposeChars, - Cmd::TransposeWords(u) => rustyline::Cmd::TransposeWords(u), - Cmd::Undo(u) => rustyline::Cmd::Undo(u), - Cmd::Unknown => rustyline::Cmd::Unknown, - Cmd::UpcaseWord => rustyline::Cmd::UpcaseWord, - Cmd::ViYankTo(movement) => rustyline::Cmd::ViYankTo(convert_movement(movement)), - Cmd::Yank { repeat, anchor } => rustyline::Cmd::Yank(repeat, convert_anchor(anchor)), - Cmd::YankPop => rustyline::Cmd::YankPop, - } -} - -fn convert_keybinding(keybinding: Keybinding) -> (rustyline::KeyEvent, rustyline::Cmd) { - let rusty_modifiers = match keybinding.modifiers { - Some(mods) => match mods { - NuModifiers::Ctrl => Some(Modifiers::CTRL), - NuModifiers::Alt => Some(Modifiers::ALT), - NuModifiers::Shift => Some(Modifiers::SHIFT), - NuModifiers::None => Some(Modifiers::NONE), - NuModifiers::CtrlShift => Some(Modifiers::CTRL_SHIFT), - NuModifiers::AltShift => Some(Modifiers::ALT_SHIFT), - NuModifiers::CtrlAlt => Some(Modifiers::CTRL_ALT), - NuModifiers::CtrlAltShift => Some(Modifiers::CTRL_ALT_SHIFT), - // _ => None, - }, - None => None, - }; - ( - convert_keyevent(keybinding.key, rusty_modifiers), - convert_cmd(keybinding.binding), - ) -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub enum KeyCode { - /// Unsupported escape sequence (on unix platform) - UnknownEscSeq, - /// ⌫ or `KeyEvent::Ctrl('H')` - Backspace, - /// ⇤ (usually Shift-Tab) - BackTab, - /// Paste (on unix platform) - BracketedPasteStart, - /// Paste (on unix platform) - BracketedPasteEnd, - /// Single char - Char(char), - /// ⌦ - Delete, - /// ↓ arrow key - Down, - /// ⇲ - End, - /// ↵ or `KeyEvent::Ctrl('M')` - Enter, - /// Escape or `KeyEvent::Ctrl('[')` - Esc, - /// Function key - F(u8), - /// ⇱ - Home, - /// Insert key - Insert, - /// ← arrow key - Left, - // /// `KeyEvent::Char('\0')` - Null, - /// ⇟ - PageDown, - /// ⇞ - PageUp, - /// → arrow key - Right, - /// ⇥ or `KeyEvent::Ctrl('I')` - Tab, - /// ↑ arrow key - Up, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub enum Cmd { - /// abort - Abort, // Miscellaneous Command - /// accept-line - AcceptLine, - /// beginning-of-history - BeginningOfHistory, - /// capitalize-word - CapitalizeWord, - /// clear-screen - ClearScreen, - /// complete - Complete, - /// complete-backward - CompleteBackward, - /// complete-hint - CompleteHint, - /// Dedent current line - Dedent(Movement), - /// downcase-word - DowncaseWord, - /// vi-eof-maybe - EndOfFile, - /// end-of-history - EndOfHistory, - /// forward-search-history - ForwardSearchHistory, - /// history-search-backward - HistorySearchBackward, - /// history-search-forward - HistorySearchForward, - /// Indent current line - Indent(Movement), - /// Insert text - Insert { repeat: RepeatCount, string: String }, - /// Interrupt signal (Ctrl-C) - Interrupt, - /// backward-delete-char, backward-kill-line, backward-kill-word - /// delete-char, kill-line, kill-word, unix-line-discard, unix-word-rubout, - /// vi-delete, vi-delete-to, vi-rubout - Kill(Movement), - /// backward-char, backward-word, beginning-of-line, end-of-line, - /// forward-char, forward-word, vi-char-search, vi-end-word, vi-next-word, - /// vi-prev-word - Move(Movement), - /// Inserts a newline - Newline, - /// next-history - NextHistory, - /// No action - Noop, - /// vi-replace - Overwrite(char), - /// Paste from the clipboard - #[cfg(windows)] - PasteFromClipboard, - /// previous-history - PreviousHistory, - /// quoted-insert - QuotedInsert, - /// vi-change-char - ReplaceChar { repeat: RepeatCount, ch: char }, - /// vi-change-to, vi-substitute - Replace { - movement: Movement, - replacement: Option, - }, - /// reverse-search-history - ReverseSearchHistory, - /// self-insert - SelfInsert { repeat: RepeatCount, ch: char }, - /// Suspend signal (Ctrl-Z on unix platform) - Suspend, - /// transpose-chars - TransposeChars, - /// transpose-words - TransposeWords(RepeatCount), - /// undo - Undo(RepeatCount), - /// Unsupported / unexpected - Unknown, - /// upcase-word - UpcaseWord, - /// vi-yank-to - ViYankTo(Movement), - /// yank, vi-put - Yank { repeat: RepeatCount, anchor: Anchor }, - /// yank-pop - YankPop, - /// moves cursor to the line above or switches to prev history entry if - /// the cursor is already on the first line - LineUpOrPreviousHistory(RepeatCount), - /// moves cursor to the line below or switches to next history entry if - /// the cursor is already on the last line - LineDownOrNextHistory(RepeatCount), - /// accepts the line when cursor is at the end of the text (non including - /// trailing whitespace), inserts newline character otherwise - AcceptOrInsertLine, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub enum Movement { - /// Whole current line (not really a movement but a range) - WholeLine, - /// beginning-of-line - BeginningOfLine, - /// end-of-line - EndOfLine, - /// backward-word, vi-prev-word - BackwardWord { repeat: RepeatCount, word: Word }, // Backward until start of word - /// forward-word, vi-end-word, vi-next-word - ForwardWord { - repeat: RepeatCount, - at: At, - word: Word, - }, // Forward until start/end of word - /// vi-char-search - ViCharSearch { - repeat: RepeatCount, - search: CharSearch, - }, - /// vi-first-print - ViFirstPrint, - /// backward-char - BackwardChar(RepeatCount), - /// forward-char - ForwardChar(RepeatCount), - /// move to the same column on the previous line - LineUp(RepeatCount), - /// move to the same column on the next line - LineDown(RepeatCount), - /// Whole user input (not really a movement but a range) - WholeBuffer, - /// beginning-of-buffer - BeginningOfBuffer, - /// end-of-buffer - EndOfBuffer, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -enum InputMode { - /// Vi Command/Alternate - Command, - /// Insert/Input mode - Insert, - /// Overwrite mode - Replace, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub enum Word { - /// non-blanks characters - Big, - /// alphanumeric characters - Emacs, - /// alphanumeric (and '_') characters - Vi, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub enum At { - /// Start of word. - Start, - /// Before end of word. - BeforeEnd, - /// After end of word. - AfterEnd, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub enum Anchor { - /// After cursor - After, - /// Before cursor - Before, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub enum CharSearch { - /// Forward search - Forward(char), - /// Forward search until - ForwardBefore(char), - /// Backward search - Backward(char), - /// Backward search until - BackwardAfter(char), -} - -/// The set of modifier keys that were triggered along with a key press. -#[derive(Debug, Serialize, Deserialize)] -#[allow(non_camel_case_types)] -pub enum NuModifiers { - /// Control modifier - #[serde(alias = "CTRL")] - Ctrl = 8, - /// Escape or Alt modifier - #[serde(alias = "ALT")] - Alt = 4, - /// Shift modifier - #[serde(alias = "SHIFT")] - Shift = 2, - /// No modifier - #[serde(alias = "NONE")] - None = 0, - /// Ctrl + Shift - #[serde(alias = "CTRL_SHIFT")] - CtrlShift = 10, - /// Alt + Shift - #[serde(alias = "ALT_SHIFT")] - AltShift = 6, - /// Ctrl + Alt - #[serde(alias = "CTRL_ALT")] - CtrlAlt = 12, - /// Ctrl + Alt + Shift - #[serde(alias = "CTRL_ALT_SHIFT")] - CtrlAltShift = 14, -} - -/// The number of times one command should be repeated. -pub type RepeatCount = usize; - -#[derive(Debug, Serialize, Deserialize)] -pub struct Keybinding { - key: KeyCode, - modifiers: Option, - binding: Cmd, -} - -type Keybindings = Vec; - -pub(crate) fn load_keybindings( - rl: &mut rustyline::Editor, -) -> Result<(), nu_errors::ShellError> { - let filename = nu_data::keybinding::keybinding_path()?; - let contents = std::fs::read_to_string(filename); - - // Silently fail if there is no file there - if let Ok(contents) = contents { - let keybindings: Keybindings = serde_yaml::from_str(&contents)?; - // eprintln!("{:#?}", keybindings); - for keybinding in keybindings { - let (k, b) = convert_keybinding(keybinding); - // eprintln!("{:?} {:?}", k, b); - - rl.bind_sequence(k, b); - } - } - - Ok(()) -} diff --git a/crates/nu-cli/src/lib.rs b/crates/nu-cli/src/lib.rs index 4b3e2fb932..c00d104f2c 100644 --- a/crates/nu-cli/src/lib.rs +++ b/crates/nu-cli/src/lib.rs @@ -1,15 +1,13 @@ -pub mod app; -mod cli; -#[cfg(feature = "rustyline-support")] -mod keybinding; -mod line_editor; -#[cfg(feature = "rustyline-support")] -mod shell; +mod completions; +mod errors; +mod nu_highlight; +mod prompt; +mod syntax_highlight; +mod validation; -#[cfg(feature = "rustyline-support")] -pub use crate::cli::cli; - -pub use crate::app::App; -pub use crate::cli::{parse_and_eval, register_plugins, run_script_file}; - -pub use nu_command::create_default_context; +pub use completions::NuCompleter; +pub use errors::CliError; +pub use nu_highlight::NuHighlight; +pub use prompt::NushellPrompt; +pub use syntax_highlight::NuHighlighter; +pub use validation::NuValidator; diff --git a/crates/nu-cli/src/line_editor.rs b/crates/nu-cli/src/line_editor.rs deleted file mode 100644 index 7c36021b82..0000000000 --- a/crates/nu-cli/src/line_editor.rs +++ /dev/null @@ -1,276 +0,0 @@ -use nu_engine::EvaluationContext; -use nu_errors::ShellError; -use std::error::Error; - -#[allow(unused_imports)] -use std::sync::atomic::Ordering; - -#[allow(unused_imports)] -use nu_engine::script::LineResult; - -#[cfg(feature = "rustyline-support")] -use crate::keybinding::{convert_keyevent, KeyCode}; - -#[cfg(feature = "rustyline-support")] -use crate::shell::Helper; - -#[cfg(feature = "rustyline-support")] -use rustyline::{ - self, - config::Configurer, - config::{ColorMode, CompletionType, Config}, - error::ReadlineError, - line_buffer::LineBuffer, - At, Cmd, ConditionalEventHandler, Editor, EventHandler, Modifiers, Movement, Word, -}; - -#[cfg(feature = "rustyline-support")] -pub fn convert_rustyline_result_to_string(input: Result) -> LineResult { - match input { - Ok(s) if s == "history -c" || s == "history --clear" => LineResult::ClearHistory, - Ok(s) => LineResult::Success(s), - Err(ReadlineError::Interrupted) => LineResult::CtrlC, - Err(ReadlineError::Eof) => LineResult::CtrlD, - Err(err) => { - eprintln!("Error: {:?}", err); - LineResult::Break - } - } -} - -#[derive(Clone)] -#[cfg(feature = "rustyline-support")] -struct PartialCompleteHintHandler; - -#[cfg(feature = "rustyline-support")] -impl ConditionalEventHandler for PartialCompleteHintHandler { - fn handle( - &self, - _evt: &rustyline::Event, - _n: rustyline::RepeatCount, - _positive: bool, - ctx: &rustyline::EventContext, - ) -> Option { - Some(match ctx.hint_text() { - Some(hint_text) if ctx.pos() == ctx.line().len() => { - let mut line_buffer = LineBuffer::with_capacity(hint_text.len()); - line_buffer.update(hint_text, 0); - line_buffer.move_to_next_word(At::AfterEnd, Word::Vi, 1); - - let text = hint_text[0..line_buffer.pos()].to_string(); - - Cmd::Insert(1, text) - } - _ => Cmd::Move(Movement::ForwardWord(1, At::AfterEnd, Word::Vi)), - }) - } -} - -#[cfg(feature = "rustyline-support")] -pub fn default_rustyline_editor_configuration() -> Editor { - #[cfg(windows)] - const DEFAULT_COMPLETION_MODE: CompletionType = CompletionType::Circular; - #[cfg(not(windows))] - const DEFAULT_COMPLETION_MODE: CompletionType = CompletionType::List; - - let config = Config::builder() - .check_cursor_position(true) - .color_mode(ColorMode::Forced) - .history_ignore_dups(false) - .max_history_size(10_000) - .build(); - let mut rl: Editor<_> = Editor::with_config(config); - - // add key bindings to move over a whole word with Ctrl+ArrowLeft and Ctrl+ArrowRight - //M modifier, E KeyEvent, K KeyCode - rl.bind_sequence( - convert_keyevent(KeyCode::Left, Some(Modifiers::CTRL)), - Cmd::Move(Movement::BackwardWord(1, Word::Vi)), - ); - - rl.bind_sequence( - convert_keyevent(KeyCode::Right, Some(Modifiers::CTRL)), - EventHandler::Conditional(Box::new(PartialCompleteHintHandler)), - ); - - // workaround for multiline-paste hang in rustyline (see https://github.com/kkawakam/rustyline/issues/202) - rl.bind_sequence( - convert_keyevent(KeyCode::BracketedPasteStart, None), - rustyline::Cmd::Noop, - ); - // Let's set the defaults up front and then override them later if the user indicates - // defaults taken from here https://github.com/kkawakam/rustyline/blob/2fe886c9576c1ea13ca0e5808053ad491a6fe049/src/config.rs#L150-L167 - rl.set_max_history_size(100); - rl.set_history_ignore_dups(true); - rl.set_history_ignore_space(false); - rl.set_completion_type(DEFAULT_COMPLETION_MODE); - rl.set_completion_prompt_limit(100); - rl.set_keyseq_timeout(-1); - rl.set_edit_mode(rustyline::config::EditMode::Emacs); - rl.set_auto_add_history(false); - rl.set_bell_style(rustyline::config::BellStyle::default()); - rl.set_color_mode(rustyline::ColorMode::Enabled); - rl.set_tab_stop(8); - - if let Err(e) = crate::keybinding::load_keybindings(&mut rl) { - println!("Error loading keybindings: {:?}", e); - } - - rl -} - -#[cfg(feature = "rustyline-support")] -pub fn configure_rustyline_editor( - rl: &mut Editor, - config: &dyn nu_data::config::Conf, -) -> Result<(), ShellError> { - #[cfg(windows)] - const DEFAULT_COMPLETION_MODE: CompletionType = CompletionType::Circular; - #[cfg(not(windows))] - const DEFAULT_COMPLETION_MODE: CompletionType = CompletionType::List; - - if let Some(line_editor_vars) = config.var("line_editor") { - for (idx, value) in line_editor_vars.row_entries() { - match idx.as_ref() { - "max_history_size" => { - if let Ok(max_history_size) = value.as_u64() { - rl.set_max_history_size(max_history_size as usize); - } - } - "history_duplicates" => { - // history_duplicates = match value.as_string() { - // Ok(s) if s.to_lowercase() == "alwaysadd" => { - // rustyline::config::HistoryDuplicates::AlwaysAdd - // } - // Ok(s) if s.to_lowercase() == "ignoreconsecutive" => { - // rustyline::config::HistoryDuplicates::IgnoreConsecutive - // } - // _ => rustyline::config::HistoryDuplicates::AlwaysAdd, - // }; - if let Ok(history_duplicates) = value.as_bool() { - rl.set_history_ignore_dups(history_duplicates); - } - } - "history_ignore_space" => { - if let Ok(history_ignore_space) = value.as_bool() { - rl.set_history_ignore_space(history_ignore_space); - } - } - "completion_type" => { - let completion_type = match value.as_string() { - Ok(s) if s.to_lowercase() == "circular" => { - rustyline::config::CompletionType::Circular - } - Ok(s) if s.to_lowercase() == "list" => { - rustyline::config::CompletionType::List - } - #[cfg(all(unix, feature = "with-fuzzy"))] - Ok(s) if s.to_lowercase() == "fuzzy" => { - rustyline::config::CompletionType::Fuzzy - } - _ => DEFAULT_COMPLETION_MODE, - }; - rl.set_completion_type(completion_type); - } - "completion_prompt_limit" => { - if let Ok(completion_prompt_limit) = value.as_u64() { - rl.set_completion_prompt_limit(completion_prompt_limit as usize); - } - } - "keyseq_timeout_ms" => { - if let Ok(keyseq_timeout_ms) = value.as_u64() { - rl.set_keyseq_timeout(keyseq_timeout_ms as i32); - } - } - "edit_mode" => { - let edit_mode = match value.as_string() { - Ok(s) if s.to_lowercase() == "vi" => rustyline::config::EditMode::Vi, - Ok(s) if s.to_lowercase() == "emacs" => rustyline::config::EditMode::Emacs, - _ => rustyline::config::EditMode::Emacs, - }; - rl.set_edit_mode(edit_mode); - // Note: When edit_mode is Emacs, the keyseq_timeout_ms is set to -1 - // no matter what you may have configured. This is so that key chords - // can be applied without having to do them in a given timeout. So, - // it essentially turns off the keyseq timeout. - } - "auto_add_history" => { - if let Ok(auto_add_history) = value.as_bool() { - rl.set_auto_add_history(auto_add_history); - } - } - "bell_style" => { - let bell_style = match value.as_string() { - Ok(s) if s.to_lowercase() == "audible" => { - rustyline::config::BellStyle::Audible - } - Ok(s) if s.to_lowercase() == "none" => rustyline::config::BellStyle::None, - Ok(s) if s.to_lowercase() == "visible" => { - rustyline::config::BellStyle::Visible - } - _ => rustyline::config::BellStyle::default(), - }; - rl.set_bell_style(bell_style); - } - "color_mode" => { - let color_mode = match value.as_string() { - Ok(s) if s.to_lowercase() == "enabled" => rustyline::ColorMode::Enabled, - Ok(s) if s.to_lowercase() == "forced" => rustyline::ColorMode::Forced, - Ok(s) if s.to_lowercase() == "disabled" => rustyline::ColorMode::Disabled, - _ => rustyline::ColorMode::Enabled, - }; - rl.set_color_mode(color_mode); - } - "tab_stop" => { - if let Ok(tab_stop) = value.as_u64() { - rl.set_tab_stop(tab_stop as usize); - } - } - _ => (), - } - } - } - - Ok(()) -} - -#[cfg(feature = "rustyline-support")] -pub fn nu_line_editor_helper( - context: &EvaluationContext, - config: &dyn nu_data::config::Conf, -) -> crate::shell::Helper { - let hinter = rustyline_hinter(config); - crate::shell::Helper::new(context.clone(), hinter) -} - -#[cfg(feature = "rustyline-support")] -pub fn rustyline_hinter( - config: &dyn nu_data::config::Conf, -) -> Option { - if let Some(line_editor_vars) = config.var("line_editor") { - for (idx, value) in line_editor_vars.row_entries() { - if idx == "show_hints" && value.as_bool() == Ok(false) { - return None; - } - } - } - - Some(rustyline::hint::HistoryHinter {}) -} - -pub fn configure_ctrl_c(_context: &EvaluationContext) -> Result<(), Box> { - #[cfg(feature = "ctrlc")] - { - let cc = _context.ctrl_c().clone(); - - ctrlc::set_handler(move || { - cc.store(true, Ordering::SeqCst); - })?; - - if _context.ctrl_c().load(Ordering::SeqCst) { - _context.ctrl_c().store(false, Ordering::SeqCst); - } - } - - Ok(()) -} diff --git a/crates/nu-cli/src/nu_highlight.rs b/crates/nu-cli/src/nu_highlight.rs new file mode 100644 index 0000000000..2e96d9616b --- /dev/null +++ b/crates/nu-cli/src/nu_highlight.rs @@ -0,0 +1,63 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Value}; +use reedline::Highlighter; + +#[derive(Clone)] +pub struct NuHighlight; + +impl Command for NuHighlight { + fn name(&self) -> &str { + "nu-highlight" + } + + fn signature(&self) -> Signature { + Signature::build("nu-highlight").category(Category::Strings) + } + + fn usage(&self) -> &str { + "Syntax highlight the input string." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + + let ctrlc = engine_state.ctrlc.clone(); + let engine_state = engine_state.clone(); + let config = stack.get_config()?; + + let highlighter = crate::NuHighlighter { + engine_state, + config, + }; + + input.map( + move |x| match x.as_string() { + Ok(line) => { + let highlights = highlighter.highlight(&line); + + Value::String { + val: highlights.render_simple(), + span: head, + } + } + Err(err) => Value::Error { error: err }, + }, + ctrlc, + ) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Describe the type of a string", + example: "'let x = 3' | nu-highlight", + result: None, + }] + } +} diff --git a/crates/nu-cli/src/prompt.rs b/crates/nu-cli/src/prompt.rs new file mode 100644 index 0000000000..890a93b83a --- /dev/null +++ b/crates/nu-cli/src/prompt.rs @@ -0,0 +1,143 @@ +use reedline::DefaultPrompt; + +use { + reedline::{ + Prompt, PromptEditMode, PromptHistorySearch, PromptHistorySearchStatus, PromptViMode, + }, + std::borrow::Cow, +}; + +/// Nushell prompt definition +#[derive(Clone)] +pub struct NushellPrompt { + left_prompt_string: Option, + right_prompt_string: Option, + default_prompt_indicator: String, + default_vi_insert_prompt_indicator: String, + default_vi_normal_prompt_indicator: String, + default_multiline_indicator: String, +} + +impl Default for NushellPrompt { + fn default() -> Self { + NushellPrompt::new() + } +} + +impl NushellPrompt { + pub fn new() -> NushellPrompt { + NushellPrompt { + left_prompt_string: None, + right_prompt_string: None, + default_prompt_indicator: "〉".to_string(), + default_vi_insert_prompt_indicator: ": ".to_string(), + default_vi_normal_prompt_indicator: "〉".to_string(), + default_multiline_indicator: "::: ".to_string(), + } + } + + pub fn update_prompt_left(&mut self, prompt_string: Option) { + self.left_prompt_string = prompt_string; + } + + pub fn update_prompt_right(&mut self, prompt_string: Option) { + self.right_prompt_string = prompt_string; + } + + pub fn update_prompt_indicator(&mut self, prompt_indicator_string: String) { + self.default_prompt_indicator = prompt_indicator_string; + } + + pub fn update_prompt_vi_insert(&mut self, prompt_vi_insert_string: String) { + self.default_vi_insert_prompt_indicator = prompt_vi_insert_string; + } + + pub fn update_prompt_vi_normal(&mut self, prompt_vi_normal_string: String) { + self.default_vi_normal_prompt_indicator = prompt_vi_normal_string; + } + + pub fn update_prompt_multiline(&mut self, prompt_multiline_indicator_string: String) { + self.default_multiline_indicator = prompt_multiline_indicator_string; + } + + pub fn update_all_prompt_strings( + &mut self, + left_prompt_string: Option, + right_prompt_string: Option, + prompt_indicator_string: String, + prompt_multiline_indicator_string: String, + prompt_vi: (String, String), + ) { + let (prompt_vi_insert_string, prompt_vi_normal_string) = prompt_vi; + + self.left_prompt_string = left_prompt_string; + self.right_prompt_string = right_prompt_string; + self.default_prompt_indicator = prompt_indicator_string; + self.default_vi_insert_prompt_indicator = prompt_vi_insert_string; + self.default_vi_normal_prompt_indicator = prompt_vi_normal_string; + self.default_multiline_indicator = prompt_multiline_indicator_string; + } + + fn default_wrapped_custom_string(&self, str: String) -> String { + format!("({})", str) + } +} + +impl Prompt for NushellPrompt { + fn render_prompt_left(&self) -> Cow { + if let Some(prompt_string) = &self.left_prompt_string { + prompt_string.replace("\n", "\r\n").into() + } else { + let default = DefaultPrompt::new(); + default + .render_prompt_left() + .to_string() + .replace("\n", "\r\n") + .into() + } + } + + fn render_prompt_right(&self) -> Cow { + if let Some(prompt_string) = &self.right_prompt_string { + prompt_string.replace("\n", "\r\n").into() + } else { + let default = DefaultPrompt::new(); + default + .render_prompt_right() + .to_string() + .replace("\n", "\r\n") + .into() + } + } + + fn render_prompt_indicator(&self, edit_mode: PromptEditMode) -> Cow { + match edit_mode { + PromptEditMode::Default => self.default_prompt_indicator.as_str().into(), + PromptEditMode::Emacs => self.default_prompt_indicator.as_str().into(), + PromptEditMode::Vi(vi_mode) => match vi_mode { + PromptViMode::Normal => self.default_vi_normal_prompt_indicator.as_str().into(), + PromptViMode::Insert => self.default_vi_insert_prompt_indicator.as_str().into(), + }, + PromptEditMode::Custom(str) => self.default_wrapped_custom_string(str).into(), + } + } + + fn render_prompt_multiline_indicator(&self) -> Cow { + Cow::Borrowed(self.default_multiline_indicator.as_str()) + } + + fn render_prompt_history_search_indicator( + &self, + history_search: PromptHistorySearch, + ) -> Cow { + let prefix = match history_search.status { + PromptHistorySearchStatus::Passing => "", + PromptHistorySearchStatus::Failing => "failing ", + }; + + Cow::Owned(format!( + "({}reverse-search: {})", + prefix, history_search.term + )) + } +} diff --git a/crates/nu-cli/src/shell.rs b/crates/nu-cli/src/shell.rs deleted file mode 100644 index ce7ef7a3ad..0000000000 --- a/crates/nu-cli/src/shell.rs +++ /dev/null @@ -1,234 +0,0 @@ -use nu_ansi_term::Color; -use nu_completion::NuCompleter; -use nu_engine::{DefaultPalette, EvaluationContext, Painter}; -use nu_source::{Tag, Tagged}; -use std::borrow::Cow::{self, Owned}; - -pub struct Helper { - completer: NuCompleter, - hinter: Option, - context: EvaluationContext, - pub colored_prompt: String, - validator: NuValidator, -} - -impl Helper { - pub(crate) fn new( - context: EvaluationContext, - hinter: Option, - ) -> Helper { - Helper { - completer: NuCompleter {}, - hinter, - context, - colored_prompt: String::new(), - validator: NuValidator {}, - } - } -} - -use nu_protocol::{SignatureRegistry, VariableRegistry}; -struct CompletionContext<'a>(&'a EvaluationContext); - -impl<'a> nu_completion::CompletionContext for CompletionContext<'a> { - fn signature_registry(&self) -> &dyn SignatureRegistry { - &self.0.scope - } - - fn scope(&self) -> &dyn nu_parser::ParserScope { - &self.0.scope - } - - fn source(&self) -> &EvaluationContext { - self.as_ref() - } - - fn variable_registry(&self) -> &dyn VariableRegistry { - self.0 - } -} - -impl<'a> AsRef for CompletionContext<'a> { - fn as_ref(&self) -> &EvaluationContext { - self.0 - } -} - -pub struct CompletionSuggestion(nu_completion::Suggestion); - -impl rustyline::completion::Candidate for CompletionSuggestion { - fn display(&self) -> &str { - &self.0.display - } - - fn replacement(&self) -> &str { - &self.0.replacement - } -} - -impl rustyline::completion::Completer for Helper { - type Candidate = CompletionSuggestion; - - fn complete( - &self, - line: &str, - pos: usize, - _ctx: &rustyline::Context<'_>, - ) -> Result<(usize, Vec), rustyline::error::ReadlineError> { - let ctx = CompletionContext(&self.context); - let (position, suggestions) = self.completer.complete(line, pos, &ctx); - let suggestions = suggestions.into_iter().map(CompletionSuggestion).collect(); - Ok((position, suggestions)) - } - - fn update(&self, line: &mut rustyline::line_buffer::LineBuffer, start: usize, elected: &str) { - let end = line.pos(); - line.replace(start..end, elected) - } -} - -impl rustyline::hint::Hinter for Helper { - type Hint = String; - fn hint(&self, line: &str, pos: usize, ctx: &rustyline::Context<'_>) -> Option { - match &self.hinter { - Some(the_hinter) => the_hinter.hint(line, pos, ctx), - None => Some("".to_string()), - } - } -} - -impl rustyline::highlight::Highlighter for Helper { - fn highlight_prompt<'b, 's: 'b, 'p: 'b>( - &'s self, - prompt: &'p str, - default: bool, - ) -> Cow<'b, str> { - use std::borrow::Cow::Borrowed; - - if default { - Borrowed(&self.colored_prompt) - } else { - Borrowed(prompt) - } - } - - fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { - Owned(Color::DarkGray.prefix().to_string() + hint + nu_ansi_term::ansi::RESET) - } - - fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> { - let cfg = &self.context.configs().lock(); - if let Some(palette) = &cfg.syntax_config { - Painter::paint_string(line, &self.context.scope, palette) - } else { - Painter::paint_string(line, &self.context.scope, &DefaultPalette {}) - } - } - - fn highlight_char(&self, _line: &str, _pos: usize) -> bool { - true - } -} - -impl rustyline::validate::Validator for Helper { - fn validate( - &self, - ctx: &mut rustyline::validate::ValidationContext, - ) -> rustyline::Result { - self.validator.validate(ctx) - } - - fn validate_while_typing(&self) -> bool { - self.validator.validate_while_typing() - } -} - -struct NuValidator {} - -impl rustyline::validate::Validator for NuValidator { - fn validate( - &self, - ctx: &mut rustyline::validate::ValidationContext, - ) -> rustyline::Result { - let src = ctx.input(); - - let (tokens, err) = nu_parser::lex(src, 0, nu_parser::NewlineMode::Normal); - if let Some(err) = err { - if let nu_errors::ParseErrorReason::Eof { .. } = err.reason() { - return Ok(rustyline::validate::ValidationResult::Incomplete); - } - } - - let (_, err) = nu_parser::parse_block(tokens); - - if let Some(err) = err { - if let nu_errors::ParseErrorReason::Eof { .. } = err.reason() { - return Ok(rustyline::validate::ValidationResult::Incomplete); - } - } - - Ok(rustyline::validate::ValidationResult::Valid(None)) - } -} - -#[allow(unused)] -fn vec_tag(input: Vec>) -> Option { - let mut iter = input.iter(); - let first = iter.next()?.tag.clone(); - let last = iter.last(); - - Some(match last { - None => first, - Some(last) => first.until(&last.tag), - }) -} - -impl rustyline::Helper for Helper {} - -#[cfg(test)] -mod tests { - use super::*; - use nu_engine::EvaluationContext; - use rustyline::completion::Completer; - use rustyline::line_buffer::LineBuffer; - - #[ignore] - #[test] - fn closing_quote_should_replaced() { - let text = "cd \"folder with spaces\\subdirectory\\\""; - let replacement = "\"folder with spaces\\subdirectory\\subsubdirectory\\\""; - - let mut buffer = LineBuffer::with_capacity(256); - buffer.insert_str(0, text); - buffer.set_pos(text.len() - 1); - - let helper = Helper::new(EvaluationContext::basic(), None); - - helper.update(&mut buffer, "cd ".len(), replacement); - - assert_eq!( - buffer.as_str(), - "cd \"folder with spaces\\subdirectory\\subsubdirectory\\\"" - ); - } - - #[ignore] - #[test] - fn replacement_with_cursor_in_text() { - let text = "cd \"folder with spaces\\subdirectory\\\""; - let replacement = "\"folder with spaces\\subdirectory\\subsubdirectory\\\""; - - let mut buffer = LineBuffer::with_capacity(256); - buffer.insert_str(0, text); - buffer.set_pos(text.len() - 30); - - let helper = Helper::new(EvaluationContext::basic(), None); - - helper.update(&mut buffer, "cd ".len(), replacement); - - assert_eq!( - buffer.as_str(), - "cd \"folder with spaces\\subdirectory\\subsubdirectory\\\"" - ); - } -} diff --git a/crates/nu-cli/src/syntax_highlight.rs b/crates/nu-cli/src/syntax_highlight.rs new file mode 100644 index 0000000000..187a80ea16 --- /dev/null +++ b/crates/nu-cli/src/syntax_highlight.rs @@ -0,0 +1,199 @@ +use log::trace; +use nu_ansi_term::Style; +use nu_color_config::get_shape_color; +use nu_parser::{flatten_block, parse, FlatShape}; +use nu_protocol::engine::{EngineState, StateWorkingSet}; +use nu_protocol::Config; +use reedline::{Highlighter, StyledText}; + +pub struct NuHighlighter { + pub engine_state: EngineState, + pub config: Config, +} + +impl Highlighter for NuHighlighter { + fn highlight(&self, line: &str) -> StyledText { + trace!("highlighting: {}", line); + + let (shapes, global_span_offset) = { + let mut working_set = StateWorkingSet::new(&self.engine_state); + let (block, _) = parse(&mut working_set, None, line.as_bytes(), false); + + let shapes = flatten_block(&working_set, &block); + (shapes, self.engine_state.next_span_start()) + }; + + let mut output = StyledText::default(); + let mut last_seen_span = global_span_offset; + + for shape in &shapes { + if shape.0.end <= last_seen_span + || last_seen_span < global_span_offset + || shape.0.start < global_span_offset + { + // We've already output something for this span + // so just skip this one + continue; + } + if shape.0.start > last_seen_span { + let gap = line + [(last_seen_span - global_span_offset)..(shape.0.start - global_span_offset)] + .to_string(); + output.push((Style::new(), gap)); + } + let next_token = line + [(shape.0.start - global_span_offset)..(shape.0.end - global_span_offset)] + .to_string(); + match shape.1 { + FlatShape::Garbage => output.push(( + // nushell Garbage + get_shape_color(shape.1.to_string(), &self.config), + next_token, + )), + FlatShape::Nothing => output.push(( + // nushell Nothing + get_shape_color(shape.1.to_string(), &self.config), + next_token, + )), + FlatShape::Bool => { + // nushell ? + output.push(( + get_shape_color(shape.1.to_string(), &self.config), + next_token, + )) + } + FlatShape::Int => { + // nushell Int + output.push(( + get_shape_color(shape.1.to_string(), &self.config), + next_token, + )) + } + FlatShape::Float => { + // nushell Decimal + output.push(( + get_shape_color(shape.1.to_string(), &self.config), + next_token, + )) + } + FlatShape::Range => output.push(( + // nushell DotDot ? + get_shape_color(shape.1.to_string(), &self.config), + next_token, + )), + FlatShape::InternalCall => output.push(( + // nushell InternalCommand + get_shape_color(shape.1.to_string(), &self.config), + next_token, + )), + FlatShape::External => { + // nushell ExternalCommand + output.push(( + get_shape_color(shape.1.to_string(), &self.config), + next_token, + )) + } + FlatShape::ExternalArg => { + // nushell ExternalWord + output.push(( + get_shape_color(shape.1.to_string(), &self.config), + next_token, + )) + } + FlatShape::Literal => { + // nushell ? + output.push(( + get_shape_color(shape.1.to_string(), &self.config), + next_token, + )) + } + FlatShape::Operator => output.push(( + // nushell Operator + get_shape_color(shape.1.to_string(), &self.config), + next_token, + )), + FlatShape::Signature => output.push(( + // nushell ? + get_shape_color(shape.1.to_string(), &self.config), + next_token, + )), + FlatShape::String => { + // nushell String + output.push(( + get_shape_color(shape.1.to_string(), &self.config), + next_token, + )) + } + FlatShape::StringInterpolation => { + // nushell ??? + output.push(( + get_shape_color(shape.1.to_string(), &self.config), + next_token, + )) + } + FlatShape::List => { + // nushell ??? + output.push(( + get_shape_color(shape.1.to_string(), &self.config), + next_token, + )) + } + FlatShape::Table => { + // nushell ??? + output.push(( + get_shape_color(shape.1.to_string(), &self.config), + next_token, + )) + } + FlatShape::Record => { + // nushell ??? + output.push(( + get_shape_color(shape.1.to_string(), &self.config), + next_token, + )) + } + FlatShape::Block => { + // nushell ??? + output.push(( + get_shape_color(shape.1.to_string(), &self.config), + next_token, + )) + } + FlatShape::Filepath => output.push(( + // nushell Path + get_shape_color(shape.1.to_string(), &self.config), + next_token, + )), + FlatShape::GlobPattern => output.push(( + // nushell GlobPattern + get_shape_color(shape.1.to_string(), &self.config), + next_token, + )), + FlatShape::Variable => output.push(( + // nushell Variable + get_shape_color(shape.1.to_string(), &self.config), + next_token, + )), + FlatShape::Flag => { + // nushell Flag + output.push(( + get_shape_color(shape.1.to_string(), &self.config), + next_token, + )) + } + FlatShape::Custom(..) => output.push(( + get_shape_color(shape.1.to_string(), &self.config), + next_token, + )), + } + last_seen_span = shape.0.end; + } + + let remainder = line[(last_seen_span - global_span_offset)..].to_string(); + if !remainder.is_empty() { + output.push((Style::new(), remainder)); + } + + output + } +} diff --git a/crates/nu-cli/src/validation.rs b/crates/nu-cli/src/validation.rs new file mode 100644 index 0000000000..9e306596c4 --- /dev/null +++ b/crates/nu-cli/src/validation.rs @@ -0,0 +1,20 @@ +use nu_parser::{parse, ParseError}; +use nu_protocol::engine::{EngineState, StateWorkingSet}; +use reedline::{ValidationResult, Validator}; + +pub struct NuValidator { + pub engine_state: EngineState, +} + +impl Validator for NuValidator { + fn validate(&self, line: &str) -> ValidationResult { + let mut working_set = StateWorkingSet::new(&self.engine_state); + let (_, err) = parse(&mut working_set, None, line.as_bytes(), false); + + if matches!(err, Some(ParseError::UnexpectedEof(..))) { + ValidationResult::Incomplete + } else { + ValidationResult::Complete + } + } +} diff --git a/crates/nu-color-config/Cargo.toml b/crates/nu-color-config/Cargo.toml new file mode 100644 index 0000000000..373a951620 --- /dev/null +++ b/crates/nu-color-config/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "nu-color-config" +version = "0.59.0" +edition = "2021" + +[dependencies] +nu-protocol = { path = "../nu-protocol", version = "0.59.0" } +# nu-ansi-term = { path = "../nu-ansi-term", version = "0.59.0" } +nu-ansi-term = "0.42.0" + +nu-json = { path = "../nu-json", version = "0.59.0" } +nu-table = { path = "../nu-table", version = "0.59.0" } + +serde = { version="1.0.123", features=["derive"] } diff --git a/crates/nu-color-config/src/color_config.rs b/crates/nu-color-config/src/color_config.rs new file mode 100644 index 0000000000..459095cadf --- /dev/null +++ b/crates/nu-color-config/src/color_config.rs @@ -0,0 +1,411 @@ +use crate::nu_style::{color_from_hex, color_string_to_nustyle}; +use nu_ansi_term::{Color, Style}; +use nu_protocol::Config; +use nu_table::{Alignment, TextStyle}; +use std::collections::HashMap; + +pub fn lookup_ansi_color_style(s: &str) -> Style { + if s.starts_with('#') { + match color_from_hex(s) { + Ok(c) => match c { + Some(c) => c.normal(), + None => Style::default(), + }, + Err(_) => Style::default(), + } + } else if s.starts_with('{') { + color_string_to_nustyle(s.to_string()) + } else { + match s { + "g" | "green" => Color::Green.normal(), + "gb" | "green_bold" => Color::Green.bold(), + "gu" | "green_underline" => Color::Green.underline(), + "gi" | "green_italic" => Color::Green.italic(), + "gd" | "green_dimmed" => Color::Green.dimmed(), + "gr" | "green_reverse" => Color::Green.reverse(), + "gbl" | "green_blink" => Color::Green.blink(), + "gst" | "green_strike" => Color::Green.strikethrough(), + + "lg" | "light_green" => Color::LightGreen.normal(), + "lgb" | "light_green_bold" => Color::LightGreen.bold(), + "lgu" | "light_green_underline" => Color::LightGreen.underline(), + "lgi" | "light_green_italic" => Color::LightGreen.italic(), + "lgd" | "light_green_dimmed" => Color::LightGreen.dimmed(), + "lgr" | "light_green_reverse" => Color::LightGreen.reverse(), + "lgbl" | "light_green_blink" => Color::LightGreen.blink(), + "lgst" | "light_green_strike" => Color::LightGreen.strikethrough(), + + "r" | "red" => Color::Red.normal(), + "rb" | "red_bold" => Color::Red.bold(), + "ru" | "red_underline" => Color::Red.underline(), + "ri" | "red_italic" => Color::Red.italic(), + "rd" | "red_dimmed" => Color::Red.dimmed(), + "rr" | "red_reverse" => Color::Red.reverse(), + "rbl" | "red_blink" => Color::Red.blink(), + "rst" | "red_strike" => Color::Red.strikethrough(), + + "lr" | "light_red" => Color::LightRed.normal(), + "lrb" | "light_red_bold" => Color::LightRed.bold(), + "lru" | "light_red_underline" => Color::LightRed.underline(), + "lri" | "light_red_italic" => Color::LightRed.italic(), + "lrd" | "light_red_dimmed" => Color::LightRed.dimmed(), + "lrr" | "light_red_reverse" => Color::LightRed.reverse(), + "lrbl" | "light_red_blink" => Color::LightRed.blink(), + "lrst" | "light_red_strike" => Color::LightRed.strikethrough(), + + "u" | "blue" => Color::Blue.normal(), + "ub" | "blue_bold" => Color::Blue.bold(), + "uu" | "blue_underline" => Color::Blue.underline(), + "ui" | "blue_italic" => Color::Blue.italic(), + "ud" | "blue_dimmed" => Color::Blue.dimmed(), + "ur" | "blue_reverse" => Color::Blue.reverse(), + "ubl" | "blue_blink" => Color::Blue.blink(), + "ust" | "blue_strike" => Color::Blue.strikethrough(), + + "lu" | "light_blue" => Color::LightBlue.normal(), + "lub" | "light_blue_bold" => Color::LightBlue.bold(), + "luu" | "light_blue_underline" => Color::LightBlue.underline(), + "lui" | "light_blue_italic" => Color::LightBlue.italic(), + "lud" | "light_blue_dimmed" => Color::LightBlue.dimmed(), + "lur" | "light_blue_reverse" => Color::LightBlue.reverse(), + "lubl" | "light_blue_blink" => Color::LightBlue.blink(), + "lust" | "light_blue_strike" => Color::LightBlue.strikethrough(), + + "b" | "black" => Color::Black.normal(), + "bb" | "black_bold" => Color::Black.bold(), + "bu" | "black_underline" => Color::Black.underline(), + "bi" | "black_italic" => Color::Black.italic(), + "bd" | "black_dimmed" => Color::Black.dimmed(), + "br" | "black_reverse" => Color::Black.reverse(), + "bbl" | "black_blink" => Color::Black.blink(), + "bst" | "black_strike" => Color::Black.strikethrough(), + + "ligr" | "light_gray" => Color::LightGray.normal(), + "ligrb" | "light_gray_bold" => Color::LightGray.bold(), + "ligru" | "light_gray_underline" => Color::LightGray.underline(), + "ligri" | "light_gray_italic" => Color::LightGray.italic(), + "ligrd" | "light_gray_dimmed" => Color::LightGray.dimmed(), + "ligrr" | "light_gray_reverse" => Color::LightGray.reverse(), + "ligrbl" | "light_gray_blink" => Color::LightGray.blink(), + "ligrst" | "light_gray_strike" => Color::LightGray.strikethrough(), + + "y" | "yellow" => Color::Yellow.normal(), + "yb" | "yellow_bold" => Color::Yellow.bold(), + "yu" | "yellow_underline" => Color::Yellow.underline(), + "yi" | "yellow_italic" => Color::Yellow.italic(), + "yd" | "yellow_dimmed" => Color::Yellow.dimmed(), + "yr" | "yellow_reverse" => Color::Yellow.reverse(), + "ybl" | "yellow_blink" => Color::Yellow.blink(), + "yst" | "yellow_strike" => Color::Yellow.strikethrough(), + + "ly" | "light_yellow" => Color::LightYellow.normal(), + "lyb" | "light_yellow_bold" => Color::LightYellow.bold(), + "lyu" | "light_yellow_underline" => Color::LightYellow.underline(), + "lyi" | "light_yellow_italic" => Color::LightYellow.italic(), + "lyd" | "light_yellow_dimmed" => Color::LightYellow.dimmed(), + "lyr" | "light_yellow_reverse" => Color::LightYellow.reverse(), + "lybl" | "light_yellow_blink" => Color::LightYellow.blink(), + "lyst" | "light_yellow_strike" => Color::LightYellow.strikethrough(), + + "p" | "purple" => Color::Purple.normal(), + "pb" | "purple_bold" => Color::Purple.bold(), + "pu" | "purple_underline" => Color::Purple.underline(), + "pi" | "purple_italic" => Color::Purple.italic(), + "pd" | "purple_dimmed" => Color::Purple.dimmed(), + "pr" | "purple_reverse" => Color::Purple.reverse(), + "pbl" | "purple_blink" => Color::Purple.blink(), + "pst" | "purple_strike" => Color::Purple.strikethrough(), + + "lp" | "light_purple" => Color::LightPurple.normal(), + "lpb" | "light_purple_bold" => Color::LightPurple.bold(), + "lpu" | "light_purple_underline" => Color::LightPurple.underline(), + "lpi" | "light_purple_italic" => Color::LightPurple.italic(), + "lpd" | "light_purple_dimmed" => Color::LightPurple.dimmed(), + "lpr" | "light_purple_reverse" => Color::LightPurple.reverse(), + "lpbl" | "light_purple_blink" => Color::LightPurple.blink(), + "lpst" | "light_purple_strike" => Color::LightPurple.strikethrough(), + + "c" | "cyan" => Color::Cyan.normal(), + "cb" | "cyan_bold" => Color::Cyan.bold(), + "cu" | "cyan_underline" => Color::Cyan.underline(), + "ci" | "cyan_italic" => Color::Cyan.italic(), + "cd" | "cyan_dimmed" => Color::Cyan.dimmed(), + "cr" | "cyan_reverse" => Color::Cyan.reverse(), + "cbl" | "cyan_blink" => Color::Cyan.blink(), + "cst" | "cyan_strike" => Color::Cyan.strikethrough(), + + "lc" | "light_cyan" => Color::LightCyan.normal(), + "lcb" | "light_cyan_bold" => Color::LightCyan.bold(), + "lcu" | "light_cyan_underline" => Color::LightCyan.underline(), + "lci" | "light_cyan_italic" => Color::LightCyan.italic(), + "lcd" | "light_cyan_dimmed" => Color::LightCyan.dimmed(), + "lcr" | "light_cyan_reverse" => Color::LightCyan.reverse(), + "lcbl" | "light_cyan_blink" => Color::LightCyan.blink(), + "lcst" | "light_cyan_strike" => Color::LightCyan.strikethrough(), + + "w" | "white" => Color::White.normal(), + "wb" | "white_bold" => Color::White.bold(), + "wu" | "white_underline" => Color::White.underline(), + "wi" | "white_italic" => Color::White.italic(), + "wd" | "white_dimmed" => Color::White.dimmed(), + "wr" | "white_reverse" => Color::White.reverse(), + "wbl" | "white_blink" => Color::White.blink(), + "wst" | "white_strike" => Color::White.strikethrough(), + + "dgr" | "dark_gray" => Color::DarkGray.normal(), + "dgrb" | "dark_gray_bold" => Color::DarkGray.bold(), + "dgru" | "dark_gray_underline" => Color::DarkGray.underline(), + "dgri" | "dark_gray_italic" => Color::DarkGray.italic(), + "dgrd" | "dark_gray_dimmed" => Color::DarkGray.dimmed(), + "dgrr" | "dark_gray_reverse" => Color::DarkGray.reverse(), + "dgrbl" | "dark_gray_blink" => Color::DarkGray.blink(), + "dgrst" | "dark_gray_strike" => Color::DarkGray.strikethrough(), + + _ => Color::White.normal(), + } + } +} + +fn update_hashmap(key: &str, val: &str, hm: &mut HashMap) { + // eprintln!("key: {}, val: {}", &key, &val); + let color = lookup_ansi_color_style(val); + if let Some(v) = hm.get_mut(key) { + *v = color; + } else { + hm.insert(key.to_string(), color); + } +} + +pub fn get_color_config(config: &Config) -> HashMap { + let config = config; + + // create the hashmap + let mut hm: HashMap = HashMap::new(); + // set some defaults + // hm.insert("primitive_line".to_string(), Color::White.normal()); + // hm.insert("primitive_pattern".to_string(), Color::White.normal()); + // hm.insert("primitive_path".to_string(), Color::White.normal()); + // hm.insert("separator_color".to_string(), Color::White.normal()); + hm.insert( + "leading_trailing_space_bg".to_string(), + Style::default().on(Color::Rgb(128, 128, 128)), + ); + hm.insert("header".to_string(), Color::Green.bold()); + hm.insert("empty".to_string(), Color::Blue.normal()); + hm.insert("bool".to_string(), Color::White.normal()); + hm.insert("int".to_string(), Color::White.normal()); + hm.insert("filesize".to_string(), Color::White.normal()); + hm.insert("duration".to_string(), Color::White.normal()); + hm.insert("date".to_string(), Color::White.normal()); + hm.insert("range".to_string(), Color::White.normal()); + hm.insert("float".to_string(), Color::White.normal()); + hm.insert("string".to_string(), Color::White.normal()); + hm.insert("nothing".to_string(), Color::White.normal()); + hm.insert("binary".to_string(), Color::White.normal()); + hm.insert("cellpath".to_string(), Color::White.normal()); + hm.insert("row_index".to_string(), Color::Green.bold()); + hm.insert("record".to_string(), Color::White.normal()); + hm.insert("list".to_string(), Color::White.normal()); + hm.insert("block".to_string(), Color::White.normal()); + hm.insert("hints".to_string(), Color::DarkGray.normal()); + + for (key, value) in &config.color_config { + let value = value + .as_string() + .expect("the only values for config color must be strings"); + update_hashmap(key, &value, &mut hm); + + // eprintln!( + // "config: {}:{}\t\t\thashmap: {}:{:?}", + // &key, &value, &key, &hm[key] + // ); + } + + hm +} + +// This function will assign a text style to a primitive, or really any string that's +// in the hashmap. The hashmap actually contains the style to be applied. +pub fn style_primitive(primitive: &str, color_hm: &HashMap) -> TextStyle { + match primitive { + "bool" => { + let style = color_hm.get(primitive); + match style { + Some(s) => TextStyle::with_style(Alignment::Left, *s), + None => TextStyle::basic_left(), + } + } + + "int" => { + let style = color_hm.get(primitive); + match style { + Some(s) => TextStyle::with_style(Alignment::Right, *s), + None => TextStyle::basic_right(), + } + } + + "filesize" => { + let style = color_hm.get(primitive); + match style { + Some(s) => TextStyle::with_style(Alignment::Right, *s), + None => TextStyle::basic_right(), + } + } + + "duration" => { + let style = color_hm.get(primitive); + match style { + Some(s) => TextStyle::with_style(Alignment::Left, *s), + None => TextStyle::basic_left(), + } + } + + "date" => { + let style = color_hm.get(primitive); + match style { + Some(s) => TextStyle::with_style(Alignment::Left, *s), + None => TextStyle::basic_left(), + } + } + + "range" => { + let style = color_hm.get(primitive); + match style { + Some(s) => TextStyle::with_style(Alignment::Left, *s), + None => TextStyle::basic_left(), + } + } + + "float" => { + let style = color_hm.get(primitive); + match style { + Some(s) => TextStyle::with_style(Alignment::Right, *s), + None => TextStyle::basic_right(), + } + } + + "string" => { + let style = color_hm.get(primitive); + match style { + Some(s) => TextStyle::with_style(Alignment::Left, *s), + None => TextStyle::basic_left(), + } + } + + "record" | "list" | "block" => { + let style = color_hm.get(primitive); + match style { + Some(s) => TextStyle::with_style(Alignment::Left, *s), + None => TextStyle::basic_left(), + } + } + + "nothing" => { + let style = color_hm.get(primitive); + match style { + Some(s) => TextStyle::with_style(Alignment::Left, *s), + None => TextStyle::basic_left(), + } + } + + // not sure what to do with error + // "error" => {} + "binary" => { + let style = color_hm.get(primitive); + match style { + Some(s) => TextStyle::with_style(Alignment::Left, *s), + None => TextStyle::basic_left(), + } + } + + "cellpath" => { + let style = color_hm.get(primitive); + match style { + Some(s) => TextStyle::with_style(Alignment::Left, *s), + None => TextStyle::basic_left(), + } + } + + "row_index" => { + let style = color_hm.get(primitive); + match style { + Some(s) => TextStyle::with_style(Alignment::Right, *s), + None => TextStyle::new() + .alignment(Alignment::Right) + .fg(Color::Green) + .bold(Some(true)), + } + } + + // types in nushell but not in engine-q + // "Line" => { + // let style = color_hm.get("Primitive::Line"); + // match style { + // Some(s) => TextStyle::with_style(Alignment::Left, *s), + // None => TextStyle::basic_left(), + // } + // } + // "GlobPattern" => { + // let style = color_hm.get("Primitive::GlobPattern"); + // match style { + // Some(s) => TextStyle::with_style(Alignment::Left, *s), + // None => TextStyle::basic_left(), + // } + // } + // "FilePath" => { + // let style = color_hm.get("Primitive::FilePath"); + // match style { + // Some(s) => TextStyle::with_style(Alignment::Left, *s), + // None => TextStyle::basic_left(), + // } + // } + // "BeginningOfStream" => { + // let style = color_hm.get("Primitive::BeginningOfStream"); + // match style { + // Some(s) => TextStyle::with_style(Alignment::Left, *s), + // None => TextStyle::basic_left(), + // } + // } + // "EndOfStream" => { + // let style = color_hm.get("Primitive::EndOfStream"); + // match style { + // Some(s) => TextStyle::with_style(Alignment::Left, *s), + // None => TextStyle::basic_left(), + // } + // } + _ => TextStyle::basic_left(), + } +} + +#[test] +fn test_hm() { + use nu_ansi_term::{Color, Style}; + + let mut hm: HashMap = HashMap::new(); + hm.insert("primitive_int".to_string(), Color::White.normal()); + hm.insert("primitive_decimal".to_string(), Color::White.normal()); + hm.insert("primitive_filesize".to_string(), Color::White.normal()); + hm.insert("primitive_string".to_string(), Color::White.normal()); + hm.insert("primitive_line".to_string(), Color::White.normal()); + hm.insert("primitive_columnpath".to_string(), Color::White.normal()); + hm.insert("primitive_pattern".to_string(), Color::White.normal()); + hm.insert("primitive_boolean".to_string(), Color::White.normal()); + hm.insert("primitive_date".to_string(), Color::White.normal()); + hm.insert("primitive_duration".to_string(), Color::White.normal()); + hm.insert("primitive_range".to_string(), Color::White.normal()); + hm.insert("primitive_path".to_string(), Color::White.normal()); + hm.insert("primitive_binary".to_string(), Color::White.normal()); + hm.insert("separator".to_string(), Color::White.normal()); + hm.insert("header_align".to_string(), Color::Green.bold()); + hm.insert("header".to_string(), Color::Green.bold()); + hm.insert("header_style".to_string(), Style::default()); + hm.insert("row_index".to_string(), Color::Green.bold()); + hm.insert( + "leading_trailing_space_bg".to_string(), + Style::default().on(Color::Rgb(128, 128, 128)), + ); + + update_hashmap("primitive_int", "green", &mut hm); + + assert_eq!(hm["primitive_int"], Color::Green.normal()); +} diff --git a/crates/nu-color-config/src/lib.rs b/crates/nu-color-config/src/lib.rs new file mode 100644 index 0000000000..f1ad25ac46 --- /dev/null +++ b/crates/nu-color-config/src/lib.rs @@ -0,0 +1,7 @@ +mod color_config; +mod nu_style; +mod shape_color; + +pub use color_config::*; +pub use nu_style::*; +pub use shape_color::*; diff --git a/crates/nu-color-config/src/nu_style.rs b/crates/nu-color-config/src/nu_style.rs new file mode 100644 index 0000000000..7d2b3d6859 --- /dev/null +++ b/crates/nu-color-config/src/nu_style.rs @@ -0,0 +1,103 @@ +use nu_ansi_term::{Color, Style}; +use serde::Deserialize; + +#[derive(Deserialize, PartialEq, Debug)] +pub struct NuStyle { + pub fg: Option, + pub bg: Option, + pub attr: Option, +} + +pub fn parse_nustyle(nu_style: NuStyle) -> Style { + // get the nu_ansi_term::Color foreground color + let fg_color = match nu_style.fg { + Some(fg) => color_from_hex(&fg).expect("error with foreground color"), + _ => None, + }; + // get the nu_ansi_term::Color background color + let bg_color = match nu_style.bg { + Some(bg) => color_from_hex(&bg).expect("error with background color"), + _ => None, + }; + // get the attributes + let color_attr = match nu_style.attr { + Some(attr) => attr, + _ => "".to_string(), + }; + + // setup the attributes available in nu_ansi_term::Style + let mut bold = false; + let mut dimmed = false; + let mut italic = false; + let mut underline = false; + let mut blink = false; + let mut reverse = false; + let mut hidden = false; + let mut strikethrough = false; + + // since we can combine styles like bold-italic, iterate through the chars + // and set the bools for later use in the nu_ansi_term::Style application + for ch in color_attr.to_lowercase().chars() { + match ch { + 'l' => blink = true, + 'b' => bold = true, + 'd' => dimmed = true, + 'h' => hidden = true, + 'i' => italic = true, + 'r' => reverse = true, + 's' => strikethrough = true, + 'u' => underline = true, + 'n' => (), + _ => (), + } + } + + // here's where we build the nu_ansi_term::Style + Style { + foreground: fg_color, + background: bg_color, + is_blink: blink, + is_bold: bold, + is_dimmed: dimmed, + is_hidden: hidden, + is_italic: italic, + is_reverse: reverse, + is_strikethrough: strikethrough, + is_underline: underline, + } +} + +pub fn color_string_to_nustyle(color_string: String) -> Style { + // eprintln!("color_string: {}", &color_string); + if color_string.chars().count() < 1 { + Style::default() + } else { + let nu_style = match nu_json::from_str::(&color_string) { + Ok(s) => s, + Err(_) => NuStyle { + fg: None, + bg: None, + attr: None, + }, + }; + + parse_nustyle(nu_style) + } +} + +pub fn color_from_hex( + hex_color: &str, +) -> std::result::Result, std::num::ParseIntError> { + // right now we only allow hex colors with hashtag and 6 characters + let trimmed = hex_color.trim_matches('#'); + if trimmed.len() != 6 { + Ok(None) + } else { + // make a nu_ansi_term::Color::Rgb color by converting hex to decimal + Ok(Some(Color::Rgb( + u8::from_str_radix(&trimmed[..2], 16)?, + u8::from_str_radix(&trimmed[2..4], 16)?, + u8::from_str_radix(&trimmed[4..6], 16)?, + ))) + } +} diff --git a/crates/nu-color-config/src/shape_color.rs b/crates/nu-color-config/src/shape_color.rs new file mode 100644 index 0000000000..a856a0ed16 --- /dev/null +++ b/crates/nu-color-config/src/shape_color.rs @@ -0,0 +1,38 @@ +use crate::color_config::lookup_ansi_color_style; +use nu_ansi_term::{Color, Style}; +use nu_protocol::Config; + +pub fn get_shape_color(shape: String, conf: &Config) -> Style { + match conf.color_config.get(shape.as_str()) { + Some(int_color) => match int_color.as_string() { + Ok(int_color) => lookup_ansi_color_style(&int_color), + Err(_) => Style::default(), + }, + None => match shape.as_ref() { + "flatshape_garbage" => Style::new().fg(Color::White).on(Color::Red).bold(), + "flatshape_bool" => Style::new().fg(Color::LightCyan), + "flatshape_int" => Style::new().fg(Color::Purple).bold(), + "flatshape_float" => Style::new().fg(Color::Purple).bold(), + "flatshape_range" => Style::new().fg(Color::Yellow).bold(), + "flatshape_internalcall" => Style::new().fg(Color::Cyan).bold(), + "flatshape_external" => Style::new().fg(Color::Cyan), + "flatshape_externalarg" => Style::new().fg(Color::Green).bold(), + "flatshape_literal" => Style::new().fg(Color::Blue), + "flatshape_operator" => Style::new().fg(Color::Yellow), + "flatshape_signature" => Style::new().fg(Color::Green).bold(), + "flatshape_string" => Style::new().fg(Color::Green), + "flatshape_string_interpolation" => Style::new().fg(Color::Cyan).bold(), + "flatshape_list" => Style::new().fg(Color::Cyan).bold(), + "flatshape_table" => Style::new().fg(Color::Blue).bold(), + "flatshape_record" => Style::new().fg(Color::Cyan).bold(), + "flatshape_block" => Style::new().fg(Color::Blue).bold(), + "flatshape_filepath" => Style::new().fg(Color::Cyan), + "flatshape_globpattern" => Style::new().fg(Color::Cyan).bold(), + "flatshape_variable" => Style::new().fg(Color::Purple), + "flatshape_flag" => Style::new().fg(Color::Blue).bold(), + "flatshape_custom" => Style::new().bold(), + "flatshape_nothing" => Style::new().fg(Color::LightCyan), + _ => Style::default(), + }, + } +} diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index d3919396b1..5c6cdbfb0c 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -1,122 +1,107 @@ [package] -authors = ["The Nu Project Contributors"] -build = "build.rs" -description = "Commands for Nushell" -edition = "2018" -license = "MIT" name = "nu-command" -version = "0.44.0" +version = "0.59.0" +edition = "2021" +build = "build.rs" -[lib] -doctest = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -nu-data = { version = "0.44.0", path="../nu-data" } -nu-engine = { version = "0.44.0", path="../nu-engine" } -nu-errors = { version = "0.44.0", path="../nu-errors" } -nu-json = { version = "0.44.0", path="../nu-json" } -nu-path = { version = "0.44.0", path="../nu-path" } -nu-parser = { version = "0.44.0", path="../nu-parser" } -nu-plugin = { version = "0.44.0", path="../nu-plugin" } -nu-protocol = { version = "0.44.0", path="../nu-protocol" } -nu-serde = { version = "0.44.0", path="../nu-serde" } -nu-source = { version = "0.44.0", path="../nu-source" } -nu-stream = { version = "0.44.0", path="../nu-stream" } -nu-table = { version = "0.44.0", path="../nu-table" } -nu-test-support = { version = "0.44.0", path="../nu-test-support" } -nu-value-ext = { version = "0.44.0", path="../nu-value-ext" } -nu-ansi-term = { version = "0.44.0", path="../nu-ansi-term" } -nu-pretty-hex = { version = "0.44.0", path="../nu-pretty-hex" } +# nu-ansi-term = {path = "../nu-ansi-term", version = "0.59.0" } +nu-ansi-term = "0.42.0" -url = "2.2.1" -mime = "0.3.16" -heck = "0.4.0" +nu-color-config = { path = "../nu-color-config", version = "0.59.0" } +nu-engine = { path = "../nu-engine", version = "0.59.0" } +nu-json = { path = "../nu-json", version = "0.59.0" } +nu-parser = { path = "../nu-parser", version = "0.59.0" } +nu-path = { path = "../nu-path", version = "0.59.0" } +nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.59.0" } +nu-protocol = { path = "../nu-protocol", version = "0.59.0" } +nu-system = { path = "../nu-system", version = "0.59.0" } +nu-table = { path = "../nu-table", version = "0.59.0" } +nu-term-grid = { path = "../nu-term-grid", version = "0.59.0" } +nu-test-support = { path = "../nu-test-support", version = "0.59.0" } + +# Potential dependencies for extras base64 = "0.13.0" -bigdecimal = { version = "0.3.0", features = ["serde"] } +bytesize = "1.1.0" calamine = "0.18.0" -chrono = { version="0.4.19", features=["serde"] } -chrono-tz = "0.5.3" -crossterm = { version="0.19.0", optional=true } +chrono = { version = "0.4.19", features = ["serde"] } +chrono-humanize = "0.2.1" +chrono-tz = "0.6.0" +crossterm = "0.22.1" csv = "1.1.3" -ctrlc = { version="3.1.7", optional=true } -derive-new = "0.5.8" -dirs-next = "2.0.0" +dialoguer = "0.9.0" +digest = "0.10.0" dtparse = "1.2.0" eml-parser = "0.1.0" -encoding_rs = "0.8.28" +encoding_rs = "0.8.30" filesize = "0.2.0" -futures = { version="0.3.12", features=["compat", "io-compat"] } glob = "0.3.0" htmlescape = "0.3.1" ical = "0.7.0" indexmap = { version="1.7", features=["serde-1"] } +Inflector = "0.11" itertools = "0.10.0" -lazy_static = "1.*" +lazy_static = "1.4.0" log = "0.4.14" -md-5 = "0.9.1" +lscolors = { version = "0.8.0", features = ["crossterm"] } +md5 = { package = "md-5", version = "0.10.0" } meval = "0.2.0" -num-bigint = { version="0.4.3", features=["serde"] } -num-format = { version="0.4.0", features=["with-num-bigint"] } -num-traits = "0.2.14" -parking_lot = "0.11.1" +mime = "0.3.16" +num = { version = "0.4.0", optional = true } +pathdiff = "0.2.1" quick-xml = "0.22" rand = "0.8" -regex = "1.4.3" -reqwest = {version = "0.11", optional = true } +rayon = "1.5.1" +regex = "1.5.4" +reqwest = {version = "0.11", features = ["blocking"] } roxmltree = "0.14.0" -rust-embed = "5.9.0" -rustyline = { version="9.0.0", optional=true } +rust-embed = "6.3.0" serde = { version="1.0.123", features=["derive"] } serde_ini = "0.2.0" -serde_json = "1.0.61" serde_urlencoded = "0.7.0" serde_yaml = "0.8.16" -sha2 = "0.9.3" -strip-ansi-escapes = "0.1.0" -sysinfo = { version = "0.23.0", optional = true } -thiserror = "1.0.26" -term = { version="0.7.0", optional=true } -term_size = "0.3.2" +sha2 = "0.10.0" +shadow-rs = "0.8.1" +strip-ansi-escapes = "0.1.1" +sysinfo = "0.22.2" +terminal_size = "0.1.17" +thiserror = "1.0.29" titlecase = "1.1.0" -tokio = { version = "1", features = ["rt-multi-thread"], optional = true } toml = "0.5.8" trash = { version = "2.0.2", optional = true } -unicode-segmentation = "1.8" -uuid_crate = { package="uuid", version="0.8.2", features=["v4"], optional=true } -which = { version="4.1.0", optional=true } -zip = { version="0.5.9", optional=true } -digest = "0.9.0" - -[dependencies.polars] -version = "0.17.0" -optional = true -default-features = false -features = ["docs", "zip_with", "csv-file", "temporal", "performant", "pretty_fmt", "dtype-slim", "parquet", "json", "random", "pivot", "strings", "is_in", "cum_agg", "rolling_window"] +unicode-segmentation = "1.8.0" +url = "2.2.1" +uuid = { version = "0.8.2", features = ["v4"] } +which = { version = "4.2.2", optional = true } +reedline = { git = "https://github.com/nushell/reedline", branch = "main" } +zip = { version="0.5.9", optional = true } [target.'cfg(unix)'.dependencies] umask = "1.0.0" users = "0.11.0" -# TODO this will be possible with new dependency resolver -# (currently on nightly behind -Zfeatures=itarget): -# https://github.com/rust-lang/cargo/issues/7914 -# [target.'cfg(not(windows))'.dependencies] -# num-format = { version = "0.4", features = ["with-system-locale"] } +[dependencies.polars] +version = "0.18.0" +optional = true +features = [ + "default", "parquet", "json", "serde", "object", + "checked_arithmetic", "strings", "cum_agg", "is_in", + "rolling_window", "strings", "pivot", "random" +] + +[features] +trash-support = ["trash"] +plugin = ["nu-parser/plugin"] +dataframe = ["polars", "num"] [build-dependencies] shadow-rs = "0.8.1" [dev-dependencies] +hamcrest2 = "0.3.0" +dirs-next = "2.0.0" quickcheck = "1.0.3" quickcheck_macros = "1.0.0" -hamcrest2 = "0.3.0" - -[features] -rustyline-support = ["rustyline"] -stable = [] -trash-support = ["trash"] -dataframe = ["nu-protocol/dataframe", "polars"] -fetch = ["reqwest", "tokio"] -post = ["reqwest", "tokio"] -sys = ["sysinfo"] -ps = ["sysinfo"] diff --git a/crates/nu-command/README.md b/crates/nu-command/README.md deleted file mode 100644 index b9f0857cb1..0000000000 --- a/crates/nu-command/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# nu-command - -The Nu command crate contains the full set of internal commands, that is, the commands that can be form the set of built-in commands in a Nushell engine. - -The default set of commands that Nushell ships with can be found in the [default context](src/default_context.rs). - -The commands themselves live in the [commands module](src/commands/). diff --git a/crates/nu-command/src/args.rs b/crates/nu-command/src/args.rs deleted file mode 100644 index 2e6961888c..0000000000 --- a/crates/nu-command/src/args.rs +++ /dev/null @@ -1,10 +0,0 @@ -use nu_protocol::Value; - -#[derive(Debug)] -pub enum LogLevel {} - -#[derive(Debug)] -pub struct LogItem { - level: LogLevel, - value: Value, -} diff --git a/crates/nu-command/src/classified/external.rs b/crates/nu-command/src/classified/external.rs deleted file mode 100644 index 716d830a23..0000000000 --- a/crates/nu-command/src/classified/external.rs +++ /dev/null @@ -1,725 +0,0 @@ -use crate::prelude::*; -use lazy_static::lazy_static; -use nu_engine::{evaluate_baseline_expr, BufCodecReader}; -use nu_engine::{MaybeTextCodec, StringOrBinary}; -use nu_test_support::NATIVE_PATH_ENV_VAR; -use parking_lot::Mutex; -use regex::Regex; - -#[allow(unused)] -use std::env; -use std::io::Write; -use std::path::PathBuf; -use std::process::{Command, Stdio}; -use std::sync::mpsc; -use std::{borrow::Cow, io::BufReader}; - -use log::trace; - -use nu_errors::ShellError; -use nu_protocol::hir::Expression; -use nu_protocol::hir::{ExternalCommand, ExternalRedirection}; -use nu_protocol::{Primitive, ShellTypeName, UntaggedValue, Value}; -use nu_source::Tag; - -#[cfg(feature = "which")] -use which::which_in; - -pub(crate) fn run_external_command( - command: ExternalCommand, - context: &mut EvaluationContext, - input: InputStream, - external_redirection: ExternalRedirection, -) -> Result { - trace!(target: "nu::run::external", "-> {}", command.name); - - context.sync_path_to_env(); - if !context.host().lock().is_external_cmd(&command.name) { - return Err(ShellError::labeled_error( - "Command not found", - format!("command {} not found", &command.name), - &command.name_tag, - )); - } - - run_with_stdin(command, context, input, external_redirection) -} - -#[allow(unused)] -fn trim_enclosing_quotes(input: &str) -> String { - let mut chars = input.chars(); - - match (chars.next(), chars.next_back()) { - (Some('"'), Some('"')) => chars.collect(), - (Some('\''), Some('\'')) => chars.collect(), - _ => input.to_string(), - } -} - -fn run_with_stdin( - command: ExternalCommand, - context: &mut EvaluationContext, - input: InputStream, - external_redirection: ExternalRedirection, -) -> Result { - let path = context.shell_manager().path(); - - let mut command_args = vec![]; - for arg in command.args.iter() { - let is_literal = matches!(arg.expr, Expression::Literal(_)); - let value = evaluate_baseline_expr(arg, context)?; - - // Skip any arguments that don't really exist, treating them as optional - // FIXME: we may want to preserve the gap in the future, though it's hard to say - // what value we would put in its place. - if value.value.is_none() { - continue; - } - - // Do the cleanup that we need to do on any argument going out: - match &value.value { - UntaggedValue::Table(table) => { - for t in table { - match &t.value { - UntaggedValue::Primitive(_) => { - command_args.push(( - t.convert_to_string().trim_end_matches('\n').to_string(), - is_literal, - )); - } - _ => { - return Err(ShellError::labeled_error( - "Could not convert to positional arguments", - "could not convert to positional arguments", - value.tag(), - )); - } - } - } - } - _ => { - let trimmed_value_string = value.as_string()?.trim_end_matches('\n').to_string(); - //let trimmed_value_string = trim_quotes(&trimmed_value_string); - command_args.push((trimmed_value_string, is_literal)); - } - } - } - - let process_args = command_args - .iter() - .map(|(arg, _is_literal)| { - let arg = nu_path::expand_tilde(arg).to_string_lossy().to_string(); - - #[cfg(not(windows))] - { - if !_is_literal { - arg - } else { - trim_enclosing_quotes(&arg) - } - } - #[cfg(windows)] - { - if let Some(unquoted) = remove_quotes(&arg) { - unquoted.to_string() - } else { - arg - } - } - }) - .collect::>(); - - spawn( - &command, - &path, - &process_args[..], - input, - external_redirection, - &context.scope, - ) -} - -/// Spawn a direct exe -#[allow(unused)] -fn spawn_exe(full_path: PathBuf, args: &[String]) -> Command { - let mut process = Command::new(full_path); - for arg in args { - process.arg(&arg); - } - process -} - -/// Spawn a cmd command with `cmd /c args...` -fn spawn_cmd_command(command: &ExternalCommand, args: &[String]) -> Command { - let mut process = Command::new("cmd"); - process.arg("/c"); - process.arg(&command.name); - for arg in args { - // Clean the args before we use them: - // https://stackoverflow.com/questions/1200235/how-to-pass-a-quoted-pipe-character-to-cmd-exe - // cmd.exe needs to have a caret to escape a pipe - let arg = arg.replace("|", "^|"); - process.arg(&arg); - } - process -} - -fn has_unsafe_shell_characters(arg: &str) -> bool { - lazy_static! { - static ref RE: Regex = Regex::new(r"[^\w@%+=:,./-]").expect("regex to be valid"); - } - - RE.is_match(arg) -} - -fn shell_arg_escape(arg: &str) -> String { - match arg { - "" => String::from("''"), - s if !has_unsafe_shell_characters(s) => String::from(s), - _ => { - let single_quotes_escaped = arg.split('\'').join("'\"'\"'"); - format!("'{}'", single_quotes_escaped) - } - } -} - -/// Spawn a sh command with `sh -c args...` -fn spawn_sh_command(command: &ExternalCommand, args: &[String]) -> Command { - let joined_and_escaped_arguments = args.iter().map(|arg| shell_arg_escape(arg)).join(" "); - let cmd_with_args = vec![command.name.clone(), joined_and_escaped_arguments].join(" "); - let mut process = Command::new("sh"); - process.arg("-c").arg(cmd_with_args); - process -} - -/// a function to spawn any external command -#[allow(unused)] // for minimal builds cwd is unused -fn spawn_any(command: &ExternalCommand, args: &[String], cwd: &str) -> Command { - // resolve the executable name if it is spawnable directly - #[cfg(feature = "which")] - // TODO add more available paths to `env::var_os("PATH")`? - if let Result::Ok(full_path) = which_in(&command.name, env::var_os("PATH"), cwd) { - if let Some(extension) = full_path.extension() { - #[cfg(windows)] - if extension.eq_ignore_ascii_case("exe") { - // if exe spawn it directly - return spawn_exe(full_path, args); - } else { - // TODO implement special care for various executable types such as .bat, .ps1, .cmd, etc - // https://github.com/mklement0/Native/blob/e0e0b8785cad39a73053e35084d1f60d87fbac58/Native.psm1#L749 - // otherwise shell out to cmd - return spawn_cmd_command(command, args); - } - #[cfg(not(windows))] - if !["sh", "bash"] - .iter() - .any(|ext| extension.eq_ignore_ascii_case(ext)) - { - // if exe spawn it directly - return spawn_exe(full_path, args); - } else { - // otherwise shell out to sh - return spawn_sh_command(command, args); - } - } - } - // in all the other cases shell out - if cfg!(windows) { - spawn_cmd_command(command, args) - } else { - // TODO what happens if that os doesn't support spawning sh? - spawn_sh_command(command, args) - } -} - -fn spawn( - command: &ExternalCommand, - path: &str, - args: &[String], - input: InputStream, - external_redirection: ExternalRedirection, - scope: &Scope, -) -> Result { - let command = command.clone(); - - let mut process = spawn_any(&command, args, path); - process.current_dir(path); - trace!(target: "nu::run::external", "cwd = {:?}", &path); - - process.env_clear(); - process.envs(scope.get_env_vars()); - - // We want stdout regardless of what - // we are doing ($it case or pipe stdin) - match external_redirection { - ExternalRedirection::Stdout => { - process.stdout(Stdio::piped()); - trace!(target: "nu::run::external", "set up stdout pipe"); - } - ExternalRedirection::Stderr => { - process.stderr(Stdio::piped()); - trace!(target: "nu::run::external", "set up stderr pipe"); - } - ExternalRedirection::StdoutAndStderr => { - process.stdout(Stdio::piped()); - trace!(target: "nu::run::external", "set up stdout pipe"); - process.stderr(Stdio::piped()); - trace!(target: "nu::run::external", "set up stderr pipe"); - } - _ => {} - } - - // open since we have some contents for stdin - if !input.is_empty() { - process.stdin(Stdio::piped()); - trace!(target: "nu::run::external", "set up stdin pipe"); - } - - trace!(target: "nu::run::external", "built command {:?}", process); - - // TODO Switch to async_std::process once it's stabilized - match process.spawn() { - Ok(mut child) => { - let (tx, rx) = mpsc::sync_channel(0); - - let mut stdin = child.stdin.take(); - - let stdin_write_tx = tx.clone(); - let stdout_read_tx = tx; - let stdin_name_tag = command.name_tag.clone(); - let stdout_name_tag = command.name_tag; - - std::thread::spawn(move || { - if !input.is_empty() { - let mut stdin_write = stdin - .take() - .expect("Internal error: could not get stdin pipe for external command"); - - for value in input { - match &value.value { - UntaggedValue::Primitive(Primitive::Nothing) => continue, - UntaggedValue::Primitive(Primitive::String(s)) => { - if stdin_write.write(s.as_bytes()).is_err() { - // Other side has closed, so exit - return Ok(()); - } - } - UntaggedValue::Primitive(Primitive::Binary(b)) => { - if stdin_write.write(b).is_err() { - // Other side has closed, so exit - return Ok(()); - } - } - unsupported => { - println!("Unsupported: {:?}", unsupported); - let _ = stdin_write_tx.send(Ok(Value { - value: UntaggedValue::Error(ShellError::labeled_error( - format!( - "Received unexpected type from pipeline ({})", - unsupported.type_name() - ), - format!( - "expected a string, got {} as input", - unsupported.type_name() - ), - stdin_name_tag.clone(), - )), - tag: stdin_name_tag, - })); - return Err(()); - } - }; - } - } - - Ok(()) - }); - - std::thread::spawn(move || { - if external_redirection == ExternalRedirection::Stdout - || external_redirection == ExternalRedirection::StdoutAndStderr - { - let stdout = if let Some(stdout) = child.stdout.take() { - stdout - } else { - let _ = stdout_read_tx.send(Ok(Value { - value: UntaggedValue::Error(ShellError::labeled_error( - "Can't redirect the stdout for external command", - "can't redirect stdout", - &stdout_name_tag, - )), - tag: stdout_name_tag, - })); - return Err(()); - }; - - // let file = futures::io::AllowStdIo::new(stdout); - // let stream = FramedRead::new(file, MaybeTextCodec::default()); - let buf_read = BufReader::new(stdout); - let buf_codec = BufCodecReader::new(buf_read, MaybeTextCodec::default()); - - for line in buf_codec { - match line { - Ok(line) => match line { - StringOrBinary::String(s) => { - let result = stdout_read_tx.send(Ok(Value { - value: UntaggedValue::Primitive(Primitive::String( - s.clone(), - )), - tag: stdout_name_tag.clone(), - })); - - if result.is_err() { - break; - } - } - StringOrBinary::Binary(b) => { - let result = stdout_read_tx.send(Ok(Value { - value: UntaggedValue::Primitive(Primitive::Binary( - b.into_iter().collect(), - )), - tag: stdout_name_tag.clone(), - })); - - if result.is_err() { - break; - } - } - }, - Err(e) => { - // If there's an exit status, it makes sense that we may error when - // trying to read from its stdout pipe (likely been closed). In that - // case, don't emit an error. - let should_error = match child.wait() { - Ok(exit_status) => !exit_status.success(), - Err(_) => true, - }; - - if should_error { - let _ = stdout_read_tx.send(Ok(Value { - value: UntaggedValue::Error(ShellError::labeled_error( - format!("Unable to read from stdout ({})", e), - "unable to read from stdout", - &stdout_name_tag, - )), - tag: stdout_name_tag.clone(), - })); - } - - return Ok(()); - } - } - } - } - if external_redirection == ExternalRedirection::Stderr - || external_redirection == ExternalRedirection::StdoutAndStderr - { - let stderr = if let Some(stderr) = child.stderr.take() { - stderr - } else { - let _ = stdout_read_tx.send(Ok(Value { - value: UntaggedValue::Error(ShellError::labeled_error( - "Can't redirect the stderr for external command", - "can't redirect stderr", - &stdout_name_tag, - )), - tag: stdout_name_tag, - })); - return Err(()); - }; - - // let file = futures::io::AllowStdIo::new(stderr); - // let stream = FramedRead::new(file, MaybeTextCodec::default()); - let buf_reader = BufReader::new(stderr); - let buf_codec = BufCodecReader::new(buf_reader, MaybeTextCodec::default()); - - for line in buf_codec { - match line { - Ok(line) => match line { - StringOrBinary::String(s) => { - let result = stdout_read_tx.send(Ok(Value { - value: UntaggedValue::Error( - ShellError::untagged_runtime_error(s), - ), - tag: stdout_name_tag.clone(), - })); - - if result.is_err() { - break; - } - } - StringOrBinary::Binary(_) => { - let result = stdout_read_tx.send(Ok(Value { - value: UntaggedValue::Error( - ShellError::untagged_runtime_error(""), - ), - tag: stdout_name_tag.clone(), - })); - - if result.is_err() { - break; - } - } - }, - Err(e) => { - // If there's an exit status, it makes sense that we may error when - // trying to read from its stdout pipe (likely been closed). In that - // case, don't emit an error. - let should_error = match child.wait() { - Ok(exit_status) => !exit_status.success(), - Err(_) => true, - }; - - if should_error { - let _ = stdout_read_tx.send(Ok(Value { - value: UntaggedValue::Error(ShellError::labeled_error( - format!("Unable to read from stdout ({})", e), - "unable to read from stdout", - &stdout_name_tag, - )), - tag: stdout_name_tag.clone(), - })); - } - - return Ok(()); - } - } - } - } - - // We can give an error when we see a non-zero exit code, but this is different - // than what other shells will do. - let external_failed = match child.wait() { - Err(_) => true, - Ok(exit_status) => !exit_status.success(), - }; - - if external_failed { - let cfg = nu_data::config::config(Tag::unknown()); - if let Ok(cfg) = cfg { - if cfg.contains_key("nonzero_exit_errors") { - let _ = stdout_read_tx.send(Ok(Value { - value: UntaggedValue::Error(ShellError::labeled_error( - "External command failed", - "command failed", - &stdout_name_tag, - )), - tag: stdout_name_tag.clone(), - })); - } - } - let _ = stdout_read_tx.send(Ok(Value { - value: UntaggedValue::nothing(), - tag: stdout_name_tag, - })); - } - - Ok(()) - }); - - let stream = ChannelReceiver::new(rx); - Ok(stream.into_input_stream()) - } - Err(e) => Err(ShellError::labeled_error( - e.to_string(), - "failed to spawn", - &command.name_tag, - )), - } -} - -struct ChannelReceiver { - rx: Arc>>>, -} - -impl ChannelReceiver { - pub fn new(rx: mpsc::Receiver>) -> Self { - Self { - rx: Arc::new(Mutex::new(rx)), - } - } -} - -impl Iterator for ChannelReceiver { - type Item = Result; - - fn next(&mut self) -> Option { - let rx = self.rx.lock(); - match rx.recv() { - Ok(v) => Some(v), - Err(_) => None, - } - } -} - -fn argument_is_quoted(argument: &str) -> bool { - if argument.len() < 2 { - return false; - } - - (argument.starts_with('"') && argument.ends_with('"')) - || (argument.starts_with('\'') && argument.ends_with('\'')) -} - -#[allow(unused)] -fn add_double_quotes(argument: &str) -> String { - format!("\"{}\"", argument) -} - -#[allow(unused)] -fn escape_double_quotes(argument: &str) -> Cow<'_, str> { - // allocate new string only if required - if argument.contains('"') { - Cow::Owned(argument.replace('"', r#"\""#)) - } else { - Cow::Borrowed(argument) - } -} - -#[allow(unused)] -fn remove_quotes(argument: &str) -> Option<&str> { - if !argument_is_quoted(argument) { - return None; - } - - let size = argument.len(); - - Some(&argument[1..size - 1]) -} - -#[allow(unused)] -fn shell_os_paths() -> Vec { - let mut original_paths = vec![]; - - if let Some(paths) = std::env::var_os(NATIVE_PATH_ENV_VAR) { - original_paths = std::env::split_paths(&paths).collect::>(); - } - - original_paths -} - -#[cfg(test)] -mod tests { - use super::{add_double_quotes, argument_is_quoted, escape_double_quotes, remove_quotes}; - #[cfg(feature = "which")] - use super::{run_external_command, InputStream}; - - #[cfg(feature = "which")] - use nu_engine::EvaluationContext; - - #[cfg(feature = "which")] - use nu_test_support::commands::ExternalBuilder; - // fn read(mut stream: OutputStream) -> Option { - // match stream.try_next() { - // Ok(val) => { - // if let Some(val) = val { - // val.raw_value() - // } else { - // None - // } - // } - // Err(_) => None, - // } - // } - - #[cfg(feature = "which")] - fn non_existent_run() { - use nu_protocol::hir::ExternalRedirection; - let cmd = ExternalBuilder::for_name("i_dont_exist.exe").build(); - - let input = InputStream::empty(); - let mut ctx = EvaluationContext::basic(); - - assert!(run_external_command(cmd, &mut ctx, input, ExternalRedirection::Stdout).is_err()); - } - - // fn failure_run() -> Result<(), ShellError> { - // let cmd = ExternalBuilder::for_name("fail").build(); - - // let mut ctx = crate::cli::EvaluationContext::basic().expect("There was a problem creating a basic context."); - // let stream = run_external_command(cmd, &mut ctx, None, false) - // ? - // .expect("There was a problem running the external command."); - - // match read(stream.into()) { - // Some(Value { - // value: UntaggedValue::Error(_), - // .. - // }) => {} - // None | _ => panic!("Command didn't fail."), - // } - - // Ok(()) - // } - - // #[test] - // fn identifies_command_failed() -> Result<(), ShellError> { - // block_on(failure_run()) - // } - - #[cfg(feature = "which")] - #[test] - fn identifies_command_not_found() { - non_existent_run() - } - - #[test] - fn checks_escape_double_quotes() { - assert_eq!(escape_double_quotes("andrés"), "andrés"); - assert_eq!(escape_double_quotes(r#"an"drés"#), r#"an\"drés"#); - assert_eq!(escape_double_quotes(r#""an"drés""#), r#"\"an\"drés\""#); - } - - #[test] - fn checks_quotes_from_argument_to_be_passed_in() { - assert!(!argument_is_quoted("")); - - assert!(!argument_is_quoted("'")); - assert!(!argument_is_quoted("'a")); - assert!(!argument_is_quoted("a")); - assert!(!argument_is_quoted("a'")); - assert!(argument_is_quoted("''")); - - assert!(!argument_is_quoted(r#"""#)); - assert!(!argument_is_quoted(r#""a"#)); - assert!(!argument_is_quoted(r#"a"#)); - assert!(!argument_is_quoted(r#"a""#)); - assert!(argument_is_quoted(r#""""#)); - - assert!(!argument_is_quoted("'andrés")); - assert!(!argument_is_quoted("andrés'")); - assert!(!argument_is_quoted(r#""andrés"#)); - assert!(!argument_is_quoted(r#"andrés""#)); - assert!(argument_is_quoted("'andrés'")); - assert!(argument_is_quoted(r#""andrés""#)); - } - - #[test] - fn adds_double_quotes_to_argument_to_be_passed_in() { - assert_eq!(add_double_quotes("andrés"), "\"andrés\""); - } - - #[test] - fn strips_quotes_from_argument_to_be_passed_in() { - assert_eq!(remove_quotes(""), None); - - assert_eq!(remove_quotes("'"), None); - assert_eq!(remove_quotes("'a"), None); - assert_eq!(remove_quotes("a"), None); - assert_eq!(remove_quotes("a'"), None); - assert_eq!(remove_quotes("''"), Some("")); - - assert_eq!(remove_quotes(r#"""#), None); - assert_eq!(remove_quotes(r#""a"#), None); - assert_eq!(remove_quotes(r#"a"#), None); - assert_eq!(remove_quotes(r#"a""#), None); - assert_eq!(remove_quotes(r#""""#), Some("")); - - assert_eq!(remove_quotes("'andrés"), None); - assert_eq!(remove_quotes("andrés'"), None); - assert_eq!(remove_quotes(r#""andrés"#), None); - assert_eq!(remove_quotes(r#"andrés""#), None); - assert_eq!(remove_quotes("'andrés'"), Some("andrés")); - assert_eq!(remove_quotes(r#""andrés""#), Some("andrés")); - } -} diff --git a/crates/nu-command/src/classified/mod.rs b/crates/nu-command/src/classified/mod.rs deleted file mode 100644 index 04016e9e7f..0000000000 --- a/crates/nu-command/src/classified/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub(crate) mod external; diff --git a/crates/nu-command/src/commands/charting/chart.rs b/crates/nu-command/src/commands/charting/chart.rs deleted file mode 100644 index edc6f59f5b..0000000000 --- a/crates/nu-command/src/commands/charting/chart.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ReturnSuccess, Signature, UntaggedValue}; - -#[derive(Clone)] -pub struct Chart; - -impl WholeStreamCommand for Chart { - fn name(&self) -> &str { - "chart" - } - - fn signature(&self) -> Signature { - Signature::build("chart") - } - - fn usage(&self) -> &str { - "Displays charts." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - if args.scope().get_command("chart bar").is_none() { - return Err(ShellError::untagged_runtime_error( - "nu_plugin_chart not installed.", - )); - } - - Ok(ActionStream::one(Ok(ReturnSuccess::Value( - UntaggedValue::string(get_full_help(&Chart, args.scope())).into_value(Tag::unknown()), - )))) - } -} - -#[cfg(test)] -mod tests { - use super::Chart; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Chart {}) - } -} diff --git a/crates/nu-command/src/commands/charting/histogram.rs b/crates/nu-command/src/commands/charting/histogram.rs deleted file mode 100644 index 4fcf6aa57b..0000000000 --- a/crates/nu-command/src/commands/charting/histogram.rs +++ /dev/null @@ -1,220 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - ColumnPath, ReturnSuccess, Signature, SyntaxShape, TaggedDictBuilder, UntaggedValue, Value, -}; -use nu_source::Tagged; - -pub struct Histogram; - -impl WholeStreamCommand for Histogram { - fn name(&self) -> &str { - "histogram" - } - - fn signature(&self) -> Signature { - Signature::build("histogram") - .named( - "use", - SyntaxShape::ColumnPath, - "Use data at the column path given as valuator", - None, - ) - .rest( - "rest", - SyntaxShape::ColumnPath, - "column name to give the histogram's frequency column", - ) - } - - fn usage(&self) -> &str { - "Creates a new table with a histogram based on the column name passed in." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - histogram(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Get a histogram for the types of files", - example: "ls | histogram type", - result: None, - }, - Example { - description: - "Get a histogram for the types of files, with frequency column named percentage", - example: "ls | histogram type percentage", - result: None, - }, - Example { - description: "Get a histogram for a list of numbers", - example: "echo [1 2 3 1 1 1 2 2 1 1] | histogram", - result: None, - }, - ] - } -} - -pub fn histogram(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - - let mut columns = args.rest::(0)?; - let evaluate_with = args.get_flag::("use")?.map(evaluator); - let values: Vec = args.input.collect(); - - let column_grouper = if !columns.is_empty() { - columns - .remove(0) - .split_last() - .map(|(key, _)| key.as_string().tagged(&name)) - } else { - None - }; - - let frequency_column_name = if columns.is_empty() { - "frequency".to_string() - } else if let Some((key, _)) = columns[0].split_last() { - key.as_string() - } else { - "frequency".to_string() - }; - - let column = if let Some(ref column) = column_grouper { - column.clone() - } else { - "value".to_string().tagged(&name) - }; - - let results = nu_data::utils::report( - &UntaggedValue::table(&values).into_value(&name), - nu_data::utils::Operation { - grouper: Some(Box::new(move |_, _| Ok(String::from("frequencies")))), - splitter: Some(splitter(column_grouper)), - format: &None, - eval: &evaluate_with, - reduction: &nu_data::utils::Reduction::Count, - }, - &name, - )?; - - let labels = results.labels.y.clone(); - let mut idx = 0; - - Ok(results - .data - .table_entries() - .cloned() - .collect::>() - .into_iter() - .zip( - results - .percentages - .table_entries() - .cloned() - .collect::>() - .into_iter(), - ) - .map(move |(counts, percentages)| { - let percentage = percentages - .table_entries() - .cloned() - .last() - .unwrap_or_else(|| { - UntaggedValue::decimal_from_float(0.0, name.span).into_value(&name) - }); - let value = counts - .table_entries() - .cloned() - .last() - .unwrap_or_else(|| UntaggedValue::int(0).into_value(&name)); - - let mut fact = TaggedDictBuilder::new(&name); - let column_value = labels - .get(idx) - .ok_or_else(|| { - ShellError::labeled_error( - "Unable to load group labels", - "unable to load group labels", - &name, - ) - })? - .clone(); - - fact.insert_value(&column.item, column_value); - fact.insert_untagged("count", value); - - let fmt_percentage = format!( - "{}%", - // Some(2) < the number of digits - // true < group the digits - crate::commands::conversions::into::string::action( - &percentage, - &name, - Some(2), - true - )? - .as_string()? - ); - fact.insert_untagged("percentage", UntaggedValue::string(fmt_percentage)); - - let string = "*".repeat(percentage.as_u64().map_err(|_| { - ShellError::labeled_error("expected a number", "expected a number", &name) - })? as usize); - - fact.insert_untagged(&frequency_column_name, UntaggedValue::string(string)); - - idx += 1; - - ReturnSuccess::value(fact.into_value()) - }) - .into_action_stream()) -} - -fn evaluator(by: ColumnPath) -> Box Result + Send> { - Box::new(move |_: usize, value: &Value| { - let path = by.clone(); - - let eval = nu_value_ext::get_data_by_column_path(value, &path, move |_, _, error| error); - - match eval { - Ok(with_value) => Ok(with_value), - Err(reason) => Err(reason), - } - }) -} - -fn splitter( - by: Option>, -) -> Box Result + Send> { - match by { - Some(column) => Box::new(move |_, row: &Value| { - let key = &column; - - match row.get_data_by_key(key.borrow_spanned()) { - Some(key) => nu_value_ext::as_string(&key), - None => Err(ShellError::labeled_error( - "unknown column", - "unknown column", - key.tag(), - )), - } - }), - None => Box::new(move |_, row: &Value| nu_value_ext::as_string(row)), - } -} - -#[cfg(test)] -mod tests { - use super::Histogram; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Histogram {}) - } -} diff --git a/crates/nu-command/src/commands/charting/mod.rs b/crates/nu-command/src/commands/charting/mod.rs deleted file mode 100644 index a690475175..0000000000 --- a/crates/nu-command/src/commands/charting/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod chart; -mod histogram; - -pub use chart::Chart; -pub use histogram::Histogram; diff --git a/crates/nu-command/src/commands/config/clear.rs b/crates/nu-command/src/commands/config/clear.rs deleted file mode 100644 index 0aa8593dfa..0000000000 --- a/crates/nu-command/src/commands/config/clear.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, UntaggedValue}; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "config clear" - } - - fn signature(&self) -> Signature { - Signature::build("config clear") - } - - fn usage(&self) -> &str { - "clear the config" - } - - fn run(&self, args: CommandArgs) -> Result { - clear(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Clear the config (be careful!)", - example: "config clear", - result: None, - }] - } -} - -pub fn clear(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - let ctx = &args.context; - - let result = if let Some(global_cfg) = &mut args.configs().lock().global_config { - global_cfg.vars.clear(); - global_cfg.write()?; - ctx.reload_config(global_cfg)?; - - let value = UntaggedValue::Row(global_cfg.vars.clone().into()).into_value(name); - Ok(OutputStream::one(value)) - } else { - let value = UntaggedValue::Error(crate::commands::config::err_no_global_cfg_present()) - .into_value(name); - - Ok(OutputStream::one(value)) - }; - - result -} diff --git a/crates/nu-command/src/commands/config/command.rs b/crates/nu-command/src/commands/config/command.rs deleted file mode 100644 index f0faea14bd..0000000000 --- a/crates/nu-command/src/commands/config/command.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::prelude::*; -use nu_engine::CommandArgs; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, UntaggedValue}; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "config" - } - - fn signature(&self) -> Signature { - Signature::build("config") - } - - fn usage(&self) -> &str { - "Configuration management." - } - - fn run(&self, args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - - if let Some(global_cfg) = &args.configs().lock().global_config { - let result = global_cfg.vars.clone(); - let value = UntaggedValue::Row(result.into()).into_value(name); - - Ok(OutputStream::one(value)) - } else { - let value = UntaggedValue::Error(crate::commands::config::err_no_global_cfg_present()) - .into_value(name); - - Ok(OutputStream::one(value)) - } - } -} diff --git a/crates/nu-command/src/commands/config/get.rs b/crates/nu-command/src/commands/config/get.rs deleted file mode 100644 index fac8e110e8..0000000000 --- a/crates/nu-command/src/commands/config/get.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value}; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "config get" - } - - fn signature(&self) -> Signature { - Signature::build("config get").required( - "get", - SyntaxShape::ColumnPath, - "value to get from the config", - ) - } - - fn usage(&self) -> &str { - "Gets a value from the config" - } - - fn run(&self, args: CommandArgs) -> Result { - get(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Get the current startup commands", - example: "config get startup", - result: None, - }] - } -} - -pub fn get(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - let ctx = &args.context; - - let column_path = args.req(0)?; - - let result = if let Some(global_cfg) = &ctx.configs().lock().global_config { - let result = UntaggedValue::row(global_cfg.vars.clone()).into_value(&name); - let value = crate::commands::filters::get::get_column_path(&column_path, &result)?; - Ok(match value { - Value { - value: UntaggedValue::Table(list), - .. - } => OutputStream::from_stream(list.into_iter()), - x => OutputStream::one(x), - }) - } else { - let value = UntaggedValue::Error(crate::commands::config::err_no_global_cfg_present()) - .into_value(name); - - Ok(OutputStream::one(value)) - }; - - result -} diff --git a/crates/nu-command/src/commands/config/mod.rs b/crates/nu-command/src/commands/config/mod.rs deleted file mode 100644 index 62f626cf89..0000000000 --- a/crates/nu-command/src/commands/config/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -pub mod clear; -pub mod command; -pub mod get; -pub mod path; -pub mod remove; -pub mod set; -pub mod set_into; - -pub use clear::SubCommand as ConfigClear; -pub use command::Command as Config; -pub use get::SubCommand as ConfigGet; -pub use path::SubCommand as ConfigPath; -pub use remove::SubCommand as ConfigRemove; -pub use set::SubCommand as ConfigSet; -pub use set_into::SubCommand as ConfigSetInto; - -use nu_errors::ShellError; - -pub fn err_no_global_cfg_present() -> ShellError { - ShellError::untagged_runtime_error("No global config found!") -} diff --git a/crates/nu-command/src/commands/config/path.rs b/crates/nu-command/src/commands/config/path.rs deleted file mode 100644 index 4e32596cc8..0000000000 --- a/crates/nu-command/src/commands/config/path.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Primitive, Signature, UntaggedValue}; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "config path" - } - - fn signature(&self) -> Signature { - Signature::build("config path") - } - - fn usage(&self) -> &str { - "return the path to the config file" - } - - fn run(&self, args: CommandArgs) -> Result { - path(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Get the path to the current config file", - example: "config path", - result: None, - }] - } -} - -pub fn path(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - - if let Some(global_cfg) = &mut args.configs().lock().global_config { - let value = UntaggedValue::Primitive(Primitive::FilePath(global_cfg.file_path.clone())) - .into_value(name); - - Ok(OutputStream::one(value)) - } else { - let value = UntaggedValue::Error(crate::commands::config::err_no_global_cfg_present()) - .into_value(name); - - Ok(OutputStream::one(value)) - } -} diff --git a/crates/nu-command/src/commands/config/remove.rs b/crates/nu-command/src/commands/config/remove.rs deleted file mode 100644 index d71ecc5c11..0000000000 --- a/crates/nu-command/src/commands/config/remove.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tagged; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "config remove" - } - - fn signature(&self) -> Signature { - Signature::build("config remove").required( - "remove", - SyntaxShape::Any, - "remove a value from the config", - ) - } - - fn usage(&self) -> &str { - "Removes a value from the config" - } - - fn run(&self, args: CommandArgs) -> Result { - remove(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Remove the startup commands", - example: "config remove startup", - result: None, - }] - } -} - -pub fn remove(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - let ctx = &args.context; - let remove: Tagged = args.req(0)?; - - let key = remove.to_string(); - - let result = if let Some(global_cfg) = &mut ctx.configs().lock().global_config { - if global_cfg.vars.contains_key(&key) { - global_cfg.vars.swap_remove(&key); - global_cfg.write()?; - ctx.reload_config(global_cfg)?; - - let value: Value = UntaggedValue::row(global_cfg.vars.clone()).into_value(remove.tag); - - Ok(OutputStream::one(value)) - } else { - Err(ShellError::labeled_error( - "Key does not exist in config", - "key", - remove.tag(), - )) - } - } else { - let value = UntaggedValue::Error(crate::commands::config::err_no_global_cfg_present()) - .into_value(name); - - Ok(OutputStream::one(value)) - }; - - result -} diff --git a/crates/nu-command/src/commands/config/set.rs b/crates/nu-command/src/commands/config/set.rs deleted file mode 100644 index b6bd986e4c..0000000000 --- a/crates/nu-command/src/commands/config/set.rs +++ /dev/null @@ -1,92 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value}; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "config set" - } - - fn signature(&self) -> Signature { - Signature::build("config set") - .required("key", SyntaxShape::ColumnPath, "variable name to set") - .required("value", SyntaxShape::Any, "value to use") - } - - fn usage(&self) -> &str { - "Sets a value in the config" - } - - fn run(&self, args: CommandArgs) -> Result { - set(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Set auto pivoting", - example: "config set pivot_mode always", - result: None, - }, - Example { - description: "Set line editor options", - example: "config set line_editor [[edit_mode, completion_type]; [emacs circular]]", - result: None, - }, - Example { - description: "Set coloring options", - example: "config set color_config [[header_align header_color]; [left white_bold]]", - result: None, - }, - Example { - description: "Set nested options", - example: "config set color_config.header_color white", - result: None, - }, - ] - } -} - -pub fn set(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - let ctx = &args.context; - - let column_path = args.req(0)?; - let mut value: Value = args.req(1)?; - - let result = if let Some(global_cfg) = &mut ctx.configs().lock().global_config { - let configuration = UntaggedValue::row(global_cfg.vars.clone()).into_value(&name); - - if let UntaggedValue::Table(rows) = &value.value { - if rows.len() == 1 && rows[0].is_row() { - value = rows[0].clone(); - } - } - - match configuration.forgiving_insert_data_at_column_path(&column_path, value) { - Ok(Value { - value: UntaggedValue::Row(changes), - .. - }) => { - global_cfg.vars = changes.entries; - global_cfg.write()?; - ctx.reload_config(global_cfg)?; - - let value = UntaggedValue::row(global_cfg.vars.clone()).into_value(name); - Ok(OutputStream::one(value)) - } - Ok(_) => Ok(OutputStream::empty()), - Err(reason) => Err(reason), - } - } else { - let value = UntaggedValue::Error(crate::commands::config::err_no_global_cfg_present()) - .into_value(name); - - Ok(OutputStream::one(value)) - }; - - result -} diff --git a/crates/nu-command/src/commands/config/set_into.rs b/crates/nu-command/src/commands/config/set_into.rs deleted file mode 100644 index f49ae71abd..0000000000 --- a/crates/nu-command/src/commands/config/set_into.rs +++ /dev/null @@ -1,81 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tagged; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "config set_into" - } - - fn signature(&self) -> Signature { - Signature::build("config set_into").required( - "set_into", - SyntaxShape::String, - "sets a variable from values in the pipeline", - ) - } - - fn usage(&self) -> &str { - "Sets a value in the config" - } - - fn run(&self, args: CommandArgs) -> Result { - set_into(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Store the contents of the pipeline as a path", - example: "echo ['/usr/bin' '/bin'] | config set_into path", - result: None, - }] - } -} - -pub fn set_into(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - let ctx = &args.context; - - let set_into: Tagged = args.req(0)?; - - let rows: Vec = args.input.collect(); - let key = set_into.to_string(); - - let result = if let Some(global_cfg) = &mut ctx.configs().lock().global_config { - if rows.is_empty() { - return Err(ShellError::labeled_error( - "No values given for set_into", - "needs value(s) from pipeline", - set_into.tag(), - )); - } else if rows.len() == 1 { - // A single value - let value = &rows[0]; - - global_cfg.vars.insert(key, value.clone()); - } else { - // Take in the pipeline as a table - let value = UntaggedValue::Table(rows).into_value(name.clone()); - - global_cfg.vars.insert(key, value); - } - - global_cfg.write()?; - ctx.reload_config(global_cfg)?; - - let value = UntaggedValue::row(global_cfg.vars.clone()).into_value(name); - - Ok(OutputStream::one(value)) - } else { - let value = UntaggedValue::Error(crate::commands::config::err_no_global_cfg_present()) - .into_value(name); - - Ok(OutputStream::one(value)) - }; - - result -} diff --git a/crates/nu-command/src/commands/conversions/into/binary.rs b/crates/nu-command/src/commands/conversions/into/binary.rs deleted file mode 100644 index 59aa35bbd5..0000000000 --- a/crates/nu-command/src/commands/conversions/into/binary.rs +++ /dev/null @@ -1,185 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ColumnPath, Primitive, Signature, SyntaxShape, UntaggedValue, Value}; -use num_bigint::{BigInt, ToBigInt}; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "into binary" - } - - fn signature(&self) -> Signature { - Signature::build("into binary").rest( - "rest", - SyntaxShape::ColumnPath, - "column paths to convert to binary (for table input)", - ) - } - - fn usage(&self) -> &str { - "Convert value to a binary primitive" - } - - fn run(&self, args: CommandArgs) -> Result { - into_binary(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "convert string to a nushell binary primitive", - example: - "echo 'This is a string that is exactly 52 characters long.' | into binary", - result: Some(vec![UntaggedValue::binary( - "This is a string that is exactly 52 characters long." - .to_string() - .as_bytes() - .to_vec(), - ) - .into()]), - }, - Example { - description: "convert a number to a nushell binary primitive", - example: "echo 1 | into binary", - result: Some(vec![UntaggedValue::binary( - i64::from(1).to_le_bytes().to_vec(), - ) - .into()]), - }, - Example { - description: "convert a boolean to a nushell binary primitive", - example: "echo $true | into binary", - result: Some(vec![UntaggedValue::binary( - i64::from(1).to_le_bytes().to_vec(), - ) - .into()]), - }, - Example { - description: "convert a filesize to a nushell binary primitive", - example: "ls | where name == LICENSE | get size | into binary", - result: None, - }, - Example { - description: "convert a filepath to a nushell binary primitive", - example: "ls | where name == LICENSE | get name | path expand | into binary", - result: None, - }, - Example { - description: "convert a decimal to a nushell binary primitive", - example: "echo 1.234 | into binary", - result: Some(vec![ - UntaggedValue::binary(BigInt::from(1).to_bytes_le().1).into() - ]), - }, - ] - } -} - -fn into_binary(args: CommandArgs) -> Result { - let column_paths: Vec = args.rest(0)?; - - Ok(args - .input - .map(move |v| { - if column_paths.is_empty() { - action(&v, v.tag()) - } else { - let mut ret = v; - for path in &column_paths { - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| action(old, old.tag())), - )?; - } - - Ok(ret) - } - }) - .into_input_stream()) -} - -fn int_to_endian(n: i64) -> Vec { - if cfg!(target_endian = "little") { - n.to_le_bytes().to_vec() - } else { - n.to_be_bytes().to_vec() - } -} - -fn bigint_to_endian(n: &BigInt) -> Vec { - if cfg!(target_endian = "little") { - n.to_bytes_le().1 - } else { - n.to_bytes_be().1 - } -} - -pub fn action(input: &Value, tag: impl Into) -> Result { - let tag = tag.into(); - - match &input.value { - UntaggedValue::Primitive(prim) => Ok(UntaggedValue::binary(match prim { - Primitive::Binary(b) => b.to_vec(), - Primitive::Int(n_ref) => int_to_endian(*n_ref), - Primitive::BigInt(n_ref) => bigint_to_endian(n_ref), - Primitive::Decimal(dec) => match dec.to_bigint() { - Some(n) => bigint_to_endian(&n), - None => { - return Err(ShellError::unimplemented( - "failed to convert decimal to int", - )); - } - }, - Primitive::Filesize(a_filesize) => match a_filesize.to_bigint() { - Some(n) => bigint_to_endian(&n), - None => { - return Err(ShellError::unimplemented( - "failed to convert filesize to bigint", - )); - } - }, - Primitive::String(a_string) => a_string.as_bytes().to_vec(), - Primitive::Boolean(a_bool) => match a_bool { - false => int_to_endian(0), - true => int_to_endian(1), - }, - Primitive::Date(a_date) => a_date.format("%c").to_string().as_bytes().to_vec(), - Primitive::FilePath(a_filepath) => a_filepath - .as_path() - .display() - .to_string() - .as_bytes() - .to_vec(), - _ => { - return Err(ShellError::unimplemented( - "'into binary' for non-numeric primitives", - )) - } - }) - .into_value(&tag)), - UntaggedValue::Row(_) => Err(ShellError::labeled_error( - "specify column name to use, with 'into binary COLUMN'", - "found table", - tag, - )), - _ => Err(ShellError::unimplemented( - "'into binary' for unsupported type", - )), - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/conversions/into/column_path.rs b/crates/nu-command/src/commands/conversions/into/column_path.rs deleted file mode 100644 index 141bb68c44..0000000000 --- a/crates/nu-command/src/commands/conversions/into/column_path.rs +++ /dev/null @@ -1,118 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ColumnPath, Primitive, Signature, SyntaxShape, UntaggedValue, Value}; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "into column-path" - } - - fn signature(&self) -> Signature { - Signature::build("into column-path").rest( - "rest", - SyntaxShape::ColumnPath, - "values to convert to column path", - ) - } - - fn usage(&self) -> &str { - "Convert value to column path" - } - - fn run(&self, args: CommandArgs) -> Result { - into_filepath(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Convert string to column path in table", - example: "echo [[name]; ['/dev/null'] ['C:\\Program Files'] ['../../Cargo.toml']] | into column-path name", - result: Some(vec![ - UntaggedValue::row(indexmap! { - "name".to_string() => UntaggedValue::column_path("/dev/null", Span::unknown()).into(), - }) - .into(), - UntaggedValue::row(indexmap! { - "name".to_string() => UntaggedValue::column_path("C:\\Program Files", Span::unknown()).into(), - }) - .into(), - UntaggedValue::row(indexmap! { - "name".to_string() => UntaggedValue::column_path("../../Cargo.toml", Span::unknown()).into(), - }) - .into(), - ]), - }, - Example { - description: "Convert string to column path", - example: "echo 'Cargo.toml' | into column-path", - result: Some(vec![UntaggedValue::column_path("Cargo.toml", Span::unknown()).into()]), - }, - ] - } -} - -fn into_filepath(args: CommandArgs) -> Result { - let column_paths: Vec = args.rest(0)?; - - Ok(args - .input - .map(move |v| { - if column_paths.is_empty() { - action(&v, v.tag()) - } else { - let mut ret = v; - for path in &column_paths { - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| action(old, old.tag())), - )?; - } - - Ok(ret) - } - }) - .into_input_stream()) -} - -pub fn action(input: &Value, tag: impl Into) -> Result { - let tag = tag.into(); - match &input.value { - UntaggedValue::Primitive(prim) => Ok(UntaggedValue::column_path( - match prim { - Primitive::String(a_string) => a_string, - _ => { - return Err(ShellError::unimplemented( - "'into column-path' for non-string primitives", - )) - } - }, - Span::unknown(), - ) - .into_value(&tag)), - UntaggedValue::Row(_) => Err(ShellError::labeled_error( - "specify column name to use, with 'into column-path COLUMN'", - "found table", - tag, - )), - _ => Err(ShellError::unimplemented( - "'into column-path' for unsupported type", - )), - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/conversions/into/command.rs b/crates/nu-command/src/commands/conversions/into/command.rs deleted file mode 100644 index 3ae2ff8972..0000000000 --- a/crates/nu-command/src/commands/conversions/into/command.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, UntaggedValue}; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "into" - } - - fn signature(&self) -> Signature { - Signature::build("into") - } - - fn usage(&self) -> &str { - "Apply into function." - } - - fn run(&self, args: CommandArgs) -> Result { - Ok(OutputStream::one( - UntaggedValue::string(get_full_help(&Command, args.scope())).into_value(Tag::unknown()), - )) - } -} - -#[cfg(test)] -mod tests { - use super::Command; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Command {}) - } -} diff --git a/crates/nu-command/src/commands/conversions/into/filepath.rs b/crates/nu-command/src/commands/conversions/into/filepath.rs deleted file mode 100644 index 08e7fff1cb..0000000000 --- a/crates/nu-command/src/commands/conversions/into/filepath.rs +++ /dev/null @@ -1,127 +0,0 @@ -use std::path::PathBuf; - -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ColumnPath, Primitive, Signature, SyntaxShape, UntaggedValue, Value}; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "into path" - } - - fn signature(&self) -> Signature { - Signature::build("into path").rest( - "rest", - SyntaxShape::ColumnPath, - "column paths to convert to filepath (for table input)", - ) - } - - fn usage(&self) -> &str { - "Convert value to filepath" - } - - fn run(&self, args: CommandArgs) -> Result { - into_filepath(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Convert string to filepath in table", - example: "echo [[name]; ['/dev/null'] ['C:\\Program Files'] ['../../Cargo.toml']] | into path name", - result: Some(vec![ - UntaggedValue::row(indexmap! { - "name".to_string() => UntaggedValue::filepath("/dev/null").into(), - }) - .into(), - UntaggedValue::row(indexmap! { - "name".to_string() => UntaggedValue::filepath("C:\\Program Files").into(), - }) - .into(), - UntaggedValue::row(indexmap! { - "name".to_string() => UntaggedValue::filepath("../../Cargo.toml").into(), - }) - .into(), - ]), - }, - Example { - description: "Convert string to filepath", - example: "echo 'Cargo.toml' | into path", - result: Some(vec![UntaggedValue::filepath("Cargo.toml").into()]), - }, - ] - } -} - -fn into_filepath(args: CommandArgs) -> Result { - let column_paths: Vec = args.rest(0)?; - - Ok(args - .input - .map(move |v| { - if column_paths.is_empty() { - action(&v, v.tag()) - } else { - let mut ret = v; - for path in &column_paths { - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| action(old, old.tag())), - )?; - } - - Ok(ret) - } - }) - .into_input_stream()) -} - -pub fn action(input: &Value, tag: impl Into) -> Result { - let tag = tag.into(); - match &input.value { - UntaggedValue::Primitive(prim) => Ok(UntaggedValue::filepath(match prim { - Primitive::String(a_string) => match filepath_from_string(a_string, &tag) { - Ok(n) => n, - Err(e) => { - return Err(e); - } - }, - Primitive::FilePath(a_filepath) => a_filepath.clone(), - _ => { - return Err(ShellError::unimplemented( - "'into path' for non-string primitives", - )) - } - }) - .into_value(&tag)), - UntaggedValue::Row(_) => Err(ShellError::labeled_error( - "specify column name to use, with 'into path COLUMN'", - "found table", - tag, - )), - _ => Err(ShellError::unimplemented( - "'into path' for unsupported type", - )), - } -} - -fn filepath_from_string(a_string: &str, _tag: &Tag) -> Result { - Ok(PathBuf::from(a_string)) -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/conversions/into/filesize.rs b/crates/nu-command/src/commands/conversions/into/filesize.rs deleted file mode 100644 index 7a3a648341..0000000000 --- a/crates/nu-command/src/commands/conversions/into/filesize.rs +++ /dev/null @@ -1,182 +0,0 @@ -use std::convert::TryInto; - -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ColumnPath, Primitive, Signature, SyntaxShape, UntaggedValue, Value}; -use num_bigint::ToBigInt; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "into filesize" - } - - fn signature(&self) -> Signature { - Signature::build("into filesize").rest( - "rest", - SyntaxShape::ColumnPath, - "column paths to convert to filesize (for table input)", - ) - } - - fn usage(&self) -> &str { - "Convert value to filesize" - } - - fn run(&self, args: CommandArgs) -> Result { - into_filesize(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Convert string to filesize in table", - example: "echo [[bytes]; ['5'] [3.2] [4] [2kb]] | into filesize bytes", - result: Some(vec![ - UntaggedValue::row(indexmap! { - "bytes".to_string() => UntaggedValue::filesize(5).into(), - }) - .into(), - UntaggedValue::row(indexmap! { - "bytes".to_string() => UntaggedValue::filesize(3).into(), - }) - .into(), - UntaggedValue::row(indexmap! { - "bytes".to_string() => UntaggedValue::filesize(4).into(), - }) - .into(), - UntaggedValue::row(indexmap! { - "bytes".to_string() => UntaggedValue::filesize(2000).into(), - }) - .into(), - ]), - }, - Example { - description: "Convert string to filesize", - example: "echo '2' | into filesize", - result: Some(vec![UntaggedValue::filesize(2).into()]), - }, - Example { - description: "Convert decimal to filesize", - example: "echo 8.3 | into filesize", - result: Some(vec![UntaggedValue::filesize(8).into()]), - }, - Example { - description: "Convert int to filesize", - example: "echo 5 | into filesize", - result: Some(vec![UntaggedValue::filesize(5).into()]), - }, - Example { - description: "Convert file size to filesize", - example: "echo 4KB | into filesize", - result: Some(vec![UntaggedValue::filesize(4000).into()]), - }, - ] - } -} - -fn into_filesize(args: CommandArgs) -> Result { - let column_paths: Vec = args.rest(0)?; - - Ok(args - .input - .map(move |v| { - if column_paths.is_empty() { - action(&v, v.tag()) - } else { - let mut ret = v; - for path in &column_paths { - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| action(old, old.tag())), - )?; - } - - Ok(ret) - } - }) - .into_input_stream()) -} - -pub fn action(input: &Value, tag: impl Into) -> Result { - let tag = tag.into(); - match &input.value { - UntaggedValue::Primitive(prim) => Ok(UntaggedValue::filesize(match prim { - Primitive::String(a_string) => match int_from_string(a_string.trim(), &tag) { - Ok(n) => n, - Err(e) => { - return Err(e); - } - }, - Primitive::Decimal(dec) => match dec.to_bigint() { - Some(n) => match n.to_u64() { - Some(i) => i, - None => { - return Err(ShellError::unimplemented( - "failed to convert decimal to filesize", - )); - } - }, - None => { - return Err(ShellError::unimplemented( - "failed to convert decimal to filesize", - )); - } - }, - Primitive::Int(n_ref) => (*n_ref).try_into().map_err(|_| { - ShellError::unimplemented("cannot convert negative integer to filesize") - })?, - Primitive::Filesize(a_filesize) => *a_filesize, - _ => { - return Err(ShellError::unimplemented( - "'into filesize' for non-numeric primitives", - )) - } - }) - .into_value(&tag)), - UntaggedValue::Row(_) => Err(ShellError::labeled_error( - "specify column name to use, with 'into filesize COLUMN'", - "found table", - tag, - )), - _ => Err(ShellError::unimplemented( - "'into filesize' for unsupported type", - )), - } -} - -fn int_from_string(a_string: &str, tag: &Tag) -> Result { - match a_string.parse::() { - Ok(n) => Ok(n), - Err(_) => match a_string.parse::() { - Ok(f) => match f.to_u64() { - Some(i) => Ok(i), - None => Err(ShellError::labeled_error( - "Could not convert string value to filesize", - "original value", - tag.clone(), - )), - }, - Err(_) => Err(ShellError::labeled_error( - "Could not convert string value to filesize", - "original value", - tag.clone(), - )), - }, - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/conversions/into/int.rs b/crates/nu-command/src/commands/conversions/into/int.rs deleted file mode 100644 index afa2a04fe3..0000000000 --- a/crates/nu-command/src/commands/conversions/into/int.rs +++ /dev/null @@ -1,198 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ColumnPath, Primitive, Signature, SyntaxShape, UntaggedValue, Value}; -use num_bigint::ToBigInt; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "into int" - } - - fn signature(&self) -> Signature { - Signature::build("into int").rest( - "rest", - SyntaxShape::ColumnPath, - "column paths to convert to int (for table input)", - ) - } - - fn usage(&self) -> &str { - "Convert value to integer" - } - - fn run(&self, args: CommandArgs) -> Result { - into_int(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Convert string to integer in table", - example: "echo [[num]; ['-5'] [4] [1.5]] | into int num", - result: Some(vec![ - UntaggedValue::row(indexmap! { - "num".to_string() => UntaggedValue::int(-5).into(), - }) - .into(), - UntaggedValue::row(indexmap! { - "num".to_string() => UntaggedValue::int(4).into(), - }) - .into(), - UntaggedValue::row(indexmap! { - "num".to_string() => UntaggedValue::int(1).into(), - }) - .into(), - ]), - }, - Example { - description: "Convert string to integer", - example: "echo '2' | into int", - result: Some(vec![UntaggedValue::int(2).into()]), - }, - Example { - description: "Convert decimal to integer", - example: "echo 5.9 | into int", - result: Some(vec![UntaggedValue::int(5).into()]), - }, - Example { - description: "Convert decimal string to integer", - example: "echo '5.9' | into int", - result: Some(vec![UntaggedValue::int(5).into()]), - }, - Example { - description: "Convert file size to integer", - example: "echo 4KB | into int", - result: Some(vec![UntaggedValue::int(4000).into()]), - }, - Example { - description: "Convert bool to integer", - example: "echo $false $true | into int", - result: Some(vec![ - UntaggedValue::int(0).into(), - UntaggedValue::int(1).into(), - ]), - }, - ] - } -} - -fn into_int(args: CommandArgs) -> Result { - let column_paths: Vec = args.rest(0)?; - - Ok(args - .input - .map(move |v| { - if column_paths.is_empty() { - action(&v, v.tag()) - } else { - let mut ret = v; - for path in &column_paths { - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| action(old, old.tag())), - )?; - } - - Ok(ret) - } - }) - .into_input_stream()) -} - -pub fn action(input: &Value, tag: impl Into) -> Result { - let tag = tag.into(); - match &input.value { - UntaggedValue::Primitive(prim) => Ok(UntaggedValue::int(match prim { - Primitive::String(a_string) => match int_from_string(a_string, &tag) { - Ok(n) => n, - Err(e) => { - return Err(e); - } - }, - Primitive::Decimal(dec) => match dec.to_bigint() { - Some(n) => match n.to_i64() { - Some(i) => i, - None => { - return Err(ShellError::unimplemented( - "failed to convert decimal to int", - )); - } - }, - None => { - return Err(ShellError::unimplemented( - "failed to convert decimal to int", - )); - } - }, - Primitive::Int(n_ref) => *n_ref, - Primitive::Boolean(a_bool) => match a_bool { - false => 0, - true => 1, - }, - Primitive::Filesize(a_filesize) => match a_filesize.to_bigint() { - Some(n) => match n.to_i64() { - Some(i) => i, - None => { - return Err(ShellError::unimplemented( - "failed to convert filesize to bigint", - )); - } - }, - None => { - return Err(ShellError::unimplemented( - "failed to convert filesize to bigint", - )); - } - }, - _ => { - return Err(ShellError::unimplemented( - "'into int' for non-numeric primitives", - )) - } - }) - .into_value(&tag)), - UntaggedValue::Row(_) => Err(ShellError::labeled_error( - "specify column name to use, with 'into int COLUMN'", - "found table", - tag, - )), - _ => Err(ShellError::unimplemented("'into int' for unsupported type")), - } -} - -fn int_from_string(a_string: &str, tag: &Tag) -> Result { - match a_string.parse::() { - Ok(n) => Ok(n), - Err(_) => match a_string.parse::() { - Ok(f) => match f.to_i64() { - Some(i) => Ok(i), - None => Err(ShellError::labeled_error( - "Could not convert string value to int", - "original value", - tag.clone(), - )), - }, - Err(_) => Err(ShellError::labeled_error( - "Could not convert string value to int", - "original value", - tag.clone(), - )), - }, - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/conversions/into/mod.rs b/crates/nu-command/src/commands/conversions/into/mod.rs deleted file mode 100644 index ae029d8871..0000000000 --- a/crates/nu-command/src/commands/conversions/into/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -mod binary; -mod column_path; -mod command; -mod filepath; -mod filesize; -mod int; -pub mod string; - -pub use self::filesize::SubCommand as IntoFilesize; -pub use binary::SubCommand as IntoBinary; -pub use column_path::SubCommand as IntoColumnPath; -pub use command::Command as Into; -pub use filepath::SubCommand as IntoFilepath; -pub use int::SubCommand as IntoInt; -pub use string::SubCommand as IntoString; diff --git a/crates/nu-command/src/commands/conversions/into/string.rs b/crates/nu-command/src/commands/conversions/into/string.rs deleted file mode 100644 index 70f19fe665..0000000000 --- a/crates/nu-command/src/commands/conversions/into/string.rs +++ /dev/null @@ -1,279 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ColumnPath, Primitive, Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tagged; -use num_bigint::{BigInt, BigUint, ToBigInt}; -// TODO num_format::SystemLocale once platform-specific dependencies are stable (see Cargo.toml) -use nu_data::base::shape::InlineShape; -use num_format::Locale; -use num_traits::{Pow, Signed}; -use std::iter; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "into string" - } - - fn signature(&self) -> Signature { - Signature::build("into string") - .rest( - "rest", - SyntaxShape::ColumnPath, - "column paths to convert to string (for table input)", - ) - .named( - "decimals", - SyntaxShape::Int, - "decimal digits to which to round", - Some('d'), - ) - } - - fn usage(&self) -> &str { - "Convert value to string" - } - - fn run(&self, args: CommandArgs) -> Result { - into_string(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "convert decimal to string and round to nearest integer", - example: "echo 1.7 | into string -d 0", - result: Some(vec![UntaggedValue::string("2").into_untagged_value()]), - }, - Example { - description: "convert decimal to string", - example: "echo 4.3 | into string", - result: Some(vec![UntaggedValue::string("4.3").into_untagged_value()]), - }, - Example { - description: "convert string to string", - example: "echo '1234' | into string", - result: Some(vec![UntaggedValue::string("1234").into_untagged_value()]), - }, - Example { - description: "convert boolean to string", - example: "echo $true | into string", - result: Some(vec![UntaggedValue::string("true").into_untagged_value()]), - }, - Example { - description: "convert date to string", - example: "date now | into string", - result: None, - }, - Example { - description: "convert filepath to string", - example: "ls Cargo.toml | get name | into string", - result: None, - }, - Example { - description: "convert filesize to string", - example: "ls Cargo.toml | get size | into string", - result: None, - }, - ] - } -} - -fn into_string(args: CommandArgs) -> Result { - let decimals: Option> = args.get_flag("decimals")?; - let column_paths: Vec = args.rest(0)?; - - let digits = decimals.as_ref().map(|tagged| tagged.item); - let group_digits = false; - - Ok(args - .input - .map(move |v| { - if column_paths.is_empty() { - action(&v, v.tag(), digits, group_digits) - } else { - let mut ret = v; - for path in &column_paths { - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| action(old, old.tag(), digits, group_digits)), - )?; - } - - Ok(ret) - } - }) - .into_input_stream()) -} - -pub fn action( - input: &Value, - tag: impl Into, - digits: Option, - group_digits: bool, -) -> Result { - match &input.value { - UntaggedValue::Primitive(prim) => Ok(UntaggedValue::string(match prim { - Primitive::Int(int) => { - if group_digits { - format_int(*int) // int.to_formatted_string(*locale) - } else { - int.to_string() - } - } - Primitive::BigInt(int) => { - if group_digits { - format_bigint(int) // int.to_formatted_string(*locale) - } else { - int.to_string() - } - } - Primitive::Decimal(dec) => format_decimal(dec.clone(), digits, group_digits), - Primitive::String(a_string) => a_string.to_string(), - Primitive::Boolean(a_bool) => a_bool.to_string(), - Primitive::Date(a_date) => a_date.format("%c").to_string(), - Primitive::FilePath(a_filepath) => a_filepath.as_path().display().to_string(), - Primitive::Filesize(a_filesize) => { - let byte_string = InlineShape::format_bytes(*a_filesize, None); - byte_string.1 - } - Primitive::Nothing => "nothing".to_string(), - _ => { - return Err(ShellError::unimplemented(&format!( - "into string for primitive: {:?}", - prim - ))) - } - }) - .into_value(tag)), - UntaggedValue::Row(_) => Err(ShellError::labeled_error( - "specify column to use 'into string'", - "found table", - input.tag.clone(), - )), - UntaggedValue::Table(_) => Err(ShellError::unimplemented("into string for table")), - _ => Err(ShellError::unimplemented("into string for non-primitive")), - } -} - -fn format_int(int: i64) -> String { - int.to_string() - - // TODO once platform-specific dependencies are stable (see Cargo.toml) - // #[cfg(windows)] - // { - // int.to_formatted_string(&Locale::en) - // } - // #[cfg(not(windows))] - // { - // match SystemLocale::default() { - // Ok(locale) => int.to_formatted_string(&locale), - // Err(_) => int.to_formatted_string(&Locale::en), - // } - // } -} - -fn format_bigint(int: &BigInt) -> String { - int.to_string() - - // TODO once platform-specific dependencies are stable (see Cargo.toml) - // #[cfg(windows)] - // { - // int.to_formatted_string(&Locale::en) - // } - // #[cfg(not(windows))] - // { - // match SystemLocale::default() { - // Ok(locale) => int.to_formatted_string(&locale), - // Err(_) => int.to_formatted_string(&Locale::en), - // } - // } -} - -fn format_decimal(mut decimal: BigDecimal, digits: Option, group_digits: bool) -> String { - if let Some(n) = digits { - decimal = round_decimal(&decimal, n) - } - - if decimal.is_integer() && (digits.is_none() || digits == Some(0)) { - let int = decimal - .to_bigint() - .expect("integer BigDecimal should convert to BigInt"); - return if group_digits { - int.to_string() - } else { - format_bigint(&int) - }; - } - - let (int, exp) = decimal.as_bigint_and_exponent(); - let factor = BigInt::from(10).pow(BigUint::from(exp as u64)); // exp > 0 for non-int decimal - let int_part = &int / &factor; - let dec_part = (&int % &factor) - .abs() - .to_biguint() - .expect("BigInt::abs should always produce positive signed BigInt and thus BigUInt") - .to_str_radix(10); - - let dec_str = if let Some(n) = digits { - dec_part - .chars() - .chain(iter::repeat('0')) - .take(n as usize) - .collect() - } else { - String::from(dec_part.trim_end_matches('0')) - }; - - let format_default_loc = |int_part: BigInt| { - let loc = Locale::en; - //TODO: when num_format is available for recent bigint, replace this with the locale-based format - let (int_str, sep) = (int_part.to_string(), String::from(loc.decimal())); - - format!("{}{}{}", int_str, sep, dec_str) - }; - - format_default_loc(int_part) - - // TODO once platform-specific dependencies are stable (see Cargo.toml) - // #[cfg(windows)] - // { - // format_default_loc(int_part) - // } - // #[cfg(not(windows))] - // { - // match SystemLocale::default() { - // Ok(sys_loc) => { - // let int_str = int_part.to_formatted_string(&sys_loc); - // let sep = String::from(sys_loc.decimal()); - // format!("{}{}{}", int_str, sep, dec_str) - // } - // Err(_) => format_default_loc(int_part), - // } - // } -} - -fn round_decimal(decimal: &BigDecimal, mut digits: u64) -> BigDecimal { - let mut mag = decimal.clone(); - while mag >= BigDecimal::from(1) { - mag = mag / 10; - digits += 1; - } - - decimal.with_prec(digits) -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/conversions/mod.rs b/crates/nu-command/src/commands/conversions/mod.rs deleted file mode 100644 index 70608f782c..0000000000 --- a/crates/nu-command/src/commands/conversions/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub(crate) mod into; - -pub use into::*; diff --git a/crates/nu-command/src/commands/core_commands/alias.rs b/crates/nu-command/src/commands/core_commands/alias.rs deleted file mode 100644 index 4ce2a426da..0000000000 --- a/crates/nu-command/src/commands/core_commands/alias.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; - -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue}; - -pub struct Alias; - -impl WholeStreamCommand for Alias { - fn name(&self) -> &str { - "alias" - } - - fn signature(&self) -> Signature { - Signature::build("alias") - .required("name", SyntaxShape::String, "the name of the alias") - .required("equals", SyntaxShape::String, "the equals sign") - .rest("rest", SyntaxShape::Any, "the expansion for the alias") - } - - fn usage(&self) -> &str { - "Alias a command to an expansion." - } - - fn run(&self, args: CommandArgs) -> Result { - alias(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Alias ll to ls -l", - example: "alias ll = ls -l", - result: None, - }] - } -} - -pub fn alias(args: CommandArgs) -> Result { - // TODO: is there a better way of checking whether no arguments were passed? - if args.nth(0).is_none() { - let aliases = UntaggedValue::string( - &args - .scope() - .get_aliases() - .iter() - .map(|val| format!("{} = '{}'", val.0, val.1.iter().map(|x| &x.item).join(" "))) - .join("\n"), - ); - return Ok(OutputStream::one(aliases)); - } - Ok(OutputStream::empty()) -} diff --git a/crates/nu-command/src/commands/core_commands/debug.rs b/crates/nu-command/src/commands/core_commands/debug.rs deleted file mode 100644 index d92380e0da..0000000000 --- a/crates/nu-command/src/commands/core_commands/debug.rs +++ /dev/null @@ -1,54 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ReturnSuccess, Signature, UntaggedValue}; - -pub struct Debug; - -impl WholeStreamCommand for Debug { - fn name(&self) -> &str { - "debug" - } - - fn signature(&self) -> Signature { - Signature::build("debug").switch("raw", "Prints the raw value representation.", Some('r')) - } - - fn usage(&self) -> &str { - "Print the Rust debug representation of the values." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - debug_value(args) - } -} - -fn debug_value(args: CommandArgs) -> Result { - let raw = args.has_flag("raw"); - let input = args.input; - - Ok(input - .map(move |v| { - if raw { - ReturnSuccess::value( - UntaggedValue::string(format!("{:#?}", v)).into_untagged_value(), - ) - } else { - ReturnSuccess::debug_value(v) - } - }) - .into_action_stream()) -} - -#[cfg(test)] -mod tests { - use super::Debug; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Debug {}) - } -} diff --git a/crates/nu-command/src/commands/core_commands/def.rs b/crates/nu-command/src/commands/core_commands/def.rs deleted file mode 100644 index f247bd7a1e..0000000000 --- a/crates/nu-command/src/commands/core_commands/def.rs +++ /dev/null @@ -1,47 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; - -use nu_errors::ShellError; -use nu_protocol::{hir::CapturedBlock, Signature, SyntaxShape, Value}; -use nu_source::Tagged; - -pub struct Def; - -#[derive(Deserialize)] -pub struct DefArgs { - pub name: Tagged, - pub args: Tagged>, - pub block: CapturedBlock, -} - -impl WholeStreamCommand for Def { - fn name(&self) -> &str { - "def" - } - - fn signature(&self) -> Signature { - Signature::build("def") - .required("name", SyntaxShape::String, "the name of the command") - .required( - "params", - SyntaxShape::Table, - "the parameters of the command", - ) - .required("block", SyntaxShape::Block, "the body of the command") - } - - fn usage(&self) -> &str { - "Create a command and set it to a definition." - } - - fn run_with_actions(&self, _args: CommandArgs) -> Result { - // Currently, we don't do anything here because we should have already - // installed the definition as we entered the scope - // We just create a command so that we can get proper coloring - Ok(ActionStream::empty()) - } - - fn examples(&self) -> Vec { - vec![] - } -} diff --git a/crates/nu-command/src/commands/core_commands/describe.rs b/crates/nu-command/src/commands/core_commands/describe.rs deleted file mode 100644 index fa48a9ee94..0000000000 --- a/crates/nu-command/src/commands/core_commands/describe.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; - -use nu_errors::ShellError; -use nu_protocol::{ReturnSuccess, Signature, UntaggedValue}; - -pub struct Describe; - -#[derive(Deserialize)] -pub struct DescribeArgs {} - -impl WholeStreamCommand for Describe { - fn name(&self) -> &str { - "describe" - } - - fn signature(&self) -> Signature { - Signature::build("describe") - } - - fn usage(&self) -> &str { - "Describes the objects in the stream." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - describe(args) - } -} - -pub fn describe(args: CommandArgs) -> Result { - Ok(args - .input - .map(|row| { - let name = value::plain_type(&row, 100); - ReturnSuccess::value( - UntaggedValue::string(name).into_value(Tag::unknown_anchor(row.tag.span)), - ) - }) - .into_action_stream()) -} - -#[cfg(test)] -mod tests { - use super::Describe; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Describe {}) - } -} diff --git a/crates/nu-command/src/commands/core_commands/do_.rs b/crates/nu-command/src/commands/core_commands/do_.rs deleted file mode 100644 index 413a9ab377..0000000000 --- a/crates/nu-command/src/commands/core_commands/do_.rs +++ /dev/null @@ -1,141 +0,0 @@ -use crate::prelude::*; -use nu_engine::run_block; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - hir::CapturedBlock, hir::ExternalRedirection, Signature, SyntaxShape, UntaggedValue, Value, -}; - -pub struct Do; - -struct DoArgs { - block: CapturedBlock, - ignore_errors: bool, - rest: Vec, -} - -impl WholeStreamCommand for Do { - fn name(&self) -> &str { - "do" - } - - fn signature(&self) -> Signature { - Signature::build("do") - .required("block", SyntaxShape::Block, "the block to run ") - .switch( - "ignore-errors", - "ignore errors as the block runs", - Some('i'), - ) - .rest("rest", SyntaxShape::Any, "the parameter(s) for the block") - } - - fn usage(&self) -> &str { - "Runs a block, optionally ignoring errors." - } - - fn run(&self, args: CommandArgs) -> Result { - do_(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Run the block", - example: r#"do { echo hello }"#, - result: Some(vec![Value::from("hello")]), - }, - Example { - description: "Run the block and ignore errors", - example: r#"do -i { thisisnotarealcommand }"#, - result: Some(vec![]), - }, - Example { - description: "Run the block with a parameter", - example: r#"do { |x| $x + 100 } 55"#, - result: Some(vec![UntaggedValue::int(155).into()]), - }, - ] - } -} - -fn do_(args: CommandArgs) -> Result { - let external_redirection = args.call_info.args.external_redirection; - - let context = args.context().clone(); - let do_args = DoArgs { - block: args.req(0)?, - ignore_errors: args.has_flag("ignore-errors"), - rest: args.rest(1)?, - }; - - let block_redirection = match external_redirection { - ExternalRedirection::None => { - if do_args.ignore_errors { - ExternalRedirection::Stderr - } else { - ExternalRedirection::None - } - } - ExternalRedirection::Stdout => { - if do_args.ignore_errors { - ExternalRedirection::StdoutAndStderr - } else { - ExternalRedirection::Stdout - } - } - x => x, - }; - - context.scope.enter_scope(); - - context.scope.add_vars(&do_args.block.captured.entries); - - for (param, value) in do_args - .block - .block - .params - .positional - .iter() - .zip(do_args.rest) - { - context.scope.add_var(param.0.name(), value.clone()); - } - - let result = run_block( - &do_args.block.block, - &context, - args.input, - block_redirection, - ); - context.scope.exit_scope(); - - if do_args.ignore_errors { - // To properly ignore errors we need to redirect stderr, consume it, and remove - // any errors we see in the process. - - match result { - Ok(mut stream) => { - let output = stream.drain_vec(); - context.clear_errors(); - Ok(output.into_iter().into_output_stream()) - } - Err(_) => Ok(OutputStream::empty()), - } - } else { - result.map(|x| x.into_output_stream()) - } -} - -#[cfg(test)] -mod tests { - use super::Do; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Do {}) - } -} diff --git a/crates/nu-command/src/commands/core_commands/echo.rs b/crates/nu-command/src/commands/core_commands/echo.rs deleted file mode 100644 index cddd5081ad..0000000000 --- a/crates/nu-command/src/commands/core_commands/echo.rs +++ /dev/null @@ -1,207 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::hir::Operator; -use nu_protocol::{Primitive, Range, RangeInclusion, Signature, SyntaxShape, UntaggedValue, Value}; - -pub struct Echo; - -impl WholeStreamCommand for Echo { - fn name(&self) -> &str { - "echo" - } - - fn signature(&self) -> Signature { - Signature::build("echo").rest("rest", SyntaxShape::Any, "the values to echo") - } - - fn usage(&self) -> &str { - "Echo the arguments back to the user." - } - - fn run(&self, args: CommandArgs) -> Result { - echo(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Put a hello message in the pipeline", - example: "echo 'hello'", - result: Some(vec![Value::from("hello")]), - }, - Example { - description: "Print the value of the special '$nu' variable", - example: "echo $nu", - result: None, - }, - ] - } -} - -pub fn expand_value_to_stream(v: Value) -> InputStream { - match v { - Value { - value: UntaggedValue::Table(table), - .. - } => InputStream::from_stream(table.into_iter()), - Value { - value: UntaggedValue::Primitive(Primitive::Range(range)), - tag, - } => InputStream::from_stream(RangeIterator::new(*range, tag)), - x => InputStream::one(x), - } -} - -fn echo(args: CommandArgs) -> Result { - let rest: Vec = args.rest(0)?; - - let stream = rest.into_iter().map(|i| match i.as_string() { - Ok(s) => InputStream::one(UntaggedValue::string(s).into_value(i.tag)), - _ => expand_value_to_stream(i), - }); - - Ok(InputStream::from_stream(stream.flatten())) -} - -struct RangeIterator { - curr: UntaggedValue, - end: UntaggedValue, - tag: Tag, - is_end_inclusive: bool, - moves_up: bool, - one: UntaggedValue, - negative_one: UntaggedValue, - done: bool, -} - -impl RangeIterator { - pub fn new(range: Range, tag: Tag) -> RangeIterator { - let start = match range.from.0.item { - Primitive::Nothing => Primitive::Int(0.into()), - x => x, - }; - - let end = match range.to.0.item { - Primitive::Nothing => Primitive::Int(i64::MAX), - x => x, - }; - - RangeIterator { - moves_up: start <= end, - curr: UntaggedValue::Primitive(start), - end: UntaggedValue::Primitive(end), - tag, - is_end_inclusive: matches!(range.to.1, RangeInclusion::Inclusive), - one: UntaggedValue::int(1), - negative_one: UntaggedValue::int(-1), - done: false, - } - } -} - -impl Iterator for RangeIterator { - type Item = Value; - fn next(&mut self) -> Option { - use std::cmp::Ordering; - if self.done { - return None; - } - - let ordering = if self.end == UntaggedValue::Primitive(Primitive::Nothing) { - Ordering::Less - } else { - match (&self.curr, &self.end) { - ( - UntaggedValue::Primitive(Primitive::Int(x)), - UntaggedValue::Primitive(Primitive::Int(y)), - ) => x.cmp(y), - ( - UntaggedValue::Primitive(Primitive::Decimal(x)), - UntaggedValue::Primitive(Primitive::Decimal(y)), - ) => x.cmp(y), - ( - UntaggedValue::Primitive(Primitive::Decimal(x)), - UntaggedValue::Primitive(Primitive::Int(y)), - ) => x.cmp(&(BigDecimal::from(*y))), - ( - UntaggedValue::Primitive(Primitive::Int(x)), - UntaggedValue::Primitive(Primitive::Decimal(y)), - ) => (BigDecimal::from(*x)).cmp(y), - _ => { - self.done = true; - return Some( - UntaggedValue::Error(ShellError::labeled_error( - "Cannot create range", - "unsupported range", - self.tag.span, - )) - .into_untagged_value(), - ); - } - } - }; - - if self.moves_up - && (ordering == Ordering::Less || self.is_end_inclusive && ordering == Ordering::Equal) - { - let next_value = nu_data::value::compute_values(Operator::Plus, &self.curr, &self.one); - - let mut next = match next_value { - Ok(result) => result, - - Err((left_type, right_type)) => { - self.done = true; - return Some( - UntaggedValue::Error(ShellError::coerce_error( - left_type.spanned(self.tag.span), - right_type.spanned(self.tag.span), - )) - .into_untagged_value(), - ); - } - }; - std::mem::swap(&mut self.curr, &mut next); - - Some(next.into_value(self.tag.clone())) - } else if !self.moves_up - && (ordering == Ordering::Greater - || self.is_end_inclusive && ordering == Ordering::Equal) - { - let next_value = - nu_data::value::compute_values(Operator::Plus, &self.curr, &self.negative_one); - - let mut next = match next_value { - Ok(result) => result, - Err((left_type, right_type)) => { - self.done = true; - return Some( - UntaggedValue::Error(ShellError::coerce_error( - left_type.spanned(self.tag.span), - right_type.spanned(self.tag.span), - )) - .into_untagged_value(), - ); - } - }; - std::mem::swap(&mut self.curr, &mut next); - - Some(next.into_value(self.tag.clone())) - } else { - None - } - } -} - -#[cfg(test)] -mod tests { - use super::Echo; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Echo {}) - } -} diff --git a/crates/nu-command/src/commands/core_commands/error/make.rs b/crates/nu-command/src/commands/core_commands/error/make.rs deleted file mode 100644 index 0d3ca69fe6..0000000000 --- a/crates/nu-command/src/commands/core_commands/error/make.rs +++ /dev/null @@ -1,109 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Primitive, Signature, UntaggedValue, Value}; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "error make" - } - - fn signature(&self) -> Signature { - Signature::build("error make") - } - - fn usage(&self) -> &str { - "Create an error." - } - - fn run(&self, args: CommandArgs) -> Result { - let input = args.input; - - Ok(input - .map(|value| { - make_error(&value) - .map(|err| UntaggedValue::Error(err).into_value(value.tag())) - .unwrap_or_else(|| { - UntaggedValue::Error(ShellError::untagged_runtime_error( - "Creating error value not supported.", - )) - .into_value(value.tag()) - }) - }) - .into_output_stream()) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Creates a labeled error", - example: r#"[ - [ msg, labels, span]; - ["The message", "Helpful message here", ([[start, end]; [0, 141]])] -] | error make"#, - result: None, - }] - } -} - -fn make_error(value: &Value) -> Option { - if let Value { - value: UntaggedValue::Row(dict), - .. - } = value - { - let msg = dict.get_data_by_key("msg".spanned_unknown()); - - let labels = - dict.get_data_by_key("labels".spanned_unknown()) - .and_then(|table| match &table.value { - UntaggedValue::Table(_) => table - .table_entries() - .map(|value| value.as_string().ok()) - .collect(), - UntaggedValue::Primitive(Primitive::String(label)) => { - Some(vec![label.to_string()]) - } - _ => None, - }); - - let _anchor = dict.get_data_by_key("tag".spanned_unknown()); - let span = dict.get_data_by_key("span".spanned_unknown()); - - if msg.is_none() || labels.is_none() || span.is_none() { - return None; - } - - let msg = msg.and_then(|msg| msg.as_string().ok()); - - if let Some(labels) = labels { - if labels.is_empty() { - return None; - } - - return Some(ShellError::labeled_error( - msg.expect("Message will always be present."), - &labels[0], - span.map(|data| match data { - Value { - value: UntaggedValue::Row(vals), - .. - } => match (vals.entries.get("start"), vals.entries.get("end")) { - (Some(start), Some(end)) => { - let start = start.as_usize().ok().unwrap_or(0); - let end = end.as_usize().ok().unwrap_or(0); - - Span::new(start, end) - } - (_, _) => Span::unknown(), - }, - _ => Span::unknown(), - }) - .unwrap_or_else(Span::unknown), - )); - } - } - - None -} diff --git a/crates/nu-command/src/commands/core_commands/error/mod.rs b/crates/nu-command/src/commands/core_commands/error/mod.rs deleted file mode 100644 index 4a2ff564fd..0000000000 --- a/crates/nu-command/src/commands/core_commands/error/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod make; - -pub use make::SubCommand as ErrorMake; diff --git a/crates/nu-command/src/commands/core_commands/find.rs b/crates/nu-command/src/commands/core_commands/find.rs deleted file mode 100644 index 559f9fa3bd..0000000000 --- a/crates/nu-command/src/commands/core_commands/find.rs +++ /dev/null @@ -1,110 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Dictionary, Signature, SyntaxShape, UntaggedValue, Value}; - -pub struct Find; - -impl WholeStreamCommand for Find { - fn name(&self) -> &str { - "find" - } - - fn signature(&self) -> Signature { - Signature::build("find").rest("rest", SyntaxShape::String, "search term") - } - - fn usage(&self) -> &str { - "Find text in the output of a previous command" - } - - fn run(&self, args: CommandArgs) -> Result { - find(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Search pipeline output for multiple terms", - example: r#"ls | find toml md sh"#, - result: None, - }, - Example { - description: "Search strings for term(s)", - example: r#"echo Cargo.toml | find toml"#, - result: Some(vec![Value::from("Cargo.toml")]), - }, - Example { - description: "Search a number list for term(s)", - example: r#"[1 2 3 4 5] | find 5"#, - result: Some(vec![UntaggedValue::int(5).into()]), - }, - Example { - description: "Search string list for term(s)", - example: r#"[moe larry curly] | find l"#, - result: Some(vec![Value::from("larry"), Value::from("curly")]), - }, - ] - } -} - -fn row_contains(row: &Dictionary, search_terms: Vec) -> bool { - for term in search_terms { - for (k, v) in &row.entries { - let key = k.to_string().trim().to_lowercase(); - let value = v.convert_to_string().trim().to_lowercase(); - if key.contains(&term) || value.contains(&term) { - return true; - } - } - } - - false -} - -fn find(args: CommandArgs) -> Result { - let rest: Vec = args.rest(0)?; - - Ok(args - .input - .filter(move |row| match &row.value { - UntaggedValue::Row(row) => { - let sterms: Vec = rest - .iter() - .map(|t| t.convert_to_string().trim().to_lowercase()) - .collect(); - row_contains(row, sterms) - } - UntaggedValue::Primitive(_p) => { - // eprint!("prim {}", p.type_name()); - let sterms: Vec = rest - .iter() - .map(|t| t.convert_to_string().trim().to_lowercase()) - .collect(); - - let prim_string = &row.convert_to_string().trim().to_lowercase(); - for term in sterms { - if prim_string.contains(&term) { - return true; - } - } - - false - } - _ => false, - }) - .into_output_stream()) -} - -#[cfg(test)] -mod tests { - use super::Find; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Find {}) - } -} diff --git a/crates/nu-command/src/commands/core_commands/help.rs b/crates/nu-command/src/commands/core_commands/help.rs deleted file mode 100644 index 08a74bffab..0000000000 --- a/crates/nu-command/src/commands/core_commands/help.rs +++ /dev/null @@ -1,353 +0,0 @@ -use crate::prelude::*; -use crate::TaggedListBuilder; -use nu_engine::{documentation::generate_docs, Command, WholeStreamCommand}; -use nu_errors::ShellError; -use nu_protocol::{ - Dictionary, NamedType, PositionalType, ReturnSuccess, Signature, SyntaxShape, - TaggedDictBuilder, UntaggedValue, Value, -}; -use nu_source::{SpannedItem, Tag, Tagged}; -use nu_value_ext::ValueExt; - -pub struct Help; - -impl WholeStreamCommand for Help { - fn name(&self) -> &str { - "help" - } - - fn signature(&self) -> Signature { - Signature::build("help") - .rest( - "rest", - SyntaxShape::String, - "the name of command to get help on", - ) - .named( - "find", - SyntaxShape::String, - "string to find in command usage", - Some('f'), - ) - } - - fn usage(&self) -> &str { - "Display help information about commands." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - help(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "show all commands and sub-commands", - example: "help commands", - result: None, - }, - Example { - description: "generate documentation", - example: "help generate_docs", - result: None, - }, - Example { - description: "show help for single command", - example: "help match", - result: None, - }, - Example { - description: "show help for single sub-command", - example: "help str lpad", - result: None, - }, - Example { - description: "search for string in command usage", - example: "help --find char", - result: None, - }, - ] - } -} - -fn help(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - let scope = args.scope().clone(); - let find: Option> = args.get_flag("find")?; - let rest: Vec> = args.rest(0)?; - - if let Some(f) = find { - let search_string = f.item; - let full_commands = scope.get_commands_info(); - let mut found_cmds_vec = Vec::new(); - - for (key, cmd) in full_commands { - let mut indexmap = IndexMap::new(); - - let c = cmd.usage().to_string(); - let e = cmd.extra_usage().to_string(); - if key.to_lowercase().contains(&search_string) - || c.to_lowercase().contains(&search_string) - || e.to_lowercase().contains(&search_string) - { - indexmap.insert( - "name".to_string(), - UntaggedValue::string(key).into_value(&name), - ); - - indexmap.insert( - "usage".to_string(), - UntaggedValue::string(cmd.usage().to_string()).into_value(&name), - ); - - indexmap.insert( - "extra_usage".to_string(), - UntaggedValue::string(cmd.extra_usage().to_string()).into_value(&name), - ); - - found_cmds_vec - .push(UntaggedValue::Row(Dictionary::from(indexmap)).into_value(&name)); - } - } - - return Ok(found_cmds_vec.into_iter().into_action_stream()); - } - - if !rest.is_empty() { - if rest[0].item == "commands" { - let mut sorted_names = scope.get_command_names(); - sorted_names.sort(); - - let (mut subcommand_names, command_names) = sorted_names - .into_iter() - // private only commands shouldn't be displayed - .filter(|cmd_name| { - scope - .get_command(cmd_name) - .filter(|command| !command.is_private()) - .is_some() - }) - .partition::, _>(|cmd_name| cmd_name.contains(' ')); - - fn process_name( - dict: &mut TaggedDictBuilder, - cmd_name: &str, - scope: Scope, - rest: Vec>, - name: Tag, - ) -> Result<(), ShellError> { - let document_tag = rest[0].tag.clone(); - let value = command_dict( - scope.get_command(cmd_name).ok_or_else(|| { - ShellError::labeled_error( - format!("Could not load {}", cmd_name), - "could not load command", - document_tag, - ) - })?, - name, - ); - - dict.insert_untagged("name", cmd_name); - dict.insert_untagged( - "description", - value - .get_data_by_key("usage".spanned_unknown()) - .ok_or_else(|| { - ShellError::labeled_error( - "Expected a usage key", - "expected a 'usage' key", - &value.tag, - ) - })? - .as_string()?, - ); - - Ok(()) - } - - fn make_subcommands_table( - subcommand_names: &mut Vec, - cmd_name: &str, - scope: Scope, - rest: Vec>, - name: Tag, - ) -> Result { - let (matching, not_matching) = - subcommand_names.drain(..).partition(|subcommand_name| { - subcommand_name.starts_with(&format!("{} ", cmd_name)) - }); - *subcommand_names = not_matching; - Ok(if !matching.is_empty() { - UntaggedValue::table( - &(matching - .into_iter() - .map(|cmd_name: String| -> Result<_, ShellError> { - let mut short_desc = TaggedDictBuilder::new(name.clone()); - process_name( - &mut short_desc, - &cmd_name, - scope.clone(), - rest.clone(), - name.clone(), - )?; - Ok(short_desc.into_value()) - }) - .collect::, _>>()?[..]), - ) - .into_value(name) - } else { - UntaggedValue::nothing().into_value(name) - }) - } - - let iterator = - command_names - .into_iter() - .map(move |cmd_name| -> Result<_, ShellError> { - let mut short_desc = TaggedDictBuilder::new(name.clone()); - process_name( - &mut short_desc, - &cmd_name, - scope.clone(), - rest.clone(), - name.clone(), - )?; - short_desc.insert_value( - "subcommands", - make_subcommands_table( - &mut subcommand_names, - &cmd_name, - scope.clone(), - rest.clone(), - name.clone(), - )?, - ); - ReturnSuccess::value(short_desc.into_value()) - }); - - Ok(iterator.into_action_stream()) - } else if rest[0].item == "generate_docs" { - Ok(ActionStream::one(ReturnSuccess::value(generate_docs( - &scope, - )))) - } else if rest.len() == 2 { - // Check for a subcommand - let command_name = format!("{} {}", rest[0].item, rest[1].item); - if let Some(command) = scope.get_command(&command_name) { - Ok(ActionStream::one(ReturnSuccess::value( - UntaggedValue::string(get_full_help(command.stream_command(), &scope)) - .into_value(Tag::unknown()), - ))) - } else { - Ok(ActionStream::empty()) - } - } else if let Some(command) = scope.get_command(&rest[0].item) { - Ok(ActionStream::one(ReturnSuccess::value( - UntaggedValue::string(get_full_help(command.stream_command(), &scope)) - .into_value(Tag::unknown()), - ))) - } else { - Err(ShellError::labeled_error( - "Can't find command (use 'help commands' for full list)", - "can't find command", - rest[0].tag.span, - )) - } - } else { - let msg = r#"Welcome to Nushell. - -Here are some tips to help you get started. - * help commands - list all available commands - * help - display help about a particular command - -Nushell works on the idea of a "pipeline". Pipelines are commands connected with the '|' character. -Each stage in the pipeline works together to load, parse, and display information to you. - -[Examples] - -List the files in the current directory, sorted by size: - ls | sort-by size - -Get information about the current system: - sys | get host - -Get the processes on your system actively using CPU: - ps | where cpu > 0 - -You can also learn more at https://www.nushell.sh/book/"#; - - Ok(ActionStream::one(ReturnSuccess::value( - UntaggedValue::string(msg).into_value(Tag::unknown()), - ))) - } -} - -fn for_spec(name: &str, ty: &str, required: bool, tag: impl Into) -> Value { - let tag = tag.into(); - - let mut spec = TaggedDictBuilder::new(tag); - - spec.insert_untagged("name", UntaggedValue::string(name)); - spec.insert_untagged("type", UntaggedValue::string(ty)); - spec.insert_untagged( - "required", - UntaggedValue::string(if required { "yes" } else { "no" }), - ); - - spec.into_value() -} - -pub fn signature_dict(signature: Signature, tag: impl Into) -> Value { - let tag = tag.into(); - let mut sig = TaggedListBuilder::new(&tag); - - for arg in &signature.positional { - let is_required = matches!(arg.0, PositionalType::Mandatory(_, _)); - - sig.push_value(for_spec(arg.0.name(), "argument", is_required, &tag)); - } - - if signature.rest_positional.is_some() { - let is_required = false; - sig.push_value(for_spec("rest", "argument", is_required, &tag)); - } - - for (name, ty) in &signature.named { - match ty.0 { - NamedType::Mandatory(_, _) => sig.push_value(for_spec(name, "flag", true, &tag)), - NamedType::Optional(_, _) => sig.push_value(for_spec(name, "flag", false, &tag)), - NamedType::Switch(_) => sig.push_value(for_spec(name, "switch", false, &tag)), - } - } - - sig.into_value() -} - -fn command_dict(command: Command, tag: impl Into) -> Value { - let tag = tag.into(); - - let mut cmd_dict = TaggedDictBuilder::new(&tag); - - cmd_dict.insert_untagged("name", UntaggedValue::string(command.name())); - - cmd_dict.insert_untagged("type", UntaggedValue::string("Command")); - - cmd_dict.insert_value("signature", signature_dict(command.signature(), tag)); - cmd_dict.insert_untagged("usage", UntaggedValue::string(command.usage())); - - cmd_dict.into_value() -} - -#[cfg(test)] -mod tests { - use super::Help; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Help {}) - } -} diff --git a/crates/nu-command/src/commands/core_commands/history.rs b/crates/nu-command/src/commands/core_commands/history.rs deleted file mode 100644 index 76447b2469..0000000000 --- a/crates/nu-command/src/commands/core_commands/history.rs +++ /dev/null @@ -1,74 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ReturnSuccess, Signature, UntaggedValue}; -use std::fs::File; -use std::io::{BufRead, BufReader}; - -pub struct History; - -impl WholeStreamCommand for History { - fn name(&self) -> &str { - "history" - } - - fn signature(&self) -> Signature { - Signature::build("history").switch("clear", "Clears out the history entries", Some('c')) - } - - fn usage(&self) -> &str { - "Display command history." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - history(args) - } -} - -fn history(args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let ctx = &args.context; - - let clear = args.has_flag("clear"); - - let path = if let Some(global_cfg) = &ctx.configs().lock().global_config { - nu_data::config::path::history_path_or_default(global_cfg) - } else { - nu_data::config::path::default_history_path() - }; - - if clear { - // This is a NOOP, the logic to clear is handled in cli.rs - Ok(ActionStream::empty()) - } else if let Ok(file) = File::open(path) { - let reader = BufReader::new(file); - // Skips the first line, which is a Rustyline internal - let output = reader.lines().skip(1).filter_map(move |line| match line { - Ok(line) => Some(ReturnSuccess::value( - UntaggedValue::string(line).into_value(tag.clone()), - )), - Err(_) => None, - }); - - Ok(output.into_action_stream()) - } else { - Err(ShellError::labeled_error( - "Could not open history", - "history file could not be opened", - tag, - )) - } -} - -#[cfg(test)] -mod tests { - use super::History; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(History {}) - } -} diff --git a/crates/nu-command/src/commands/core_commands/if_.rs b/crates/nu-command/src/commands/core_commands/if_.rs deleted file mode 100644 index 85451ec19f..0000000000 --- a/crates/nu-command/src/commands/core_commands/if_.rs +++ /dev/null @@ -1,154 +0,0 @@ -use crate::prelude::*; -use nu_engine::evaluate_baseline_expr; -use nu_engine::run_block; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - hir::CapturedBlock, hir::ClassifiedCommand, Signature, SyntaxShape, UntaggedValue, -}; -use nu_stream::OutputStream; - -pub struct If; - -impl WholeStreamCommand for If { - fn name(&self) -> &str { - "if" - } - - fn signature(&self) -> Signature { - Signature::build("if") - .required( - "condition", - SyntaxShape::MathExpression, - "the condition that must match", - ) - .required( - "then_case", - SyntaxShape::Block, - "block to run if condition is true", - ) - .required( - "else_case", - SyntaxShape::Block, - "block to run if condition is false", - ) - } - - fn usage(&self) -> &str { - "Run blocks if a condition is true or false." - } - - fn run(&self, args: CommandArgs) -> Result { - if_command(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Run a block if a condition is true", - example: "let x = 10; if $x > 5 { echo 'greater than 5' } { echo 'less than or equal to 5' }", - result: Some(vec![UntaggedValue::string("greater than 5").into()]), - }, - Example { - description: "Run a block if a condition is false", - example: "let x = 1; if $x > 5 { echo 'greater than 5' } { echo 'less than or equal to 5' }", - result: Some(vec![UntaggedValue::string("less than or equal to 5").into()]), - }, - ] - } -} -fn if_command(args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let external_redirection = args.call_info.args.external_redirection; - let context = Arc::new(args.context.clone()); - - let condition: CapturedBlock = args.req(0)?; - let then_case: CapturedBlock = args.req(1)?; - let else_case: CapturedBlock = args.req(2)?; - let input = args.input; - - let cond = { - if condition.block.block.len() != 1 { - return Err(ShellError::labeled_error( - "Expected a condition", - "expected a condition", - tag, - )); - } - match condition.block.block[0].pipelines.get(0) { - Some(item) => match item.list.get(0) { - Some(ClassifiedCommand::Expr(expr)) => expr, - _ => { - return Err(ShellError::labeled_error( - "Expected a condition", - "expected a condition", - tag, - )); - } - }, - None => { - return Err(ShellError::labeled_error( - "Expected a condition", - "expected a condition", - tag, - )); - } - } - }; - - context.scope.enter_scope(); - context.scope.add_vars(&condition.captured.entries); - - //FIXME: should we use the scope that's brought in as well? - let condition = evaluate_baseline_expr(cond, &context); - let result = match condition { - Ok(condition) => match condition.as_bool() { - Ok(b) => { - if b { - run_block(&then_case.block, &context, input, external_redirection) - } else { - run_block(&else_case.block, &context, input, external_redirection) - } - } - Err(e) => Ok(OutputStream::from_stream( - vec![UntaggedValue::Error(e).into_untagged_value()].into_iter(), - )), - }, - Err(e) => Ok(OutputStream::from_stream( - vec![UntaggedValue::Error(e).into_untagged_value()].into_iter(), - )), - }; - context.scope.exit_scope(); - result -} - -#[cfg(test)] -mod tests { - use super::If; - use super::ShellError; - use nu_test_support::nu; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(If {}) - } - - #[test] - fn if_doesnt_leak_on_error() { - let actual = nu!( - ".", - r#" - def test-leak [] { - let var = "hello" - if 0 == "" {echo ok} {echo not} - } - test-leak - echo $var - "# - ); - - assert!(actual.err.contains("unknown variable")); - } -} diff --git a/crates/nu-command/src/commands/core_commands/ignore.rs b/crates/nu-command/src/commands/core_commands/ignore.rs deleted file mode 100644 index 9283a48858..0000000000 --- a/crates/nu-command/src/commands/core_commands/ignore.rs +++ /dev/null @@ -1,49 +0,0 @@ -extern crate unicode_segmentation; - -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::Signature; - -pub struct Ignore; - -impl WholeStreamCommand for Ignore { - fn name(&self) -> &str { - "ignore" - } - - fn signature(&self) -> Signature { - Signature::build("ignore") - } - - fn usage(&self) -> &str { - "Ignore the output of the previous command in the pipeline" - } - - fn run(&self, args: CommandArgs) -> Result { - let _: Vec<_> = args.input.collect(); - - Ok(OutputStream::empty()) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Ignore the output of an echo command", - example: r#"echo done | ignore"#, - result: None, - }] - } -} - -#[cfg(test)] -mod tests { - use super::Ignore; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Ignore {}) - } -} diff --git a/crates/nu-command/src/commands/core_commands/let_.rs b/crates/nu-command/src/commands/core_commands/let_.rs deleted file mode 100644 index fb52df72b9..0000000000 --- a/crates/nu-command/src/commands/core_commands/let_.rs +++ /dev/null @@ -1,112 +0,0 @@ -use crate::prelude::*; -use nu_engine::{evaluate_baseline_expr, FromValue, WholeStreamCommand}; - -use nu_errors::ShellError; -use nu_protocol::{ - hir::{CapturedBlock, ClassifiedCommand}, - Signature, SyntaxShape, UntaggedValue, -}; - -pub struct Let; - -impl WholeStreamCommand for Let { - fn name(&self) -> &str { - "let" - } - - fn signature(&self) -> Signature { - Signature::build("let") - .required("name", SyntaxShape::String, "the name of the variable") - .required("equals", SyntaxShape::String, "the equals sign") - .required( - "expr", - SyntaxShape::MathExpression, - "the value for the variable", - ) - } - - fn usage(&self) -> &str { - "Create a variable and give it a value." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - letcmd(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Assign a simple value to a variable", - example: "let x = 3", - result: Some(vec![]), - }, - Example { - description: "Assign the result of an expression to a variable", - example: "let result = (3 + 7); echo $result", - result: Some(vec![UntaggedValue::int(1).into()]), - }, - Example { - description: "Create a variable using the full name", - example: "let $three = 3", - result: Some(vec![]), - }, - ] - } -} - -pub fn letcmd(args: CommandArgs) -> Result { - let ctx = &args.context; - let positional = args - .call_info - .args - .positional - .expect("Internal error: type checker should require args"); - - let var_name = positional[0].var_name()?; - let rhs_raw = evaluate_baseline_expr(&positional[2], ctx)?; - let tag: Tag = positional[2].span.into(); - - let rhs: CapturedBlock = FromValue::from_value(&rhs_raw)?; - - let (expr, _) = { - if rhs.block.block.len() != 1 { - return Err(ShellError::labeled_error( - "Expected a value", - "expected a value", - tag, - )); - } - match rhs.block.block[0].pipelines.get(0) { - Some(item) => match item.list.get(0) { - Some(ClassifiedCommand::Expr(expr)) => (expr, &rhs.captured), - _ => { - return Err(ShellError::labeled_error( - "Expected a value", - "expected a value", - tag, - )); - } - }, - None => { - return Err(ShellError::labeled_error( - "Expected a value", - "expected a value", - tag, - )); - } - } - }; - - ctx.scope.enter_scope(); - let value = evaluate_baseline_expr(expr, ctx); - ctx.scope.exit_scope(); - - let value = value?; - - // Note: this is a special case for setting the context from a command - // In this case, if we don't set it now, we'll lose the scope that this - // variable should be set into. - ctx.scope.add_var(var_name, value); - - Ok(ActionStream::empty()) -} diff --git a/crates/nu-command/src/commands/core_commands/mod.rs b/crates/nu-command/src/commands/core_commands/mod.rs deleted file mode 100644 index c7fd932d59..0000000000 --- a/crates/nu-command/src/commands/core_commands/mod.rs +++ /dev/null @@ -1,43 +0,0 @@ -mod alias; -mod debug; -mod def; -mod describe; -mod do_; -pub(crate) mod echo; -mod error; -mod find; -mod help; -mod history; -mod if_; -mod ignore; -mod let_; -mod nu_plugin; -mod nu_signature; -mod source; -mod tags; -mod tutor; -mod unalias; -mod version; - -pub use self::nu_plugin::SubCommand as NuPlugin; -pub use self::nu_signature::{ - loglevels, testbins, version as core_version, Command as NuSignature, -}; -pub use alias::Alias; -pub use debug::Debug; -pub use def::Def; -pub use describe::Describe; -pub use do_::Do; -pub use echo::Echo; -pub use error::*; -pub use find::Find; -pub use help::Help; -pub use history::History; -pub use if_::If; -pub use ignore::Ignore; -pub use let_::Let; -pub use source::Source; -pub use tags::Tags; -pub use tutor::Tutor; -pub use unalias::Unalias; -pub use version::{version, Version}; diff --git a/crates/nu-command/src/commands/core_commands/nu_plugin.rs b/crates/nu-command/src/commands/core_commands/nu_plugin.rs deleted file mode 100644 index 0f91a6c14e..0000000000 --- a/crates/nu-command/src/commands/core_commands/nu_plugin.rs +++ /dev/null @@ -1,120 +0,0 @@ -use std::path::PathBuf; - -use crate::prelude::*; -use nu_engine::WholeStreamCommand; - -use nu_errors::ShellError; -use nu_path::canonicalize_with; -use nu_protocol::{CommandAction, ReturnSuccess, Signature, SyntaxShape, UntaggedValue}; -use nu_source::Tagged; - -#[cfg(unix)] -use std::os::unix::fs::PermissionsExt; - -pub struct SubCommand; - -#[derive(Deserialize)] -pub struct Arguments { - #[serde(rename = "load")] - pub load_path: Option>, -} - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "nu plugin" - } - - fn signature(&self) -> Signature { - Signature::build("nu plugin").named( - "load", - SyntaxShape::FilePath, - "a path to load the plugins from", - Some('l'), - ) - } - - fn usage(&self) -> &str { - "Nu Plugin" - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Load all plugins in the current directory", - example: "nu plugin --load .", - result: None, - }] - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - let scope = args.scope().clone(); - let shell_manager = args.shell_manager(); - - let load_path: Option> = args.get_flag("load")?; - - if let Some(Tagged { - item: load_path, - tag, - }) = load_path - { - let path = canonicalize_with(load_path, shell_manager.path()).map_err(|_| { - ShellError::labeled_error( - "Cannot load plugins from directory", - "directory not found", - &tag, - ) - })?; - - if !path.is_dir() { - return Err(ShellError::labeled_error( - "Cannot load plugins from directory", - "is not a directory", - &tag, - )); - } - - #[cfg(unix)] - { - let has_exec = path - .metadata() - .map(|m| umask::Mode::from(m.permissions().mode()).has(umask::USER_READ)) - .map_err(|e| { - ShellError::labeled_error( - "Cannot load plugins from directory", - format!("cannot stat ({})", e), - &tag, - ) - })?; - - if !has_exec { - return Err(ShellError::labeled_error( - "Cannot load plugins from directory", - "permission denied", - &tag, - )); - } - } - - return Ok(vec![ReturnSuccess::action(CommandAction::AddPlugins( - path.to_string_lossy().to_string(), - ))] - .into()); - } - - Ok(ActionStream::one(ReturnSuccess::value( - UntaggedValue::string(get_full_help(&SubCommand, &scope)).into_value(Tag::unknown()), - ))) - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/core_commands/nu_signature.rs b/crates/nu-command/src/commands/core_commands/nu_signature.rs deleted file mode 100644 index d8a74295db..0000000000 --- a/crates/nu-command/src/commands/core_commands/nu_signature.rs +++ /dev/null @@ -1,71 +0,0 @@ -use nu_engine::WholeStreamCommand; -use nu_protocol::{Signature, SyntaxShape}; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "nu" - } - - fn signature(&self) -> Signature { - Signature::build("nu") - .switch("version", "Display Nu version", Some('v')) - .switch("stdin", "redirect stdin", None) - .switch("skip-plugins", "do not load plugins", None) - .switch("no-history", "don't save history", None) - .switch("perf", "show startup performance metrics", None) - .switch("login", "start Nu as if it was a login shell", Some('l')) - .named( - "commands", - SyntaxShape::String, - "commands to run", - Some('c'), - ) - .named( - "testbin", - SyntaxShape::String, - "test bin: echo_env, cococo, iecho, fail, nonu, chop, repeater, meow", - None, - ) - .named("develop", SyntaxShape::String, "trace mode", None) - .named("debug", SyntaxShape::String, "debug mode", None) - .named( - "loglevel", - SyntaxShape::String, - "LEVEL: error, warn, info, debug, trace", - None, - ) - .named( - "config-file", - SyntaxShape::FilePath, - "custom configuration source file", - None, - ) - .rest("rest", SyntaxShape::String, "source file(s) to run") - } - - fn usage(&self) -> &str { - "Nu - A new type of shell." - } -} - -pub fn version() -> &'static str { - env!("CARGO_PKG_VERSION") -} - -pub fn testbins() -> Vec { - vec![ - "echo_env", "cococo", "iecho", "fail", "nonu", "chop", "repeater", "meow", - ] - .into_iter() - .map(String::from) - .collect() -} - -pub fn loglevels() -> Vec { - vec!["error", "warn", "info", "debug", "trace"] - .into_iter() - .map(String::from) - .collect() -} diff --git a/crates/nu-command/src/commands/core_commands/source.rs b/crates/nu-command/src/commands/core_commands/source.rs deleted file mode 100644 index 862ef62865..0000000000 --- a/crates/nu-command/src/commands/core_commands/source.rs +++ /dev/null @@ -1,123 +0,0 @@ -use crate::prelude::*; -use nu_engine::{script, WholeStreamCommand}; - -use nu_errors::ShellError; -use nu_path::{canonicalize, canonicalize_with}; -use nu_protocol::{Signature, SyntaxShape}; -use nu_source::Tagged; - -use std::path::Path; - -pub struct Source; - -#[derive(Deserialize)] -pub struct SourceArgs { - pub filename: Tagged, -} - -impl WholeStreamCommand for Source { - fn name(&self) -> &str { - "source" - } - - fn signature(&self) -> Signature { - Signature::build("source").required( - "filename", - SyntaxShape::FilePath, - "the filepath to the script file to source", - ) - } - - fn usage(&self) -> &str { - "Runs a script file in the current context." - } - - fn run(&self, args: CommandArgs) -> Result { - source(args) - } - - fn examples(&self) -> Vec { - vec![] - } -} - -pub fn source(args: CommandArgs) -> Result { - let ctx = &args.context; - let filename: Tagged = args.req(0)?; - - let source_file = Path::new(&filename.item); - - // Note: this is a special case for setting the context from a command - // In this case, if we don't set it now, we'll lose the scope that this - // variable should be set into. - - let lib_dirs = &ctx - .configs() - .lock() - .global_config - .as_ref() - .map(|configuration| match configuration.var("lib_dirs") { - Some(paths) => paths - .table_entries() - .cloned() - .map(|path| path.as_string()) - .collect(), - None => vec![], - }); - - if let Some(dir) = lib_dirs { - for lib_path in dir { - match lib_path { - Ok(name) => { - let path = if let Ok(p) = canonicalize_with(&source_file, name) { - p - } else { - continue; - }; - - if let Ok(contents) = std::fs::read_to_string(path) { - let result = script::run_script_standalone(contents, true, ctx, false); - - if let Err(err) = result { - ctx.error(err); - } - return Ok(OutputStream::empty()); - } - } - Err(reason) => { - ctx.error(reason.clone()); - } - } - } - } - - let path = canonicalize(source_file).map_err(|e| { - ShellError::labeled_error( - format!("Can't load source file. Reason: {}", e), - "Can't load this file", - filename.span(), - ) - })?; - - let contents = std::fs::read_to_string(path); - - match contents { - Ok(contents) => { - let result = script::run_script_standalone(contents, true, ctx, false); - - if let Err(err) = result { - ctx.error(err); - } - Ok(OutputStream::empty()) - } - Err(e) => { - ctx.error(ShellError::labeled_error( - format!("Can't load source file. Reason: {}", e), - "Can't load this file", - filename.span(), - )); - - Ok(OutputStream::empty()) - } - } -} diff --git a/crates/nu-command/src/commands/core_commands/tags.rs b/crates/nu-command/src/commands/core_commands/tags.rs deleted file mode 100644 index 73e39b2741..0000000000 --- a/crates/nu-command/src/commands/core_commands/tags.rs +++ /dev/null @@ -1,92 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Primitive, Signature, TaggedDictBuilder, UntaggedValue, Value}; - -pub struct Tags; - -impl WholeStreamCommand for Tags { - fn name(&self) -> &str { - "tags" - } - - fn signature(&self) -> Signature { - Signature::build("tags") - } - - fn usage(&self) -> &str { - "Read the tags (metadata) for values." - } - - fn run(&self, args: CommandArgs) -> Result { - Ok(tags(args)) - } -} - -fn build_tag_table(tag: impl Into) -> Value { - let tag = tag.into(); - let span = tag.span; - - TaggedDictBuilder::build(tag.clone(), |tags| { - if let Some(anchor) = anchor_as_value(&tag) { - tags.insert_value("anchor", anchor); - } - - tags.insert_value( - "span", - TaggedDictBuilder::build(tag.clone(), |span_dict| { - span_dict.insert_untagged("start", UntaggedValue::int(span.start() as i64)); - span_dict.insert_untagged("end", UntaggedValue::int(span.end() as i64)); - }), - ); - }) -} - -fn tags(args: CommandArgs) -> OutputStream { - if args.input.is_empty() { - OutputStream::one(build_tag_table(&args.name_tag())) - } else { - args.input - .map(move |v| build_tag_table(v.tag())) - .into_output_stream() - } -} - -fn anchor_as_value(tag: &Tag) -> Option { - let anchor = tag.anchor.as_ref(); - - anchor.as_ref()?; - - Some(TaggedDictBuilder::build(tag, |table| { - let value = match anchor { - Some(AnchorLocation::File(path)) => { - Some(("file", UntaggedValue::from(path.to_string()))) - } - Some(AnchorLocation::Url(destination)) => { - Some(("url", UntaggedValue::from(destination.to_string()))) - } - Some(AnchorLocation::Source(text)) => Some(( - "source", - UntaggedValue::Primitive(Primitive::String(text.to_string())), - )), - None => None, - }; - - if let Some((key, value)) = value { - table.insert_untagged(key, value); - } - })) -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::Tags; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Tags {}) - } -} diff --git a/crates/nu-command/src/commands/core_commands/tutor.rs b/crates/nu-command/src/commands/core_commands/tutor.rs deleted file mode 100644 index 284515ae55..0000000000 --- a/crates/nu-command/src/commands/core_commands/tutor.rs +++ /dev/null @@ -1,431 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue}; - -pub struct Tutor; - -impl WholeStreamCommand for Tutor { - fn name(&self) -> &str { - "tutor" - } - - fn signature(&self) -> Signature { - Signature::build("tutor") - .optional( - "search", - SyntaxShape::String, - "item to search for, or 'list' to list available tutorials", - ) - .named( - "find", - SyntaxShape::String, - "Search tutorial for a phrase", - Some('f'), - ) - } - - fn usage(&self) -> &str { - "Run the tutorial. To begin, run: tutor" - } - - fn run(&self, args: CommandArgs) -> Result { - tutor(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Begin the tutorial", - example: "tutor begin", - result: None, - }, - Example { - description: "Search a tutorial by phrase", - example: "tutor -f \"$in\"", - result: None, - }, - ] - } -} - -fn tutor(args: CommandArgs) -> Result { - let tag = args.name_tag(); - let scope = args.scope().clone(); - - let search: Option = args.opt(0).unwrap_or(None); - let find: Option = args.get_flag("find")?; - - let search_space = [ - (vec!["begin"], begin_tutor()), - ( - vec!["table", "tables", "row", "rows", "column", "columns"], - table_tutor(), - ), - (vec!["cell", "cells"], cell_tutor()), - ( - vec![ - "expr", - "exprs", - "expressions", - "subexpression", - "subexpressions", - "sub-expression", - "sub-expressions", - ], - expression_tutor(), - ), - (vec!["echo"], echo_tutor()), - (vec!["each", "iteration", "iter"], each_tutor()), - ( - vec!["var", "vars", "variable", "variables"], - variable_tutor(), - ), - (vec!["engine-q", "e-q"], engineq_tutor()), - (vec!["block", "blocks"], block_tutor()), - (vec!["shorthand", "shorthands"], shorthand_tutor()), - ]; - - if let Some(find) = find { - let mut results = vec![]; - for search_group in search_space { - if search_group.1.contains(&find) { - results.push(search_group.0[0].to_string()) - } - } - - let message = format!("You can find '{}' in the following topics:\n{}\n\nYou can learn about a topic using `tutor` followed by the name of the topic.\nFor example: `tutor table` to open the table topic.\n\n", - find, - results.into_iter().map(|x| format!("- {}", x)).join("\n") - ); - - return Ok(display(tag, &scope, &message)); - } else if let Some(search) = search { - for search_group in search_space { - if search_group.0.contains(&search.as_str()) { - return Ok(display(tag, &scope, search_group.1)); - } - } - } - Ok(display(tag, &scope, default_tutor())) -} - -fn default_tutor() -> &'static str { - r#" -Welcome to the Nushell tutorial! - -With the `tutor` command, you'll be able to learn a lot about how Nushell -works along with many fun tips and tricks to speed up everyday tasks. - -To get started, you can use `tutor begin`. - -"# -} - -fn begin_tutor() -> &'static str { - r#" -Nushell is a structured shell and programming language. One way to begin -using it is to try a few of the commands. - -The first command to try is `ls`. The `ls` command will show you a list -of the files in the current directory. Notice that these files are shown -as a table. Each column of this table not only tells us what is being -shown, but also gives us a way to work with the data. - -You can combine the `ls` command with other commands using the pipeline -symbol '|'. This allows data to flow from one command to the next. - -For example, if we only wanted the name column, we could do: -``` -ls | select name -``` -Notice that we still get a table, but this time it only has one column: -the name column. - -You can continue to learn more about tables by running: -``` -tutor tables -``` -If at any point, you'd like to restart this tutorial, you can run: -``` -tutor begin -``` -"# -} - -fn table_tutor() -> &'static str { - r#" -The most common form of data in Nushell is the table. Tables contain rows and -columns of data. In each cell of the table, there is data that you can access -using Nushell commands. - -To get the 3rd row in the table, you can use the `nth` command: -``` -ls | nth 2 -``` -This will get the 3rd (note that `nth` is zero-based) row in the table created -by the `ls` command. You can use `nth` on any table created by other commands -as well. - -You can also access the column of data in one of two ways. If you want -to keep the column as part of a new table, you can use `select`. -``` -ls | select name -``` -This runs `ls` and returns only the "name" column of the table. - -If, instead, you'd like to get access to the values inside of the column, you -can use the `get` command. -``` -ls | get name -``` -This allows us to get to the list of strings that are the filenames rather -than having a full table. In some cases, this can make the names easier to -work with. - -You can continue to learn more about working with cells of the table by -running: -``` -tutor cells -``` -"# -} - -fn cell_tutor() -> &'static str { - r#" -Working with cells of data in the table is a key part of working with data in -Nushell. Because of this, there is a rich list of commands to work with cells -as well as handy shorthands for accessing cells. - -Cells can hold simple values like strings and numbers, or more complex values -like lists and tables. - -To reach a cell of data from a table, you can combine a row operation and a -column operation. -``` -ls | nth 4 | get name -``` -You can combine these operations into one step using a shortcut. -``` -(ls).4.name -``` -Names/strings represent columns names and numbers represent row numbers. - -The `(ls)` is a form of expression. You can continue to learn more about -expressions by running: -``` -tutor expressions -``` -You can also learn about these cell shorthands by running: -``` -tutor shorthands -``` -"# -} - -fn expression_tutor() -> &'static str { - r#" -Expressions give you the power to mix calls to commands with math. The -simplest expression is a single value like a string or number. -``` -3 -``` -Expressions can also include math operations like addition or division. -``` -10 / 2 -``` -Normally, an expression is one type of operation: math or commands. You can -mix these types by using subexpressions. Subexpressions are just like -expressions, but they're wrapped in parentheses `()`. -``` -10 * (3 + 4) -``` -Here we use parentheses to create a higher math precedence in the math -expression. -``` -echo (2 + 3) -``` -You can continue to learn more about the `echo` command by running: -``` -tutor echo -``` -"# -} - -fn echo_tutor() -> &'static str { - r#" -The `echo` command in Nushell is a powerful tool for not only seeing values, -but also for creating new ones. -``` -echo "Hello" -``` -You can echo output. This output, if it's not redirected using a "|" pipeline -will be displayed to the screen. -``` -echo 1..10 -``` -You can also use echo to work with individual values of a range. In this -example, `echo` will create the values from 1 to 10 as a list. -``` -echo 1 2 3 4 5 -``` -You can also create lists of values by passing `echo` multiple arguments. -This can be helpful if you want to later processes these values. - -The `echo` command can pair well with the `each` command which can run -code on each row, or item, of input. - -You can continue to learn more about the `each` command by running: -``` -tutor each -``` -"# -} - -fn each_tutor() -> &'static str { - r#" -The `each` command gives us a way of working with each individual row or -element of a list one at a time. It reads these in from the pipeline and -runs a block on each element. A block is a group of pipelines. -``` -echo 1 2 3 | each { $it + 10} -``` -This example iterates over each element sent by `echo`, giving us three new -values that are the original value + 10. Here, the `$it` is a variable that -is the name given to the block's parameter by default. - -You can learn more about blocks by running: -``` -tutor blocks -``` -You can also learn more about variables by running: -``` -tutor variables -``` -"# -} - -fn variable_tutor() -> &'static str { - r#" -Variables are an important way to store values to be used later. To create a -variable, you can use the `let` keyword. The `let` command will create a -variable and then assign it a value in one step. -``` -let $x = 3 -``` -Once created, we can refer to this variable by name. -``` -$x -``` -Nushell also comes with built-in variables. The `$nu` variable is a reserved -variable that contains a lot of information about the currently running -instance of Nushell. The `$it` variable is the name given to block parameters -if you don't specify one. And `$in` is the variable that allows you to work -with all of the data coming in from the pipeline in one place. - -"# -} - -fn block_tutor() -> &'static str { - r#" -Blocks are a special form of expression that hold code to be run at a later -time. Often, you'll see blocks as one of the arguments given to commands -like `each` and `if`. -``` -ls | each {|x| $x.name} -``` -The above will create a list of the filenames in the directory. -``` -if $true { echo "it's true" } { echo "it's not true" } -``` -This `if` call will run the first block if the expression is true, or the -second block if the expression is false. - -"# -} - -fn shorthand_tutor() -> &'static str { - r#" -You can access cells in a table using a shorthand notation sometimes called a -"column path" or "cell path". These paths allow you to go from a table to -rows, columns, or cells inside of the table. - -Shorthand paths are made from rows numbers, column names, or both. You can use -them on any variable or subexpression. -``` -$nu.cwd -``` -The above accesses the built-in `$nu` variable, gets its table, and then uses -the shorthand path to retrieve only the cell data inside the "cwd" column. -``` -(ls).name.4 -``` -This will retrieve the cell data in the "name" column on the 5th row (note: -row numbers are zero-based). - -Rows and columns don't need to come in any specific order. You can get the -same value using: -``` -(ls).4.name -``` -"# -} - -fn engineq_tutor() -> &'static str { - r#" -Engine-q is the upcoming engine for Nushell. Build for speed and correctness, -it also comes with a set of changes from Nushell versions prior to 0.60. To -get ready for engine-q look for some of these changes that might impact your -current scripts: - -* Engine-q now uses a few new data structures, including a record syntax - that allows you to model key-value pairs similar to JSON objects. -* Environment variables can now contain more than just strings. Structured - values are converted to strings for external commands using converters. -* `if` will now use an `else` keyword before the else block. -* We're moving from "config.toml" to "config.nu". This means startup will - now be a script file. -* `config` and its subcommands are being replaced by a record that you can - update in the shell which contains all the settings under the variable - `$config`. -* bigint/bigdecimal values are now machine i64 and f64 values -* And more, you can read more about upcoming changes in the up-to-date list - at: https://github.com/nushell/engine-q/issues/522 -"# -} - -fn display(tag: Tag, scope: &Scope, help: &str) -> OutputStream { - let help = help.split('`'); - - let mut build = String::new(); - let mut code_mode = false; - let palette = nu_engine::DefaultPalette {}; - - for item in help { - if code_mode { - code_mode = false; - - //TODO: support no-color mode - let colored_example = nu_engine::Painter::paint_string(item, scope, &palette); - build.push_str(&colored_example); - } else { - code_mode = true; - build.push_str(item); - } - } - - OutputStream::one(UntaggedValue::string(build).into_value(tag)) -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::Tutor; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Tutor {}) - } -} diff --git a/crates/nu-command/src/commands/core_commands/unalias.rs b/crates/nu-command/src/commands/core_commands/unalias.rs deleted file mode 100644 index 75de72b957..0000000000 --- a/crates/nu-command/src/commands/core_commands/unalias.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; - -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape}; - -pub struct Unalias; - -impl WholeStreamCommand for Unalias { - fn name(&self) -> &str { - "unalias" - } - - fn signature(&self) -> Signature { - Signature::build("unalias").required("name", SyntaxShape::String, "the name of the alias") - } - - fn usage(&self) -> &str { - "Removes an alias" - } - - fn run(&self, args: CommandArgs) -> Result { - unalias(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Remove the 'v' alias", - example: "unalias v", - result: None, - }] - } -} - -pub fn unalias(_: CommandArgs) -> Result { - Ok(OutputStream::empty()) -} diff --git a/crates/nu-command/src/commands/core_commands/version.rs b/crates/nu-command/src/commands/core_commands/version.rs deleted file mode 100644 index dcd08bcf0f..0000000000 --- a/crates/nu-command/src/commands/core_commands/version.rs +++ /dev/null @@ -1,315 +0,0 @@ -use crate::prelude::*; -use indexmap::IndexMap; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{value::StrExt, value::StringExt, Dictionary, Signature, UntaggedValue}; - -pub mod shadow { - include!(concat!(env!("OUT_DIR"), "/shadow.rs")); -} - -pub struct Version; - -impl WholeStreamCommand for Version { - fn name(&self) -> &str { - "version" - } - - fn signature(&self) -> Signature { - Signature::build("version") - } - - fn usage(&self) -> &str { - "Display Nu version." - } - - fn run(&self, args: CommandArgs) -> Result { - version(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Display Nu version", - example: "version", - result: None, - }] - } -} - -pub fn version(args: CommandArgs) -> Result { - let tag = args.call_info.args.span; - - let mut indexmap = IndexMap::with_capacity(4); - - indexmap.insert( - "version".to_string(), - UntaggedValue::string(super::nu_signature::version()).into_value(&tag), - ); - - let branch: Option<&str> = Some(shadow::BRANCH).filter(|x| !x.is_empty()); - if let Some(branch) = branch { - indexmap.insert( - "branch".to_string(), - branch.to_pattern_untagged_value().into_value(&tag), - ); - } - - let short_commit: Option<&str> = Some(shadow::SHORT_COMMIT).filter(|x| !x.is_empty()); - if let Some(short_commit) = short_commit { - indexmap.insert( - "short_commit".to_string(), - short_commit.to_pattern_untagged_value().into_value(&tag), - ); - } - let commit_hash: Option<&str> = Some(shadow::COMMIT_HASH).filter(|x| !x.is_empty()); - if let Some(commit_hash) = commit_hash { - indexmap.insert( - "commit_hash".to_string(), - commit_hash.to_pattern_untagged_value().into_value(&tag), - ); - } - let commit_date: Option<&str> = Some(shadow::COMMIT_DATE).filter(|x| !x.is_empty()); - if let Some(commit_date) = commit_date { - indexmap.insert( - "commit_date".to_string(), - commit_date.to_pattern_untagged_value().into_value(&tag), - ); - } - - let build_os: Option<&str> = Some(shadow::BUILD_OS).filter(|x| !x.is_empty()); - if let Some(build_os) = build_os { - indexmap.insert( - "build_os".to_string(), - build_os.to_pattern_untagged_value().into_value(&tag), - ); - } - - let rust_version: Option<&str> = Some(shadow::RUST_VERSION).filter(|x| !x.is_empty()); - if let Some(rust_version) = rust_version { - indexmap.insert( - "rust_version".to_string(), - rust_version.to_pattern_untagged_value().into_value(&tag), - ); - } - - let rust_channel: Option<&str> = Some(shadow::RUST_CHANNEL).filter(|x| !x.is_empty()); - if let Some(rust_channel) = rust_channel { - indexmap.insert( - "rust_channel".to_string(), - rust_channel.to_pattern_untagged_value().into_value(&tag), - ); - } - - let cargo_version: Option<&str> = Some(shadow::CARGO_VERSION).filter(|x| !x.is_empty()); - if let Some(cargo_version) = cargo_version { - indexmap.insert( - "cargo_version".to_string(), - cargo_version.to_pattern_untagged_value().into_value(&tag), - ); - } - - let pkg_version: Option<&str> = Some(shadow::PKG_VERSION).filter(|x| !x.is_empty()); - if let Some(pkg_version) = pkg_version { - indexmap.insert( - "pkg_version".to_string(), - pkg_version.to_pattern_untagged_value().into_value(&tag), - ); - } - - let build_time: Option<&str> = Some(shadow::BUILD_TIME).filter(|x| !x.is_empty()); - if let Some(build_time) = build_time { - indexmap.insert( - "build_time".to_string(), - build_time.to_pattern_untagged_value().into_value(&tag), - ); - } - - let build_rust_channel: Option<&str> = - Some(shadow::BUILD_RUST_CHANNEL).filter(|x| !x.is_empty()); - if let Some(build_rust_channel) = build_rust_channel { - indexmap.insert( - "build_rust_channel".to_string(), - build_rust_channel - .to_pattern_untagged_value() - .into_value(&tag), - ); - } - - indexmap.insert( - "features".to_string(), - features_enabled().join(", ").to_string_value_create_tag(), - ); - - // Manually create a list of all possible plugin names - let all_plugins = vec![ - "fetch", - "inc", - "match", - "post", - "ps", - "sys", - "textview", - "binaryview", - "chart bar", - "chart line", - "from bson", - "from sqlite", - "query json", - "s3", - "selector", - "start", - "to bson", - "to sqlite", - "tree", - "xpath", - ]; - - // Get a list of command names and check for plugins - let installed_plugins = args - .scope() - .get_command_names() - .into_iter() - .filter(|cmd| all_plugins.contains(&cmd.as_str())) - .collect::>(); - - indexmap.insert( - "installed_plugins".to_string(), - installed_plugins.join(", ").to_string_value_create_tag(), - ); - - let value = UntaggedValue::Row(Dictionary::from(indexmap)).into_value(&tag); - Ok(OutputStream::one(value)) -} - -fn features_enabled() -> Vec { - let mut names = vec!["default".to_string()]; - - // NOTE: There should be another way to know - // features on. - #[cfg(feature = "ctrlc")] - { - names.push("ctrlc".to_string()); - } - - // #[cfg(feature = "rich-benchmark")] - // { - // names.push("rich-benchmark".to_string()); - // } - - #[cfg(feature = "rustyline-support")] - { - names.push("rustyline".to_string()); - } - - #[cfg(feature = "term")] - { - names.push("term".to_string()); - } - - #[cfg(feature = "uuid_crate")] - { - names.push("uuid".to_string()); - } - - #[cfg(feature = "which")] - { - names.push("which".to_string()); - } - - #[cfg(feature = "zip")] - { - names.push("zip".to_string()); - } - - #[cfg(feature = "trash-support")] - { - names.push("trash".to_string()); - } - - #[cfg(feature = "dataframe")] - { - names.push("dataframe".to_string()); - } - - #[cfg(feature = "table-pager")] - { - names.push("table-pager".to_string()); - } - - // #[cfg(feature = "binaryview")] - // { - // names.push("binaryview".to_string()); - // } - - // #[cfg(feature = "start")] - // { - // names.push("start".to_string()); - // } - - // #[cfg(feature = "bson")] - // { - // names.push("bson".to_string()); - // } - - // #[cfg(feature = "sqlite")] - // { - // names.push("sqlite".to_string()); - // } - - // #[cfg(feature = "s3")] - // { - // names.push("s3".to_string()); - // } - - // #[cfg(feature = "chart")] - // { - // names.push("chart".to_string()); - // } - - // #[cfg(feature = "xpath")] - // { - // names.push("xpath".to_string()); - // } - - // #[cfg(feature = "selector")] - // { - // names.push("selector".to_string()); - // } - - // #[cfg(feature = "extra")] - // { - // names.push("extra".to_string()); - // } - - // #[cfg(feature = "preserve_order")] - // { - // names.push("preserve_order".to_string()); - // } - - // #[cfg(feature = "wee_alloc")] - // { - // names.push("wee_alloc".to_string()); - // } - - // #[cfg(feature = "console_error_panic_hook")] - // { - // names.push("console_error_panic_hook".to_string()); - // } - - names.sort(); - - names -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::Version; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Version {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/aggregate.rs b/crates/nu-command/src/commands/dataframe/aggregate.rs deleted file mode 100644 index b5649a56bf..0000000000 --- a/crates/nu-command/src/commands/dataframe/aggregate.rs +++ /dev/null @@ -1,293 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, FrameStruct, NuDataFrame}, - Signature, SyntaxShape, UntaggedValue, -}; -use nu_source::Tagged; -use polars::{frame::groupby::GroupBy, prelude::PolarsError}; - -enum Operation { - Mean, - Sum, - Min, - Max, - First, - Last, - Nunique, - Quantile(f64), - Median, - Var, - Std, - Count, -} - -impl Operation { - fn from_tagged( - name: &Tagged, - quantile: Option>, - ) -> Result { - match name.item.as_ref() { - "mean" => Ok(Operation::Mean), - "sum" => Ok(Operation::Sum), - "min" => Ok(Operation::Min), - "max" => Ok(Operation::Max), - "first" => Ok(Operation::First), - "last" => Ok(Operation::Last), - "nunique" => Ok(Operation::Nunique), - "quantile" => { - match quantile { - None => Err(ShellError::labeled_error( - "Quantile value not fount", - "Quantile operation requires quantile value", - &name.tag, - )), - Some(value ) => { - if (value.item < 0.0) | (value.item > 1.0) { - Err(ShellError::labeled_error( - "Inappropriate quantile", - "Quantile value should be between 0.0 and 1.0", - &value.tag, - )) - } else { - Ok(Operation::Quantile(value.item)) - } - } - } - } - "median" => Ok(Operation::Median), - "var" => Ok(Operation::Var), - "std" => Ok(Operation::Std), - "count" => Ok(Operation::Count), - _ => Err(ShellError::labeled_error_with_secondary( - "Operation not fount", - "Operation does not exist", - &name.tag, - "Perhaps you want: mean, sum, min, max, first, last, nunique, quantile, median, var, std, or count", - &name.tag, - )), - } - } -} - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe aggregate" - } - - fn usage(&self) -> &str { - "[DataFrame, GroupBy, Series] Performs an aggregation operation on a dataframe, groupby or series object" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe aggregate") - .required("operation", SyntaxShape::String, "aggregate operation") - .named( - "quantile", - SyntaxShape::Number, - "quantile value for quantile operation", - Some('q'), - ) - .switch( - "explicit", - "returns explicit names for groupby aggregations", - Some('e'), - ) - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Aggregate sum by grouping by column a and summing on col b", - example: - "[[a b]; [one 1] [one 2]] | dataframe to-df | dataframe group-by a | dataframe aggregate sum", - result: Some(vec![NuDataFrame::try_from_columns( - vec![ - Column::new("a".to_string(), vec![UntaggedValue::string("one").into()]), - Column::new("b".to_string(), vec![UntaggedValue::int(3).into()]), - ], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }, - Example { - description: "Aggregate sum in dataframe columns", - example: "[[a b]; [4 1] [5 2]] | dataframe to-df | dataframe aggregate sum", - result: Some(vec![NuDataFrame::try_from_columns( - vec![ - Column::new("a".to_string(), vec![UntaggedValue::int(9).into()]), - Column::new("b".to_string(), vec![UntaggedValue::int(3).into()]), - ], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }, - Example { - description: "Aggregate sum in series", - example: "[4 1 5 6] | dataframe to-df | dataframe aggregate sum", - result: Some(vec![NuDataFrame::try_from_columns( - vec![ - Column::new("0".to_string(), vec![UntaggedValue::int(16).into()]), - ], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }, - ] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let quantile: Option> = args.get_flag("quantile")?; - let operation: Tagged = args.req(0)?; - let op = Operation::from_tagged(&operation, quantile)?; - - let value = args.input.next().ok_or_else(|| { - ShellError::labeled_error("Empty stream", "No value found in the stream", &tag) - })?; - - match value.value { - UntaggedValue::FrameStruct(FrameStruct::GroupBy(nu_groupby)) => { - let groupby = nu_groupby.to_groupby()?; - - let res = perform_groupby_aggregation( - groupby, - op, - &operation.tag, - &tag.span, - args.has_flag("explicit"), - )?; - - Ok(OutputStream::one(NuDataFrame::dataframe_to_value(res, tag))) - } - UntaggedValue::DataFrame(df) => { - let df = df.as_ref(); - - let res = perform_dataframe_aggregation(df, op, &operation.tag)?; - - Ok(OutputStream::one(NuDataFrame::dataframe_to_value(res, tag))) - } - _ => Err(ShellError::labeled_error( - "No groupby, dataframe or series in stream", - "no groupby, dataframe or series found in input stream", - &value.tag.span, - )), - } -} - -fn perform_groupby_aggregation( - groupby: GroupBy, - operation: Operation, - operation_tag: &Tag, - agg_span: &Span, - explicit: bool, -) -> Result { - let mut res = match operation { - Operation::Mean => groupby.mean(), - Operation::Sum => groupby.sum(), - Operation::Min => groupby.min(), - Operation::Max => groupby.max(), - Operation::First => groupby.first(), - Operation::Last => groupby.last(), - Operation::Nunique => groupby.n_unique(), - Operation::Quantile(quantile) => groupby.quantile(quantile), - Operation::Median => groupby.median(), - Operation::Var => groupby.var(), - Operation::Std => groupby.std(), - Operation::Count => groupby.count(), - } - .map_err(|e| { - let span = match &e { - PolarsError::NotFound(_) => agg_span, - _ => &operation_tag.span, - }; - - parse_polars_error::<&str>(&e, span, None) - })?; - - if !explicit { - let col_names = res - .get_column_names() - .iter() - .map(|name| name.to_string()) - .collect::>(); - - for col in col_names { - let from = match operation { - Operation::Mean => "_mean", - Operation::Sum => "_sum", - Operation::Min => "_min", - Operation::Max => "_max", - Operation::First => "_first", - Operation::Last => "_last", - Operation::Nunique => "_n_unique", - Operation::Quantile(_) => "_quantile", - Operation::Median => "_median", - Operation::Var => "_agg_var", - Operation::Std => "_agg_std", - Operation::Count => "_count", - }; - - let new_col = match col.find(from) { - Some(index) => &col[..index], - None => &col[..], - }; - - res.rename(&col, new_col) - .expect("Column is always there. Looping with known names"); - } - } - - Ok(res) -} - -fn perform_dataframe_aggregation( - dataframe: &polars::prelude::DataFrame, - operation: Operation, - operation_tag: &Tag, -) -> Result { - match operation { - Operation::Mean => Ok(dataframe.mean()), - Operation::Sum => Ok(dataframe.sum()), - Operation::Min => Ok(dataframe.min()), - Operation::Max => Ok(dataframe.max()), - Operation::Quantile(quantile) => dataframe - .quantile(quantile) - .map_err(|e| parse_polars_error::<&str>(&e, &operation_tag.span, None)), - Operation::Median => Ok(dataframe.median()), - Operation::Var => Ok(dataframe.var()), - Operation::Std => Ok(dataframe.std()), - _ => Err(ShellError::labeled_error_with_secondary( - "Not valid operation", - "operation not valid for dataframe", - &operation_tag.span, - "Perhaps you want: mean, sum, min, max, quantile, median, var, or std", - &operation_tag.span, - )), - } -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/append.rs b/crates/nu-command/src/commands/dataframe/append.rs deleted file mode 100644 index 503daac655..0000000000 --- a/crates/nu-command/src/commands/dataframe/append.rs +++ /dev/null @@ -1,138 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Axis, Column, NuDataFrame}, - Signature, SyntaxShape, UntaggedValue, Value, -}; -use nu_source::Tagged; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe append" - } - - fn usage(&self) -> &str { - "[DataFrame] Appends a new dataframe" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe append") - .required_named( - "other", - SyntaxShape::Any, - "dataframe to be appended", - Some('o'), - ) - .required_named( - "axis", - SyntaxShape::String, - "row or col axis orientation", - Some('a'), - ) - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Appends a dataframe as new columns", - example: r#"let a = ([[a b]; [1 2] [3 4]] | dataframe to-df); - $a | dataframe append -o $a -a row"#, - result: Some(vec![NuDataFrame::try_from_columns( - vec![ - Column::new( - "a".to_string(), - vec![UntaggedValue::int(1).into(), UntaggedValue::int(3).into()], - ), - Column::new( - "b".to_string(), - vec![UntaggedValue::int(2).into(), UntaggedValue::int(4).into()], - ), - Column::new( - "a_x".to_string(), - vec![UntaggedValue::int(1).into(), UntaggedValue::int(3).into()], - ), - Column::new( - "b_x".to_string(), - vec![UntaggedValue::int(2).into(), UntaggedValue::int(4).into()], - ), - ], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }, - Example { - description: "Appends a dataframe merging at the end of columns", - example: r#"let a = ([[a b]; [1 2] [3 4]] | dataframe to-df); - $a | dataframe append -o $a -a col"#, - result: Some(vec![NuDataFrame::try_from_columns( - vec![ - Column::new( - "a".to_string(), - vec![ - UntaggedValue::int(1).into(), - UntaggedValue::int(3).into(), - UntaggedValue::int(1).into(), - UntaggedValue::int(3).into(), - ], - ), - Column::new( - "b".to_string(), - vec![ - UntaggedValue::int(2).into(), - UntaggedValue::int(4).into(), - UntaggedValue::int(2).into(), - UntaggedValue::int(4).into(), - ], - ), - ], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }, - ] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let other: Value = args.req_named("other")?; - let axis: Tagged = args.req_named("axis")?; - - let axis = Axis::try_from_str(&axis.item, &axis.tag.span)?; - - let df_other = match other.value { - UntaggedValue::DataFrame(df) => Ok(df), - _ => Err(ShellError::labeled_error( - "Incorrect type", - "can only append a dataframe to a dataframe", - other.tag.span, - )), - }?; - - let (df, _) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let df_new = df.append_df(&df_other, axis, &tag.span)?; - Ok(OutputStream::one(df_new.into_value(tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/column.rs b/crates/nu-command/src/commands/dataframe/column.rs deleted file mode 100644 index e0b566818b..0000000000 --- a/crates/nu-command/src/commands/dataframe/column.rs +++ /dev/null @@ -1,74 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, SyntaxShape, UntaggedValue, -}; - -use nu_source::Tagged; - -use super::utils::parse_polars_error; -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe column" - } - - fn usage(&self) -> &str { - "[DataFrame] Returns the selected column as Series" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe column").required("column", SyntaxShape::String, "column name") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Returns the selected column as series", - example: "[[a b]; [1 2] [3 4]] | dataframe to-df | dataframe column a", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "a".to_string(), - vec![UntaggedValue::int(1).into(), UntaggedValue::int(3).into()], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let column: Tagged = args.req(0)?; - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let res = df - .as_ref() - .column(&column.item) - .map_err(|e| parse_polars_error::<&str>(&e, &column.tag.span, None))?; - - let df = NuDataFrame::try_from_series(vec![res.clone()], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/command.rs b/crates/nu-command/src/commands/dataframe/command.rs deleted file mode 100644 index 09ea55aba6..0000000000 --- a/crates/nu-command/src/commands/dataframe/command.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, UntaggedValue}; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "dataframe" - } - - fn usage(&self) -> &str { - "Commands to work with polars dataframes" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe") - } - - fn run(&self, args: CommandArgs) -> Result { - Ok(OutputStream::one( - UntaggedValue::string(get_full_help(&Command, args.scope())).into_value(Tag::unknown()), - )) - } -} diff --git a/crates/nu-command/src/commands/dataframe/describe.rs b/crates/nu-command/src/commands/dataframe/describe.rs deleted file mode 100644 index 6cd06bfbca..0000000000 --- a/crates/nu-command/src/commands/dataframe/describe.rs +++ /dev/null @@ -1,232 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, -}; -use polars::{ - chunked_array::ChunkedArray, - prelude::{ - AnyValue, DataFrame as PolarsDF, DataType, Float64Type, IntoSeries, NewChunkedArray, - Series, Utf8Type, - }, -}; - -use super::utils::parse_polars_error; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe describe" - } - - fn usage(&self) -> &str { - "[DataFrame] Describes dataframes numeric columns" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe describe") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Describes dataframe", - example: "[[a b]; [1 1] [1 1]] | dataframe to-df | dataframe describe", - result: Some(vec![NuDataFrame::try_from_columns( - vec![ - Column::new( - "descriptor".to_string(), - vec![ - UntaggedValue::string("count").into(), - UntaggedValue::string("sum").into(), - UntaggedValue::string("mean").into(), - UntaggedValue::string("median").into(), - UntaggedValue::string("std").into(), - UntaggedValue::string("min").into(), - UntaggedValue::string("25%").into(), - UntaggedValue::string("50%").into(), - UntaggedValue::string("75%").into(), - UntaggedValue::string("max").into(), - ], - ), - Column::new( - "a (i64)".to_string(), - vec![ - UntaggedValue::decimal_from_float(2.0, Span::default()).into(), - UntaggedValue::decimal_from_float(2.0, Span::default()).into(), - UntaggedValue::decimal_from_float(1.0, Span::default()).into(), - UntaggedValue::decimal_from_float(1.0, Span::default()).into(), - UntaggedValue::decimal_from_float(0.0, Span::default()).into(), - UntaggedValue::decimal_from_float(1.0, Span::default()).into(), - UntaggedValue::decimal_from_float(1.0, Span::default()).into(), - UntaggedValue::decimal_from_float(1.0, Span::default()).into(), - UntaggedValue::decimal_from_float(1.0, Span::default()).into(), - UntaggedValue::decimal_from_float(1.0, Span::default()).into(), - ], - ), - Column::new( - "b (i64)".to_string(), - vec![ - UntaggedValue::decimal_from_float(2.0, Span::default()).into(), - UntaggedValue::decimal_from_float(2.0, Span::default()).into(), - UntaggedValue::decimal_from_float(1.0, Span::default()).into(), - UntaggedValue::decimal_from_float(1.0, Span::default()).into(), - UntaggedValue::decimal_from_float(0.0, Span::default()).into(), - UntaggedValue::decimal_from_float(1.0, Span::default()).into(), - UntaggedValue::decimal_from_float(1.0, Span::default()).into(), - UntaggedValue::decimal_from_float(1.0, Span::default()).into(), - UntaggedValue::decimal_from_float(1.0, Span::default()).into(), - UntaggedValue::decimal_from_float(1.0, Span::default()).into(), - ], - ), - ], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, _) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let names = ChunkedArray::::new_from_opt_slice( - "descriptor", - &[ - Some("count"), - Some("sum"), - Some("mean"), - Some("median"), - Some("std"), - Some("min"), - Some("25%"), - Some("50%"), - Some("75%"), - Some("max"), - ], - ) - .into_series(); - - let head = std::iter::once(names); - - let tail = df.as_ref().get_columns().iter().map(|col| { - let count = col.len() as f64; - - let sum = match col.sum_as_series().cast(&DataType::Float64) { - Ok(ca) => match ca.get(0) { - AnyValue::Float64(v) => Some(v), - _ => None, - }, - Err(_) => None, - }; - - let mean = match col.mean_as_series().get(0) { - AnyValue::Float64(v) => Some(v), - _ => None, - }; - - let median = match col.median_as_series().get(0) { - AnyValue::Float64(v) => Some(v), - _ => None, - }; - - let std = match col.std_as_series().get(0) { - AnyValue::Float64(v) => Some(v), - _ => None, - }; - - let min = match col.min_as_series().cast(&DataType::Float64) { - Ok(ca) => match ca.get(0) { - AnyValue::Float64(v) => Some(v), - _ => None, - }, - Err(_) => None, - }; - - let q_25 = match col.quantile_as_series(0.25) { - Ok(ca) => match ca.cast(&DataType::Float64) { - Ok(ca) => match ca.get(0) { - AnyValue::Float64(v) => Some(v), - _ => None, - }, - Err(_) => None, - }, - Err(_) => None, - }; - - let q_50 = match col.quantile_as_series(0.50) { - Ok(ca) => match ca.cast(&DataType::Float64) { - Ok(ca) => match ca.get(0) { - AnyValue::Float64(v) => Some(v), - _ => None, - }, - Err(_) => None, - }, - Err(_) => None, - }; - - let q_75 = match col.quantile_as_series(0.75) { - Ok(ca) => match ca.cast(&DataType::Float64) { - Ok(ca) => match ca.get(0) { - AnyValue::Float64(v) => Some(v), - _ => None, - }, - Err(_) => None, - }, - Err(_) => None, - }; - - let max = match col.max_as_series().cast(&DataType::Float64) { - Ok(ca) => match ca.get(0) { - AnyValue::Float64(v) => Some(v), - _ => None, - }, - Err(_) => None, - }; - - let name = format!("{} ({})", col.name(), col.dtype()); - ChunkedArray::::new_from_opt_slice( - &name, - &[ - Some(count), - sum, - mean, - median, - std, - min, - q_25, - q_50, - q_75, - max, - ], - ) - .into_series() - }); - - let res = head.chain(tail).collect::>(); - let df = PolarsDF::new(res).map_err(|e| parse_polars_error::<&str>(&e, &tag.span, None))?; - let df = NuDataFrame::dataframe_to_value(df, tag); - Ok(OutputStream::one(df)) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/drop.rs b/crates/nu-command/src/commands/dataframe/drop.rs deleted file mode 100644 index eef03a3ff7..0000000000 --- a/crates/nu-command/src/commands/dataframe/drop.rs +++ /dev/null @@ -1,93 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, SyntaxShape, UntaggedValue, Value, -}; - -use super::utils::{convert_columns, parse_polars_error}; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe drop" - } - - fn usage(&self) -> &str { - "[DataFrame] Creates a new dataframe by dropping the selected columns" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe drop").rest( - "rest", - SyntaxShape::Any, - "column names to be dropped", - ) - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "drop column a", - example: "[[a b]; [1 2] [3 4]] | dataframe to-df | dataframe drop a", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "b".to_string(), - vec![UntaggedValue::int(2).into(), UntaggedValue::int(4).into()], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let columns: Vec = args.rest(0)?; - let (col_string, col_span) = convert_columns(&columns, &tag)?; - - let (df, _) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let new_df = match col_string.get(0) { - Some(col) => df - .as_ref() - .drop(col) - .map_err(|e| parse_polars_error::<&str>(&e, &col_span, None)), - None => Err(ShellError::labeled_error( - "Empty names list", - "No column names where found", - &col_span, - )), - }?; - - // If there are more columns in the drop selection list, these - // are added from the resulting dataframe - let res = col_string.iter().skip(1).try_fold(new_df, |new_df, col| { - new_df - .drop(col) - .map_err(|e| parse_polars_error::<&str>(&e, &col_span, None)) - })?; - - Ok(OutputStream::one(NuDataFrame::dataframe_to_value(res, tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/drop_duplicates.rs b/crates/nu-command/src/commands/dataframe/drop_duplicates.rs deleted file mode 100644 index 31420c3bc9..0000000000 --- a/crates/nu-command/src/commands/dataframe/drop_duplicates.rs +++ /dev/null @@ -1,95 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, SyntaxShape, UntaggedValue, Value, -}; - -use super::utils::{convert_columns, parse_polars_error}; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe drop-duplicates" - } - - fn usage(&self) -> &str { - "[DataFrame] Drops duplicate values in dataframe" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe drop-duplicates") - .optional( - "subset", - SyntaxShape::Table, - "subset of columns to drop duplicates", - ) - .switch("maintain", "maintain order", Some('m')) - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "drop duplicates", - example: "[[a b]; [1 2] [3 4] [1 2]] | dataframe to-df | dataframe drop-duplicates", - result: Some(vec![NuDataFrame::try_from_columns( - vec![ - Column::new( - "a".to_string(), - vec![UntaggedValue::int(1).into(), UntaggedValue::int(3).into()], - ), - Column::new( - "b".to_string(), - vec![UntaggedValue::int(2).into(), UntaggedValue::int(4).into()], - ), - ], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - // Extracting the selection columns of the columns to perform the aggregation - let columns: Option> = args.opt(0)?; - let (subset, col_span) = match columns { - Some(cols) => { - let (agg_string, col_span) = convert_columns(&cols, &tag)?; - (Some(agg_string), col_span) - } - None => (None, Span::unknown()), - }; - - let (df, _) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let subset_slice = subset.as_ref().map(|cols| &cols[..]); - - let res = df - .as_ref() - .drop_duplicates(args.has_flag("maintain"), subset_slice) - .map_err(|e| parse_polars_error::<&str>(&e, &col_span, None))?; - - Ok(OutputStream::one(NuDataFrame::dataframe_to_value(res, tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/drop_nulls.rs b/crates/nu-command/src/commands/dataframe/drop_nulls.rs deleted file mode 100644 index f974d914ac..0000000000 --- a/crates/nu-command/src/commands/dataframe/drop_nulls.rs +++ /dev/null @@ -1,132 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, SyntaxShape, UntaggedValue, Value, -}; - -use super::utils::{convert_columns, parse_polars_error}; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe drop-nulls" - } - - fn usage(&self) -> &str { - "[DataFrame, Series] Drops null values in dataframe" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe drop-nulls").optional( - "subset", - SyntaxShape::Table, - "subset of columns to drop duplicates", - ) - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "drop null values in dataframe", - example: r#"let df = ([[a b]; [1 2] [3 0] [1 2]] | dataframe to-df); - let res = ($df.b / $df.b); - let df = ($df | dataframe with-column $res --name res); - $df | dataframe drop-nulls"#, - result: Some(vec![NuDataFrame::try_from_columns( - vec![ - Column::new( - "a".to_string(), - vec![UntaggedValue::int(1).into(), UntaggedValue::int(1).into()], - ), - Column::new( - "b".to_string(), - vec![UntaggedValue::int(2).into(), UntaggedValue::int(2).into()], - ), - Column::new( - "res".to_string(), - vec![UntaggedValue::int(1).into(), UntaggedValue::int(1).into()], - ), - ], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }, - Example { - description: "drop null values in dataframe", - example: r#"let s = ([1 2 0 0 3 4] | dataframe to-df); - ($s / $s) | dataframe drop-nulls"#, - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "div_0_0".to_string(), - vec![ - UntaggedValue::int(1).into(), - UntaggedValue::int(1).into(), - UntaggedValue::int(1).into(), - UntaggedValue::int(1).into(), - ], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }, - ] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let value = args.input.next().ok_or_else(|| { - ShellError::labeled_error("Empty stream", "No value found in stream", &tag.span) - })?; - - match value.value { - UntaggedValue::DataFrame(df) => { - // Extracting the selection columns of the columns to perform the aggregation - let columns: Option> = args.opt(0)?; - let (subset, col_span) = match columns { - Some(cols) => { - let (agg_string, col_span) = convert_columns(&cols, &tag)?; - (Some(agg_string), col_span) - } - None => (None, Span::unknown()), - }; - - let subset_slice = subset.as_ref().map(|cols| &cols[..]); - - let res = df - .as_ref() - .drop_nulls(subset_slice) - .map_err(|e| parse_polars_error::<&str>(&e, &col_span, None))?; - - Ok(OutputStream::one(NuDataFrame::dataframe_to_value(res, tag))) - } - _ => Err(ShellError::labeled_error( - "Incorrect type", - "drop nulls cannot be done with this value", - &value.tag.span, - )), - } -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/dtypes.rs b/crates/nu-command/src/commands/dataframe/dtypes.rs deleted file mode 100644 index ac997ef558..0000000000 --- a/crates/nu-command/src/commands/dataframe/dtypes.rs +++ /dev/null @@ -1,106 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, Value, -}; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe dtypes" - } - - fn usage(&self) -> &str { - "[DataFrame] Show dataframe data types" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe dtypes") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "drop column a", - example: "[[a b]; [1 2] [3 4]] | dataframe to-df | dataframe dtypes", - result: Some(vec![NuDataFrame::try_from_columns( - vec![ - Column::new( - "column".to_string(), - vec![ - UntaggedValue::string("a").into(), - UntaggedValue::string("b").into(), - ], - ), - Column::new( - "dtype".to_string(), - vec![ - UntaggedValue::string("i64").into(), - UntaggedValue::string("i64").into(), - ], - ), - ], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -#[allow(clippy::needless_collect)] -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, _) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let mut dtypes: Vec = Vec::new(); - let names: Vec = df - .as_ref() - .get_column_names() - .iter() - .map(|v| { - let dtype = df - .as_ref() - .column(v) - .expect("using name from list of names from dataframe") - .dtype(); - - let dtype_str = dtype.to_string(); - dtypes.push(Value { - value: dtype_str.into(), - tag: Tag::default(), - }); - - Value { - value: v.to_string().into(), - tag: Tag::default(), - } - }) - .collect(); - - let names_col = Column::new("column".to_string(), names); - let dtypes_col = Column::new("dtype".to_string(), dtypes); - - let df = NuDataFrame::try_from_columns(vec![names_col, dtypes_col], &tag.span)?; - Ok(OutputStream::one(df.into_value(tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/dummies.rs b/crates/nu-command/src/commands/dataframe/dummies.rs deleted file mode 100644 index cd977712e6..0000000000 --- a/crates/nu-command/src/commands/dataframe/dummies.rs +++ /dev/null @@ -1,142 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, -}; - -use super::utils::parse_polars_error; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe to-dummies" - } - - fn usage(&self) -> &str { - "[DataFrame] Creates a new dataframe with dummy variables" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe to-dummies") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Create new dataframe with dummy variables from a dataframe", - example: "[[a b]; [1 2] [3 4]] | dataframe to-df | dataframe to-dummies", - result: Some(vec![NuDataFrame::try_from_columns( - vec![ - Column::new( - "a_1".to_string(), - vec![UntaggedValue::int(1).into(), UntaggedValue::int(0).into()], - ), - Column::new( - "a_3".to_string(), - vec![UntaggedValue::int(0).into(), UntaggedValue::int(1).into()], - ), - Column::new( - "b_2".to_string(), - vec![UntaggedValue::int(1).into(), UntaggedValue::int(0).into()], - ), - Column::new( - "b_4".to_string(), - vec![UntaggedValue::int(0).into(), UntaggedValue::int(1).into()], - ), - ], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }, - Example { - description: "Create new dataframe with dummy variables from a series", - example: "[1 2 2 3 3] | dataframe to-df | dataframe to-dummies", - result: Some(vec![NuDataFrame::try_from_columns( - vec![ - Column::new( - "0_1".to_string(), - vec![ - UntaggedValue::int(1).into(), - UntaggedValue::int(0).into(), - UntaggedValue::int(0).into(), - UntaggedValue::int(0).into(), - UntaggedValue::int(0).into(), - ], - ), - Column::new( - "0_2".to_string(), - vec![ - UntaggedValue::int(0).into(), - UntaggedValue::int(1).into(), - UntaggedValue::int(1).into(), - UntaggedValue::int(0).into(), - UntaggedValue::int(0).into(), - ], - ), - Column::new( - "0_3".to_string(), - vec![ - UntaggedValue::int(0).into(), - UntaggedValue::int(0).into(), - UntaggedValue::int(0).into(), - UntaggedValue::int(1).into(), - UntaggedValue::int(1).into(), - ], - ), - ], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }, - ] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let value = args.input.next().ok_or_else(|| { - ShellError::labeled_error("Empty stream", "No value found in stream", &tag.span) - })?; - - match value.value { - UntaggedValue::DataFrame(df) => { - let res = df.as_ref().to_dummies().map_err(|e| { - parse_polars_error( - &e, - &tag.span, - Some("The only allowed column types for dummies are String or Int"), - ) - })?; - - Ok(OutputStream::one(NuDataFrame::dataframe_to_value(res, tag))) - } - _ => Err(ShellError::labeled_error( - "Incorrect type", - "dummies cannot be done with this value", - &value.tag.span, - )), - } -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/filter.rs b/crates/nu-command/src/commands/dataframe/filter.rs deleted file mode 100644 index 26a485028d..0000000000 --- a/crates/nu-command/src/commands/dataframe/filter.rs +++ /dev/null @@ -1,102 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, SyntaxShape, UntaggedValue, Value, -}; - -use super::utils::parse_polars_error; -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe filter-with" - } - - fn usage(&self) -> &str { - "[DataFrame] Filters dataframe using a mask as reference" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe filter-with").required( - "mask", - SyntaxShape::Any, - "boolean mask used to filter data", - ) - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Filter dataframe using a bool mask", - example: r#"let mask = ([$true $false] | dataframe to-df); - [[a b]; [1 2] [3 4]] | dataframe to-df | dataframe filter-with $mask"#, - result: Some(vec![NuDataFrame::try_from_columns( - vec![ - Column::new("a".to_string(), vec![UntaggedValue::int(1).into()]), - Column::new("b".to_string(), vec![UntaggedValue::int(2).into()]), - ], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }, - Example { - description: "Filter dataframe by creating a mask from operation", - example: r#"let mask = (([5 6] | dataframe to-df) > 5); - [[a b]; [1 2] [3 4]] | dataframe to-df | dataframe filter-with $mask"#, - result: None, - }, - ] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let value: Value = args.req(0)?; - - let series_span = value.tag.span; - let df = match value.value { - UntaggedValue::DataFrame(df) => Ok(df), - _ => Err(ShellError::labeled_error( - "Incorrect type", - "can only add a series to a dataframe", - value.tag.span, - )), - }?; - let series = df.as_series(&series_span)?; - let casted = series.bool().map_err(|e| { - parse_polars_error( - &e, - &series_span, - Some("Perhaps you want to use a series with booleans as mask"), - ) - })?; - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let res = df - .as_ref() - .filter(casted) - .map_err(|e| parse_polars_error::<&str>(&e, &df_tag.span, None))?; - - Ok(OutputStream::one(NuDataFrame::dataframe_to_value(res, tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/first.rs b/crates/nu-command/src/commands/dataframe/first.rs deleted file mode 100644 index 0c23f4d59f..0000000000 --- a/crates/nu-command/src/commands/dataframe/first.rs +++ /dev/null @@ -1,77 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, SyntaxShape, UntaggedValue, -}; - -use nu_source::Tagged; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe first" - } - - fn usage(&self) -> &str { - "[DataFrame] Creates new dataframe with first rows" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe select").optional( - "rows", - SyntaxShape::Number, - "Number of rows for head", - ) - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Create new dataframe with head rows", - example: "[[a b]; [1 2] [3 4]] | dataframe to-df | dataframe first 1", - result: Some(vec![NuDataFrame::try_from_columns( - vec![ - Column::new("a".to_string(), vec![UntaggedValue::int(1).into()]), - Column::new("b".to_string(), vec![UntaggedValue::int(2).into()]), - ], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let rows: Option> = args.opt(0)?; - - let rows = match rows { - Some(val) => val.item, - None => 5, - }; - - let (df, _) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - let res = df.as_ref().head(Some(rows)); - - Ok(OutputStream::one(NuDataFrame::dataframe_to_value(res, tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/get.rs b/crates/nu-command/src/commands/dataframe/get.rs deleted file mode 100644 index ebb8e6b853..0000000000 --- a/crates/nu-command/src/commands/dataframe/get.rs +++ /dev/null @@ -1,77 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, SyntaxShape, UntaggedValue, Value, -}; - -use super::utils::{convert_columns, parse_polars_error}; -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe get" - } - - fn usage(&self) -> &str { - "[DataFrame] Creates dataframe with the selected columns" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe get").rest( - "rest", - SyntaxShape::Any, - "column names to sort dataframe", - ) - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Creates dataframe with selected columns", - example: "[[a b]; [1 2] [3 4]] | dataframe to-df | dataframe get a", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "a".to_string(), - vec![UntaggedValue::int(1).into(), UntaggedValue::int(3).into()], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let columns: Vec = args.rest(0)?; - - let (col_string, col_span) = convert_columns(&columns, &tag)?; - - let (df, _) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let res = df - .as_ref() - .select(&col_string) - .map_err(|e| parse_polars_error::<&str>(&e, &col_span, None))?; - - Ok(OutputStream::one(NuDataFrame::dataframe_to_value(res, tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/groupby.rs b/crates/nu-command/src/commands/dataframe/groupby.rs deleted file mode 100644 index 3236f6d1c8..0000000000 --- a/crates/nu-command/src/commands/dataframe/groupby.rs +++ /dev/null @@ -1,68 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{FrameStruct, NuDataFrame, NuGroupBy}, - Signature, SyntaxShape, UntaggedValue, Value, -}; - -use super::utils::convert_columns; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe group-by" - } - - fn usage(&self) -> &str { - "[DataFrame] Creates a groupby object that can be used for other aggregations" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe group-by").rest("rest", SyntaxShape::Any, "groupby columns") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Grouping by column a", - example: "[[a b]; [one 1] [one 2]] | dataframe to-df | dataframe group-by a", - result: None, - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - // Extracting the names of the columns to perform the groupby - let by_columns: Vec = args.rest(0)?; - let (columns_string, col_span) = convert_columns(&by_columns, &tag)?; - - let (df, _) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - // This is the expensive part of the groupby; to create the - // groups that will be used for grouping the data in the - // dataframe. Once it has been done these values can be stored - // in a NuGroupBy - let groupby = df - .as_ref() - .groupby(&columns_string) - .map_err(|e| parse_polars_error::<&str>(&e, &col_span, None))?; - - let groups = groupby.get_groups().to_vec(); - let groupby = Value { - tag, - value: UntaggedValue::FrameStruct(FrameStruct::GroupBy(NuGroupBy::new( - NuDataFrame::new(df.as_ref().clone()), - columns_string, - groups, - ))), - }; - - Ok(OutputStream::one(groupby)) -} diff --git a/crates/nu-command/src/commands/dataframe/join.rs b/crates/nu-command/src/commands/dataframe/join.rs deleted file mode 100644 index da18eb8bfd..0000000000 --- a/crates/nu-command/src/commands/dataframe/join.rs +++ /dev/null @@ -1,233 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, SyntaxShape, UntaggedValue, Value, -}; - -use super::utils::{convert_columns, parse_polars_error}; - -use polars::prelude::JoinType; - -use nu_source::Tagged; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe join" - } - - fn usage(&self) -> &str { - "[DataFrame] Joins a dataframe using columns as reference" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe join") - .required("dataframe", SyntaxShape::Any, "right dataframe to join") - .required_named( - "left", - SyntaxShape::Table, - "left column names to perform join", - Some('l'), - ) - .required_named( - "right", - SyntaxShape::Table, - "right column names to perform join", - Some('r'), - ) - .named( - "type", - SyntaxShape::String, - "type of join. Inner by default", - Some('t'), - ) - .named( - "suffix", - SyntaxShape::String, - "suffix for the columns of the right dataframe", - Some('s'), - ) - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "inner join dataframe", - example: r#"let right = ([[a b c]; [1 2 5] [3 4 5] [5 6 6]] | dataframe to-df); - $right | dataframe join $right -l [a b] -r [a b]"#, - result: Some(vec![NuDataFrame::try_from_columns( - vec![ - Column::new( - "a".to_string(), - vec![ - UntaggedValue::int(1).into(), - UntaggedValue::int(3).into(), - UntaggedValue::int(5).into(), - ], - ), - Column::new( - "b".to_string(), - vec![ - UntaggedValue::int(2).into(), - UntaggedValue::int(4).into(), - UntaggedValue::int(6).into(), - ], - ), - Column::new( - "c".to_string(), - vec![ - UntaggedValue::int(5).into(), - UntaggedValue::int(5).into(), - UntaggedValue::int(6).into(), - ], - ), - Column::new( - "c_right".to_string(), - vec![ - UntaggedValue::int(5).into(), - UntaggedValue::int(5).into(), - UntaggedValue::int(6).into(), - ], - ), - ], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let r_df: Value = args.req(0)?; - let l_col: Vec = args.req_named("left")?; - let r_col: Vec = args.req_named("right")?; - let r_suffix: Option> = args.get_flag("suffix")?; - let join_type_op: Option> = args.get_flag("type")?; - - let join_type = match join_type_op { - None => JoinType::Inner, - Some(val) => match val.item.as_ref() { - "inner" => JoinType::Inner, - "outer" => JoinType::Outer, - "left" => JoinType::Left, - _ => { - return Err(ShellError::labeled_error_with_secondary( - "Incorrect join type", - "Invalid join type", - &val.tag, - "Perhaps you mean: inner, outer or left", - &val.tag, - )) - } - }, - }; - - let suffix = r_suffix.map(|s| s.item); - - let (l_col_string, l_col_span) = convert_columns(&l_col, &tag)?; - let (r_col_string, r_col_span) = convert_columns(&r_col, &tag)?; - - let (df, _) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let res = match r_df.value { - UntaggedValue::DataFrame(r_df) => { - // Checking the column types before performing the join - check_column_datatypes( - df.as_ref(), - r_df.as_ref(), - &l_col_string, - &l_col_span, - &r_col_string, - &r_col_span, - )?; - - df.as_ref() - .join( - r_df.as_ref(), - &l_col_string, - &r_col_string, - join_type, - suffix, - ) - .map_err(|e| parse_polars_error::<&str>(&e, &l_col_span, None)) - } - _ => Err(ShellError::labeled_error( - "Not a dataframe", - "not a dataframe type value", - &r_df.tag, - )), - }?; - - Ok(OutputStream::one(NuDataFrame::dataframe_to_value(res, tag))) -} - -fn check_column_datatypes>( - df_l: &polars::prelude::DataFrame, - df_r: &polars::prelude::DataFrame, - l_cols: &[T], - l_col_span: &Span, - r_cols: &[T], - r_col_span: &Span, -) -> Result<(), ShellError> { - if l_cols.len() != r_cols.len() { - return Err(ShellError::labeled_error_with_secondary( - "Mismatched number of column names", - format!( - "found {} left names vs {} right names", - l_cols.len(), - r_cols.len() - ), - l_col_span, - "perhaps you need to change the number of columns to join", - r_col_span, - )); - } - - for (l, r) in l_cols.iter().zip(r_cols) { - let l_series = df_l - .column(l.as_ref()) - .map_err(|e| parse_polars_error::<&str>(&e, l_col_span, None))?; - - let r_series = df_r - .column(r.as_ref()) - .map_err(|e| parse_polars_error::<&str>(&e, r_col_span, None))?; - - if l_series.dtype() != r_series.dtype() { - return Err(ShellError::labeled_error_with_secondary( - "Mismatched datatypes", - format!( - "left column type '{}' doesn't match '{}' right column match", - l_series.dtype(), - r_series.dtype() - ), - l_col_span, - "perhaps you need to select other column to match", - r_col_span, - )); - } - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/last.rs b/crates/nu-command/src/commands/dataframe/last.rs deleted file mode 100644 index 1838570cae..0000000000 --- a/crates/nu-command/src/commands/dataframe/last.rs +++ /dev/null @@ -1,77 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, SyntaxShape, UntaggedValue, -}; - -use nu_source::Tagged; -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe last" - } - - fn usage(&self) -> &str { - "[DataFrame] Creates new dataframe with tail rows" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe last").optional( - "n_rows", - SyntaxShape::Number, - "Number of rows for tail", - ) - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Create new dataframe with last rows", - example: "[[a b]; [1 2] [3 4]] | dataframe to-df | dataframe last 1", - result: Some(vec![NuDataFrame::try_from_columns( - vec![ - Column::new("a".to_string(), vec![UntaggedValue::int(3).into()]), - Column::new("b".to_string(), vec![UntaggedValue::int(4).into()]), - ], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let rows: Option> = args.opt(0)?; - - let rows = match rows { - Some(val) => val.item, - None => 5, - }; - - let (df, _) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let res = df.as_ref().tail(Some(rows)); - - Ok(OutputStream::one(NuDataFrame::dataframe_to_value(res, tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/list.rs b/crates/nu-command/src/commands/dataframe/list.rs deleted file mode 100644 index 56e78ce8bf..0000000000 --- a/crates/nu-command/src/commands/dataframe/list.rs +++ /dev/null @@ -1,115 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, Value, -}; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe list" - } - - fn usage(&self) -> &str { - "Lists stored dataframes" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe list") - } - - fn run(&self, args: CommandArgs) -> Result { - let data = args - .context - .scope - .get_vars() - .into_iter() - .filter_map(|(name, value)| { - if let UntaggedValue::DataFrame(df) = &value.value { - let rows = Value { - value: (df.as_ref().height() as i64).into(), - tag: Tag::default(), - }; - - let cols = Value { - value: (df.as_ref().width() as i64).into(), - tag: Tag::default(), - }; - - let location = match value.tag.anchor { - Some(AnchorLocation::File(name)) => name, - Some(AnchorLocation::Url(name)) => name, - Some(AnchorLocation::Source(text)) => text.slice(0..text.end).text, - None => "stream".to_string(), - }; - - let location = Value { - value: location.into(), - tag: Tag::default(), - }; - - let name = Value { - value: name.into(), - tag: Tag::default(), - }; - - Some((name, rows, cols, location)) - } else { - None - } - }); - - let mut name = Column::new_empty("name".to_string()); - let mut rows = Column::new_empty("rows".to_string()); - let mut cols = Column::new_empty("columns".to_string()); - let mut location = Column::new_empty("location".to_string()); - - for tuple in data { - name.push(tuple.0); - rows.push(tuple.1); - cols.push(tuple.2); - location.push(tuple.3); - } - - let tag = args.call_info.name_tag; - let df = NuDataFrame::try_from_columns(vec![name, rows, cols, location], &tag.span)?; - Ok(OutputStream::one(df.into_value(tag))) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Lists loaded dataframes in current scope", - example: "let a = ([[a b];[1 2] [3 4]] | dataframe to-df); dataframe list", - result: Some(vec![NuDataFrame::try_from_columns( - vec![ - Column::new("name".to_string(), vec![UntaggedValue::string("$a").into()]), - Column::new("rows".to_string(), vec![UntaggedValue::int(2).into()]), - Column::new("columns".to_string(), vec![UntaggedValue::int(2).into()]), - Column::new( - "location".to_string(), - vec![UntaggedValue::string("stream").into()], - ), - ], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/melt.rs b/crates/nu-command/src/commands/dataframe/melt.rs deleted file mode 100644 index 702571920e..0000000000 --- a/crates/nu-command/src/commands/dataframe/melt.rs +++ /dev/null @@ -1,204 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, SyntaxShape, UntaggedValue, Value, -}; -use nu_source::Tagged; - -use super::utils::convert_columns; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe melt" - } - - fn usage(&self) -> &str { - "[DataFrame] Unpivot a DataFrame from wide to long format" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe melt") - .required_named( - "columns", - SyntaxShape::Table, - "column names for melting", - Some('c'), - ) - .required_named( - "values", - SyntaxShape::Table, - "column names used as value columns", - Some('v'), - ) - .named( - "variable_name", - SyntaxShape::String, - "optional name for variable column", - Some('r'), - ) - .named( - "value_name", - SyntaxShape::String, - "optional name for value column", - Some('l'), - ) - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "melt dataframe", - example: - "[[a b c d]; [x 1 4 a] [y 2 5 b] [z 3 6 c]] | dataframe to-df | dataframe melt -c [b c] -v [a d]", - result: Some(vec![NuDataFrame::try_from_columns( - vec![ - Column::new( - "b".to_string(), - vec![ - UntaggedValue::int(1).into(), - UntaggedValue::int(2).into(), - UntaggedValue::int(3).into(), - UntaggedValue::int(1).into(), - UntaggedValue::int(2).into(), - UntaggedValue::int(3).into(), - ], - ), - Column::new( - "c".to_string(), - vec![ - UntaggedValue::int(4).into(), - UntaggedValue::int(5).into(), - UntaggedValue::int(6).into(), - UntaggedValue::int(4).into(), - UntaggedValue::int(5).into(), - UntaggedValue::int(6).into(), - ], - ), - Column::new( - "variable".to_string(), - vec![ - UntaggedValue::string("a").into(), - UntaggedValue::string("a").into(), - UntaggedValue::string("a").into(), - UntaggedValue::string("d").into(), - UntaggedValue::string("d").into(), - UntaggedValue::string("d").into(), - ], - ), - Column::new( - "value".to_string(), - vec![ - UntaggedValue::string("x").into(), - UntaggedValue::string("y").into(), - UntaggedValue::string("z").into(), - UntaggedValue::string("a").into(), - UntaggedValue::string("b").into(), - UntaggedValue::string("c").into(), - ], - ), - ], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let id_col: Vec = args.req_named("columns")?; - let val_col: Vec = args.req_named("values")?; - - let value_name: Option> = args.get_flag("value_name")?; - let variable_name: Option> = args.get_flag("variable_name")?; - - let (id_col_string, id_col_span) = convert_columns(&id_col, &tag)?; - let (val_col_string, val_col_span) = convert_columns(&val_col, &tag)?; - - let (df, _) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - check_column_datatypes(df.as_ref(), &id_col_string, &id_col_span)?; - check_column_datatypes(df.as_ref(), &val_col_string, &val_col_span)?; - - let mut res = df - .as_ref() - .melt(&id_col_string, &val_col_string) - .map_err(|e| parse_polars_error::<&str>(&e, &tag.span, None))?; - - if let Some(name) = &variable_name { - res.rename("variable", &name.item) - .map_err(|e| parse_polars_error::<&str>(&e, &name.tag.span, None))?; - } - - if let Some(name) = &value_name { - res.rename("value", &name.item) - .map_err(|e| parse_polars_error::<&str>(&e, &name.tag.span, None))?; - } - - Ok(OutputStream::one(NuDataFrame::dataframe_to_value(res, tag))) -} - -fn check_column_datatypes>( - df: &polars::prelude::DataFrame, - cols: &[T], - col_span: &Span, -) -> Result<(), ShellError> { - if cols.is_empty() { - return Err(ShellError::labeled_error( - "Merge error", - "empty column list", - col_span, - )); - } - - // Checking if they are same type - if cols.len() > 1 { - for w in cols.windows(2) { - let l_series = df - .column(w[0].as_ref()) - .map_err(|e| parse_polars_error::<&str>(&e, col_span, None))?; - - let r_series = df - .column(w[1].as_ref()) - .map_err(|e| parse_polars_error::<&str>(&e, col_span, None))?; - - if l_series.dtype() != r_series.dtype() { - return Err(ShellError::labeled_error_with_secondary( - "Merge error", - "found different column types in list", - col_span, - format!( - "datatypes {} and {} are incompatible", - l_series.dtype(), - r_series.dtype() - ), - col_span, - )); - } - } - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/mod.rs b/crates/nu-command/src/commands/dataframe/mod.rs deleted file mode 100644 index 4df4a935f9..0000000000 --- a/crates/nu-command/src/commands/dataframe/mod.rs +++ /dev/null @@ -1,112 +0,0 @@ -pub mod aggregate; -pub mod append; -pub mod column; -pub mod command; -pub mod describe; -pub mod drop; -pub mod drop_duplicates; -pub mod drop_nulls; -pub mod dtypes; -pub mod dummies; -pub mod filter; -pub mod first; -pub mod get; -pub mod groupby; -pub mod join; -pub mod last; -pub mod list; -pub mod melt; -pub mod open; -pub mod pivot; -pub mod rename; -pub mod sample; -pub mod select; -pub mod shape; -pub mod show; -pub mod slice; -pub mod sort; -pub mod take; -pub mod to_csv; -pub mod to_df; -pub mod to_parquet; -pub(crate) mod utils; -pub mod where_; -pub mod with_column; - -pub use aggregate::DataFrame as DataFrameAggregate; -pub use append::DataFrame as DataFrameAppend; -pub use column::DataFrame as DataFrameColumn; -pub use command::Command as DataFrame; -pub use describe::DataFrame as DataFrameDescribe; -pub use drop::DataFrame as DataFrameDrop; -pub use drop_duplicates::DataFrame as DataFrameDropDuplicates; -pub use drop_nulls::DataFrame as DataFrameDropNulls; -pub use dtypes::DataFrame as DataFrameDTypes; -pub use dummies::DataFrame as DataFrameDummies; -pub use filter::DataFrame as DataFrameFilter; -pub use first::DataFrame as DataFrameFirst; -pub use get::DataFrame as DataFrameGet; -pub use groupby::DataFrame as DataFrameGroupBy; -pub use join::DataFrame as DataFrameJoin; -pub use last::DataFrame as DataFrameLast; -pub use list::DataFrame as DataFrameList; -pub use melt::DataFrame as DataFrameMelt; -pub use open::DataFrame as DataFrameOpen; -pub use pivot::DataFrame as DataFramePivot; -pub use rename::DataFrame as DataFrameRename; -pub use sample::DataFrame as DataFrameSample; -pub use select::DataFrame as DataFrameSelect; -pub use shape::DataFrame as DataFrameShape; -pub use show::DataFrame as DataFrameShow; -pub use slice::DataFrame as DataFrameSlice; -pub use sort::DataFrame as DataFrameSort; -pub use take::DataFrame as DataFrameTake; -pub use to_csv::DataFrame as DataFrameToCsv; -pub use to_df::DataFrame as DataFrameToDF; -pub use to_parquet::DataFrame as DataFrameToParquet; -pub use where_::DataFrame as DataFrameWhere; -pub use with_column::DataFrame as DataFrameWithColumn; - -pub mod series; -pub use series::DataFrameAllFalse; -pub use series::DataFrameAllTrue; -pub use series::DataFrameArgMax; -pub use series::DataFrameArgMin; -pub use series::DataFrameArgSort; -pub use series::DataFrameArgTrue; -pub use series::DataFrameArgUnique; -pub use series::DataFrameConcatenate; -pub use series::DataFrameContains; -pub use series::DataFrameCumulative; -pub use series::DataFrameGetDay; -pub use series::DataFrameGetHour; -pub use series::DataFrameGetMinute; -pub use series::DataFrameGetMonth; -pub use series::DataFrameGetNanoSecond; -pub use series::DataFrameGetOrdinal; -pub use series::DataFrameGetSecond; -pub use series::DataFrameGetWeek; -pub use series::DataFrameGetWeekDay; -pub use series::DataFrameGetYear; -pub use series::DataFrameIsDuplicated; -pub use series::DataFrameIsIn; -pub use series::DataFrameIsNotNull; -pub use series::DataFrameIsNull; -pub use series::DataFrameIsUnique; -pub use series::DataFrameNNull; -pub use series::DataFrameNUnique; -pub use series::DataFrameNot; -pub use series::DataFrameReplace; -pub use series::DataFrameReplaceAll; -pub use series::DataFrameRolling; -pub use series::DataFrameSeriesRename; -pub use series::DataFrameSet; -pub use series::DataFrameSetWithIdx; -pub use series::DataFrameShift; -pub use series::DataFrameStrFTime; -pub use series::DataFrameStringLengths; -pub use series::DataFrameStringSlice; -pub use series::DataFrameToLowercase; -pub use series::DataFrameToUppercase; -pub use series::DataFrameUnique; -pub use series::DataFrameValueCounts; diff --git a/crates/nu-command/src/commands/dataframe/open.rs b/crates/nu-command/src/commands/dataframe/open.rs deleted file mode 100644 index 72ce158d04..0000000000 --- a/crates/nu-command/src/commands/dataframe/open.rs +++ /dev/null @@ -1,211 +0,0 @@ -use std::path::PathBuf; - -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::NuDataFrame, Primitive, Signature, SyntaxShape, UntaggedValue, Value, -}; - -use nu_source::Tagged; -use polars::prelude::{CsvEncoding, CsvReader, JsonReader, ParquetReader, SerReader}; -use std::fs::File; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe open" - } - - fn usage(&self) -> &str { - "Opens csv, json or parquet file to create dataframe" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe open") - .required( - "file", - SyntaxShape::FilePath, - "file path to load values from", - ) - .named( - "delimiter", - SyntaxShape::String, - "file delimiter character. CSV file", - Some('d'), - ) - .switch( - "no_header", - "Indicates if file doesn't have header. CSV file", - None, - ) - .named( - "infer_schema", - SyntaxShape::Number, - "Set number of rows to infer the schema of the file. CSV file", - None, - ) - .named( - "skip_rows", - SyntaxShape::Number, - "Number of rows to skip from file. CSV file", - None, - ) - .named( - "columns", - SyntaxShape::Table, - "Columns to be selected from csv file. CSV file", - None, - ) - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Takes a file name and creates a dataframe", - example: "dataframe open test.csv", - result: None, - }] - } -} - -fn command(args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let file: Tagged = args.req(0)?; - - let df = match file.item().extension() { - Some(e) => match e.to_str() { - Some("csv") => from_csv(args), - Some("parquet") => from_parquet(args), - Some("json") => from_json(args), - _ => Err(ShellError::labeled_error( - "Error with file", - "Not a csv, parquet or json file", - &file.tag, - )), - }, - None => Err(ShellError::labeled_error( - "Error with file", - "File without extension", - &file.tag, - )), - }?; - - let file_name = match file.item.into_os_string().into_string() { - Ok(name) => name, - Err(e) => { - return Err(ShellError::labeled_error( - "File Name Error", - format!("{:?}", e), - &file.tag, - )) - } - }; - - let df_tag = Tag { - anchor: Some(AnchorLocation::File(file_name)), - span: tag.span, - }; - - Ok(OutputStream::one(NuDataFrame::dataframe_to_value( - df, df_tag, - ))) -} - -fn from_parquet(args: CommandArgs) -> Result { - let file: Tagged = args.req(0)?; - - let r = File::open(&file.item) - .map_err(|e| ShellError::labeled_error("Error with file", format!("{:?}", e), &file.tag))?; - - let reader = ParquetReader::new(r); - - reader - .finish() - .map_err(|e| parse_polars_error::<&str>(&e, &file.tag.span, None)) -} - -fn from_json(args: CommandArgs) -> Result { - let file: Tagged = args.req(0)?; - - let r = File::open(&file.item) - .map_err(|e| ShellError::labeled_error("Error with file", format!("{:?}", e), &file.tag))?; - - let reader = JsonReader::new(r); - - reader - .finish() - .map_err(|e| parse_polars_error::<&str>(&e, &file.tag.span, None)) -} - -fn from_csv(args: CommandArgs) -> Result { - let file: Tagged = args.req(0)?; - let delimiter: Option> = args.get_flag("delimiter")?; - let no_header: bool = args.has_flag("no_header"); - let infer_schema: Option> = args.get_flag("infer_schema")?; - let skip_rows: Option> = args.get_flag("skip_rows")?; - let columns: Option> = args.get_flag("columns")?; - - let csv_reader = CsvReader::from_path(&file.item) - .map_err(|e| parse_polars_error::<&str>(&e, &file.tag.span, None))? - .with_encoding(CsvEncoding::LossyUtf8); - - let csv_reader = match delimiter { - None => csv_reader, - Some(d) => { - if d.item.len() != 1 { - return Err(ShellError::labeled_error( - "Incorrect delimiter", - "Delimiter has to be one char", - &d.tag, - )); - } else { - let delimiter = match d.item.chars().next() { - Some(d) => d as u8, - None => unreachable!(), - }; - csv_reader.with_delimiter(delimiter) - } - } - }; - - let csv_reader = csv_reader.has_header(!no_header); - - let csv_reader = match infer_schema { - None => csv_reader, - Some(r) => csv_reader.infer_schema(Some(r.item)), - }; - - let csv_reader = match skip_rows { - None => csv_reader, - Some(r) => csv_reader.with_skip_rows(r.item), - }; - - let csv_reader = match columns { - None => csv_reader, - Some(c) => { - let columns = c - .into_iter() - .map(|value| match value.value { - UntaggedValue::Primitive(Primitive::String(s)) => Ok(s), - _ => Err(ShellError::labeled_error( - "Incorrect type for column", - "Only string as columns", - &value.tag, - )), - }) - .collect::, ShellError>>(); - - csv_reader.with_columns(Some(columns?)) - } - }; - - match csv_reader.finish() { - Ok(df) => Ok(df), - Err(e) => Err(parse_polars_error::<&str>(&e, &file.tag.span, None)), - } -} diff --git a/crates/nu-command/src/commands/dataframe/pivot.rs b/crates/nu-command/src/commands/dataframe/pivot.rs deleted file mode 100644 index 22dfa27288..0000000000 --- a/crates/nu-command/src/commands/dataframe/pivot.rs +++ /dev/null @@ -1,169 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{NuDataFrame, NuGroupBy}, - Signature, SyntaxShape, -}; -use nu_source::Tagged; - -use polars::prelude::DataType; - -enum Operation { - First, - Sum, - Min, - Max, - Mean, - Median, -} - -impl Operation { - fn from_tagged(name: &Tagged) -> Result { - match name.item.as_ref() { - "first" => Ok(Operation::First), - "sum" => Ok(Operation::Sum), - "min" => Ok(Operation::Min), - "max" => Ok(Operation::Max), - "mean" => Ok(Operation::Mean), - "median" => Ok(Operation::Median), - _ => Err(ShellError::labeled_error_with_secondary( - "Operation not fount", - "Operation does not exist for pivot", - &name.tag, - "Perhaps you want: first, sum, min, max, mean, median", - &name.tag, - )), - } - } -} - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe pivot" - } - - fn usage(&self) -> &str { - "[GroupBy] Performs a pivot operation on a groupby object" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe pivot") - .required( - "pivot column", - SyntaxShape::String, - "pivot column to perform pivot", - ) - .required( - "value column", - SyntaxShape::String, - "value column to perform pivot", - ) - .required("operation", SyntaxShape::String, "aggregate operation") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Pivot a dataframe on b and aggregation on col c", - example: - "[[a b c]; [one x 1] [two y 2]] | dataframe to-df | dataframe group-by a | dataframe pivot b c sum", - result: None, // No sample because there are nulls in the result dataframe - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - // Extracting the pivot col from arguments - let pivot_col: Tagged = args.req(0)?; - - // Extracting the value col from arguments - let value_col: Tagged = args.req(1)?; - - let operation: Tagged = args.req(2)?; - let op = Operation::from_tagged(&operation)?; - - // The operation is only done in one groupby. Only one input is - // expected from the InputStream - let nu_groupby = NuGroupBy::try_from_stream(&mut args.input, &tag.span)?; - let df_ref = nu_groupby.as_ref(); - - check_pivot_column(df_ref, &pivot_col)?; - check_value_column(df_ref, &value_col)?; - - let mut groupby = nu_groupby.to_groupby()?; - - let pivot = groupby.pivot(&pivot_col.item, &value_col.item); - - let res = match op { - Operation::Mean => pivot.mean(), - Operation::Sum => pivot.sum(), - Operation::Min => pivot.min(), - Operation::Max => pivot.max(), - Operation::First => pivot.first(), - Operation::Median => pivot.median(), - } - .map_err(|e| parse_polars_error::<&str>(&e, &tag.span, None))?; - - Ok(OutputStream::one(NuDataFrame::dataframe_to_value(res, tag))) -} - -fn check_pivot_column( - df: &polars::prelude::DataFrame, - col: &Tagged, -) -> Result<(), ShellError> { - let series = df - .column(&col.item) - .map_err(|e| parse_polars_error::<&str>(&e, &col.tag.span, None))?; - - match series.dtype() { - DataType::UInt8 - | DataType::UInt16 - | DataType::UInt32 - | DataType::UInt64 - | DataType::Int8 - | DataType::Int16 - | DataType::Int32 - | DataType::Int64 - | DataType::Utf8 => Ok(()), - _ => Err(ShellError::labeled_error( - "Pivot error", - format!("Unsupported datatype {}", series.dtype()), - col.tag.span, - )), - } -} - -fn check_value_column( - df: &polars::prelude::DataFrame, - col: &Tagged, -) -> Result<(), ShellError> { - let series = df - .column(&col.item) - .map_err(|e| parse_polars_error::<&str>(&e, &col.tag.span, None))?; - - match series.dtype() { - DataType::UInt8 - | DataType::UInt16 - | DataType::UInt32 - | DataType::UInt64 - | DataType::Int8 - | DataType::Int16 - | DataType::Int32 - | DataType::Int64 - | DataType::Float32 - | DataType::Float64 => Ok(()), - _ => Err(ShellError::labeled_error( - "Pivot error", - format!("Unsupported datatype {}", series.dtype()), - col.tag.span, - )), - } -} diff --git a/crates/nu-command/src/commands/dataframe/rename.rs b/crates/nu-command/src/commands/dataframe/rename.rs deleted file mode 100644 index 28e922b6db..0000000000 --- a/crates/nu-command/src/commands/dataframe/rename.rs +++ /dev/null @@ -1,81 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, SyntaxShape, UntaggedValue, -}; - -use nu_source::Tagged; - -use super::utils::parse_polars_error; -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe rename-col" - } - - fn usage(&self) -> &str { - "[DataFrame] rename a dataframe column" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe rename-col") - .required("from", SyntaxShape::String, "column name to be renamed") - .required("to", SyntaxShape::String, "new column name") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Renames a dataframe column", - example: "[[a b]; [1 2] [3 4]] | dataframe to-df | dataframe rename-col a ab", - result: Some(vec![NuDataFrame::try_from_columns( - vec![ - Column::new( - "ab".to_string(), - vec![UntaggedValue::int(1).into(), UntaggedValue::int(3).into()], - ), - Column::new( - "b".to_string(), - vec![UntaggedValue::int(2).into(), UntaggedValue::int(4).into()], - ), - ], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let from: Tagged = args.req(0)?; - let to: Tagged = args.req(1)?; - - let (mut df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - df.as_mut() - .rename(&from.item, &to.item) - .map_err(|e| parse_polars_error::<&str>(&e, &df_tag.span, None))?; - - Ok(OutputStream::one(df.into_value(tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/sample.rs b/crates/nu-command/src/commands/dataframe/sample.rs deleted file mode 100644 index d32a092831..0000000000 --- a/crates/nu-command/src/commands/dataframe/sample.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{dataframe::NuDataFrame, Signature, SyntaxShape}; - -use nu_source::Tagged; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe sample" - } - - fn usage(&self) -> &str { - "[DataFrame] Create sample dataframe" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe sample") - .named( - "n_rows", - SyntaxShape::Number, - "number of rows to be taken from dataframe", - Some('n'), - ) - .named( - "fraction", - SyntaxShape::Number, - "fraction of dataframe to be taken", - Some('f'), - ) - .switch("replace", "sample with replace", Some('e')) - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Sample rows from dataframe", - example: "[[a b]; [1 2] [3 4]] | dataframe to-df | dataframe sample -n 1", - result: None, // No expected value because sampling is random - }, - Example { - description: "Shows sample row using fraction and replace", - example: - "[[a b]; [1 2] [3 4] [5 6]] | dataframe to-df | dataframe sample -f 0.5 -e", - result: None, // No expected value because sampling is random - }, - ] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let rows: Option> = args.get_flag("n_rows")?; - let fraction: Option> = args.get_flag("fraction")?; - let replace: bool = args.has_flag("replace"); - - let (df, _) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let res = match (rows, fraction) { - (Some(rows), None) => df - .as_ref() - .sample_n(rows.item, replace) - .map_err(|e| parse_polars_error::<&str>(&e, &rows.tag.span, None)), - (None, Some(frac)) => df - .as_ref() - .sample_frac(frac.item, replace) - .map_err(|e| parse_polars_error::<&str>(&e, &frac.tag.span, None)), - (Some(_), Some(_)) => Err(ShellError::labeled_error( - "Incompatible flags", - "Only one selection criterion allowed", - &tag, - )), - (None, None) => Err(ShellError::labeled_error_with_secondary( - "No selection", - "No selection criterion was found", - &tag, - "Perhaps you want to use the flag -n or -f", - &tag, - )), - }?; - - Ok(OutputStream::one(NuDataFrame::dataframe_to_value(res, tag))) -} diff --git a/crates/nu-command/src/commands/dataframe/select.rs b/crates/nu-command/src/commands/dataframe/select.rs deleted file mode 100644 index 4d6c7d817e..0000000000 --- a/crates/nu-command/src/commands/dataframe/select.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, SyntaxShape, UntaggedValue, Value, -}; - -use super::utils::{convert_columns, parse_polars_error}; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe select" - } - - fn usage(&self) -> &str { - "[DataFrame] Creates a new dataframe with the selected columns" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe select").rest("rest", SyntaxShape::Any, "selected column names") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Create new dataframe with column a", - example: "[[a b]; [1 2] [3 4]] | dataframe to-df | dataframe select a", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "a".to_string(), - vec![UntaggedValue::int(1).into(), UntaggedValue::int(3).into()], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let columns: Vec = args.rest(0)?; - - let (col_string, col_span) = convert_columns(&columns, &tag)?; - - let (df, _) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let res = df - .as_ref() - .select(&col_string) - .map_err(|e| parse_polars_error::<&str>(&e, &col_span, None))?; - - Ok(OutputStream::one(NuDataFrame::dataframe_to_value(res, tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/all_false.rs b/crates/nu-command/src/commands/dataframe/series/all_false.rs deleted file mode 100644 index 88c5b3fdbb..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/all_false.rs +++ /dev/null @@ -1,102 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, Value, -}; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe all-false" - } - - fn usage(&self) -> &str { - "[Series] Returns true if all values are false" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe all-false") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Returns true if all values are false", - example: "[$false $false $false] | dataframe to-df | dataframe all-false", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "all_false".to_string(), - vec![UntaggedValue::boolean(true).into()], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }, - Example { - description: "Checks the result from a comparison", - example: r#"let s = ([5 6 2 10] | dataframe to-df); - let res = ($s > 9); - $res | dataframe all-false"#, - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "all_false".to_string(), - vec![UntaggedValue::boolean(false).into()], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }, - ] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let series = df.as_series(&df_tag.span)?; - let bool = series.bool().map_err(|e| { - parse_polars_error::<&str>( - &e, - &tag.span, - Some("all-false only works with series of type bool"), - ) - })?; - - let res = bool.all_false(); - - let value = Value { - value: UntaggedValue::Primitive(res.into()), - tag: tag.clone(), - }; - - let df = NuDataFrame::try_from_columns( - vec![Column::new("all_false".to_string(), vec![value])], - &tag.span, - )?; - - Ok(OutputStream::one(df.into_value(tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/all_true.rs b/crates/nu-command/src/commands/dataframe/series/all_true.rs deleted file mode 100644 index 46a8e3e3cb..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/all_true.rs +++ /dev/null @@ -1,102 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, Value, -}; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe all-true" - } - - fn usage(&self) -> &str { - "[Series] Returns true if all values are true" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe all-true") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Returns true if all values are true", - example: "[$true $true $true] | dataframe to-df | dataframe all-true", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "all_true".to_string(), - vec![UntaggedValue::boolean(true).into()], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }, - Example { - description: "Checks the result from a comparison", - example: r#"let s = ([5 6 2 8] | dataframe to-df); - let res = ($s > 9); - $res | dataframe all-true"#, - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "all_true".to_string(), - vec![UntaggedValue::boolean(false).into()], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }, - ] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let series = df.as_series(&df_tag.span)?; - let bool = series.bool().map_err(|e| { - parse_polars_error::<&str>( - &e, - &tag.span, - Some("all-true only works with series of type bool"), - ) - })?; - - let res = bool.all_true(); - - let value = Value { - value: UntaggedValue::Primitive(res.into()), - tag: tag.clone(), - }; - - let df = NuDataFrame::try_from_columns( - vec![Column::new("all_true".to_string(), vec![value])], - &tag.span, - )?; - - Ok(OutputStream::one(df.into_value(tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/arg_max.rs b/crates/nu-command/src/commands/dataframe/series/arg_max.rs deleted file mode 100644 index 050f4534fc..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/arg_max.rs +++ /dev/null @@ -1,77 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, -}; - -use polars::prelude::{IntoSeries, NewChunkedArray, UInt32Chunked}; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe arg-max" - } - - fn usage(&self) -> &str { - "[Series] Return index for max value in series" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe arg-max") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Returns index for max value", - example: "[1 3 2] | dataframe to-df | dataframe arg-max", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "arg_max".to_string(), - vec![UntaggedValue::int(1).into()], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - let series = df.as_series(&df_tag.span)?; - - let res = series.arg_max(); - - let chunked = match res { - Some(index) => UInt32Chunked::new_from_slice("arg_max", &[index as u32]), - None => UInt32Chunked::new_from_slice("arg_max", &[]), - }; - - let res = chunked.into_series(); - let df = NuDataFrame::try_from_series(vec![res], &tag.span)?; - - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/arg_min.rs b/crates/nu-command/src/commands/dataframe/series/arg_min.rs deleted file mode 100644 index 710410bdb1..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/arg_min.rs +++ /dev/null @@ -1,76 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, -}; - -use polars::prelude::{IntoSeries, NewChunkedArray, UInt32Chunked}; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe arg-min" - } - - fn usage(&self) -> &str { - "[Series] Return index for min value in series" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe arg-min") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Returns index for min value", - example: "[1 3 2] | dataframe to-df | dataframe arg-min", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "arg_min".to_string(), - vec![UntaggedValue::int(0).into()], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let res = df.as_series(&df_tag.span)?.arg_min(); - - let chunked = match res { - Some(index) => UInt32Chunked::new_from_slice("arg_min", &[index as u32]), - None => UInt32Chunked::new_from_slice("arg_min", &[]), - }; - - let res = chunked.into_series(); - let df = NuDataFrame::try_from_series(vec![res], &tag.span)?; - - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/arg_sort.rs b/crates/nu-command/src/commands/dataframe/series/arg_sort.rs deleted file mode 100644 index af5cf7a48d..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/arg_sort.rs +++ /dev/null @@ -1,76 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, -}; -use polars::prelude::IntoSeries; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe arg-sort" - } - - fn usage(&self) -> &str { - "[Series] Returns indexes for a sorted series" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe arg-sort").switch("reverse", "reverse order", Some('r')) - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Returns indexes for a sorted series", - example: "[1 2 2 3 3] | dataframe to-df | dataframe arg-sort", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "arg_sort".to_string(), - vec![ - UntaggedValue::int(0).into(), - UntaggedValue::int(1).into(), - UntaggedValue::int(2).into(), - UntaggedValue::int(3).into(), - UntaggedValue::int(4).into(), - ], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let reverse = args.has_flag("reverse"); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let mut res = df.as_series(&df_tag.span)?.argsort(reverse).into_series(); - res.rename("arg_sort"); - - let df = NuDataFrame::try_from_series(vec![res], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/arg_true.rs b/crates/nu-command/src/commands/dataframe/series/arg_true.rs deleted file mode 100644 index 1f278f91d6..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/arg_true.rs +++ /dev/null @@ -1,78 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, -}; -use polars::prelude::IntoSeries; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe arg-true" - } - - fn usage(&self) -> &str { - "[Series] Returns indexes where values are true" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe arg-true") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Returns indexes where values are true", - example: "[$false $true $false] | dataframe to-df | dataframe arg-true", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "arg_true".to_string(), - vec![UntaggedValue::int(1).into()], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let series = df.as_series(&df_tag.span)?; - let bool = series.bool().map_err(|e| { - parse_polars_error::<&str>( - &e, - &tag.span, - Some("arg-true only works with series of type bool"), - ) - })?; - - let mut res = bool.arg_true().into_series(); - res.rename("arg_true"); - - let df = NuDataFrame::try_from_series(vec![res], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/arg_unique.rs b/crates/nu-command/src/commands/dataframe/series/arg_unique.rs deleted file mode 100644 index dc33e14e29..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/arg_unique.rs +++ /dev/null @@ -1,78 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, -}; -use polars::prelude::IntoSeries; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe arg-unique" - } - - fn usage(&self) -> &str { - "[Series] Returns indexes for unique values" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe arg-unique") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Returns indexes for unique values", - example: "[1 2 2 3 3] | dataframe to-df | dataframe arg-unique", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "arg_unique".to_string(), - vec![ - UntaggedValue::int(0).into(), - UntaggedValue::int(1).into(), - UntaggedValue::int(3).into(), - ], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let mut res = df - .as_series(&df_tag.span)? - .arg_unique() - .map_err(|e| parse_polars_error::<&str>(&e, &tag.span, None))? - .into_series(); - - res.rename("arg_unique"); - - let df = NuDataFrame::try_from_series(vec![res], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/concatenate.rs b/crates/nu-command/src/commands/dataframe/series/concatenate.rs deleted file mode 100644 index e6dc0ef267..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/concatenate.rs +++ /dev/null @@ -1,107 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, SyntaxShape, UntaggedValue, Value, -}; -use polars::prelude::IntoSeries; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe concatenate" - } - - fn usage(&self) -> &str { - "[Series] Concatenates strings with other array" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe concatenate").required( - "other", - SyntaxShape::Any, - "Other array with string to be concatenated", - ) - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Concatenate string", - example: r#"let other = ([za xs cd] | dataframe to-df); - [abc abc abc] | dataframe to-df | dataframe concatenate $other"#, - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0".to_string(), - vec![ - UntaggedValue::string("abcza").into(), - UntaggedValue::string("abcxs").into(), - UntaggedValue::string("abccd").into(), - ], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let other: Value = args.req(0)?; - - let other_df = match &other.value { - UntaggedValue::DataFrame(df) => Ok(df), - _ => Err(ShellError::labeled_error( - "Incorrect type", - "can only concatenate another series", - other.tag.span, - )), - }?; - - let other_series = other_df.as_series(&other.tag.span)?; - let other_chunked = other_series.utf8().map_err(|e| { - parse_polars_error::<&str>( - &e, - &other.tag.span, - Some("The concatenate command can only be used with string columns"), - ) - })?; - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let series = df.as_series(&df_tag.span)?; - let chunked = series.utf8().map_err(|e| { - parse_polars_error::<&str>( - &e, - &df_tag.span, - Some("The concatenate command can only be used with string columns"), - ) - })?; - - let mut res = chunked.concat(other_chunked); - - res.rename(series.name()); - - let df = NuDataFrame::try_from_series(vec![res.into_series()], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/contains.rs b/crates/nu-command/src/commands/dataframe/series/contains.rs deleted file mode 100644 index b99f6f7159..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/contains.rs +++ /dev/null @@ -1,89 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, SyntaxShape, UntaggedValue, -}; -use nu_source::Tagged; -use polars::prelude::IntoSeries; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe contains" - } - - fn usage(&self) -> &str { - "[Series] Checks if a pattern is contained in a string" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe contains").required( - "pattern", - SyntaxShape::String, - "Regex pattern to be searched", - ) - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Returns boolean indicating if pattern was found", - example: "[abc acb acb] | dataframe to-df | dataframe contains ab", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0".to_string(), - vec![ - UntaggedValue::boolean(true).into(), - UntaggedValue::boolean(false).into(), - UntaggedValue::boolean(false).into(), - ], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let pattern: Tagged = args.req(0)?; - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let series = df.as_series(&df_tag.span)?; - let chunked = series.utf8().map_err(|e| { - parse_polars_error::<&str>( - &e, - &df_tag.span, - Some("The contains command can only be used with string columns"), - ) - })?; - - let res = chunked - .contains(&pattern.item) - .map_err(|e| parse_polars_error::<&str>(&e, &tag.span, None))?; - - let df = NuDataFrame::try_from_series(vec![res.into_series()], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/cumulative.rs b/crates/nu-command/src/commands/dataframe/series/cumulative.rs deleted file mode 100644 index c2cb727286..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/cumulative.rs +++ /dev/null @@ -1,127 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, SyntaxShape, UntaggedValue, -}; -use nu_source::Tagged; -use polars::prelude::DataType; - -enum CumType { - Min, - Max, - Sum, -} - -impl CumType { - fn from_str(roll_type: &str, span: &Span) -> Result { - match roll_type { - "min" => Ok(Self::Min), - "max" => Ok(Self::Max), - "sum" => Ok(Self::Sum), - _ => Err(ShellError::labeled_error_with_secondary( - "Wrong operation", - "Operation not valid for cumulative", - span, - "Perhaps you want to use: max, min, sum", - span, - )), - } - } - - fn to_str(&self) -> &'static str { - match self { - CumType::Min => "cum_min", - CumType::Max => "cum_max", - CumType::Sum => "cum_sum", - } - } -} - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe cum" - } - - fn usage(&self) -> &str { - "[Series] Cumulative calculation for a series" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe cum") - .required("type", SyntaxShape::String, "rolling operation") - .switch("reverse", "Reverse cumulative calculation", Some('r')) - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Cumulative sum for a series", - example: "[1 2 3 4 5] | dataframe to-df | dataframe cum sum", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0_cum_sum".to_string(), - vec![ - UntaggedValue::int(1).into(), - UntaggedValue::int(3).into(), - UntaggedValue::int(6).into(), - UntaggedValue::int(10).into(), - UntaggedValue::int(15).into(), - ], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let cum_type: Tagged = args.req(0)?; - let reverse = args.has_flag("reverse"); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - let series = df.as_series(&df_tag.span)?; - - if let DataType::Object(_) = series.dtype() { - return Err(ShellError::labeled_error( - "Found object series", - "Series of type object cannot be used for cumulative operation", - &df_tag.span, - )); - } - - let cum_type = CumType::from_str(&cum_type.item, &cum_type.tag.span)?; - let mut res = match cum_type { - CumType::Max => series.cummax(reverse), - CumType::Min => series.cummin(reverse), - CumType::Sum => series.cumsum(reverse), - }; - - let name = format!("{}_{}", series.name(), cum_type.to_str()); - res.rename(&name); - - let df = NuDataFrame::try_from_series(vec![res], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/get_day.rs b/crates/nu-command/src/commands/dataframe/series/get_day.rs deleted file mode 100644 index 7a2d964056..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/get_day.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, -}; - -use polars::prelude::IntoSeries; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe get-day" - } - - fn usage(&self) -> &str { - "[Series] Gets day from date" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe get-day") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Returns day from a date", - example: r#"let dt = ('2020-08-04T16:39:18+00:00' | str to-datetime -z 'UTC'); - let df = ([$dt $dt] | dataframe to-df); - $df | dataframe get-day"#, - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0".to_string(), - vec![UntaggedValue::int(4).into(), UntaggedValue::int(4).into()], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - let series = df.as_series(&df_tag.span)?; - - let casted = series - .datetime() - .map_err(|e| parse_polars_error::<&str>(&e, &df_tag.span, None))?; - - let res = casted.day().into_series(); - let df = NuDataFrame::try_from_series(vec![res], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/get_hour.rs b/crates/nu-command/src/commands/dataframe/series/get_hour.rs deleted file mode 100644 index 96baef7fdb..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/get_hour.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, -}; - -use polars::prelude::IntoSeries; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe get-hour" - } - - fn usage(&self) -> &str { - "[Series] Gets hour from date" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe get-hour") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Returns hour from a date", - example: r#"let dt = ('2020-08-04T16:39:18+00:00' | str to-datetime -z 'UTC'); - let df = ([$dt $dt] | dataframe to-df); - $df | dataframe get-hour"#, - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0".to_string(), - vec![UntaggedValue::int(16).into(), UntaggedValue::int(16).into()], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - let series = df.as_series(&df_tag.span)?; - - let casted = series - .datetime() - .map_err(|e| parse_polars_error::<&str>(&e, &df_tag.span, None))?; - - let res = casted.hour().into_series(); - let df = NuDataFrame::try_from_series(vec![res], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/get_minute.rs b/crates/nu-command/src/commands/dataframe/series/get_minute.rs deleted file mode 100644 index 7500404528..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/get_minute.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, -}; - -use polars::prelude::IntoSeries; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe get-minute" - } - - fn usage(&self) -> &str { - "[Series] Gets minute from date" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe get-minute") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Returns minute from a date", - example: r#"let dt = ('2020-08-04T16:39:18+00:00' | str to-datetime -z 'UTC'); - let df = ([$dt $dt] | dataframe to-df); - $df | dataframe get-minute"#, - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0".to_string(), - vec![UntaggedValue::int(39).into(), UntaggedValue::int(39).into()], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - let series = df.as_series(&df_tag.span)?; - - let casted = series - .datetime() - .map_err(|e| parse_polars_error::<&str>(&e, &df_tag.span, None))?; - - let res = casted.minute().into_series(); - let df = NuDataFrame::try_from_series(vec![res], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/get_month.rs b/crates/nu-command/src/commands/dataframe/series/get_month.rs deleted file mode 100644 index dba70abef8..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/get_month.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, -}; - -use polars::prelude::IntoSeries; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe get-month" - } - - fn usage(&self) -> &str { - "[Series] Gets month from date" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe get-month") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Returns month from a date", - example: r#"let dt = ('2020-08-04T16:39:18+00:00' | str to-datetime -z 'UTC'); - let df = ([$dt $dt] | dataframe to-df); - $df | dataframe get-month"#, - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0".to_string(), - vec![UntaggedValue::int(8).into(), UntaggedValue::int(8).into()], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - let series = df.as_series(&df_tag.span)?; - - let casted = series - .datetime() - .map_err(|e| parse_polars_error::<&str>(&e, &df_tag.span, None))?; - - let res = casted.month().into_series(); - let df = NuDataFrame::try_from_series(vec![res], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/get_nanosecond.rs b/crates/nu-command/src/commands/dataframe/series/get_nanosecond.rs deleted file mode 100644 index 39f89af772..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/get_nanosecond.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, -}; - -use polars::prelude::IntoSeries; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe get-nanosecond" - } - - fn usage(&self) -> &str { - "[Series] Gets nanosecond from date" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe get-nanosecond") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Returns nanosecond from a date", - example: r#"let dt = ('2020-08-04T16:39:18+00:00' | str to-datetime -z 'UTC'); - let df = ([$dt $dt] | dataframe to-df); - $df | dataframe get-nanosecond"#, - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0".to_string(), - vec![UntaggedValue::int(0).into(), UntaggedValue::int(0).into()], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - let series = df.as_series(&df_tag.span)?; - - let casted = series - .datetime() - .map_err(|e| parse_polars_error::<&str>(&e, &df_tag.span, None))?; - - let res = casted.nanosecond().into_series(); - let df = NuDataFrame::try_from_series(vec![res], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/get_ordinal.rs b/crates/nu-command/src/commands/dataframe/series/get_ordinal.rs deleted file mode 100644 index af8537d382..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/get_ordinal.rs +++ /dev/null @@ -1,78 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, -}; - -use polars::prelude::IntoSeries; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe get-ordinal" - } - - fn usage(&self) -> &str { - "[Series] Gets ordinal date from date" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe get-ordinal") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Returns ordinal from a date", - example: r#"let dt = ('2020-08-04T16:39:18+00:00' | str to-datetime -z 'UTC'); - let df = ([$dt $dt] | dataframe to-df); - $df | dataframe get-ordinal"#, - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0".to_string(), - vec![ - UntaggedValue::int(217).into(), - UntaggedValue::int(217).into(), - ], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - let series = df.as_series(&df_tag.span)?; - - let casted = series - .datetime() - .map_err(|e| parse_polars_error::<&str>(&e, &df_tag.span, None))?; - - let res = casted.ordinal().into_series(); - let df = NuDataFrame::try_from_series(vec![res], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/get_second.rs b/crates/nu-command/src/commands/dataframe/series/get_second.rs deleted file mode 100644 index 13984dbef5..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/get_second.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, -}; - -use polars::prelude::IntoSeries; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe get-second" - } - - fn usage(&self) -> &str { - "[Series] Gets second from date" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe get-second") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Returns second from a date", - example: r#"let dt = ('2020-08-04T16:39:18+00:00' | str to-datetime -z 'UTC'); - let df = ([$dt $dt] | dataframe to-df); - $df | dataframe get-second"#, - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0".to_string(), - vec![UntaggedValue::int(18).into(), UntaggedValue::int(18).into()], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - let series = df.as_series(&df_tag.span)?; - - let casted = series - .datetime() - .map_err(|e| parse_polars_error::<&str>(&e, &df_tag.span, None))?; - - let res = casted.second().into_series(); - let df = NuDataFrame::try_from_series(vec![res], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/get_week.rs b/crates/nu-command/src/commands/dataframe/series/get_week.rs deleted file mode 100644 index 4d51a470c8..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/get_week.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, -}; - -use polars::prelude::IntoSeries; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe get-week" - } - - fn usage(&self) -> &str { - "[Series] Gets week from date" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe get-week") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Returns week from a date", - example: r#"let dt = ('2020-08-04T16:39:18+00:00' | str to-datetime -z 'UTC'); - let df = ([$dt $dt] | dataframe to-df); - $df | dataframe get-week"#, - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0".to_string(), - vec![UntaggedValue::int(32).into(), UntaggedValue::int(32).into()], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - let series = df.as_series(&df_tag.span)?; - - let casted = series - .datetime() - .map_err(|e| parse_polars_error::<&str>(&e, &df_tag.span, None))?; - - let res = casted.week().into_series(); - let df = NuDataFrame::try_from_series(vec![res], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/get_weekday.rs b/crates/nu-command/src/commands/dataframe/series/get_weekday.rs deleted file mode 100644 index 75cc997541..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/get_weekday.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, -}; - -use polars::prelude::IntoSeries; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe get-weekday" - } - - fn usage(&self) -> &str { - "[Series] Gets weekday from date" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe get-weekday") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Returns weekday from a date", - example: r#"let dt = ('2020-08-04T16:39:18+00:00' | str to-datetime -z 'UTC'); - let df = ([$dt $dt] | dataframe to-df); - $df | dataframe get-weekday"#, - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0".to_string(), - vec![UntaggedValue::int(1).into(), UntaggedValue::int(1).into()], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - let series = df.as_series(&df_tag.span)?; - - let casted = series - .datetime() - .map_err(|e| parse_polars_error::<&str>(&e, &df_tag.span, None))?; - - let res = casted.weekday().into_series(); - let df = NuDataFrame::try_from_series(vec![res], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/get_year.rs b/crates/nu-command/src/commands/dataframe/series/get_year.rs deleted file mode 100644 index 3a03e4ef17..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/get_year.rs +++ /dev/null @@ -1,78 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, -}; - -use polars::prelude::IntoSeries; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe get-year" - } - - fn usage(&self) -> &str { - "[Series] Gets year from date" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe get-year") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Returns year from a date", - example: r#"let dt = ('2020-08-04T16:39:18+00:00' | str to-datetime -z 'UTC'); - let df = ([$dt $dt] | dataframe to-df); - $df | dataframe get-year"#, - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0".to_string(), - vec![ - UntaggedValue::int(2020).into(), - UntaggedValue::int(2020).into(), - ], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - let series = df.as_series(&df_tag.span)?; - - let casted = series - .datetime() - .map_err(|e| parse_polars_error::<&str>(&e, &df_tag.span, None))?; - - let res = casted.year().into_series(); - let df = NuDataFrame::try_from_series(vec![res], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/is_duplicated.rs b/crates/nu-command/src/commands/dataframe/series/is_duplicated.rs deleted file mode 100644 index 071cb879b4..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/is_duplicated.rs +++ /dev/null @@ -1,82 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, -}; -use polars::prelude::IntoSeries; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe is-duplicated" - } - - fn usage(&self) -> &str { - "[Series] Creates mask indicating duplicated values" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe is-duplicated") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Create mask indicating duplicated values", - example: "[5 6 6 6 8 8 8] | dataframe to-df | dataframe is-duplicated", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "is_duplicated".to_string(), - vec![ - UntaggedValue::boolean(false).into(), - UntaggedValue::boolean(true).into(), - UntaggedValue::boolean(true).into(), - UntaggedValue::boolean(true).into(), - UntaggedValue::boolean(true).into(), - UntaggedValue::boolean(true).into(), - UntaggedValue::boolean(true).into(), - ], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let mut res = df - .as_series(&df_tag.span)? - .is_duplicated() - .map_err(|e| parse_polars_error::<&str>(&e, &tag.span, None))? - .into_series(); - - res.rename("is_duplicated"); - - let df = NuDataFrame::try_from_series(vec![res], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/is_in.rs b/crates/nu-command/src/commands/dataframe/series/is_in.rs deleted file mode 100644 index 01f2557a11..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/is_in.rs +++ /dev/null @@ -1,95 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, SyntaxShape, UntaggedValue, Value, -}; -use polars::prelude::IntoSeries; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe is-in" - } - - fn usage(&self) -> &str { - "[Series] Checks if elements from a series are contained in right series" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe is-in").required("other", SyntaxShape::Any, "right series") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Checks if elements from a series are contained in right series", - example: r#"let other = ([1 3 6] | dataframe to-df); - [5 6 6 6 8 8 8] | dataframe to-df | dataframe is-in $other"#, - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "is_in".to_string(), - vec![ - UntaggedValue::boolean(false).into(), - UntaggedValue::boolean(true).into(), - UntaggedValue::boolean(true).into(), - UntaggedValue::boolean(true).into(), - UntaggedValue::boolean(false).into(), - UntaggedValue::boolean(false).into(), - UntaggedValue::boolean(false).into(), - ], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let value: Value = args.req(0)?; - - let other_df = match value.value { - UntaggedValue::DataFrame(df) => Ok(df), - _ => Err(ShellError::labeled_error( - "Incorrect type", - "can only search in a series", - value.tag.span, - )), - }?; - - let other = other_df.as_series(&value.tag.span)?; - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let mut res = df - .as_series(&df_tag.span)? - .is_in(&other) - .map_err(|e| parse_polars_error::<&str>(&e, &tag.span, None))? - .into_series(); - - res.rename("is_in"); - - let df = NuDataFrame::try_from_series(vec![res], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/is_not_null.rs b/crates/nu-command/src/commands/dataframe/series/is_not_null.rs deleted file mode 100644 index 6dc6c5e885..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/is_not_null.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, -}; -use polars::prelude::IntoSeries; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe is-not-null" - } - - fn usage(&self) -> &str { - "[Series] Creates mask where value is not null" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe is-not-null") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Create mask where values are not null", - example: r#"let s = ([5 6 0 8] | dataframe to-df); - let res = ($s / $s); - $res | dataframe is-not-null"#, - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "is_not_null".to_string(), - vec![ - UntaggedValue::boolean(true).into(), - UntaggedValue::boolean(true).into(), - UntaggedValue::boolean(false).into(), - UntaggedValue::boolean(true).into(), - ], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let res = df.as_series(&df_tag.span)?.is_not_null(); - - let df = NuDataFrame::try_from_series(vec![res.into_series()], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/is_null.rs b/crates/nu-command/src/commands/dataframe/series/is_null.rs deleted file mode 100644 index dcd8f37d13..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/is_null.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, -}; -use polars::prelude::IntoSeries; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe is-null" - } - - fn usage(&self) -> &str { - "[Series] Creates mask where value is null" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe is-null") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Create mask where values are null", - example: r#"let s = ([5 6 0 8] | dataframe to-df); - let res = ($s / $s); - $res | dataframe is-null"#, - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "is_null".to_string(), - vec![ - UntaggedValue::boolean(false).into(), - UntaggedValue::boolean(false).into(), - UntaggedValue::boolean(true).into(), - UntaggedValue::boolean(false).into(), - ], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let res = df.as_series(&df_tag.span)?.is_null(); - - let df = NuDataFrame::try_from_series(vec![res.into_series()], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/is_unique.rs b/crates/nu-command/src/commands/dataframe/series/is_unique.rs deleted file mode 100644 index 85c539663d..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/is_unique.rs +++ /dev/null @@ -1,82 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, -}; -use polars::prelude::IntoSeries; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe is-unique" - } - - fn usage(&self) -> &str { - "[Series] Creates mask indicating unique values" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe is-unique") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Create mask indicating unique values", - example: "[5 6 6 6 8 8 8] | dataframe to-df | dataframe is-unique", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "is_unique".to_string(), - vec![ - UntaggedValue::boolean(true).into(), - UntaggedValue::boolean(false).into(), - UntaggedValue::boolean(false).into(), - UntaggedValue::boolean(false).into(), - UntaggedValue::boolean(false).into(), - UntaggedValue::boolean(false).into(), - UntaggedValue::boolean(false).into(), - ], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let mut res = df - .as_series(&df_tag.span)? - .is_unique() - .map_err(|e| parse_polars_error::<&str>(&e, &tag.span, None))? - .into_series(); - - res.rename("is_unique"); - - let df = NuDataFrame::try_from_series(vec![res], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/mod.rs b/crates/nu-command/src/commands/dataframe/series/mod.rs deleted file mode 100644 index a51930f4cf..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/mod.rs +++ /dev/null @@ -1,85 +0,0 @@ -pub mod all_false; -pub mod all_true; -pub mod arg_max; -pub mod arg_min; -pub mod arg_sort; -pub mod arg_true; -pub mod arg_unique; -pub mod concatenate; -pub mod contains; -pub mod cumulative; -pub mod get_day; -pub mod get_hour; -pub mod get_minute; -pub mod get_month; -pub mod get_nanosecond; -pub mod get_ordinal; -pub mod get_second; -pub mod get_week; -pub mod get_weekday; -pub mod get_year; -pub mod is_duplicated; -pub mod is_in; -pub mod is_not_null; -pub mod is_null; -pub mod is_unique; -pub mod n_null; -pub mod n_unique; -pub mod not; -pub mod rename; -pub mod replace; -pub mod replace_all; -pub mod rolling; -pub mod set; -pub mod set_with_idx; -pub mod shift; -pub mod str_lengths; -pub mod str_slice; -pub mod strftime; -pub mod to_lowercase; -pub mod to_uppercase; -pub mod unique; -pub mod value_counts; - -pub use all_false::DataFrame as DataFrameAllFalse; -pub use all_true::DataFrame as DataFrameAllTrue; -pub use arg_max::DataFrame as DataFrameArgMax; -pub use arg_min::DataFrame as DataFrameArgMin; -pub use arg_sort::DataFrame as DataFrameArgSort; -pub use arg_true::DataFrame as DataFrameArgTrue; -pub use arg_unique::DataFrame as DataFrameArgUnique; -pub use concatenate::DataFrame as DataFrameConcatenate; -pub use contains::DataFrame as DataFrameContains; -pub use cumulative::DataFrame as DataFrameCumulative; -pub use get_day::DataFrame as DataFrameGetDay; -pub use get_hour::DataFrame as DataFrameGetHour; -pub use get_minute::DataFrame as DataFrameGetMinute; -pub use get_month::DataFrame as DataFrameGetMonth; -pub use get_nanosecond::DataFrame as DataFrameGetNanoSecond; -pub use get_ordinal::DataFrame as DataFrameGetOrdinal; -pub use get_second::DataFrame as DataFrameGetSecond; -pub use get_week::DataFrame as DataFrameGetWeek; -pub use get_weekday::DataFrame as DataFrameGetWeekDay; -pub use get_year::DataFrame as DataFrameGetYear; -pub use is_duplicated::DataFrame as DataFrameIsDuplicated; -pub use is_in::DataFrame as DataFrameIsIn; -pub use is_not_null::DataFrame as DataFrameIsNotNull; -pub use is_null::DataFrame as DataFrameIsNull; -pub use is_unique::DataFrame as DataFrameIsUnique; -pub use n_null::DataFrame as DataFrameNNull; -pub use n_unique::DataFrame as DataFrameNUnique; -pub use not::DataFrame as DataFrameNot; -pub use rename::DataFrame as DataFrameSeriesRename; -pub use replace::DataFrame as DataFrameReplace; -pub use replace_all::DataFrame as DataFrameReplaceAll; -pub use rolling::DataFrame as DataFrameRolling; -pub use set::DataFrame as DataFrameSet; -pub use set_with_idx::DataFrame as DataFrameSetWithIdx; -pub use shift::DataFrame as DataFrameShift; -pub use str_lengths::DataFrame as DataFrameStringLengths; -pub use str_slice::DataFrame as DataFrameStringSlice; -pub use strftime::DataFrame as DataFrameStrFTime; -pub use to_lowercase::DataFrame as DataFrameToLowercase; -pub use to_uppercase::DataFrame as DataFrameToUppercase; -pub use unique::DataFrame as DataFrameUnique; -pub use value_counts::DataFrame as DataFrameValueCounts; diff --git a/crates/nu-command/src/commands/dataframe/series/n_null.rs b/crates/nu-command/src/commands/dataframe/series/n_null.rs deleted file mode 100644 index 42cac40c26..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/n_null.rs +++ /dev/null @@ -1,77 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Primitive, Signature, UntaggedValue, Value, -}; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe count-null" - } - - fn usage(&self) -> &str { - "[Series] Counts null values" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe count-null") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Counts null values", - example: r#"let s = ([1 1 0 0 3 3 4] | dataframe to-df); - ($s / $s) | dataframe count-null"#, - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "count_null".to_string(), - vec![UntaggedValue::int(2).into()], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let res = df.as_series(&df_tag.span)?.null_count(); - - let value = Value { - value: UntaggedValue::Primitive(Primitive::Int(res as i64)), - tag: tag.clone(), - }; - - let df = NuDataFrame::try_from_columns( - vec![Column::new("count_null".to_string(), vec![value])], - &tag.span, - )?; - - Ok(OutputStream::one(df.into_value(tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/n_unique.rs b/crates/nu-command/src/commands/dataframe/series/n_unique.rs deleted file mode 100644 index cc774f77d3..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/n_unique.rs +++ /dev/null @@ -1,79 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Primitive, Signature, UntaggedValue, Value, -}; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe count-unique" - } - - fn usage(&self) -> &str { - "[Series] Counts unique value" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe count-unique") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Counts unique values", - example: "[1 1 2 2 3 3 4] | dataframe to-df | dataframe count-unique", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "count_unique".to_string(), - vec![UntaggedValue::int(4).into()], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let res = df - .as_series(&df_tag.span)? - .n_unique() - .map_err(|e| parse_polars_error::<&str>(&e, &tag.span, None))?; - - let value = Value { - value: UntaggedValue::Primitive(Primitive::Int(res as i64)), - tag: tag.clone(), - }; - - let df = NuDataFrame::try_from_columns( - vec![Column::new("count_unique".to_string(), vec![value])], - &tag.span, - )?; - - Ok(OutputStream::one(df.into_value(tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/not.rs b/crates/nu-command/src/commands/dataframe/series/not.rs deleted file mode 100644 index d51fe7e27e..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/not.rs +++ /dev/null @@ -1,82 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, -}; -use polars::prelude::IntoSeries; -use std::ops::Not; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe not" - } - - fn usage(&self) -> &str { - "[Series] Inverts boolean mask" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe not") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Inverts boolean mask", - example: "[$true $false $true] | dataframe to-df | dataframe not", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0".to_string(), - vec![ - UntaggedValue::boolean(false).into(), - UntaggedValue::boolean(true).into(), - UntaggedValue::boolean(false).into(), - ], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - let series = df.as_series(&df_tag.span)?; - - let bool = series.bool().map_err(|e| { - parse_polars_error::<&str>( - &e, - &tag.span, - Some("not only works with series of type bool"), - ) - })?; - - let res = bool.not(); - - let df = NuDataFrame::try_from_series(vec![res.into_series()], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/rename.rs b/crates/nu-command/src/commands/dataframe/series/rename.rs deleted file mode 100644 index d14b49a7ae..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/rename.rs +++ /dev/null @@ -1,80 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, SyntaxShape, UntaggedValue, -}; -use nu_source::Tagged; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe rename" - } - - fn usage(&self) -> &str { - "[Series] Renames a series" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe rename").required( - "name", - SyntaxShape::String, - "new series name", - ) - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Renames a series", - example: "[5 6 7 8] | dataframe to-df | dataframe rename new_name", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "new_name".to_string(), - vec![ - UntaggedValue::int(5).into(), - UntaggedValue::int(6).into(), - UntaggedValue::int(7).into(), - UntaggedValue::int(8).into(), - ], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let name: Tagged = args.req(0)?; - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let mut series = df.as_series(&df_tag.span)?; - - series.rename(&name.item); - - let df = NuDataFrame::try_from_series(vec![series], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/replace.rs b/crates/nu-command/src/commands/dataframe/series/replace.rs deleted file mode 100644 index 7e3fc6ef61..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/replace.rs +++ /dev/null @@ -1,100 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, SyntaxShape, UntaggedValue, -}; -use nu_source::{Span, Tagged}; -use polars::prelude::IntoSeries; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe replace" - } - - fn usage(&self) -> &str { - "[Series] Replace the leftmost (sub)string by a regex pattern" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe replace") - .required_named( - "pattern", - SyntaxShape::String, - "Regex pattern to be matched", - Some('p'), - ) - .required_named( - "replace", - SyntaxShape::String, - "replacing string", - Some('r'), - ) - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Replaces string", - example: "[abc abc abc] | dataframe to-df | dataframe replace -p ab -r AB", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0".to_string(), - vec![ - UntaggedValue::string("ABc").into(), - UntaggedValue::string("ABc").into(), - UntaggedValue::string("ABc").into(), - ], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let pattern: Tagged = args.req_named("pattern")?; - let replace: Tagged = args.req_named("replace")?; - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let series = df.as_series(&df_tag.span)?; - let chunked = series.utf8().map_err(|e| { - parse_polars_error::<&str>( - &e, - &df_tag.span, - Some("The replace-all command can only be used with string columns"), - ) - })?; - - let mut res = chunked - .replace(&pattern.item, &replace.item) - .map_err(|e| parse_polars_error::<&str>(&e, &tag.span, None))?; - - res.rename(series.name()); - - let df = NuDataFrame::try_from_series(vec![res.into_series()], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/replace_all.rs b/crates/nu-command/src/commands/dataframe/series/replace_all.rs deleted file mode 100644 index 6834391e41..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/replace_all.rs +++ /dev/null @@ -1,100 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, SyntaxShape, UntaggedValue, -}; -use nu_source::Tagged; -use polars::prelude::IntoSeries; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe replace-all" - } - - fn usage(&self) -> &str { - "[Series] Replace all (sub)strings by a regex pattern" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe replace") - .required_named( - "pattern", - SyntaxShape::String, - "Regex pattern to be matched", - Some('p'), - ) - .required_named( - "replace", - SyntaxShape::String, - "replacing string", - Some('r'), - ) - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Replaces string", - example: "[abac abac abac] | dataframe to-df | dataframe replace-all -p a -r A", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0".to_string(), - vec![ - UntaggedValue::string("AbAc").into(), - UntaggedValue::string("AbAc").into(), - UntaggedValue::string("AbAc").into(), - ], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let pattern: Tagged = args.req_named("pattern")?; - let replace: Tagged = args.req_named("replace")?; - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let series = df.as_series(&df_tag.span)?; - let chunked = series.utf8().map_err(|e| { - parse_polars_error::<&str>( - &e, - &df_tag.span, - Some("The replace command can only be used with string columns"), - ) - })?; - - let mut res = chunked - .replace_all(&pattern.item, &replace.item) - .map_err(|e| parse_polars_error::<&str>(&e, &tag.span, None))?; - - res.rename(series.name()); - - let df = NuDataFrame::try_from_series(vec![res.into_series()], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/rolling.rs b/crates/nu-command/src/commands/dataframe/series/rolling.rs deleted file mode 100644 index 8a37242b77..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/rolling.rs +++ /dev/null @@ -1,160 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, SyntaxShape, UntaggedValue, -}; -use nu_source::Tagged; -use polars::prelude::{DataType, RollingOptions}; - -enum RollType { - Min, - Max, - Sum, - Mean, -} - -impl RollType { - fn from_str(roll_type: &str, span: &Span) -> Result { - match roll_type { - "min" => Ok(Self::Min), - "max" => Ok(Self::Max), - "sum" => Ok(Self::Sum), - "mean" => Ok(Self::Mean), - _ => Err(ShellError::labeled_error_with_secondary( - "Wrong operation", - "Operation not valid for rolling", - span, - "Perhaps you want to use: max, min, sum, mean", - span, - )), - } - } - - fn to_str(&self) -> &'static str { - match self { - RollType::Min => "rolling_min", - RollType::Max => "rolling_max", - RollType::Sum => "rolling_sum", - RollType::Mean => "rolling_mean", - } - } -} - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe rolling" - } - - fn usage(&self) -> &str { - "[Series] Rolling calculation for a series" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe rolling") - .required("type", SyntaxShape::String, "rolling operation") - .required("window", SyntaxShape::Int, "Window size for rolling") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Rolling sum for a series", - example: - "[1 2 3 4 5] | dataframe to-df | dataframe rolling sum 2 | dataframe drop-nulls", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0_rolling_sum".to_string(), - vec![ - UntaggedValue::int(3).into(), - UntaggedValue::int(5).into(), - UntaggedValue::int(7).into(), - UntaggedValue::int(9).into(), - ], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }, - Example { - description: "Rolling max for a series", - example: - "[1 2 3 4 5] | dataframe to-df | dataframe rolling max 2 | dataframe drop-nulls", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0_rolling_max".to_string(), - vec![ - UntaggedValue::int(2).into(), - UntaggedValue::int(3).into(), - UntaggedValue::int(4).into(), - UntaggedValue::int(5).into(), - ], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }, - ] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let roll_type: Tagged = args.req(0)?; - let window_size: Tagged = args.req(1)?; - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - let series = df.as_series(&df_tag.span)?; - - if let DataType::Object(_) = series.dtype() { - return Err(ShellError::labeled_error( - "Found object series", - "Series of type object cannot be used for rolling operation", - &df_tag.span, - )); - } - - let roll_type = RollType::from_str(&roll_type.item, &roll_type.tag.span)?; - let rolling_opts = RollingOptions { - window_size: window_size.item as usize, - min_periods: window_size.item as usize, - weights: None, - center: false, - }; - let res = match roll_type { - RollType::Max => series.rolling_max(rolling_opts), - RollType::Min => series.rolling_min(rolling_opts), - RollType::Sum => series.rolling_sum(rolling_opts), - RollType::Mean => series.rolling_mean(rolling_opts), - }; - - let mut res = res.map_err(|e| parse_polars_error::<&str>(&e, &df_tag.span, None))?; - - let name = format!("{}_{}", series.name(), roll_type.to_str()); - res.rename(&name); - - let df = NuDataFrame::try_from_series(vec![res], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/set.rs b/crates/nu-command/src/commands/dataframe/series/set.rs deleted file mode 100644 index 68b13a1251..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/set.rs +++ /dev/null @@ -1,171 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Primitive, Signature, SyntaxShape, UntaggedValue, Value, -}; -use polars::prelude::{ChunkSet, DataType, IntoSeries}; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe set" - } - - fn usage(&self) -> &str { - "[Series] Sets value where given mask is true" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe set") - .required("value", SyntaxShape::Any, "value to be inserted in series") - .required_named( - "mask", - SyntaxShape::Any, - "mask indicating insertions", - Some('m'), - ) - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Shifts the values by a given period", - example: r#"let s = ([1 2 2 3 3] | dataframe to-df | dataframe shift 2); - let mask = ($s | dataframe is-null); - $s | dataframe set 0 --mask $mask"#, - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0".to_string(), - vec![ - UntaggedValue::int(0).into(), - UntaggedValue::int(0).into(), - UntaggedValue::int(1).into(), - UntaggedValue::int(2).into(), - UntaggedValue::int(2).into(), - ], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let value: Value = args.req(0)?; - let mask: Value = args.req_named("mask")?; - - let mask_df = match &mask.value { - UntaggedValue::DataFrame(df) => Ok(df), - _ => Err(ShellError::labeled_error( - "Incorrect type", - "can only use a series as mask", - value.tag.span, - )), - }?; - - let mask_series = mask_df.as_series(&mask.tag.span)?; - - let bool_mask = match mask_series.dtype() { - DataType::Boolean => mask_series - .bool() - .map_err(|e| parse_polars_error::<&str>(&e, &mask.tag.span, None)), - _ => Err(ShellError::labeled_error( - "Incorrect type", - "can only use bool series as mask", - value.tag.span, - )), - }?; - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - let series = df.as_series(&df_tag.span)?; - - match &value.value { - UntaggedValue::Primitive(Primitive::Int(val)) => { - let chunked = series.i64().map_err(|e| { - parse_polars_error::<&str>( - &e, - &value.tag.span, - Some("The value has to match the set value type"), - ) - })?; - - let res = chunked - .set(bool_mask, Some(*val)) - .map_err(|e| parse_polars_error::<&str>(&e, &value.tag.span, None))?; - - let df = NuDataFrame::try_from_series(vec![res.into_series()], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) - } - UntaggedValue::Primitive(Primitive::Decimal(val)) => { - let chunked = series.as_ref().f64().map_err(|e| { - parse_polars_error::<&str>( - &e, - &value.tag.span, - Some("The value has to match the series type"), - ) - })?; - - let res = chunked - .set( - bool_mask, - Some( - val.to_f64() - .expect("internal error: expected f64-compatible decimal"), - ), - ) - .map_err(|e| parse_polars_error::<&str>(&e, &value.tag.span, None))?; - - let df = NuDataFrame::try_from_series(vec![res.into_series()], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) - } - UntaggedValue::Primitive(Primitive::String(val)) => { - let chunked = series.as_ref().utf8().map_err(|e| { - parse_polars_error::<&str>( - &e, - &value.tag.span, - Some("The value has to match the series type"), - ) - })?; - - let res = chunked - .set(bool_mask, Some(val.as_ref())) - .map_err(|e| parse_polars_error::<&str>(&e, &value.tag.span, None))?; - - let mut res = res.into_series(); - res.rename("string"); - - let df = NuDataFrame::try_from_series(vec![res.into_series()], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) - } - _ => Err(ShellError::labeled_error( - "Incorrect type", - format!( - "this value cannot be set in a series of type '{}'", - series.dtype() - ), - value.tag.span, - )), - } -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/set_with_idx.rs b/crates/nu-command/src/commands/dataframe/series/set_with_idx.rs deleted file mode 100644 index e541be455b..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/set_with_idx.rs +++ /dev/null @@ -1,181 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Primitive, Signature, SyntaxShape, UntaggedValue, Value, -}; -use polars::prelude::{ChunkSet, DataType, IntoSeries}; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe set-with-idx" - } - - fn usage(&self) -> &str { - "[Series] Sets value in the given index" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe set-with-idx") - .required("value", SyntaxShape::Any, "value to be inserted in series") - .required_named( - "indices", - SyntaxShape::Any, - "list of indices indicating where to set the value", - Some('i'), - ) - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Set value in selected rows from series", - example: r#"let series = ([4 1 5 2 4 3] | dataframe to-df); - let indices = ([0 2] | dataframe to-df); - $series | dataframe set-with-idx 6 -i $indices"#, - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0".to_string(), - vec![ - UntaggedValue::int(6).into(), - UntaggedValue::int(1).into(), - UntaggedValue::int(6).into(), - UntaggedValue::int(2).into(), - UntaggedValue::int(4).into(), - UntaggedValue::int(3).into(), - ], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let value: Value = args.req(0)?; - let indices: Value = args.req_named("indices")?; - - let indices = match &indices.value { - UntaggedValue::DataFrame(df) => Ok(df), - _ => Err(ShellError::labeled_error( - "Incorrect type", - "can only use a series for set command", - value.tag.span, - )), - }?; - - let indices = indices.as_series(&value.tag.span)?; - - let casted = match indices.dtype() { - DataType::UInt32 | DataType::UInt64 | DataType::Int32 | DataType::Int64 => indices - .as_ref() - .cast(&DataType::UInt32) - .map_err(|e| parse_polars_error::<&str>(&e, &value.tag.span, None)), - _ => Err(ShellError::labeled_error_with_secondary( - "Incorrect type", - "Series with incorrect type", - &value.tag.span, - "Consider using a Series with type int type", - &value.tag.span, - )), - }?; - - let indices = casted - .u32() - .map_err(|e| parse_polars_error::<&str>(&e, &value.tag.span, None))? - .into_iter() - .filter_map(|val| val.map(|v| v as usize)); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - let series = df.as_series(&df_tag.span)?; - - match &value.value { - UntaggedValue::Primitive(Primitive::Int(val)) => { - let chunked = series.i64().map_err(|e| { - parse_polars_error::<&str>( - &e, - &value.tag.span, - Some("The value has to match the set value type"), - ) - })?; - - let res = chunked - .set_at_idx(indices, Some(*val)) - .map_err(|e| parse_polars_error::<&str>(&e, &value.tag.span, None))?; - - let df = NuDataFrame::try_from_series(vec![res.into_series()], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) - } - UntaggedValue::Primitive(Primitive::Decimal(val)) => { - let chunked = series.as_ref().f64().map_err(|e| { - parse_polars_error::<&str>( - &e, - &value.tag.span, - Some("The value has to match the series type"), - ) - })?; - - let res = chunked - .set_at_idx( - indices, - Some( - val.to_f64() - .expect("internal error: expected f64-compatible decimal"), - ), - ) - .map_err(|e| parse_polars_error::<&str>(&e, &value.tag.span, None))?; - - let df = NuDataFrame::try_from_series(vec![res.into_series()], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) - } - UntaggedValue::Primitive(Primitive::String(val)) => { - let chunked = series.as_ref().utf8().map_err(|e| { - parse_polars_error::<&str>( - &e, - &value.tag.span, - Some("The value has to match the series type"), - ) - })?; - - let res = chunked - .set_at_idx(indices, Some(val.as_ref())) - .map_err(|e| parse_polars_error::<&str>(&e, &value.tag.span, None))?; - - let mut res = res.into_series(); - res.rename("string"); - - let df = NuDataFrame::try_from_series(vec![res.into_series()], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) - } - _ => Err(ShellError::labeled_error( - "Incorrect type", - format!( - "this value cannot be set in a series of type '{}'", - series.as_ref().dtype() - ), - value.tag.span, - )), - } -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/shift.rs b/crates/nu-command/src/commands/dataframe/series/shift.rs deleted file mode 100644 index 6aaa6b60bd..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/shift.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{dataframe::NuDataFrame, Signature, SyntaxShape}; -use nu_source::Tagged; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe shift" - } - - fn usage(&self) -> &str { - "[Series] Shifts the values by a given period" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe unique").required("period", SyntaxShape::Int, "shift period") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Shifts the values by a given period", - example: "[1 2 2 3 3] | dataframe to-df | dataframe shift 2", - result: None, - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let period: Tagged = args.req(0)?; - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let res = df.as_series(&df_tag.span)?.shift(period.item); - - let df = NuDataFrame::try_from_series(vec![res], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} diff --git a/crates/nu-command/src/commands/dataframe/series/str_lengths.rs b/crates/nu-command/src/commands/dataframe/series/str_lengths.rs deleted file mode 100644 index a2d31e1605..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/str_lengths.rs +++ /dev/null @@ -1,81 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, -}; -use polars::prelude::IntoSeries; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe str-lengths" - } - - fn usage(&self) -> &str { - "[Series] Get lengths of all strings" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe str-lengths") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Returns string lengths", - example: "[a ab abc] | dataframe to-df | dataframe str-lengths", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0".to_string(), - vec![ - UntaggedValue::int(1).into(), - UntaggedValue::int(2).into(), - UntaggedValue::int(3).into(), - ], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let series = df.as_series(&df_tag.span)?; - let chunked = series.utf8().map_err(|e| { - parse_polars_error::<&str>( - &e, - &df_tag.span, - Some("The str-lengths command can only be used with string columns"), - ) - })?; - - let res = chunked.as_ref().str_lengths(); - - let df = NuDataFrame::try_from_series(vec![res.into_series()], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/str_slice.rs b/crates/nu-command/src/commands/dataframe/series/str_slice.rs deleted file mode 100644 index 2689348c3a..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/str_slice.rs +++ /dev/null @@ -1,92 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, SyntaxShape, UntaggedValue, -}; -use nu_source::Tagged; -use polars::prelude::IntoSeries; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe str-slice" - } - - fn usage(&self) -> &str { - "[Series] Slices the string from the start position until the selected length" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe replace") - .required_named("start", SyntaxShape::Int, "start of slice", Some('s')) - .named("length", SyntaxShape::Int, "optional length", Some('l')) - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Creates slices from the strings", - example: "[abcded abc321 abc123] | dataframe to-df | dataframe str-slice -s 1 -l 2", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0".to_string(), - vec![ - UntaggedValue::string("bc").into(), - UntaggedValue::string("bc").into(), - UntaggedValue::string("bc").into(), - ], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let start: Tagged = args.req_named("start")?; - - let length: Option> = args.get_flag("length")?; - let length = length.map(|v| v.item as u64); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let series = df.as_series(&df_tag.span)?; - let chunked = series.utf8().map_err(|e| { - parse_polars_error::<&str>( - &e, - &df_tag.span, - Some("The str-slice command can only be used with string columns"), - ) - })?; - - let mut res = chunked - .str_slice(start.item, length) - .map_err(|e| parse_polars_error::<&str>(&e, &tag.span, None))?; - - res.rename(series.name()); - - let df = NuDataFrame::try_from_series(vec![res.into_series()], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/strftime.rs b/crates/nu-command/src/commands/dataframe/series/strftime.rs deleted file mode 100644 index 8be3f31fb7..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/strftime.rs +++ /dev/null @@ -1,80 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, SyntaxShape, UntaggedValue, -}; - -use nu_source::Tagged; -use polars::prelude::IntoSeries; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe strftime" - } - - fn usage(&self) -> &str { - "[Series] Formats date based on string rule" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe strftime").required("fmt", SyntaxShape::String, "Format rule") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Formats date", - example: r#"let dt = ('2020-08-04T16:39:18+00:00' | str to-datetime -z 'UTC'); - let df = ([$dt $dt] | dataframe to-df); - $df | dataframe strftime "%Y/%m/%d""#, - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0".to_string(), - vec![ - UntaggedValue::string("2020/08/04").into(), - UntaggedValue::string("2020/08/04").into(), - ], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let fmt: Tagged = args.req(0)?; - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - let series = df.as_series(&df_tag.span)?; - - let casted = series - .datetime() - .map_err(|e| parse_polars_error::<&str>(&e, &df_tag.span, None))?; - - let res = casted.strftime(&fmt.item).into_series(); - let df = NuDataFrame::try_from_series(vec![res], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/to_lowercase.rs b/crates/nu-command/src/commands/dataframe/series/to_lowercase.rs deleted file mode 100644 index 7ea88f3b1b..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/to_lowercase.rs +++ /dev/null @@ -1,82 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, -}; -use polars::prelude::IntoSeries; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe to-lowercase" - } - - fn usage(&self) -> &str { - "[Series] Lowercase the strings in the column" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe to-lowercase") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Modifies strings to lowercase", - example: "[Abc aBc abC] | dataframe to-df | dataframe to-lowercase", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0".to_string(), - vec![ - UntaggedValue::string("abc").into(), - UntaggedValue::string("abc").into(), - UntaggedValue::string("abc").into(), - ], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let series = df.as_series(&df_tag.span)?; - let chunked = series.utf8().map_err(|e| { - parse_polars_error::<&str>( - &e, - &df_tag.span, - Some("The to-lowercase command can only be used with string columns"), - ) - })?; - - let mut res = chunked.to_lowercase(); - res.rename(series.name()); - - let df = NuDataFrame::try_from_series(vec![res.into_series()], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/to_uppercase.rs b/crates/nu-command/src/commands/dataframe/series/to_uppercase.rs deleted file mode 100644 index ab06655723..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/to_uppercase.rs +++ /dev/null @@ -1,82 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, -}; -use polars::prelude::IntoSeries; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe to-uppercase" - } - - fn usage(&self) -> &str { - "[Series] Uppercase the strings in the column" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe to-uppercase") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Modifies strings to uppercase", - example: "[Abc aBc abC] | dataframe to-df | dataframe to-uppercase", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0".to_string(), - vec![ - UntaggedValue::string("ABC").into(), - UntaggedValue::string("ABC").into(), - UntaggedValue::string("ABC").into(), - ], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let series = df.as_series(&df_tag.span)?; - let chunked = series.utf8().map_err(|e| { - parse_polars_error::<&str>( - &e, - &df_tag.span, - Some("The to-uppercase command can only be used with string columns"), - ) - })?; - - let mut res = chunked.to_uppercase(); - res.rename(series.name()); - - let df = NuDataFrame::try_from_series(vec![res.into_series()], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/unique.rs b/crates/nu-command/src/commands/dataframe/series/unique.rs deleted file mode 100644 index eac8994b59..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/unique.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::{commands::dataframe::utils::parse_polars_error, prelude::*}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, -}; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe unique" - } - - fn usage(&self) -> &str { - "[Series] Returns unique values from a series" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe unique") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Returns unique values from a series", - example: "[2 2 2 2 2] | dataframe to-df | dataframe unique", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0".to_string(), - vec![UntaggedValue::int(2).into()], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let res = df - .as_series(&df_tag.span)? - .unique() - .map_err(|e| parse_polars_error::<&str>(&e, &tag.span, None))?; - - let df = NuDataFrame::try_from_series(vec![res], &tag.span)?; - Ok(OutputStream::one(df.into_value(df_tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/series/value_counts.rs b/crates/nu-command/src/commands/dataframe/series/value_counts.rs deleted file mode 100644 index a992db6a50..0000000000 --- a/crates/nu-command/src/commands/dataframe/series/value_counts.rs +++ /dev/null @@ -1,79 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, -}; - -use crate::commands::dataframe::utils::parse_polars_error; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe value-counts" - } - - fn usage(&self) -> &str { - "[Series] Returns a dataframe with the counts for unique values in series" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe value-counts") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Calculates value counts", - example: "[5 5 5 5 6 6] | dataframe to-df | dataframe value-counts", - result: Some(vec![NuDataFrame::try_from_columns( - vec![ - Column::new( - "0".to_string(), - vec![UntaggedValue::int(5).into(), UntaggedValue::int(6).into()], - ), - Column::new( - "counts".to_string(), - vec![UntaggedValue::int(4).into(), UntaggedValue::int(2).into()], - ), - ], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, df_tag) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let df_new = df - .as_series(&df_tag.span)? - .value_counts() - .map_err(|e| parse_polars_error::<&str>(&e, &tag.span, None))?; - - Ok(OutputStream::one(NuDataFrame::dataframe_to_value( - df_new, tag, - ))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/shape.rs b/crates/nu-command/src/commands/dataframe/shape.rs deleted file mode 100644 index 29424b034d..0000000000 --- a/crates/nu-command/src/commands/dataframe/shape.rs +++ /dev/null @@ -1,78 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, Value, -}; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe shape" - } - - fn usage(&self) -> &str { - "[DataFrame] Shows column and row size for a dataframe" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe shape") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Shows row and column shape", - example: "[[a b]; [1 2] [3 4]] | dataframe to-df | dataframe shape", - result: Some(vec![NuDataFrame::try_from_columns( - vec![ - Column::new("rows".to_string(), vec![UntaggedValue::int(2).into()]), - Column::new("columns".to_string(), vec![UntaggedValue::int(2).into()]), - ], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (df, _) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let rows = Value { - value: (df.as_ref().height() as i64).into(), - tag: Tag::default(), - }; - - let cols = Value { - value: (df.as_ref().width() as i64).into(), - tag: Tag::default(), - }; - - let rows_col = Column::new("rows".to_string(), vec![rows]); - let cols_col = Column::new("columns".to_string(), vec![cols]); - - let df = NuDataFrame::try_from_columns(vec![rows_col, cols_col], &tag.span)?; - Ok(OutputStream::one(df.into_value(tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/show.rs b/crates/nu-command/src/commands/dataframe/show.rs deleted file mode 100644 index 6a21d24dae..0000000000 --- a/crates/nu-command/src/commands/dataframe/show.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{dataframe::NuDataFrame, Signature, SyntaxShape}; - -use nu_source::Tagged; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe show" - } - - fn usage(&self) -> &str { - "[DataFrame] Converts a section of the dataframe to a Table or List value" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe show") - .named( - "n_rows", - SyntaxShape::Number, - "number of rows to be shown", - Some('n'), - ) - .switch("tail", "shows tail rows", Some('t')) - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Shows head rows from dataframe", - example: "[[a b]; [1 2] [3 4]] | dataframe to-df | dataframe show", - result: None, - }, - Example { - description: "Shows tail rows from dataframe", - example: "[[a b]; [1 2] [3 4] [5 6]] | dataframe to-df | dataframe show -t -n 1", - result: None, - }, - ] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let rows: Option> = args.get_flag("n_rows")?; - let tail: bool = args.has_flag("tail"); - - let (df, _) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - let rows = rows.map(|v| v.item); - let values = if tail { df.tail(rows)? } else { df.head(rows)? }; - - Ok(OutputStream::from_stream(values.into_iter())) -} diff --git a/crates/nu-command/src/commands/dataframe/slice.rs b/crates/nu-command/src/commands/dataframe/slice.rs deleted file mode 100644 index 5a7b5c6f2f..0000000000 --- a/crates/nu-command/src/commands/dataframe/slice.rs +++ /dev/null @@ -1,71 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, SyntaxShape, UntaggedValue, -}; - -use nu_source::Tagged; -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe slice" - } - - fn usage(&self) -> &str { - "[DataFrame] Creates new dataframe from a slice of rows" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe slice") - .required("offset", SyntaxShape::Number, "start of slice") - .required("size", SyntaxShape::Number, "size of slice") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Create new dataframe from a slice of the rows", - example: "[[a b]; [1 2] [3 4]] | dataframe to-df | dataframe slice 0 1", - result: Some(vec![NuDataFrame::try_from_columns( - vec![ - Column::new("a".to_string(), vec![UntaggedValue::int(1).into()]), - Column::new("b".to_string(), vec![UntaggedValue::int(2).into()]), - ], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let offset: Tagged = args.req(0)?; - let size: Tagged = args.req(1)?; - - let (df, _) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - let res = df.as_ref().slice(offset.item as i64, size.item); - - Ok(OutputStream::one(NuDataFrame::dataframe_to_value(res, tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/sort.rs b/crates/nu-command/src/commands/dataframe/sort.rs deleted file mode 100644 index 6146847a8b..0000000000 --- a/crates/nu-command/src/commands/dataframe/sort.rs +++ /dev/null @@ -1,134 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, SyntaxShape, UntaggedValue, Value, -}; - -use super::utils::{convert_columns, parse_polars_error}; -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe sort" - } - - fn usage(&self) -> &str { - "[DataFrame, Series] Creates new sorted dataframe or series" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe sort") - .switch("reverse", "invert sort", Some('r')) - .rest("rest", SyntaxShape::Any, "column names to sort dataframe") - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Create new sorted dataframe", - example: "[[a b]; [3 4] [1 2]] | dataframe to-df | dataframe sort a", - result: Some(vec![NuDataFrame::try_from_columns( - vec![ - Column::new( - "a".to_string(), - vec![UntaggedValue::int(1).into(), UntaggedValue::int(3).into()], - ), - Column::new( - "b".to_string(), - vec![UntaggedValue::int(2).into(), UntaggedValue::int(4).into()], - ), - ], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }, - Example { - description: "Create new sorted series", - example: "[3 4 1 2] | dataframe to-df | dataframe sort", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0".to_string(), - vec![ - UntaggedValue::int(1).into(), - UntaggedValue::int(2).into(), - UntaggedValue::int(3).into(), - UntaggedValue::int(4).into(), - ], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }, - ] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let value = args.input.next().ok_or_else(|| { - ShellError::labeled_error("Empty stream", "No value found in stream", &tag.span) - })?; - - let reverse = args.has_flag("reverse"); - - match &value.value { - UntaggedValue::DataFrame(df) => { - if df.is_series() { - let columns = df.as_ref().get_column_names(); - - let res = df - .as_ref() - .sort(columns, reverse) - .map_err(|e| parse_polars_error::<&str>(&e, &value.tag.span, None))?; - - Ok(OutputStream::one(NuDataFrame::dataframe_to_value(res, tag))) - } else { - let columns: Vec = args.rest(0)?; - - if !columns.is_empty() { - let (col_string, col_span) = convert_columns(&columns, &tag)?; - - let res = df - .as_ref() - .sort(&col_string, reverse) - .map_err(|e| parse_polars_error::<&str>(&e, &col_span, None))?; - - Ok(OutputStream::one(NuDataFrame::dataframe_to_value(res, tag))) - } else { - Err(ShellError::labeled_error( - "Missing columns", - "missing column name to perform sort", - &tag.span, - )) - } - } - } - _ => Err(ShellError::labeled_error( - "Incorrect type", - "sort cannot be done with this value", - &value.tag.span, - )), - } -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/take.rs b/crates/nu-command/src/commands/dataframe/take.rs deleted file mode 100644 index 1373d8c7db..0000000000 --- a/crates/nu-command/src/commands/dataframe/take.rs +++ /dev/null @@ -1,142 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, SyntaxShape, UntaggedValue, Value, -}; -use polars::prelude::DataType; - -use super::utils::parse_polars_error; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe take" - } - - fn usage(&self) -> &str { - "[DataFrame, Series] Creates new dataframe using the given indices" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe take").required( - "indices", - SyntaxShape::Any, - "list of indices used to take data", - ) - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Takes selected rows from dataframe", - example: r#"let df = ([[a b]; [4 1] [5 2] [4 3]] | dataframe to-df); - let indices = ([0 2] | dataframe to-df); - $df | dataframe take $indices"#, - result: Some(vec![NuDataFrame::try_from_columns( - vec![ - Column::new( - "a".to_string(), - vec![UntaggedValue::int(4).into(), UntaggedValue::int(4).into()], - ), - Column::new( - "b".to_string(), - vec![UntaggedValue::int(1).into(), UntaggedValue::int(3).into()], - ), - ], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }, - Example { - description: "Takes selected rows from series", - example: r#"let series = ([4 1 5 2 4 3] | dataframe to-df); - let indices = ([0 2] | dataframe to-df); - $series | dataframe take $indices"#, - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0".to_string(), - vec![UntaggedValue::int(4).into(), UntaggedValue::int(5).into()], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }, - ] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let value: Value = args.req(0)?; - - let df = match &value.value { - UntaggedValue::DataFrame(df) => Ok(df), - _ => Err(ShellError::labeled_error( - "Incorrect type", - "can only use a series for take command", - value.tag.span, - )), - }?; - - let series = df.as_series(&value.tag.span)?; - - let casted = match series.dtype() { - DataType::UInt32 | DataType::UInt64 | DataType::Int32 | DataType::Int64 => series - .as_ref() - .cast(&DataType::UInt32) - .map_err(|e| parse_polars_error::<&str>(&e, &value.tag.span, None)), - _ => Err(ShellError::labeled_error_with_secondary( - "Incorrect type", - "Series with incorrect type", - &value.tag.span, - "Consider using a Series with type int type", - &value.tag.span, - )), - }?; - - let indices = casted - .u32() - .map_err(|e| parse_polars_error::<&str>(&e, &value.tag.span, None))?; - - let value = args.input.next().ok_or_else(|| { - ShellError::labeled_error("Empty stream", "No value found in the stream", &tag) - })?; - - match &value.value { - UntaggedValue::DataFrame(df) => { - let res = df - .as_ref() - .take(indices) - .map_err(|e| parse_polars_error::<&str>(&e, &value.tag.span, None))?; - - Ok(OutputStream::one(NuDataFrame::dataframe_to_value(res, tag))) - } - _ => Err(ShellError::labeled_error( - "No dataframe or series in stream", - "no dataframe or series found in input stream", - &value.tag.span, - )), - } -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/to_csv.rs b/crates/nu-command/src/commands/dataframe/to_csv.rs deleted file mode 100644 index d0d9a600af..0000000000 --- a/crates/nu-command/src/commands/dataframe/to_csv.rs +++ /dev/null @@ -1,114 +0,0 @@ -use std::fs::File; -use std::path::PathBuf; - -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::dataframe::NuDataFrame; -use nu_protocol::Primitive; -use nu_protocol::Value; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue}; - -use polars::prelude::{CsvWriter, SerWriter}; - -use nu_source::Tagged; - -use super::utils::parse_polars_error; -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe to-csv" - } - - fn usage(&self) -> &str { - "[DataFrame] Saves dataframe to csv file" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe to-csv") - .required("file", SyntaxShape::FilePath, "file path to save dataframe") - .named( - "delimiter", - SyntaxShape::String, - "file delimiter character", - Some('d'), - ) - .switch("no_header", "Indicates if file doesn't have header", None) - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Saves dataframe to csv file", - example: "[[a b]; [1 2] [3 4]] | dataframe to-df | dataframe to-csv test.csv", - result: None, - }, - Example { - description: "Saves dataframe to csv file using other delimiter", - example: - "[[a b]; [1 2] [3 4]] | dataframe to-df | dataframe to-csv test.csv -d '|'", - result: None, - }, - ] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let file_name: Tagged = args.req(0)?; - let delimiter: Option> = args.get_flag("delimiter")?; - let no_header: bool = args.has_flag("no_header"); - - let (df, _) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let mut file = File::create(&file_name.item).map_err(|e| { - ShellError::labeled_error("Error with file name", e.to_string(), &file_name.tag.span) - })?; - - let writer = CsvWriter::new(&mut file); - - let writer = if no_header { - writer.has_header(false) - } else { - writer.has_header(true) - }; - - let writer = match delimiter { - None => writer, - Some(d) => { - if d.item.len() != 1 { - return Err(ShellError::labeled_error( - "Incorrect delimiter", - "Delimiter has to be one char", - &d.tag, - )); - } else { - let delimiter = match d.item.chars().next() { - Some(d) => d as u8, - None => unreachable!(), - }; - - writer.with_delimiter(delimiter) - } - } - }; - - writer - .finish(df.as_ref()) - .map_err(|e| parse_polars_error::<&str>(&e, &file_name.tag.span, None))?; - - let tagged_value = Value { - value: UntaggedValue::Primitive(Primitive::String(format!( - "saved {}", - &file_name.item.to_str().expect("csv file") - ))), - tag: Tag::unknown(), - }; - - Ok(InputStream::one(tagged_value).into_output_stream()) -} diff --git a/crates/nu-command/src/commands/dataframe/to_df.rs b/crates/nu-command/src/commands/dataframe/to_df.rs deleted file mode 100644 index cb85d6b08e..0000000000 --- a/crates/nu-command/src/commands/dataframe/to_df.rs +++ /dev/null @@ -1,137 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, UntaggedValue, -}; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe to-df" - } - - fn usage(&self) -> &str { - "Converts a List, Table or Dictionary into a polars dataframe" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe to-df") - } - - fn run(&self, args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let df = NuDataFrame::try_from_iter(args.input, &tag)?; - - Ok(InputStream::one(df.into_value(tag))) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Takes a dictionary and creates a dataframe", - example: "[[a b];[1 2] [3 4]] | dataframe to-df", - result: Some(vec![NuDataFrame::try_from_columns( - vec![ - Column::new( - "a".to_string(), - vec![UntaggedValue::int(1).into(), UntaggedValue::int(3).into()], - ), - Column::new( - "b".to_string(), - vec![UntaggedValue::int(2).into(), UntaggedValue::int(4).into()], - ), - ], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }, - Example { - description: "Takes a list of tables and creates a dataframe", - example: "[[1 2 a] [3 4 b] [5 6 c]] | dataframe to-df", - result: Some(vec![NuDataFrame::try_from_columns( - vec![ - Column::new( - "0".to_string(), - vec![ - UntaggedValue::int(1).into(), - UntaggedValue::int(3).into(), - UntaggedValue::int(5).into(), - ], - ), - Column::new( - "1".to_string(), - vec![ - UntaggedValue::int(2).into(), - UntaggedValue::int(4).into(), - UntaggedValue::int(6).into(), - ], - ), - Column::new( - "2".to_string(), - vec![ - UntaggedValue::string("a").into(), - UntaggedValue::string("b").into(), - UntaggedValue::string("c").into(), - ], - ), - ], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }, - Example { - description: "Takes a list and creates a dataframe", - example: "[a b c] | dataframe to-df", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0".to_string(), - vec![ - UntaggedValue::string("a").into(), - UntaggedValue::string("b").into(), - UntaggedValue::string("c").into(), - ], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }, - Example { - description: "Takes a list of booleans and creates a dataframe", - example: "[$true $true $false] | dataframe to-df", - result: Some(vec![NuDataFrame::try_from_columns( - vec![Column::new( - "0".to_string(), - vec![ - UntaggedValue::boolean(true).into(), - UntaggedValue::boolean(true).into(), - UntaggedValue::boolean(false).into(), - ], - )], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }, - ] - } -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/to_parquet.rs b/crates/nu-command/src/commands/dataframe/to_parquet.rs deleted file mode 100644 index 87bb4b925e..0000000000 --- a/crates/nu-command/src/commands/dataframe/to_parquet.rs +++ /dev/null @@ -1,70 +0,0 @@ -use std::fs::File; -use std::path::PathBuf; - -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::dataframe::NuDataFrame; -use nu_protocol::{Primitive, Signature, SyntaxShape, UntaggedValue, Value}; - -use polars::prelude::ParquetWriter; - -use nu_source::Tagged; - -use super::utils::parse_polars_error; -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe to-parquet" - } - - fn usage(&self) -> &str { - "[DataFrame] Saves dataframe to parquet file" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe to-parquet").required( - "file", - SyntaxShape::FilePath, - "file path to save dataframe", - ) - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Saves dataframe to parquet file", - example: "[[a b]; [1 2] [3 4]] | dataframe to-df | dataframe to-parquet test.parquet", - result: None, - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let file_name: Tagged = args.req(0)?; - - let (df, _) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - let file = File::create(&file_name.item).map_err(|e| { - ShellError::labeled_error("Error with file name", e.to_string(), &file_name.tag.span) - })?; - - ParquetWriter::new(file) - .finish(df.as_ref()) - .map_err(|e| parse_polars_error::<&str>(&e, &file_name.tag.span, None))?; - - let tagged_value = Value { - value: UntaggedValue::Primitive(Primitive::String(format!( - "saved {}", - &file_name.item.to_str().expect("parquet file") - ))), - tag: Tag::unknown(), - }; - - Ok(InputStream::one(tagged_value).into_output_stream()) -} diff --git a/crates/nu-command/src/commands/dataframe/utils.rs b/crates/nu-command/src/commands/dataframe/utils.rs deleted file mode 100644 index 20245c25c3..0000000000 --- a/crates/nu-command/src/commands/dataframe/utils.rs +++ /dev/null @@ -1,78 +0,0 @@ -use crate::prelude::*; -use nu_errors::ShellError; -use nu_protocol::{Primitive, UntaggedValue, Value}; -use polars::prelude::PolarsError; - -// Converts a Vec to a Vec with a Span marking the whole -// location of the columns for error referencing -pub(crate) fn convert_columns( - columns: &[Value], - tag: &Tag, -) -> Result<(Vec, Span), ShellError> { - let mut col_span = match columns - .get(0) - .map(|v| Span::new(v.tag.span.start(), v.tag.span.end())) - { - Some(span) => span, - None => { - return Err(ShellError::labeled_error( - "Empty column list", - "Empty list found for command", - tag, - )) - } - }; - - let res = columns - .iter() - .map(|value| match &value.value { - UntaggedValue::Primitive(Primitive::String(s)) => { - col_span = col_span.until(value.tag.span); - Ok(s.clone()) - } - _ => Err(ShellError::labeled_error( - "Incorrect column format", - "Only string as column name", - &value.tag, - )), - }) - .collect::, _>>()?; - - Ok((res, col_span)) -} - -pub(crate) fn parse_polars_error>( - e: &PolarsError, - span: &Span, - secondary: Option, -) -> ShellError { - let msg = match e { - PolarsError::PolarsArrowError(_) => "PolarsArrow Error", - PolarsError::ArrowError(_) => "Arrow Error", - PolarsError::InvalidOperation(_) => "Invalid Operation", - PolarsError::DataTypeMisMatch(_) => "Data Type Mismatch", - PolarsError::NotFound(_) => "Not Found", - PolarsError::ShapeMisMatch(_) => "Shape Mismatch", - PolarsError::ComputeError(_) => "Computer error", - PolarsError::OutOfBounds(_) => "Out Of Bounds", - PolarsError::NoSlice => "No Slice", - PolarsError::NoData(_) => "No Data", - PolarsError::ValueError(_) => "Value Error", - PolarsError::MemoryNotAligned => "Memory Not Aligned", - PolarsError::RandError(_) => "Rand Error", - PolarsError::HasNullValues(_) => "Has Null Values", - PolarsError::UnknownSchema(_) => "Unknown Schema", - PolarsError::Various(_) => "Various", - PolarsError::Io(_) => "Io Error", - PolarsError::Regex(_) => "Regex Error", - PolarsError::Duplicate(_) => "Duplicate Error", - PolarsError::ImplementationError => "Implementation Error", - }; - - let label = e.to_string(); - - match secondary { - None => ShellError::labeled_error(msg, label, span), - Some(s) => ShellError::labeled_error_with_secondary(msg, label, span, s.as_ref(), span), - } -} diff --git a/crates/nu-command/src/commands/dataframe/where_.rs b/crates/nu-command/src/commands/dataframe/where_.rs deleted file mode 100644 index aaf5d2b257..0000000000 --- a/crates/nu-command/src/commands/dataframe/where_.rs +++ /dev/null @@ -1,237 +0,0 @@ -use crate::prelude::*; -use nu_engine::{evaluate_baseline_expr, WholeStreamCommand}; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - hir::{CapturedBlock, ClassifiedCommand, Expression, Literal, Operator, SpannedExpression}, - Primitive, Signature, SyntaxShape, UnspannedPathMember, UntaggedValue, Value, -}; - -use super::utils::parse_polars_error; -use polars::prelude::{ChunkCompare, DataType, Series}; - -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe where" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe where").required( - "condition", - SyntaxShape::RowCondition, - "the condition that must match", - ) - } - - fn usage(&self) -> &str { - "[DataFrame] Filter dataframe to match the condition" - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Filter dataframe based on column a", - example: "[[a b]; [1 2] [3 4]] | dataframe to-df | dataframe where a == 1", - result: Some(vec![NuDataFrame::try_from_columns( - vec![ - Column::new("a".to_string(), vec![UntaggedValue::int(1).into()]), - Column::new("b".to_string(), vec![UntaggedValue::int(2).into()]), - ], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let block: CapturedBlock = args.req(0)?; - - let expression = block - .block - .block - .get(0) - .and_then(|group| { - group - .pipelines - .get(0) - .and_then(|v| v.list.get(0)) - .and_then(|expr| match &expr { - ClassifiedCommand::Expr(expr) => match &expr.as_ref().expr { - Expression::Binary(expr) => Some(expr), - _ => None, - }, - _ => None, - }) - }) - .ok_or_else(|| { - ShellError::labeled_error("Expected a condition", "expected a condition", &tag.span) - })?; - - let lhs = match &expression.left.expr { - Expression::FullColumnPath(p) => p.as_ref().tail.get(0), - _ => None, - } - .ok_or_else(|| { - ShellError::labeled_error( - "No column name", - "Not a column name found in left hand side of comparison", - &expression.left.span, - ) - })?; - - let (col_name, col_name_span) = match &lhs.unspanned { - UnspannedPathMember::String(name) => Ok((name, &lhs.span)), - _ => Err(ShellError::labeled_error( - "No column name", - "Not a string as column name", - &lhs.span, - )), - }?; - - let rhs = evaluate_baseline_expr(&expression.right, &args.context)?; - - filter_dataframe(args, col_name, col_name_span, &rhs, &expression.op) -} - -macro_rules! comparison_arm { - ($comparison:expr, $col:expr, $condition:expr, $span:expr) => { - match $condition { - Primitive::Int(val) => Ok($comparison($col, *val)), - Primitive::BigInt(val) => Ok($comparison( - $col, - val.to_i64() - .expect("Internal error: protocol did not use compatible decimal"), - )), - Primitive::Decimal(val) => Ok($comparison( - $col, - val.to_f64() - .expect("Internal error: protocol did not use compatible decimal"), - )), - Primitive::String(val) => { - let temp: &str = val.as_ref(); - Ok($comparison($col, temp)) - } - _ => Err(ShellError::labeled_error( - "Invalid datatype", - format!( - "this operator cannot be used with the selected '{}' datatype", - $col.dtype() - ), - &$span, - )), - } - }; -} - -// With the information extracted from the block we can filter the dataframe using -// polars operations -fn filter_dataframe( - mut args: CommandArgs, - col_name: &str, - col_name_span: &Span, - rhs: &Value, - operator: &SpannedExpression, -) -> Result { - let right_condition = match &rhs.value { - UntaggedValue::Primitive(primitive) => Ok(primitive), - _ => Err(ShellError::labeled_error( - "Incorrect argument", - "Expected primitive values", - &rhs.tag.span, - )), - }?; - - let span = args.call_info.name_tag.span; - let (df, _) = NuDataFrame::try_from_stream(&mut args.input, &span)?; - - let col = df - .as_ref() - .column(col_name) - .map_err(|e| parse_polars_error::<&str>(&e, col_name_span, None))?; - - let op = match &operator.expr { - Expression::Literal(Literal::Operator(op)) => Ok(op), - _ => Err(ShellError::labeled_error( - "Incorrect argument", - "Expected operator", - &operator.span, - )), - }?; - - let mask = match op { - Operator::Equal => comparison_arm!(Series::eq, col, right_condition, operator.span), - Operator::NotEqual => comparison_arm!(Series::neq, col, right_condition, operator.span), - Operator::LessThan => comparison_arm!(Series::lt, col, right_condition, operator.span), - Operator::LessThanOrEqual => { - comparison_arm!(Series::lt_eq, col, right_condition, operator.span) - } - Operator::GreaterThan => comparison_arm!(Series::gt, col, right_condition, operator.span), - Operator::GreaterThanOrEqual => { - comparison_arm!(Series::gt_eq, col, right_condition, operator.span) - } - Operator::Contains => match col.dtype() { - DataType::Utf8 => match right_condition { - Primitive::String(pat) => { - let casted = col.utf8().map_err(|e| { - parse_polars_error::<&str>(&e, &args.call_info.name_tag.span, None) - })?; - - casted.contains(pat).map_err(|e| { - parse_polars_error::<&str>(&e, &args.call_info.name_tag.span, None) - }) - } - _ => Err(ShellError::labeled_error_with_secondary( - "Incorrect argument", - "Can't perform contains with this value", - &rhs.tag.span, - "Contains only works with strings", - &rhs.tag.span, - )), - }, - _ => Err(ShellError::labeled_error_with_secondary( - "Incorrect datatype", - format!("The selected column is of type '{}'", col.dtype()), - col_name_span, - "Perhaps you want to select a column of 'str' type", - col_name_span, - )), - }, - _ => Err(ShellError::labeled_error( - "Incorrect operator", - "Not implemented operator for dataframes filter", - &operator.span, - )), - }?; - - let res = df - .as_ref() - .filter(&mask) - .map_err(|e| parse_polars_error::<&str>(&e, &args.call_info.name_tag.span, None))?; - - Ok(OutputStream::one(NuDataFrame::dataframe_to_value( - res, - args.call_info.name_tag, - ))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/dataframe/with_column.rs b/crates/nu-command/src/commands/dataframe/with_column.rs deleted file mode 100644 index da2225add9..0000000000 --- a/crates/nu-command/src/commands/dataframe/with_column.rs +++ /dev/null @@ -1,107 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - dataframe::{Column, NuDataFrame}, - Signature, SyntaxShape, UntaggedValue, Value, -}; -use nu_source::Tagged; - -use super::utils::parse_polars_error; -pub struct DataFrame; - -impl WholeStreamCommand for DataFrame { - fn name(&self) -> &str { - "dataframe with-column" - } - - fn usage(&self) -> &str { - "[DataFrame] Adds a series to the dataframe" - } - - fn signature(&self) -> Signature { - Signature::build("dataframe with-column") - .required("series", SyntaxShape::Any, "series to be added") - .required_named("name", SyntaxShape::String, "column name", Some('n')) - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Adds a series to the dataframe", - example: - "[[a b]; [1 2] [3 4]] | dataframe to-df | dataframe with-column ([5 6] | dataframe to-df) --name c", - result: Some(vec![NuDataFrame::try_from_columns( - vec![ - Column::new( - "a".to_string(), - vec![ - UntaggedValue::int(1).into(), - UntaggedValue::int(3).into(), - ], - ), - Column::new( - "b".to_string(), - vec![ - UntaggedValue::int(2).into(), - UntaggedValue::int(4).into(), - ], - ), - Column::new( - "c".to_string(), - vec![ - UntaggedValue::int(5).into(), - UntaggedValue::int(6).into(), - ], - ), - ], - &Span::default(), - ) - .expect("simple df for test should not fail") - .into_value(Tag::default())]), - }] - } -} - -fn command(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let value: Value = args.req(0)?; - let name: Tagged = args.req_named("name")?; - - let df = match value.value { - UntaggedValue::DataFrame(df) => Ok(df), - _ => Err(ShellError::labeled_error( - "Incorrect type", - "can only add a series to a dataframe", - value.tag.span, - )), - }?; - - let mut series = df.as_series(&value.tag.span)?; - - let series = series.rename(&name.item).clone(); - - let (mut df, _) = NuDataFrame::try_from_stream(&mut args.input, &tag.span)?; - - df.as_mut() - .with_column(series) - .map_err(|e| parse_polars_error::<&str>(&e, &tag.span, None))?; - - Ok(OutputStream::one(df.into_value(tag))) -} - -#[cfg(test)] -mod tests { - use super::DataFrame; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test_dataframe as test_examples; - - test_examples(DataFrame {}) - } -} diff --git a/crates/nu-command/src/commands/env/autoenv.rs b/crates/nu-command/src/commands/env/autoenv.rs deleted file mode 100644 index d8a8b492ae..0000000000 --- a/crates/nu-command/src/commands/env/autoenv.rs +++ /dev/null @@ -1,47 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ReturnSuccess, Signature, UntaggedValue}; -pub struct Autoenv; - -impl WholeStreamCommand for Autoenv { - fn name(&self) -> &str { - "autoenv" - } - fn usage(&self) -> &str { - "Manage directory specific environment variables and scripts." - } - - fn extra_usage(&self) -> &str { - // "Mark a .nu-env file in a directory as trusted. Needs to be re-run after each change to the file or its filepath." - r#"Create a file called .nu-env in any directory and run 'autoenv trust' to let nushell load it when entering the directory. -The .nu-env file has the same format as your $HOME/nu/config.toml file. By loading a .nu-env file the following applies: - - environment variables (section \"[env]\") are loaded from the .nu-env file. Those env variables only exist in this directory (and children directories) - - the \"startup\" commands are run when entering the directory - - the \"on_exit\" commands are run when leaving the directory -"# - } - - fn signature(&self) -> Signature { - Signature::build("autoenv") - } - fn run_with_actions(&self, args: CommandArgs) -> Result { - Ok(ActionStream::one(ReturnSuccess::value( - UntaggedValue::string(get_full_help(&Autoenv, args.scope())).into_value(Tag::unknown()), - ))) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Example .nu-env file", - example: r#"cat .nu-env - startup = ["echo ...entering the directory", "echo 1 2 3"] - on_exit = ["echo ...leaving the directory"] - - [env] - mykey = "myvalue" - "#, - result: None, - }] - } -} diff --git a/crates/nu-command/src/commands/env/autoenv_trust.rs b/crates/nu-command/src/commands/env/autoenv_trust.rs deleted file mode 100644 index d3f62ca053..0000000000 --- a/crates/nu-command/src/commands/env/autoenv_trust.rs +++ /dev/null @@ -1,85 +0,0 @@ -use crate::prelude::*; -use nu_data::config::read_trusted; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::SyntaxShape; -use nu_protocol::{Primitive, ReturnSuccess, Signature, UntaggedValue, Value}; -use sha2::{Digest, Sha256}; -use std::{fs, path::PathBuf}; -pub struct AutoenvTrust; - -impl WholeStreamCommand for AutoenvTrust { - fn name(&self) -> &str { - "autoenv trust" - } - - fn signature(&self) -> Signature { - Signature::build("autoenv trust") - .optional("dir", SyntaxShape::String, "Directory to allow") - .switch("quiet", "Don't output success message", Some('q')) - } - - fn usage(&self) -> &str { - "Trust a .nu-env file in the current or given directory" - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let file_to_trust = match args.opt(0)? { - Some(Value { - value: UntaggedValue::Primitive(Primitive::String(ref path)), - tag: _, - }) => { - let mut dir = nu_path::canonicalize(path)?; - dir.push(".nu-env"); - dir - } - _ => { - let mut dir = nu_path::canonicalize(std::env::current_dir()?)?; - dir.push(".nu-env"); - dir - } - }; - let quiet = args.has_flag("quiet"); - - let content = std::fs::read(&file_to_trust)?; - - let filename = file_to_trust.to_string_lossy().to_string(); - let mut allowed = read_trusted()?; - allowed - .files - .insert(filename, Sha256::digest(&content).as_slice().to_vec()); - - let config_path = config::default_path_for(&Some(PathBuf::from("nu-env.toml")))?; - let tomlstr = toml::to_string(&allowed).map_err(|_| { - ShellError::untagged_runtime_error("Couldn't serialize allowed dirs to nu-env.toml") - })?; - fs::write(config_path, tomlstr).expect("Couldn't write to toml file"); - - if quiet { - Ok(ActionStream::empty()) - } else { - Ok(ActionStream::one(ReturnSuccess::value( - UntaggedValue::string(".nu-env trusted!").into_value(tag), - ))) - } - } - fn is_binary(&self) -> bool { - false - } - fn examples(&self) -> Vec { - vec![ - Example { - description: "Allow .nu-env file in current directory", - example: "autoenv trust", - result: None, - }, - Example { - description: "Allow .nu-env file in directory foo", - example: "autoenv trust foo", - result: None, - }, - ] - } -} diff --git a/crates/nu-command/src/commands/env/autoenv_untrust.rs b/crates/nu-command/src/commands/env/autoenv_untrust.rs deleted file mode 100644 index 61e1448055..0000000000 --- a/crates/nu-command/src/commands/env/autoenv_untrust.rs +++ /dev/null @@ -1,105 +0,0 @@ -use crate::prelude::*; -use nu_data::config::Trusted; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::SyntaxShape; -use nu_protocol::{Primitive, ReturnSuccess, Signature, UntaggedValue, Value}; -use std::io::Read; -use std::{fs, path::PathBuf}; -pub struct AutoenvUntrust; - -impl WholeStreamCommand for AutoenvUntrust { - fn name(&self) -> &str { - "autoenv untrust" - } - - fn signature(&self) -> Signature { - Signature::build("autoenv untrust") - .optional("dir", SyntaxShape::String, "Directory to disallow") - .switch("quiet", "Don't output success message", Some('q')) - } - - fn usage(&self) -> &str { - "Untrust a .nu-env file in the current or given directory" - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let file_to_untrust = match args.opt(0)? { - Some(Value { - value: UntaggedValue::Primitive(Primitive::String(ref path)), - tag: _, - }) => { - let mut dir = nu_path::canonicalize(path)?; - dir.push(".nu-env"); - dir - } - _ => { - let mut dir = std::env::current_dir()?; - dir.push(".nu-env"); - dir - } - }; - let quiet = args.has_flag("quiet"); - - let config_path = config::default_path_for(&Some(PathBuf::from("nu-env.toml")))?; - - let mut file = match std::fs::OpenOptions::new() - .read(true) - .create(true) - .write(true) - .open(config_path.clone()) - { - Ok(p) => p, - Err(_) => { - return Err(ShellError::untagged_runtime_error( - "Couldn't open nu-env.toml", - )); - } - }; - - let mut doc = String::new(); - file.read_to_string(&mut doc)?; - - let mut allowed: Trusted = toml::from_str(&doc).unwrap_or_else(|_| Trusted::new()); - - let file_to_untrust = file_to_untrust.to_string_lossy().to_string(); - - if allowed.files.remove(&file_to_untrust).is_none() { - return - Err(ShellError::untagged_runtime_error( - "No .nu-env file to untrust in the given directory. Is it missing, or already untrusted?", - )); - } - - let tomlstr = toml::to_string(&allowed).map_err(|_| { - ShellError::untagged_runtime_error("Couldn't serialize allowed dirs to nu-env.toml") - })?; - fs::write(config_path, tomlstr).expect("Couldn't write to toml file"); - - if quiet { - Ok(ActionStream::empty()) - } else { - Ok(ActionStream::one(ReturnSuccess::value( - UntaggedValue::string(".nu-env untrusted!").into_value(tag), - ))) - } - } - fn is_binary(&self) -> bool { - false - } - fn examples(&self) -> Vec { - vec![ - Example { - description: "Disallow .nu-env file in current directory", - example: "autoenv untrust", - result: None, - }, - Example { - description: "Disallow .nu-env file in directory foo", - example: "autoenv untrust foo", - result: None, - }, - ] - } -} diff --git a/crates/nu-command/src/commands/env/let_env.rs b/crates/nu-command/src/commands/env/let_env.rs deleted file mode 100644 index 30885a92ed..0000000000 --- a/crates/nu-command/src/commands/env/let_env.rs +++ /dev/null @@ -1,104 +0,0 @@ -use std::convert::TryInto; - -use crate::prelude::*; -use nu_engine::{evaluate_baseline_expr, EnvVar, WholeStreamCommand}; - -use nu_errors::ShellError; -use nu_protocol::{hir::CapturedBlock, hir::ClassifiedCommand, Signature, SyntaxShape}; -use nu_source::Tagged; - -pub struct LetEnv; - -#[derive(Deserialize)] -pub struct LetEnvArgs { - pub name: Tagged, - pub equals: Tagged, - pub rhs: CapturedBlock, -} - -impl WholeStreamCommand for LetEnv { - fn name(&self) -> &str { - "let-env" - } - - fn signature(&self) -> Signature { - Signature::build("let-env") - .required( - "name", - SyntaxShape::String, - "the name of the environment variable", - ) - .required("equals", SyntaxShape::String, "the equals sign") - .required( - "expr", - SyntaxShape::MathExpression, - "the value for the environment variable", - ) - } - - fn usage(&self) -> &str { - "Create an environment variable and give it a value." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - set_env(args) - } - - fn examples(&self) -> Vec { - vec![] - } -} - -pub fn set_env(args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let ctx = &args.context; - - let name: Tagged = args.req(0)?; - let rhs: CapturedBlock = args.req(2)?; - - let (expr, captured) = { - if rhs.block.block.len() != 1 { - return Err(ShellError::labeled_error( - "Expected a value", - "expected a value", - tag, - )); - } - match rhs.block.block[0].pipelines.get(0) { - Some(item) => match item.list.get(0) { - Some(ClassifiedCommand::Expr(expr)) => (expr.clone(), rhs.captured.clone()), - _ => { - return Err(ShellError::labeled_error( - "Expected a value", - "expected a value", - tag, - )); - } - }, - None => { - return Err(ShellError::labeled_error( - "Expected a value", - "expected a value", - tag, - )); - } - } - }; - - ctx.scope.enter_scope(); - ctx.scope.add_vars(&captured.entries); - - let value = evaluate_baseline_expr(&expr, ctx); - - ctx.scope.exit_scope(); - - let value: EnvVar = value?.try_into()?; - let name = name.item; - - // Note: this is a special case for setting the context from a command - // In this case, if we don't set it now, we'll lose the scope that this - // variable should be set into. - ctx.scope.add_env_var(name, value); - - Ok(ActionStream::empty()) -} diff --git a/crates/nu-command/src/commands/env/load_env.rs b/crates/nu-command/src/commands/env/load_env.rs deleted file mode 100644 index e7a4eb1e1f..0000000000 --- a/crates/nu-command/src/commands/env/load_env.rs +++ /dev/null @@ -1,99 +0,0 @@ -use std::convert::TryInto; - -use crate::prelude::*; -use nu_engine::{EnvVar, WholeStreamCommand}; - -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, Value}; - -pub struct LoadEnv; - -impl WholeStreamCommand for LoadEnv { - fn name(&self) -> &str { - "load-env" - } - - fn signature(&self) -> Signature { - Signature::build("load-env").optional( - "environ", - SyntaxShape::Any, - "Optional environment table to load in. If not provided, will use the table provided on the input stream", - ) - } - - fn usage(&self) -> &str { - "Set environment variables using a table stream" - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - load_env(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Load variables from an input stream", - example: r#"echo [[name, value]; ["NAME", "JT"] ["AGE", "UNKNOWN"]] | load-env; echo $nu.env.NAME"#, - result: Some(vec![Value::from("JT")]), - }, - Example { - description: "Load variables from an argument", - example: r#"load-env [[name, value]; ["NAME", "JT"] ["AGE", "UNKNOWN"]]; echo $nu.env.NAME"#, - result: Some(vec![Value::from("JT")]), - }, - Example { - description: "Load variables from an argument and an input stream", - example: r#"echo [[name, value]; ["NAME", "JT"]] | load-env [[name, value]; ["VALUE", "FOO"]]; echo $nu.env.NAME $nu.env.VALUE"#, - result: Some(vec![Value::from("JT"), Value::from("UNKNOWN")]), - }, - ] - } -} - -fn load_env_from_table( - values: impl IntoIterator, - ctx: &EvaluationContext, -) -> Result<(), ShellError> { - for value in values { - let mut var_name = None; - let mut var_value = None; - - let tag = value.tag(); - - for (key, value) in value.row_entries() { - if key == "name" { - var_name = Some(value); - } else if key == "value" { - var_value = Some(value); - } - } - - match (var_name, var_value) { - (Some(name), Some(value)) => { - let env_var: EnvVar = value.try_into()?; - ctx.scope.add_env_var(name.as_string()?, env_var); - } - _ => { - return Err(ShellError::labeled_error( - r#"Expected each row in the table to have a "name" and "value" field."#, - r#"expected a "name" and "value" field"#, - tag, - )) - } - } - } - - Ok(()) -} - -pub fn load_env(args: CommandArgs) -> Result { - let ctx = &args.context; - - if let Some(values) = args.opt::>(0)? { - load_env_from_table(values, ctx)?; - } - - load_env_from_table(args.input, ctx)?; - - Ok(ActionStream::empty()) -} diff --git a/crates/nu-command/src/commands/env/mod.rs b/crates/nu-command/src/commands/env/mod.rs deleted file mode 100644 index 26b343ac5c..0000000000 --- a/crates/nu-command/src/commands/env/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -mod autoenv; -mod autoenv_trust; -mod autoenv_untrust; -mod let_env; -mod load_env; -mod unlet_env; -mod with_env; - -pub use autoenv::Autoenv; -pub use autoenv_trust::AutoenvTrust; -pub use autoenv_untrust::AutoenvUntrust; -pub use let_env::LetEnv; -pub use load_env::LoadEnv; -pub use unlet_env::UnletEnv; -pub use with_env::WithEnv; diff --git a/crates/nu-command/src/commands/env/unlet_env.rs b/crates/nu-command/src/commands/env/unlet_env.rs deleted file mode 100644 index d639f88030..0000000000 --- a/crates/nu-command/src/commands/env/unlet_env.rs +++ /dev/null @@ -1,59 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; - -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape}; -use nu_source::Tagged; - -pub struct UnletEnv; - -#[derive(Deserialize)] -pub struct UnletEnvArgs { - pub name: Tagged, -} - -impl WholeStreamCommand for UnletEnv { - fn name(&self) -> &str { - "unlet-env" - } - - fn signature(&self) -> Signature { - Signature::build("unlet-env").required( - "name", - SyntaxShape::String, - "the name of the environment variable", - ) - } - - fn usage(&self) -> &str { - "Delete an environment variable." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - unlet_env(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Remove the environment variable named FOO.", - example: "unlet-env FOO", - result: None, - }] - } -} - -pub fn unlet_env(args: CommandArgs) -> Result { - let ctx = &args.context; - - let name: Tagged = args.req(0)?; - - if ctx.scope.remove_env_var(&name.item) == None { - return Err(ShellError::labeled_error( - "Not an environment variable. Run `echo $nu.env` to view the available variables.", - "not an environment variable", - name.span(), - )); - } - - Ok(ActionStream::empty()) -} diff --git a/crates/nu-command/src/commands/env/with_env.rs b/crates/nu-command/src/commands/env/with_env.rs deleted file mode 100644 index 4dc95326df..0000000000 --- a/crates/nu-command/src/commands/env/with_env.rs +++ /dev/null @@ -1,126 +0,0 @@ -use std::convert::TryInto; - -use crate::prelude::*; -use nu_engine::run_block; -use nu_engine::EnvVar; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - hir::CapturedBlock, Signature, SpannedTypeName, SyntaxShape, UntaggedValue, Value, -}; - -pub struct WithEnv; - -impl WholeStreamCommand for WithEnv { - fn name(&self) -> &str { - "with-env" - } - - fn signature(&self) -> Signature { - Signature::build("with-env") - .required( - "variable", - SyntaxShape::Any, - "the environment variable to temporarily set", - ) - .required( - "block", - SyntaxShape::Block, - "the block to run once the variable is set", - ) - } - - fn usage(&self) -> &str { - "Runs a block with an environment variable set." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - with_env(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Set the MYENV environment variable", - example: r#"with-env [MYENV "my env value"] { echo $nu.env.MYENV }"#, - result: Some(vec![Value::from("my env value")]), - }, - Example { - description: "Set by primitive value list", - example: r#"with-env [X Y W Z] { echo $nu.env.X $nu.env.W }"#, - result: Some(vec![Value::from("Y"), Value::from("Z")]), - }, - Example { - description: "Set by single row table", - example: r#"with-env [[X W]; [Y Z]] { echo $nu.env.X $nu.env.W }"#, - result: Some(vec![Value::from("Y"), Value::from("Z")]), - }, - Example { - description: "Set by row(e.g. `open x.json` or `from json`)", - example: r#"echo '{"X":"Y","W":"Z"}'|from json|with-env $it { echo $nu.env.X $nu.env.W }"#, - result: None, - }, - ] - } -} - -fn with_env(args: CommandArgs) -> Result { - let external_redirection = args.call_info.args.external_redirection; - let context = &args.context; - let variable: Value = args.req(0)?; - let block: CapturedBlock = args.req(1)?; - - let mut env: IndexMap = IndexMap::new(); - - match &variable.value { - UntaggedValue::Table(table) => { - if table.len() == 1 { - // single row([[X W]; [Y Z]]) - for (k, v) in table[0].row_entries() { - env.insert(k.clone(), v.try_into()?); - } - } else { - // primitive values([X Y W Z]) - for row in table.chunks(2) { - if row.len() == 2 && row[0].is_primitive() && row[1].is_primitive() { - env.insert(row[0].convert_to_string(), (&row[1]).try_into()?); - } - } - } - } - // when get object by `open x.json` or `from json` - UntaggedValue::Row(row) => { - for (k, v) in &row.entries { - env.insert(k.clone(), v.try_into()?); - } - } - _ => { - return Err(ShellError::type_error( - "string list or single row", - variable.spanned_type_name(), - )); - } - }; - - context.scope.enter_scope(); - context.scope.add_env(env); - context.scope.add_vars(&block.captured.entries); - - let result = run_block(&block.block, context, args.input, external_redirection); - context.scope.exit_scope(); - - result.map(|x| x.into_action_stream()) -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::WithEnv; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(WithEnv {}) - } -} diff --git a/crates/nu-command/src/commands/filesystem/cd.rs b/crates/nu-command/src/commands/filesystem/cd.rs deleted file mode 100644 index f6c29130ea..0000000000 --- a/crates/nu-command/src/commands/filesystem/cd.rs +++ /dev/null @@ -1,72 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; - -use nu_engine::shell::CdArgs; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape}; - -pub struct Cd; - -impl WholeStreamCommand for Cd { - fn name(&self) -> &str { - "cd" - } - - fn signature(&self) -> Signature { - Signature::build("cd").optional( - "directory", - SyntaxShape::FilePath, - "the directory to change to", - ) - } - - fn usage(&self) -> &str { - "Change to a new path." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - let shell_manager = args.shell_manager(); - let args = CdArgs { path: args.opt(0)? }; - - shell_manager.cd(args, name) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Change to a new directory called 'dirname'", - example: "cd dirname", - result: None, - }, - Example { - description: "Change to your home directory", - example: "cd", - result: None, - }, - Example { - description: "Change to your home directory (alternate version)", - example: "cd ~", - result: None, - }, - Example { - description: "Change to the previous directory", - example: "cd -", - result: None, - }, - ] - } -} - -#[cfg(test)] -mod tests { - use super::Cd; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Cd {}) - } -} diff --git a/crates/nu-command/src/commands/filesystem/cp.rs b/crates/nu-command/src/commands/filesystem/cp.rs deleted file mode 100644 index 6794424e2a..0000000000 --- a/crates/nu-command/src/commands/filesystem/cp.rs +++ /dev/null @@ -1,67 +0,0 @@ -use crate::prelude::*; -use nu_engine::{shell::CopyArgs, WholeStreamCommand}; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape}; - -pub struct Cpy; - -impl WholeStreamCommand for Cpy { - fn name(&self) -> &str { - "cp" - } - - fn signature(&self) -> Signature { - Signature::build("cp") - .required("src", SyntaxShape::GlobPattern, "the place to copy from") - .required("dst", SyntaxShape::FilePath, "the place to copy to") - .switch( - "recursive", - "copy recursively through subdirectories", - Some('r'), - ) - } - - fn usage(&self) -> &str { - "Copy files." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - let shell_manager = args.shell_manager(); - let name = args.call_info.name_tag.clone(); - - let args = CopyArgs { - src: args.req(0)?, - dst: args.req(1)?, - recursive: args.has_flag("recursive"), - }; - shell_manager.cp(args, name) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Copy myfile to dir_b", - example: "cp myfile dir_b", - result: None, - }, - Example { - description: "Recursively copy dir_a to dir_b", - example: "cp -r dir_a dir_b", - result: None, - }, - ] - } -} - -#[cfg(test)] -mod tests { - use super::Cpy; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Cpy {}) - } -} diff --git a/crates/nu-command/src/commands/filesystem/ls.rs b/crates/nu-command/src/commands/filesystem/ls.rs deleted file mode 100644 index 23ac1e6591..0000000000 --- a/crates/nu-command/src/commands/filesystem/ls.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::prelude::*; -use nu_engine::{shell::LsArgs, WholeStreamCommand}; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape}; - -pub struct Ls; - -impl WholeStreamCommand for Ls { - fn name(&self) -> &str { - "ls" - } - - fn signature(&self) -> Signature { - Signature::build("ls") - .optional( - "path", - SyntaxShape::GlobPattern, - "a path to get the directory contents from", - ) - .switch("all", "Show hidden files", Some('a')) - .switch( - "long", - "List all available columns for each entry", - Some('l'), - ) - .switch( - "short-names", - "Only print the file names and not the path", - Some('s'), - ) - .switch( - "du", - "Display the apparent directory size in place of the directory metadata size", - Some('d'), - ) - } - - fn usage(&self) -> &str { - "View the contents of the current or given path." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - let ctrl_c = args.ctrl_c(); - let shell_manager = args.shell_manager(); - - let args = LsArgs { - path: args.opt(0)?, - all: args.has_flag("all"), - long: args.has_flag("long"), - short_names: args.has_flag("short-names"), - du: args.has_flag("du"), - }; - - shell_manager.ls(args, name, ctrl_c) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "List all files in the current directory", - example: "ls", - result: None, - }, - Example { - description: "List all files in a subdirectory", - example: "ls subdir", - result: None, - }, - Example { - description: "List all rust files", - example: "ls *.rs", - result: None, - }, - ] - } -} - -#[cfg(test)] -mod tests { - use super::Ls; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Ls {}) - } -} diff --git a/crates/nu-command/src/commands/filesystem/mkdir.rs b/crates/nu-command/src/commands/filesystem/mkdir.rs deleted file mode 100644 index 42950454ea..0000000000 --- a/crates/nu-command/src/commands/filesystem/mkdir.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crate::prelude::*; -use nu_engine::{shell::MkdirArgs, WholeStreamCommand}; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape}; -pub struct Mkdir; - -impl WholeStreamCommand for Mkdir { - fn name(&self) -> &str { - "mkdir" - } - - fn signature(&self) -> Signature { - Signature::build("mkdir") - .rest( - "rest", - SyntaxShape::FilePath, - "the name(s) of the path(s) to create", - ) - .switch("show-created-paths", "show the path(s) created.", Some('s')) - } - - fn usage(&self) -> &str { - "Make directories, creates intermediary directories as required." - } - - fn run(&self, args: CommandArgs) -> Result { - mkdir(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Make a directory named foo", - example: "mkdir foo", - result: None, - }] - } -} - -fn mkdir(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - let shell_manager = args.shell_manager(); - - let args = MkdirArgs { - rest: args.rest(0)?, - show_created_paths: args.has_flag("show-created-paths"), - }; - - shell_manager.mkdir(args, name) -} - -#[cfg(test)] -mod tests { - use super::Mkdir; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Mkdir {}) - } -} diff --git a/crates/nu-command/src/commands/filesystem/mod.rs b/crates/nu-command/src/commands/filesystem/mod.rs deleted file mode 100644 index f8eb69d6f4..0000000000 --- a/crates/nu-command/src/commands/filesystem/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -mod cd; -mod cp; -mod ls; -mod mkdir; -mod mv; -pub(crate) mod open; -mod rm; -mod save; -mod touch; - -pub use cd::Cd; -pub use cp::Cpy; -pub use ls::Ls; -pub use mkdir::Mkdir; -pub use mv::Mv; -pub use open::Open; -pub use rm::Remove; -pub use save::Save; -pub use touch::Touch; diff --git a/crates/nu-command/src/commands/filesystem/mv.rs b/crates/nu-command/src/commands/filesystem/mv.rs deleted file mode 100644 index b38178d139..0000000000 --- a/crates/nu-command/src/commands/filesystem/mv.rs +++ /dev/null @@ -1,79 +0,0 @@ -use crate::prelude::*; -use nu_engine::{shell::MvArgs, WholeStreamCommand}; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape}; - -pub struct Mv; - -impl WholeStreamCommand for Mv { - fn name(&self) -> &str { - "mv" - } - - fn signature(&self) -> Signature { - Signature::build("mv") - .required( - "source", - SyntaxShape::GlobPattern, - "the location to move files/directories from", - ) - .required( - "destination", - SyntaxShape::FilePath, - "the location to move files/directories to", - ) - } - - fn usage(&self) -> &str { - "Move files or directories." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - mv(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Rename a file", - example: "mv before.txt after.txt", - result: None, - }, - Example { - description: "Move a file into a directory", - example: "mv test.txt my/subdirectory", - result: None, - }, - Example { - description: "Move many files into a directory", - example: "mv *.txt my/subdirectory", - result: None, - }, - ] - } -} - -fn mv(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - let shell_manager = args.shell_manager(); - - let args = MvArgs { - src: args.req(0)?, - dst: args.req(1)?, - }; - - shell_manager.mv(args, name) -} - -#[cfg(test)] -mod tests { - use super::Mv; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Mv {}) - } -} diff --git a/crates/nu-command/src/commands/filesystem/open.rs b/crates/nu-command/src/commands/filesystem/open.rs deleted file mode 100644 index d8ddbcfb2e..0000000000 --- a/crates/nu-command/src/commands/filesystem/open.rs +++ /dev/null @@ -1,265 +0,0 @@ -use crate::commands::viewers::BAT_LANGUAGES; -use crate::prelude::*; -use encoding_rs::{Encoding, UTF_8}; - -use log::debug; -use nu_engine::StringOrBinary; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_path::canonicalize; -use nu_protocol::{CommandAction, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::{AnchorLocation, Span, Tagged}; -use std::path::{Path, PathBuf}; - -pub struct Open; - -impl WholeStreamCommand for Open { - fn name(&self) -> &str { - "open" - } - - fn signature(&self) -> Signature { - Signature::build(self.name()) - .required( - "path", - SyntaxShape::FilePath, - "the file path to load values from", - ) - .switch( - "raw", - "load content as a string instead of a table", - Some('r'), - ) - .named( - "encoding", - SyntaxShape::String, - "encoding to use to open file", - Some('e'), - ) - } - - fn usage(&self) -> &str { - "Load a file into a cell, convert to table if possible (avoid by appending '--raw')." - } - - fn extra_usage(&self) -> &str { - r#"Multiple encodings are supported for reading text files by using -the '--encoding ' parameter. Here is an example of a few: -big5, euc-jp, euc-kr, gbk, iso-8859-1, utf-16, cp1252, latin5 - -For a more complete list of encodings please refer to the encoding_rs -documentation link at https://docs.rs/encoding_rs/0.8.28/encoding_rs/#statics"# - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - open(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Opens \"users.csv\" and creates a table from the data", - example: "open users.csv", - result: None, - }, - Example { - description: "Opens file with iso-8859-1 encoding", - example: "open file.csv --encoding iso-8859-1 | from csv", - result: None, - }, - Example { - description: "Lists the contents of a directory (identical to `ls ../projectB`)", - example: "open ../projectB", - result: None, - }, - ] - } -} - -pub fn get_encoding(opt: Option>) -> Result<&'static Encoding, ShellError> { - match opt { - None => Ok(UTF_8), - Some(label) => match Encoding::for_label((&label.item).as_bytes()) { - None => Err(ShellError::labeled_error( - format!( - r#"{} is not a valid encoding, refer to https://docs.rs/encoding_rs/0.8.23/encoding_rs/#statics for a valid list of encodings"#, - label.item - ), - "invalid encoding", - label.span(), - )), - Some(encoding) => Ok(encoding), - }, - } -} - -fn open(args: CommandArgs) -> Result { - let scope = args.scope().clone(); - let shell_manager = args.shell_manager(); - let cwd = PathBuf::from(shell_manager.path()); - let name = args.call_info.name_tag.clone(); - let ctrl_c = args.ctrl_c(); - - let path: Tagged = args.req(0)?; - let raw = args.has_flag("raw"); - let encoding: Option> = args.get_flag("encoding")?; - - if path.is_dir() { - let args = nu_engine::shell::LsArgs { - path: Some(path), - all: false, - long: false, - short_names: false, - du: false, - }; - return shell_manager.ls(args, name, ctrl_c); - } - - // TODO: Remove once Streams are supported everywhere! - // As a short term workaround for getting AutoConvert and Bat functionality (Those don't currently support Streams) - - // Check if the extension has a "from *" command OR "bat" supports syntax highlighting - // AND the user doesn't want the raw output - // In these cases, we will collect the Stream - let ext = if raw { - None - } else { - path.extension() - .map(|name| name.to_string_lossy().to_string()) - }; - - if let Some(ext) = ext { - // Check if we have a conversion command - if let Some(_command) = scope.get_command(&format!("from {}", ext)) { - let (_, tagged_contents) = crate::commands::open::fetch( - &cwd, - &PathBuf::from(&path.item), - path.tag.span, - encoding, - )?; - return Ok(ActionStream::one(ReturnSuccess::action( - CommandAction::AutoConvert(tagged_contents, ext), - ))); - } - // Check if bat does syntax highlighting - if BAT_LANGUAGES.contains(&ext.as_ref()) { - let (_, tagged_contents) = crate::commands::open::fetch( - &cwd, - &PathBuf::from(&path.item), - path.tag.span, - encoding, - )?; - return Ok(ActionStream::one(ReturnSuccess::value(tagged_contents))); - } - } - - // Normal Streaming operation - let with_encoding = if encoding.is_none() { - None - } else { - Some(get_encoding(encoding)?) - }; - - let sob_stream = shell_manager.open(&path.item, path.tag.span, with_encoding)?; - - let final_stream = sob_stream.map(move |x| { - // The tag that will used when returning a Value - let file_tag = Tag { - span: path.tag.span, - anchor: Some(AnchorLocation::File(path.to_string_lossy().to_string())), - }; - - match x { - Ok(StringOrBinary::String(s)) => { - ReturnSuccess::value(UntaggedValue::string(s).into_value(file_tag)) - } - Ok(StringOrBinary::Binary(b)) => { - ReturnSuccess::value(UntaggedValue::binary(b).into_value(file_tag)) - } - Err(se) => Err(se), - } - }); - - Ok(ActionStream::new(final_stream)) -} - -// Note that we do not output a Stream in "fetch" since it is only used by "enter" command -// Which we expect to use a concrete Value a not a Stream -pub fn fetch( - cwd: &Path, - location: &Path, - span: Span, - encoding_choice: Option>, -) -> Result<(Option, Value), ShellError> { - // TODO: I don't understand the point of this? Maybe for better error reporting - let mut cwd = PathBuf::from(cwd); - cwd.push(location); - let nice_location = canonicalize(&cwd).map_err(|e| match e.kind() { - std::io::ErrorKind::NotFound => ShellError::labeled_error( - format!("Cannot find file {:?}", cwd), - "cannot find file", - span, - ), - std::io::ErrorKind::PermissionDenied => { - ShellError::labeled_error("Permission denied", "permission denied", span) - } - _ => ShellError::labeled_error( - format!("Cannot open file {:?} because {:?}", &cwd, e), - "Cannot open", - span, - ), - })?; - - // The extension may be used in AutoConvert later on - let ext = location - .extension() - .map(|name| name.to_string_lossy().to_string()); - - // The tag that will used when returning a Value - let file_tag = Tag { - span, - anchor: Some(AnchorLocation::File( - nice_location.to_string_lossy().to_string(), - )), - }; - - let res = std::fs::read(location) - .map_err(|_| ShellError::labeled_error("Can't open filename given", "can't open", span))?; - - // If no encoding is provided we try to guess the encoding to read the file with - let encoding = if encoding_choice.is_none() { - UTF_8 - } else { - get_encoding(encoding_choice.clone())? - }; - - // If the user specified an encoding, then do not do BOM sniffing - let decoded_res = if encoding_choice.is_some() { - let (cow_res, _replacements) = encoding.decode_with_bom_removal(&res); - cow_res - } else { - // Otherwise, use the default UTF-8 encoder with BOM sniffing - let (cow_res, actual_encoding, replacements) = encoding.decode(&res); - // If we had to use replacement characters then fallback to binary - if replacements { - return Ok((ext, UntaggedValue::binary(res).into_value(file_tag))); - } - debug!("Decoded using {:?}", actual_encoding); - cow_res - }; - let v = UntaggedValue::string(decoded_res.to_string()).into_value(file_tag); - Ok((ext, v)) -} - -#[cfg(test)] -mod tests { - use super::Open; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Open {}) - } -} diff --git a/crates/nu-command/src/commands/filesystem/rm.rs b/crates/nu-command/src/commands/filesystem/rm.rs deleted file mode 100644 index b45c839d43..0000000000 --- a/crates/nu-command/src/commands/filesystem/rm.rs +++ /dev/null @@ -1,116 +0,0 @@ -use crate::prelude::*; -use nu_engine::shell::RemoveArgs; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Primitive, Signature, SyntaxShape, UntaggedValue}; -use nu_source::Tagged; - -pub struct Remove; - -impl WholeStreamCommand for Remove { - fn name(&self) -> &str { - "rm" - } - - fn signature(&self) -> Signature { - Signature::build("rm") - .switch( - "trash", - "use the platform's recycle bin instead of permanently deleting", - Some('t'), - ) - .switch( - "permanent", - "don't use recycle bin, delete permanently", - Some('p'), - ) - .switch("recursive", "delete subdirectories recursively", Some('r')) - .switch("force", "suppress error when no file", Some('f')) - .rest( - "rest", - SyntaxShape::GlobPattern, - "the file path(s) to remove", - ) - } - - fn usage(&self) -> &str { - "Remove file(s)." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - rm(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Delete or move a file to the system trash (depending on 'rm_always_trash' config option)", - example: "rm file.txt", - result: None, - }, - Example { - description: "Move a file to the system trash", - example: "rm --trash file.txt", - result: None, - }, - Example { - description: "Delete a file permanently", - example: "rm --permanent file.txt", - result: None, - }, - Example { - description: "Delete a file, and suppress errors if no file is found", - example: "rm --force file.txt", - result: None, - } - ] - } -} - -fn rm(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - let shell_manager = args.shell_manager(); - - let mut rm_args = RemoveArgs { - rest: args.rest(0)?, - recursive: args.has_flag("recursive"), - trash: args.has_flag("trash"), - permanent: args.has_flag("permanent"), - force: args.has_flag("force"), - }; - - if rm_args.trash && rm_args.permanent { - return Ok(ActionStream::one(Err(ShellError::labeled_error( - "only one of --permanent and --trash can be used", - "conflicting flags", - name, - )))); - } - - if rm_args.rest.is_empty() { - let mut input_peek = args.input.peekable(); - while let Some(v) = &input_peek.next() { - if let UntaggedValue::Primitive(Primitive::FilePath(path)) = &v.value { - rm_args.rest.push(Tagged { - item: path.to_path_buf(), - tag: args.call_info.name_tag.clone(), - }) - }; - } - } - - shell_manager.rm(rm_args, name) -} - -#[cfg(test)] -mod tests { - use super::Remove; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Remove {}) - } -} diff --git a/crates/nu-command/src/commands/filesystem/save.rs b/crates/nu-command/src/commands/filesystem/save.rs deleted file mode 100644 index 5f342683dd..0000000000 --- a/crates/nu-command/src/commands/filesystem/save.rs +++ /dev/null @@ -1,270 +0,0 @@ -use crate::prelude::*; -use nu_engine::{UnevaluatedCallInfo, WholeStreamCommand}; -use nu_errors::ShellError; -use nu_protocol::{ - hir::ExternalRedirection, Primitive, Signature, SyntaxShape, UntaggedValue, Value, -}; -use nu_source::Tagged; -use std::path::{Path, PathBuf}; - -pub struct Save; - -macro_rules! process_unknown { - ($scope:tt, $input:ident, $name_tag:ident) => {{ - if $input.len() > 0 { - match $input[0] { - Value { - value: UntaggedValue::Primitive(Primitive::Binary(_)), - .. - } => process_binary!($scope, $input, $name_tag), - _ => process_string!($scope, $input, $name_tag), - } - } else { - process_string!($scope, $input, $name_tag) - } - }}; -} - -macro_rules! process_string { - ($scope:tt, $input:ident, $name_tag:ident) => {{ - let mut result_string = String::new(); - for res in $input { - match res { - Value { - value: UntaggedValue::Primitive(Primitive::String(s)), - .. - } => { - result_string.push_str(&s); - } - _ => { - break $scope Err(ShellError::labeled_error( - "Save requires string data", - "consider converting data to string (see `help commands`)", - $name_tag, - )); - } - } - } - Ok(result_string.into_bytes()) - }}; -} - -macro_rules! process_binary { - ($scope:tt, $input:ident, $name_tag:ident) => {{ - let mut result_binary: Vec = Vec::new(); - for res in $input { - match res { - Value { - value: UntaggedValue::Primitive(Primitive::Binary(b)), - .. - } => { - for u in b.into_iter() { - result_binary.push(u); - } - } - _ => { - break $scope Err(ShellError::labeled_error( - "Save could not successfully save", - "unexpected data during binary save", - $name_tag, - )); - } - } - } - Ok(result_binary) - }}; -} - -macro_rules! process_string_return_success { - ($scope:tt, $result_vec:ident, $name_tag:ident) => {{ - let mut result_string = String::new(); - for res in $result_vec { - match res { - Value { - value: UntaggedValue::Primitive(Primitive::String(s)), - .. - } => { - result_string.push_str(&s); - } - _ => { - break $scope Err(ShellError::labeled_error( - "Save could not successfully save", - "unexpected data during text save", - $name_tag, - )); - } - } - } - Ok(result_string.into_bytes()) - }}; -} - -macro_rules! process_binary_return_success { - ($scope:tt, $result_vec:ident, $name_tag:ident) => {{ - let mut result_binary: Vec = Vec::new(); - for res in $result_vec { - match res { - Value { - value: UntaggedValue::Primitive(Primitive::Binary(b)), - .. - } => { - for u in b.into_iter() { - result_binary.push(u); - } - } - _ => { - break $scope Err(ShellError::labeled_error( - "Save could not successfully save", - "unexpected data during binary save", - $name_tag, - )); - } - } - } - Ok(result_binary) - }}; -} - -impl WholeStreamCommand for Save { - fn name(&self) -> &str { - "save" - } - - fn signature(&self) -> Signature { - Signature::build("save") - .optional( - "path", - SyntaxShape::FilePath, - "the path to save contents to", - ) - .switch( - "raw", - "treat values as-is rather than auto-converting based on file extension", - Some('r'), - ) - .switch("append", "append values rather than overriding", Some('a')) - } - - fn usage(&self) -> &str { - "Save the contents of the pipeline to a file." - } - - fn run(&self, args: CommandArgs) -> Result { - save(args) - } -} - -fn save(args: CommandArgs) -> Result { - let shell_manager = args.shell_manager(); - let mut full_path = PathBuf::from(shell_manager.path()); - let name_tag = args.call_info.name_tag.clone(); - let name = args.call_info.name_tag.clone(); - let context = args.context.clone(); - let scope = args.scope().clone(); - - let head = args.call_info.args.head.clone(); - - let path: Option> = args.opt(0)?; - let save_raw = args.has_flag("raw"); - let append = args.has_flag("append"); - - let input: Vec = args.input.collect(); - if path.is_none() { - let mut should_return_file_path_error = true; - - // If there is no filename, check the metadata for the anchor filename - if !input.is_empty() { - let anchor = input[0].tag.anchor(); - - if let Some(AnchorLocation::File(file)) = anchor { - should_return_file_path_error = false; - full_path.push(Path::new(&file)); - } - } - - if should_return_file_path_error { - return Err(ShellError::labeled_error( - "Save requires a filepath", - "needs path", - name_tag, - )); - } - } else if let Some(file) = path { - full_path.push(file.item()); - } - - // TODO use label_break_value once it is stable: - // https://github.com/rust-lang/rust/issues/48594 - #[allow(clippy::never_loop)] - let content: Result, ShellError> = 'scope: loop { - break if !save_raw { - if let Some(extension) = full_path.extension() { - let command_name = format!("to {}", extension.to_string_lossy()); - if let Some(converter) = scope.get_command(&command_name) { - let new_args = CommandArgs { - context, - call_info: UnevaluatedCallInfo { - args: nu_protocol::hir::Call { - head, - positional: None, - named: None, - span: Span::unknown(), - external_redirection: ExternalRedirection::Stdout, - }, - name_tag: name_tag.clone(), - }, - input: InputStream::from_stream(input.into_iter()), - }; - let mut result = converter.run(new_args)?; - let result_vec: Vec = result.drain_vec(); - if converter.is_binary() { - process_binary_return_success!('scope, result_vec, name_tag) - } else { - process_string_return_success!('scope, result_vec, name_tag) - } - } else { - process_unknown!('scope, input, name_tag) - } - } else { - process_unknown!('scope, input, name_tag) - } - } else { - Ok(string_from(&input).into_bytes()) - }; - }; - - shell_manager.save(&full_path, &content?, name.span, append) -} - -fn string_from(input: &[Value]) -> String { - let mut save_data = String::new(); - - if !input.is_empty() { - let mut first = true; - for i in input { - if !first { - save_data.push('\n'); - } else { - first = false; - } - if let Ok(data) = &i.as_string() { - save_data.push_str(data); - } - } - } - - save_data -} - -#[cfg(test)] -mod tests { - use super::Save; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Save {}) - } -} diff --git a/crates/nu-command/src/commands/filesystem/touch.rs b/crates/nu-command/src/commands/filesystem/touch.rs deleted file mode 100644 index 6b2907f48b..0000000000 --- a/crates/nu-command/src/commands/filesystem/touch.rs +++ /dev/null @@ -1,78 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape}; -use nu_source::Tagged; -use std::fs::OpenOptions; -use std::path::PathBuf; - -pub struct Touch; - -impl WholeStreamCommand for Touch { - fn name(&self) -> &str { - "touch" - } - fn signature(&self) -> Signature { - Signature::build("touch") - .required( - "filename", - SyntaxShape::FilePath, - "the path of the file you want to create", - ) - .rest("rest", SyntaxShape::FilePath, "additional files to create") - } - fn usage(&self) -> &str { - "Creates one or more files." - } - fn run_with_actions(&self, args: CommandArgs) -> Result { - touch(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Creates \"fixture.json\"", - example: "touch fixture.json", - result: None, - }, - Example { - description: "Creates files a, b and c", - example: "touch a b c", - result: None, - }, - ] - } -} - -fn touch(args: CommandArgs) -> Result { - let target: Tagged = args.req(0)?; - let rest: Vec> = args.rest(1)?; - - for item in vec![target].into_iter().chain(rest) { - match OpenOptions::new().write(true).create(true).open(&item) { - Ok(_) => continue, - Err(err) => { - return Err(ShellError::labeled_error( - "File Error", - err.to_string(), - &item.tag, - )) - } - } - } - - Ok(ActionStream::empty()) -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::Touch; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Touch {}) - } -} diff --git a/crates/nu-command/src/commands/filters/all.rs b/crates/nu-command/src/commands/filters/all.rs deleted file mode 100644 index 81e104196c..0000000000 --- a/crates/nu-command/src/commands/filters/all.rs +++ /dev/null @@ -1,139 +0,0 @@ -use crate::prelude::*; -use nu_engine::evaluate_baseline_expr; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - hir::CapturedBlock, hir::ClassifiedCommand, Signature, SyntaxShape, UntaggedValue, -}; - -pub struct Command; - -struct AllArgs { - predicate: CapturedBlock, -} - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "all?" - } - - fn signature(&self) -> Signature { - Signature::build("all?").required( - "condition", - SyntaxShape::RowCondition, - "the condition that must match", - ) - } - - fn usage(&self) -> &str { - "Find if the table rows matches the condition." - } - - fn run(&self, args: CommandArgs) -> Result { - all(args) - } - - fn examples(&self) -> Vec { - use nu_protocol::Value; - - vec![ - Example { - description: "Find if services are running", - example: "echo [[status]; [UP] [UP]] | all? status == UP", - result: Some(vec![Value::from(true)]), - }, - Example { - description: "Check that all values are even", - example: "echo [2 4 6 8] | all? ($it mod 2) == 0", - result: Some(vec![Value::from(true)]), - }, - ] - } -} - -fn all(args: CommandArgs) -> Result { - let ctx = &args.context; - let tag = args.call_info.name_tag.clone(); - let all_args = AllArgs { - predicate: args.req(0)?, - }; - - let err = Err(ShellError::labeled_error( - "Expected a condition", - "expected a condition", - args.call_info.name_tag.clone(), - )); - - //This seems a little odd. Can't we have predicates with pipelines/multiple statements? - let condition = { - if all_args.predicate.block.block.len() != 1 { - return err; - } - match all_args.predicate.block.block[0].pipelines.get(0) { - Some(item) => match item.list.get(0) { - Some(ClassifiedCommand::Expr(expr)) => expr.clone(), - _ => { - return err; - } - }, - None => { - return err; - } - } - }; - - let scope = args.scope().clone(); - - let init = Ok(InputStream::one( - UntaggedValue::boolean(true).into_value(&tag), - )); - - // Variables in nu are immutable. Having the same variable across invocations - // of evaluate_baseline_expr does not mutate the variables and those each - // invocations are independent of each other! - scope.enter_scope(); - scope.add_vars(&all_args.predicate.captured.entries); - let result = args.input.fold(init, move |acc, row| { - let condition = condition.clone(); - let ctx = ctx.clone(); - if let Some(positional) = all_args.predicate.block.params.positional.get(0) { - ctx.scope.add_var(positional.0.name(), row); - } else { - ctx.scope.add_var("$it", row); - } - - let condition = evaluate_baseline_expr(&condition, &ctx); - - let curr = acc?.drain_vec(); - let curr = curr - .get(0) - .ok_or_else(|| ShellError::unexpected("No value to check with"))?; - let cond = curr.as_bool()?; - - match condition { - Ok(condition) => match condition.as_bool() { - Ok(b) => Ok(InputStream::one( - UntaggedValue::boolean(cond && b).into_value(&curr.tag), - )), - Err(e) => Err(e), - }, - Err(e) => Err(e), - } - }); - scope.exit_scope(); - - Ok(result?.into_output_stream()) -} - -#[cfg(test)] -mod tests { - use super::Command; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Command {}) - } -} diff --git a/crates/nu-command/src/commands/filters/any.rs b/crates/nu-command/src/commands/filters/any.rs deleted file mode 100644 index ddcad4f168..0000000000 --- a/crates/nu-command/src/commands/filters/any.rs +++ /dev/null @@ -1,138 +0,0 @@ -use crate::prelude::*; -use nu_engine::evaluate_baseline_expr; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - hir::CapturedBlock, hir::ClassifiedCommand, Signature, SyntaxShape, UntaggedValue, -}; - -pub struct Command; - -struct AnyArgs { - predicate: CapturedBlock, -} - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "any?" - } - - fn signature(&self) -> Signature { - Signature::build("any?").required( - "condition", - SyntaxShape::RowCondition, - "the condition that must match", - ) - } - - fn usage(&self) -> &str { - "Find if the table rows matches the condition." - } - - fn run(&self, args: CommandArgs) -> Result { - any(args) - } - - fn examples(&self) -> Vec { - use nu_protocol::Value; - - vec![ - Example { - description: "Find if a service is not running", - example: "echo [[status]; [UP] [DOWN] [UP]] | any? status == DOWN", - result: Some(vec![Value::from(true)]), - }, - Example { - description: "Check if any of the values is odd", - example: "echo [2 4 1 6 8] | any? ($it mod 2) == 1", - result: Some(vec![Value::from(true)]), - }, - ] - } -} - -fn any(args: CommandArgs) -> Result { - let ctx = &args.context; - let tag = args.call_info.name_tag.clone(); - let any_args = AnyArgs { - predicate: args.req(0)?, - }; - - let err = Err(ShellError::labeled_error( - "Expected a condition", - "expected a condition", - args.call_info.name_tag.clone(), - )); - - //This seems a little odd. Can't we have predicates with pipelines/multiple statements? - let condition = { - if any_args.predicate.block.block.len() != 1 { - return err; - } - match any_args.predicate.block.block[0].pipelines.get(0) { - Some(item) => match item.list.get(0) { - Some(ClassifiedCommand::Expr(expr)) => expr.clone(), - _ => { - return err; - } - }, - None => { - return err; - } - } - }; - - let scope = args.scope().clone(); - - let init = Ok(InputStream::one( - UntaggedValue::boolean(false).into_value(&tag), - )); - - // Variables in nu are immutable. Having the same variable across invocations - // of evaluate_baseline_expr does not mutate the variables and thus each - // invocations are independent of each other! - scope.enter_scope(); - scope.add_vars(&any_args.predicate.captured.entries); - - let result = args.input.fold(init, move |acc, row| { - let condition = condition.clone(); - let ctx = ctx.clone(); - if let Some((arg, _)) = any_args.predicate.block.params.positional.first() { - ctx.scope.add_var(arg.name(), row); - } - - let condition = evaluate_baseline_expr(&condition, &ctx); - - let curr = acc?.drain_vec(); - let curr = curr - .get(0) - .ok_or_else(|| ShellError::unexpected("No value to check with"))?; - let cond = curr.as_bool()?; - - match condition { - Ok(condition) => match condition.as_bool() { - Ok(b) => Ok(InputStream::one( - UntaggedValue::boolean(cond || b).into_value(&curr.tag), - )), - Err(e) => Err(e), - }, - Err(e) => Err(e), - } - }); - scope.exit_scope(); - - Ok(result?.into_output_stream()) -} - -#[cfg(test)] -mod tests { - use super::Command; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Command {}) - } -} diff --git a/crates/nu-command/src/commands/filters/append.rs b/crates/nu-command/src/commands/filters/append.rs deleted file mode 100644 index f43a4f5658..0000000000 --- a/crates/nu-command/src/commands/filters/append.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value}; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "append" - } - - fn signature(&self) -> Signature { - Signature::build("append").required( - "row value", - SyntaxShape::Any, - "the value of the row to append to the table", - ) - } - - fn usage(&self) -> &str { - "Append a row to the table." - } - - fn run(&self, mut args: CommandArgs) -> Result { - let mut value: Value = args.req(0)?; - - let mut prepend = vec![]; - - if let Some(first) = args.input.next() { - value.tag = first.tag(); - prepend.push(first); - } - - // Checks if we are trying to append a row literal - if let Value { - value: UntaggedValue::Table(values), - tag, - } = &value - { - if values.len() == 1 && values[0].is_row() { - value = values[0].value.clone().into_value(tag); - } - } - - Ok(prepend - .into_iter() - .chain(args.input.into_iter().chain([value])) - .into_output_stream()) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Add values to the end of the table", - example: "echo [1 2 3] | append 4", - result: Some(vec![ - UntaggedValue::int(1).into(), - UntaggedValue::int(2).into(), - UntaggedValue::int(3).into(), - UntaggedValue::int(4).into(), - ]), - }, - Example { - description: "Add row value to the end of the table", - example: "echo [[country]; [Ecuador] ['New Zealand']] | append [[country]; [USA]]", - result: Some(vec![ - row! { "country".into() => Value::from("Ecuador")}, - row! { "country".into() => Value::from("New Zealand")}, - row! { "country".into() => Value::from("USA")}, - ]), - }, - ] - } -} diff --git a/crates/nu-command/src/commands/filters/collect.rs b/crates/nu-command/src/commands/filters/collect.rs deleted file mode 100644 index 10b4a3e154..0000000000 --- a/crates/nu-command/src/commands/filters/collect.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::prelude::*; -use nu_engine::run_block; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{hir::CapturedBlock, Signature, SyntaxShape, UntaggedValue}; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "collect" - } - - fn signature(&self) -> Signature { - Signature::build("collect").required( - "block", - SyntaxShape::Block, - "the block to run once the stream is collected", - ) - } - - fn usage(&self) -> &str { - "Collect the stream and pass it to a block." - } - - fn run(&self, args: CommandArgs) -> Result { - collect(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Use the second value in the stream", - example: "echo 1 2 3 | collect { |x| echo $x.1 }", - result: Some(vec![UntaggedValue::int(2).into()]), - }] - } -} - -fn collect(args: CommandArgs) -> Result { - let external_redirection = args.call_info.args.external_redirection; - let context = &args.context; - let tag = args.call_info.name_tag.clone(); - let block: CapturedBlock = args.req(0)?; - let mut input = args.input; - let param = if !block.block.params.positional.is_empty() { - block.block.params.positional[0].0.name() - } else { - "$it" - }; - - context.scope.enter_scope(); - - context.scope.add_vars(&block.captured.entries); - let mut input = input.drain_vec(); - match input.len() { - x if x > 1 => { - context - .scope - .add_var(param, UntaggedValue::Table(input).into_value(tag)); - } - 1 => { - let item = input.swap_remove(0); - context.scope.add_var(param, item); - } - _ => {} - } - - let result = run_block( - &block.block, - context, - InputStream::empty(), - external_redirection, - ); - context.scope.exit_scope(); - - Ok(result?.into_output_stream()) -} - -#[cfg(test)] -mod tests { - use super::Command; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Command {}) - } -} diff --git a/crates/nu-command/src/commands/filters/compact.rs b/crates/nu-command/src/commands/filters/compact.rs deleted file mode 100644 index 814f569c72..0000000000 --- a/crates/nu-command/src/commands/filters/compact.rs +++ /dev/null @@ -1,82 +0,0 @@ -use crate::prelude::*; - -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tagged; - -pub struct Compact; - -pub struct CompactArgs { - columns: Vec>, -} - -impl WholeStreamCommand for Compact { - fn name(&self) -> &str { - "compact" - } - - fn signature(&self) -> Signature { - Signature::build("compact").rest( - "rest", - SyntaxShape::Any, - "the columns to compact from the table", - ) - } - - fn usage(&self) -> &str { - "Creates a table with non-empty rows." - } - - fn run(&self, args: CommandArgs) -> Result { - compact(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Filter out all directory entries having no 'target'", - example: "ls -la | compact target", - result: None, - }] - } -} - -pub fn compact(args: CommandArgs) -> Result { - let (args, input) = ( - CompactArgs { - columns: args.rest(0)?, - }, - args.input, - ); - - Ok(input - .filter(move |item| { - if args.columns.is_empty() { - !item.is_empty() - } else if let Value { - value: UntaggedValue::Row(ref r), - .. - } = item - { - args.columns - .iter() - .all(|field| r.get_data(field).borrow().is_some()) - } else { - false - } - }) - .into_output_stream()) -} - -#[cfg(test)] -mod tests { - use super::Compact; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Compact {}) - } -} diff --git a/crates/nu-command/src/commands/filters/default.rs b/crates/nu-command/src/commands/filters/default.rs deleted file mode 100644 index 5778f8e5b5..0000000000 --- a/crates/nu-command/src/commands/filters/default.rs +++ /dev/null @@ -1,81 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tagged; -use nu_value_ext::ValueExt; - -pub struct Default; - -impl WholeStreamCommand for Default { - fn name(&self) -> &str { - "default" - } - - fn signature(&self) -> Signature { - Signature::build("default") - .required("column name", SyntaxShape::String, "the name of the column") - .required( - "column value", - SyntaxShape::Any, - "the value of the column to default", - ) - } - - fn usage(&self) -> &str { - "Sets a default row's column if missing." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - default(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Give a default 'target' to all file entries", - example: "ls -la | default target 'nothing'", - result: None, - }] - } -} - -fn default(args: CommandArgs) -> Result { - let column: Tagged = args.req(0)?; - let value: Value = args.req(1)?; - - let input = args.input; - - Ok(input - .map(move |item| { - let should_add = matches!( - item, - Value { - value: UntaggedValue::Row(ref r), - .. - } if r.get_data(&column.item).borrow().is_none() - ); - - if should_add { - match item.insert_data_at_path(&column.item, value.clone()) { - Some(new_value) => ReturnSuccess::value(new_value), - None => ReturnSuccess::value(item), - } - } else { - ReturnSuccess::value(item) - } - }) - .into_action_stream()) -} - -#[cfg(test)] -mod tests { - use super::Default; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Default {}) - } -} diff --git a/crates/nu-command/src/commands/filters/drop/column.rs b/crates/nu-command/src/commands/filters/drop/column.rs deleted file mode 100644 index 717f7accac..0000000000 --- a/crates/nu-command/src/commands/filters/drop/column.rs +++ /dev/null @@ -1,80 +0,0 @@ -use crate::prelude::*; -use nu_data::base::select_fields; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape}; -use nu_source::Tagged; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "drop column" - } - - fn signature(&self) -> Signature { - Signature::build("drop column").optional( - "columns", - SyntaxShape::Number, - "starting from the end, the number of columns to remove", - ) - } - - fn usage(&self) -> &str { - "Remove the last number of columns. If you want to remove columns by name, try 'reject'." - } - - fn run(&self, args: CommandArgs) -> Result { - drop(args) - } - - fn examples(&self) -> Vec { - use nu_protocol::Value; - - vec![Example { - description: "Remove the last column of a table", - example: "echo [[lib, extension]; [nu-lib, rs] [nu-core, rb]] | drop column", - result: Some(vec![ - row! { "lib".into() => Value::from("nu-lib") }, - row! { "lib".into() => Value::from("nu-core") }, - ]), - }] - } -} - -fn drop(args: CommandArgs) -> Result { - let columns: Option> = args.opt(0)?; - - let to_drop = if let Some(quantity) = columns { - *quantity as usize - } else { - 1 - }; - - Ok(args - .input - .map(move |item| { - let headers = item.data_descriptors(); - - let descs = match headers.len() { - 0 => &headers[..], - n if to_drop > n => &[], - n => &headers[..n - to_drop], - }; - - Ok(select_fields(&item, descs, item.tag())) - }) - .into_input_stream()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/filters/drop/command.rs b/crates/nu-command/src/commands/filters/drop/command.rs deleted file mode 100644 index e5d80626c5..0000000000 --- a/crates/nu-command/src/commands/filters/drop/command.rs +++ /dev/null @@ -1,72 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue}; -use nu_source::Tagged; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "drop" - } - - fn signature(&self) -> Signature { - Signature::build("drop").optional( - "rows", - SyntaxShape::Number, - "starting from the back, the number of rows to remove", - ) - } - - fn usage(&self) -> &str { - "Remove the last number of rows or columns." - } - - fn run(&self, args: CommandArgs) -> Result { - drop(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Remove the last item of a list/table", - example: "echo [1 2 3] | drop", - result: Some(vec![ - UntaggedValue::int(1).into(), - UntaggedValue::int(2).into(), - ]), - }, - Example { - description: "Remove the last 2 items of a list/table", - example: "echo [1 2 3] | drop 2", - result: Some(vec![UntaggedValue::int(1).into()]), - }, - ] - } -} - -fn drop(args: CommandArgs) -> Result { - let rows: Option> = args.opt(0)?; - let v: Vec<_> = args.input.into_vec(); - - let rows_to_drop = if let Some(quantity) = rows { - *quantity as usize - } else { - 1 - }; - - Ok(if rows_to_drop == 0 { - v.into_iter().map(Ok).into_input_stream() - } else { - let k = if v.len() < rows_to_drop { - 0 - } else { - v.len() - rows_to_drop - }; - - let iter = v.into_iter().map(Ok).take(k); - - iter.into_input_stream() - }) -} diff --git a/crates/nu-command/src/commands/filters/drop/mod.rs b/crates/nu-command/src/commands/filters/drop/mod.rs deleted file mode 100644 index 407d1c1006..0000000000 --- a/crates/nu-command/src/commands/filters/drop/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod column; -mod command; -mod nth; - -pub use column::SubCommand as DropColumn; -pub use command::Command as Drop; -pub use nth::SubCommand as DropNth; diff --git a/crates/nu-command/src/commands/filters/drop/nth.rs b/crates/nu-command/src/commands/filters/drop/nth.rs deleted file mode 100644 index 9e5b461e87..0000000000 --- a/crates/nu-command/src/commands/filters/drop/nth.rs +++ /dev/null @@ -1,125 +0,0 @@ -use crate::prelude::*; -use itertools::Either; -use nu_engine::{FromValue, WholeStreamCommand}; -use nu_errors::ShellError; -use nu_protocol::{Range, Signature, SpannedTypeName, SyntaxShape, Value}; -use nu_source::Tagged; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "drop nth" - } - - fn signature(&self) -> Signature { - Signature::build("drop nth") - .required( - "row number or row range", - // FIXME: we can make this accept either Int or Range when we can compose SyntaxShapes - SyntaxShape::Any, - "the number of the row to drop or a range to drop consecutive rows", - ) - .rest( - "rest", - SyntaxShape::Any, - "Optionally drop more rows (Ignored if first argument is a range)", - ) - } - - fn usage(&self) -> &str { - "Drops the selected rows." - } - - fn run(&self, args: CommandArgs) -> Result { - drop(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Drop the second row", - example: "echo [first second third] | drop nth 1", - result: Some(vec![Value::from("first"), Value::from("third")]), - }, - Example { - description: "Drop the first and third rows", - example: "echo [first second third] | drop nth 0 2", - result: Some(vec![Value::from("second")]), - }, - Example { - description: "Drop range rows from second to fourth", - example: "echo [first second third fourth fifth] | drop nth (1..3)", - result: Some(vec![Value::from("first"), Value::from("fifth")]), - }, - ] - } -} - -fn extract_int_or_range(args: &CommandArgs) -> Result, ShellError> { - let value = args.req::(0)?; - - let int_opt = value.as_u64().map(Either::Left).ok(); - let range_opt = FromValue::from_value(&value).map(Either::Right).ok(); - - int_opt - .or(range_opt) - .ok_or_else(|| ShellError::type_error("int or range", value.spanned_type_name())) -} - -fn drop(args: CommandArgs) -> Result { - let number_or_range = extract_int_or_range(&args)?; - let rows = match number_or_range { - Either::Left(row_number) => { - let and_rows: Vec> = args.rest(1)?; - - let mut rows: Vec<_> = and_rows.into_iter().map(|x| x.item as usize).collect(); - rows.push(row_number as usize); - rows.sort_unstable(); - rows - } - Either::Right(row_range) => { - let from = row_range.min_u64()? as usize; - let to = row_range.max_u64()? as usize; - - (from..=to).collect() - } - }; - - let input = args.input; - - Ok(DropNthIterator { - input, - rows, - current: 0, - } - .into_output_stream()) -} - -struct DropNthIterator { - input: InputStream, - rows: Vec, - current: usize, -} - -impl Iterator for DropNthIterator { - type Item = Value; - - fn next(&mut self) -> Option { - loop { - if let Some(row) = self.rows.get(0) { - if self.current == *row { - self.rows.remove(0); - self.current += 1; - let _ = self.input.next(); - continue; - } else { - self.current += 1; - return self.input.next(); - } - } else { - return self.input.next(); - } - } - } -} diff --git a/crates/nu-command/src/commands/filters/each/command.rs b/crates/nu-command/src/commands/filters/each/command.rs deleted file mode 100644 index c3782233b6..0000000000 --- a/crates/nu-command/src/commands/filters/each/command.rs +++ /dev/null @@ -1,168 +0,0 @@ -use crate::prelude::*; -use nu_engine::run_block; -use nu_engine::WholeStreamCommand; - -use nu_errors::ShellError; -use nu_protocol::{ - hir::{CapturedBlock, ExternalRedirection}, - Signature, SyntaxShape, TaggedDictBuilder, UntaggedValue, Value, -}; - -pub struct Each; - -impl WholeStreamCommand for Each { - fn name(&self) -> &str { - "each" - } - - fn signature(&self) -> Signature { - Signature::build("each") - .required("block", SyntaxShape::Block, "the block to run on each row") - .switch( - "numbered", - "returned a numbered item ($it.index and $it.item)", - Some('n'), - ) - } - - fn usage(&self) -> &str { - "Run a block on each row of the table." - } - - fn run(&self, args: CommandArgs) -> Result { - each(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Echo the sum of each row", - example: "echo [[1 2] [3 4]] | each { echo $it | math sum }", - result: None, - }, - Example { - description: "Echo the square of each integer", - example: "echo [1 2 3] | each { echo ($it * $it) }", - result: Some(vec![ - UntaggedValue::int(1).into(), - UntaggedValue::int(4).into(), - UntaggedValue::int(9).into(), - ]), - }, - Example { - description: "Number each item and echo a message", - example: - "echo ['bob' 'fred'] | each --numbered { echo $\"($it.index) is ($it.item)\" }", - result: Some(vec![Value::from("0 is bob"), Value::from("1 is fred")]), - }, - Example { - description: "Name the block variable that each uses", - example: "[1, 2, 3] | each {|x| $x + 100}", - result: Some(vec![ - UntaggedValue::int(101).into(), - UntaggedValue::int(102).into(), - UntaggedValue::int(103).into(), - ]), - }, - ] - } -} - -pub fn process_row( - captured_block: Arc, - context: Arc, - input: Value, - external_redirection: ExternalRedirection, -) -> Result { - let input_clone = input.clone(); - // When we process a row, we need to know whether the block wants to have the contents of the row as - // a parameter to the block (so it gets assigned to a variable that can be used inside the block) or - // if it wants the contents as as an input stream - - let input_stream = if !captured_block.block.params.positional.is_empty() { - InputStream::empty() - } else { - vec![Ok(input_clone)].into_iter().into_input_stream() - }; - - context.scope.enter_scope(); - context.scope.add_vars(&captured_block.captured.entries); - - if let Some((arg, _)) = captured_block.block.params.positional.first() { - context.scope.add_var(arg.name(), input); - } else { - context.scope.add_var("$it", input); - } - - let result = run_block( - &captured_block.block, - &context, - input_stream, - external_redirection, - ); - - context.scope.exit_scope(); - - result -} - -pub(crate) fn make_indexed_item(index: usize, item: Value) -> Value { - let mut dict = TaggedDictBuilder::new(item.tag()); - dict.insert_untagged("index", UntaggedValue::int(index as i64)); - dict.insert_value("item", item); - - dict.into_value() -} - -fn each(args: CommandArgs) -> Result { - let context = Arc::new(args.context.clone()); - let external_redirection = args.call_info.args.external_redirection; - - let block: CapturedBlock = args.req(0)?; - let numbered: bool = args.has_flag("numbered"); - - let block = Arc::new(block); - - if numbered { - Ok(args - .input - .enumerate() - .flat_map(move |input| { - let block = block.clone(); - let context = context.clone(); - let row = make_indexed_item(input.0, input.1); - - match process_row(block, context, row, external_redirection) { - Ok(s) => s, - Err(e) => OutputStream::one(Value::error(e)), - } - }) - .into_output_stream()) - } else { - Ok(args - .input - .flat_map(move |input| { - let block = block.clone(); - let context = context.clone(); - - match process_row(block, context, input, external_redirection) { - Ok(s) => s, - Err(e) => OutputStream::one(Value::error(e)), - } - }) - .into_output_stream()) - } -} - -#[cfg(test)] -mod tests { - use super::Each; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Each {}) - } -} diff --git a/crates/nu-command/src/commands/filters/each/group.rs b/crates/nu-command/src/commands/filters/each/group.rs deleted file mode 100644 index 2d82b3f972..0000000000 --- a/crates/nu-command/src/commands/filters/each/group.rs +++ /dev/null @@ -1,142 +0,0 @@ -use crate::commands::filters::each::process_row; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - hir::{CapturedBlock, ExternalRedirection}, - Signature, SyntaxShape, UntaggedValue, Value, -}; -use nu_source::Tagged; - -pub struct EachGroup; - -impl WholeStreamCommand for EachGroup { - fn name(&self) -> &str { - "each group" - } - - fn signature(&self) -> Signature { - Signature::build("each group") - .required("group_size", SyntaxShape::Int, "the size of each group") - .required( - "block", - SyntaxShape::Block, - "the block to run on each group", - ) - } - - fn usage(&self) -> &str { - "Runs a block on groups of `group_size` rows of a table at a time." - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Echo the sum of each pair", - example: "echo [1 2 3 4] | each group 2 { echo $it | math sum }", - result: None, - }] - } - - fn run(&self, args: CommandArgs) -> Result { - let context = Arc::new(args.context.clone()); - let external_redirection = args.call_info.args.external_redirection; - - let group_size: Tagged = args.req(0)?; - let block: CapturedBlock = args.req(1)?; - let block = Arc::new(block); - - let each_group_iterator = EachGroupIterator { - block, - context, - group_size: group_size.item, - input: args.input, - external_redirection, - }; - - Ok(each_group_iterator.flatten().map(Ok).into_input_stream()) - } -} - -struct EachGroupIterator { - block: Arc, - context: Arc, - group_size: usize, - input: InputStream, - external_redirection: ExternalRedirection, -} - -impl Iterator for EachGroupIterator { - type Item = OutputStream; - - fn next(&mut self) -> Option { - let mut group = vec![]; - let mut current_count = 0; - - for next in &mut self.input { - group.push(next); - - current_count += 1; - if current_count >= self.group_size { - break; - } - } - - if group.is_empty() { - return None; - } - - Some(run_block_on_vec( - group, - self.block.clone(), - self.context.clone(), - self.external_redirection, - )) - } -} - -pub(crate) fn run_block_on_vec( - input: Vec, - block: Arc, - context: Arc, - external_redirection: ExternalRedirection, -) -> OutputStream { - let value = Value { - value: UntaggedValue::Table(input), - tag: Tag::unknown(), - }; - - match process_row(block, context, value, external_redirection) { - Ok(s) => { - // We need to handle this differently depending on whether process_row - // returned just 1 value or if it returned multiple as a stream. - let vec = s.collect::>(); - - // If it returned just one value, just take that value - if vec.len() == 1 { - return OutputStream::one( - vec.into_iter() - .next() - .expect("This should be impossible, we just checked that vec.len() == 1."), - ); - } - - // If it returned multiple values, we need to put them into a table and - // return that. - OutputStream::one(UntaggedValue::Table(vec).into_untagged_value()) - } - Err(e) => OutputStream::one(Value::error(e)), - } -} - -#[cfg(test)] -mod tests { - use super::EachGroup; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(EachGroup {}) - } -} diff --git a/crates/nu-command/src/commands/filters/each/mod.rs b/crates/nu-command/src/commands/filters/each/mod.rs deleted file mode 100644 index cd656685f0..0000000000 --- a/crates/nu-command/src/commands/filters/each/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub mod command; -pub mod group; -pub mod window; - -pub(crate) use command::make_indexed_item; -pub use command::process_row; -pub use command::Each; -pub use group::EachGroup; -pub use window::EachWindow; diff --git a/crates/nu-command/src/commands/filters/each/window.rs b/crates/nu-command/src/commands/filters/each/window.rs deleted file mode 100644 index e5d855719c..0000000000 --- a/crates/nu-command/src/commands/filters/each/window.rs +++ /dev/null @@ -1,105 +0,0 @@ -use crate::commands::filters::each::group::run_block_on_vec; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -//use itertools::Itertools; -use nu_errors::ShellError; -use nu_protocol::{hir::CapturedBlock, Primitive, Signature, SyntaxShape, UntaggedValue}; -use nu_source::Tagged; - -pub struct EachWindow; - -impl WholeStreamCommand for EachWindow { - fn name(&self) -> &str { - "each window" - } - - fn signature(&self) -> Signature { - Signature::build("each window") - .required("window_size", SyntaxShape::Int, "the size of each window") - .named( - "stride", - SyntaxShape::Int, - "the number of rows to slide over between windows", - Some('s'), - ) - .required( - "block", - SyntaxShape::Block, - "the block to run on each group", - ) - } - - fn usage(&self) -> &str { - "Runs a block on sliding windows of `window_size` rows of a table at a time." - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Echo the sum of each window", - example: "echo [1 2 3 4] | each window 2 { echo $it | math sum }", - result: None, - }] - } - - fn run(&self, mut args: CommandArgs) -> Result { - let context = Arc::new(args.context.clone()); - let external_redirection = args.call_info.args.external_redirection; - - let window_size: Tagged = args.req(0)?; - let block: CapturedBlock = args.req(1)?; - let stride: Option> = args.get_flag("stride")?; - - let block = Arc::new(block); - - let mut window: Vec<_> = args - .input - .by_ref() - .take(*window_size - 1) - .collect::>(); - - // `window` must start with dummy values, which will be removed on the first iteration - let stride = stride.map(|x| *x).unwrap_or(1); - window.insert(0, UntaggedValue::Primitive(Primitive::Nothing).into()); - - Ok(args - .input - .enumerate() - .flat_map(move |(i, input)| { - // This would probably be more efficient if `last` was a VecDeque - // But we can't have that because it needs to be put into a Table - window.remove(0); - window.push(input); - - let block = block.clone(); - let context = context.clone(); - let local_window = window.clone(); - - if i % stride == 0 { - Some(run_block_on_vec( - local_window, - block, - context, - external_redirection, - )) - } else { - None - } - }) - .flatten() - .map(Ok) - .into_input_stream()) - } -} - -#[cfg(test)] -mod tests { - use super::EachWindow; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(EachWindow {}) - } -} diff --git a/crates/nu-command/src/commands/filters/empty.rs b/crates/nu-command/src/commands/filters/empty.rs deleted file mode 100644 index c84fdbad63..0000000000 --- a/crates/nu-command/src/commands/filters/empty.rs +++ /dev/null @@ -1,238 +0,0 @@ -use crate::prelude::*; -use nu_engine::run_block; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - hir::CapturedBlock, hir::ExternalRedirection, ColumnPath, Primitive, ReturnSuccess, Signature, - SyntaxShape, UntaggedValue, Value, -}; - -use nu_value_ext::{as_string, ValueExt}; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "empty?" - } - - fn signature(&self) -> Signature { - Signature::build("empty?") - .rest( - "rest", - SyntaxShape::ColumnPath, - "the names of the columns to check emptiness", - ) - .named( - "block", - SyntaxShape::Block, - "an optional block to replace if empty", - Some('b'), - ) - } - - fn usage(&self) -> &str { - "Check for empty values." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - is_empty(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Check if a value is empty", - example: "echo '' | empty?", - result: Some(vec![UntaggedValue::boolean(true).into()]), - }, - Example { - description: "more than one column", - example: "echo [[meal size]; [arepa small] [taco '']] | empty? meal size", - result: Some( - vec![ - UntaggedValue::row(indexmap! { - "meal".to_string() => Value::from(false), - "size".to_string() => Value::from(false), - }) - .into(), - UntaggedValue::row(indexmap! { - "meal".to_string() => Value::from(false), - "size".to_string() => Value::from(true), - }) - .into(), - ], - ), - },Example { - description: "use a block if setting the empty cell contents is wanted", - example: "echo [[2020/04/16 2020/07/10 2020/11/16]; ['' [27] [37]]] | empty? 2020/04/16 -b { [33 37] }", - result: Some( - vec![ - UntaggedValue::row(indexmap! { - "2020/04/16".to_string() => UntaggedValue::table(&[UntaggedValue::int(33).into(), UntaggedValue::int(37).into()]).into(), - "2020/07/10".to_string() => UntaggedValue::table(&[UntaggedValue::int(27).into()]).into(), - "2020/11/16".to_string() => UntaggedValue::table(&[UntaggedValue::int(37).into()]).into(), - }) - .into(), - ], - ), - }, - ] - } -} - -fn is_empty(args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let context = args.context.clone(); - let block: Option = args.get_flag("block")?; - let columns: Vec = args.rest(0)?; - - let input = args.input; - - if input.is_empty() { - let stream = - vec![UntaggedValue::Primitive(Primitive::Nothing).into_value(&tag)].into_iter(); - - return Ok(InputStream::from_stream(stream) - .map(move |input| { - let columns = vec![]; - - match process_row(&context, input, &block, columns) { - Ok(s) => s, - Err(e) => ActionStream::one(Err(e)), - } - }) - .flatten() - .into_action_stream()); - } - - Ok(input - .map(move |input| { - let columns = columns.clone(); - - match process_row(&context, input, &block, columns) { - Ok(s) => s, - Err(e) => ActionStream::one(Err(e)), - } - }) - .flatten() - .into_action_stream()) -} - -fn process_row( - context: &EvaluationContext, - input: Value, - default_block: &Option, - column_paths: Vec, -) -> Result { - let mut out = Arc::new(None); - let results = Arc::make_mut(&mut out); - - if let Some(default_block) = &*default_block { - let for_block = input.clone(); - let input_stream = vec![Ok(for_block)].into_iter().into_input_stream(); - - context.scope.enter_scope(); - context.scope.add_vars(&default_block.captured.entries); - if let Some((arg, _)) = default_block.block.params.positional.first() { - context.scope.add_var(arg.name(), input.clone()); - } - - let stream = run_block( - &default_block.block, - context, - input_stream, - ExternalRedirection::Stdout, - ); - context.scope.exit_scope(); - - let mut stream = stream?; - *results = Some({ - let values = stream.drain_vec(); - - let errors = context.get_errors(); - - if let Some(error) = errors.first() { - return Err(error.clone()); - } - - if values.len() == 1 { - let value = values - .get(0) - .ok_or_else(|| ShellError::unexpected("No value."))?; - - Value { - value: value.value.clone(), - tag: input.tag.clone(), - } - } else if values.is_empty() { - UntaggedValue::nothing().into_value(&input.tag) - } else { - UntaggedValue::table(&values).into_value(&input.tag) - } - }); - } - - match input { - Value { - value: UntaggedValue::Row(ref r), - ref tag, - } => { - if column_paths.is_empty() { - Ok(ActionStream::one(ReturnSuccess::value({ - let is_empty = input.is_empty(); - - if default_block.is_some() { - if is_empty { - results - .clone() - .unwrap_or_else(|| UntaggedValue::boolean(true).into_value(tag)) - } else { - input.clone() - } - } else { - UntaggedValue::boolean(is_empty).into_value(tag) - } - }))) - } else { - let mut obj = input.clone(); - - for column in column_paths { - let path = UntaggedValue::Primitive(Primitive::ColumnPath(column.clone())) - .into_value(tag); - let data = r.get_data(&as_string(&path)?).borrow().clone(); - let is_empty = data.is_empty(); - - let default = if default_block.is_some() { - if is_empty { - results - .clone() - .unwrap_or_else(|| UntaggedValue::boolean(true).into_value(tag)) - } else { - data.clone() - } - } else { - UntaggedValue::boolean(is_empty).into_value(tag) - }; - - if let Ok(value) = - obj.swap_data_by_column_path(&column, Box::new(move |_| Ok(default))) - { - obj = value; - } - } - - Ok(ActionStream::one(ReturnSuccess::value(obj))) - } - } - other => Ok(ActionStream::one(ReturnSuccess::value({ - if other.is_empty() { - results - .clone() - .unwrap_or_else(|| UntaggedValue::boolean(true).into_value(other.tag)) - } else { - UntaggedValue::boolean(false).into_value(other.tag) - } - }))), - } -} diff --git a/crates/nu-command/src/commands/filters/every.rs b/crates/nu-command/src/commands/filters/every.rs deleted file mode 100644 index aeac25e23b..0000000000 --- a/crates/nu-command/src/commands/filters/every.rs +++ /dev/null @@ -1,89 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ReturnSuccess, Signature, SyntaxShape, UntaggedValue}; - -pub struct Every; - -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." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - every(args) - } - - 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(), - ]), - }, - ] - } -} - -fn every(args: CommandArgs) -> Result { - let stride: u64 = args.req(0)?; - let skip: bool = args.has_flag("skip"); - let input = args.input; - - Ok(input - .enumerate() - .filter_map(move |(i, value)| { - let stride_desired = if stride < 1 { 1 } else { stride } as usize; - let should_include = skip == (i % stride_desired != 0); - - if should_include { - Some(ReturnSuccess::value(value)) - } else { - None - } - }) - .into_action_stream()) -} - -#[cfg(test)] -mod tests { - use super::Every; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Every {}) - } -} diff --git a/crates/nu-command/src/commands/filters/first.rs b/crates/nu-command/src/commands/filters/first.rs deleted file mode 100644 index c592fda64d..0000000000 --- a/crates/nu-command/src/commands/filters/first.rs +++ /dev/null @@ -1,152 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue}; -use nu_source::Tagged; - -pub struct First; - -impl WholeStreamCommand for First { - fn name(&self) -> &str { - "first" - } - - fn signature(&self) -> Signature { - Signature::build("first").optional( - "rows", - SyntaxShape::Int, - "starting from the front, the number of rows to return", - ) - } - - fn usage(&self) -> &str { - "Show only the first number of rows." - } - - fn run(&self, args: CommandArgs) -> Result { - first(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Return the first item of a list/table", - example: "echo [1 2 3] | first", - result: Some(vec![UntaggedValue::int(1).into()]), - }, - Example { - description: "Return the first 2 items of a list/table", - example: "echo [1 2 3] | first 2", - result: Some(vec![ - UntaggedValue::int(1).into(), - UntaggedValue::int(2).into(), - ]), - }, - ] - } -} - -fn first(args: CommandArgs) -> Result { - let rows: Option> = args.opt(0)?; - let tag = args.call_info.name_tag; - - let mut rows_desired = if let Some(quantity) = rows { - *quantity - } else { - 1 - }; - - let mut input_peek = args.input.peekable(); - match &mut input_peek.next_if(|val| val.is_binary()) { - Some(v) => match &v.value { - // We already know it's a binary so we don't have to match - // on the type of primitive - UntaggedValue::Primitive(_) => { - let bytes = match v.as_binary_vec() { - Ok(b) => b, - _ => { - return Err(ShellError::labeled_error( - "error converting data as_binary_vec", - "error conversion", - tag, - )) - } - }; - // if the current 8192 chunk fits inside our rows_desired - // carve it up and return it - if bytes.len() >= rows_desired { - // We only want to see a certain amount of the binary - // so let's grab those parts - let output_bytes = bytes[0..rows_desired].to_vec(); - Ok(OutputStream::one(UntaggedValue::binary(output_bytes))) - } else { - // if we want more rows that the current chunk size (8192) - // we must gradually get bigger chunks while testing - // if it's within the requested rows_desired size - let mut bigger: Vec = vec![]; - bigger.extend(bytes); - while bigger.len() < rows_desired { - match input_peek.next() { - Some(data) => match data.value.into_value(&tag).as_binary_vec() { - Ok(bits) => bigger.extend(bits), - _ => { - return Err(ShellError::labeled_error( - "error converting data as_binary_vec", - "error conversion", - tag, - )) - } - }, - _ => { - // We're at the end of our data so let's break out of this loop - // and set the rows_desired to the size of our data - rows_desired = bigger.len(); - break; - } - } - } - let output_bytes = bigger[0..rows_desired].to_vec(); - Ok(OutputStream::one(UntaggedValue::binary(output_bytes))) - } - } - UntaggedValue::Row(_) => Ok(input_peek.take(rows_desired).into_output_stream()), - UntaggedValue::Table(_) => Err(ShellError::labeled_error( - "unsure how to handle UntaggedValue::Table", - "found table", - tag, - )), - UntaggedValue::Error(_) => Err(ShellError::labeled_error( - "unsure how to handle UntaggedValue::Error", - "found error", - tag, - )), - UntaggedValue::Block(_) => Err(ShellError::labeled_error( - "unsure how to handled UntaggedValue::Block", - "found block", - tag, - )), - #[cfg(all(not(target_arch = "wasm32"), feature = "dataframe"))] - UntaggedValue::DataFrame(_) | UntaggedValue::FrameStruct(_) => { - Err(ShellError::labeled_error( - "unsure how to handled dataframe struct", - "found dataframe", - tag, - )) - } - }, - None => Ok(input_peek.take(rows_desired).into_output_stream()), - } -} - -#[cfg(test)] -mod tests { - use super::First; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(First {}) - } -} diff --git a/crates/nu-command/src/commands/filters/flatten.rs b/crates/nu-command/src/commands/filters/flatten.rs deleted file mode 100644 index fef4b5153e..0000000000 --- a/crates/nu-command/src/commands/filters/flatten.rs +++ /dev/null @@ -1,179 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - Dictionary, ReturnSuccess, Signature, SyntaxShape, TaggedDictBuilder, UntaggedValue, Value, -}; -use nu_source::Tagged; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "flatten" - } - - fn signature(&self) -> Signature { - Signature::build("flatten").rest( - "rest", - SyntaxShape::String, - "optionally flatten data by column", - ) - } - - fn usage(&self) -> &str { - "Flatten the table." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - flatten(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "flatten a table", - example: "echo [[N, u, s, h, e, l, l]] | flatten | first", - result: Some(vec![Value::from("N")]), - }, - Example { - description: "flatten a column having a nested table", - example: "echo [[origin, people]; [Ecuador, (echo [[name, meal]; ['Andres', 'arepa']])]] | flatten | get meal", - result: Some(vec![Value::from("arepa")]), - }, - Example { - description: "restrict the flattening by passing column names", - example: "echo [[origin, crate, versions]; [World, (echo [[name]; ['nu-cli']]), ['0.21', '0.22']]] | flatten versions | last | get versions", - result: Some(vec![Value::from("0.22")]), - } - ] - } -} - -fn flatten(args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let columns: Vec> = args.rest(0)?; - let input = args.input; - - Ok(input - .flat_map(move |item| flat_value(&columns, &item, &tag)) - .into_action_stream()) -} - -enum TableInside<'a> { - Entries(&'a str, &'a Tag, Vec<&'a Value>), -} - -fn flat_value( - columns: &[Tagged], - item: &Value, - name_tag: impl Into, -) -> Vec> { - let tag = item.tag.clone(); - let name_tag = name_tag.into(); - - let res = { - if item.is_row() { - let mut out = TaggedDictBuilder::new(tag); - let mut a_table = None; - let mut tables_explicitly_flattened = 0; - - for (column, value) in item.row_entries() { - let column_requested = columns.iter().find(|c| c.item == *column); - - if let Value { - value: UntaggedValue::Row(Dictionary { entries: mapa }), - .. - } = value - { - if column_requested.is_none() && !columns.is_empty() { - if out.contains_key(column) { - out.insert_value(format!("{}_{}", column, column), value.clone()); - } else { - out.insert_value(column, value.clone()); - } - continue; - } - - for (k, v) in mapa { - if out.contains_key(k) { - out.insert_value(format!("{}_{}", column, k), v.clone()); - } else { - out.insert_value(k, v.clone()); - } - } - } else if value.is_table() { - if tables_explicitly_flattened >= 1 && column_requested.is_some() { - let attempted = if let Some(name) = column_requested { - name.span() - } else { - name_tag.span - }; - - let already_flattened = - if let Some(TableInside::Entries(_, column_tag, _)) = a_table { - column_tag.span - } else { - name_tag.span - }; - - return vec![ReturnSuccess::value( - UntaggedValue::Error(ShellError::labeled_error_with_secondary( - "can only flatten one inner table at the same time", - "tried flattening more than one column with inner tables", - attempted, - "...but is flattened already", - already_flattened, - )) - .into_value(name_tag), - )]; - } - - if !columns.is_empty() { - if let Some(requested) = column_requested { - a_table = Some(TableInside::Entries( - &requested.item, - &requested.tag, - value.table_entries().collect(), - )); - - tables_explicitly_flattened += 1; - } else { - out.insert_value(column, value.clone()); - } - } else if a_table.is_none() { - a_table = Some(TableInside::Entries( - column, - &value.tag, - value.table_entries().collect(), - )) - } else { - out.insert_value(column, value.clone()); - } - } else { - out.insert_value(column, value.clone()); - } - } - - let mut expanded = vec![]; - - if let Some(TableInside::Entries(column, _, entries)) = a_table { - for entry in entries { - let mut base = out.clone(); - base.insert_value(column, entry.clone()); - expanded.push(base.into_value()); - } - } else { - expanded.push(out.into_value()); - } - - expanded - } else if item.is_table() { - item.table_entries().cloned().collect() - } else { - vec![item.clone()] - } - }; - - res.into_iter().map(ReturnSuccess::value).collect() -} diff --git a/crates/nu-command/src/commands/filters/get.rs b/crates/nu-command/src/commands/filters/get.rs deleted file mode 100644 index 3926bcbb60..0000000000 --- a/crates/nu-command/src/commands/filters/get.rs +++ /dev/null @@ -1,252 +0,0 @@ -use crate::prelude::*; -use indexmap::set::IndexSet; -use log::trace; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - did_you_mean, ColumnPath, Dictionary, PathMember, Primitive, ReturnSuccess, Signature, - SyntaxShape, UnspannedPathMember, UntaggedValue, Value, -}; -use nu_source::HasFallibleSpan; -use nu_value_ext::get_data_by_column_path; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "get" - } - - fn signature(&self) -> Signature { - Signature::build("get").rest( - "rest", - SyntaxShape::ColumnPath, - "optionally return additional data by path", - ) - } - - fn usage(&self) -> &str { - "Open given cells as text." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - get(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Extract the name of files as a list", - example: "ls | get name", - result: None, - }, - Example { - description: "Extract the cpu list from the sys information", - example: "sys | get cpu", - result: None, - }, - ] - } -} - -pub fn get(args: CommandArgs) -> Result { - let column_paths: Vec = args.rest(0)?; - let mut input = args.input; - - if column_paths.is_empty() { - let vec = input.drain_vec(); - - let descs = nu_protocol::merge_descriptors(&vec); - - Ok(descs - .into_iter() - .map(ReturnSuccess::value) - .into_action_stream()) - } else { - trace!("get {:?}", column_paths); - let output_stream = input - .flat_map(move |item| { - column_paths - .iter() - .flat_map(move |path| get_output(&item, path)) - .collect::>() - }) - .into_action_stream(); - Ok(output_stream) - } -} - -fn get_output(item: &Value, path: &ColumnPath) -> Vec> { - match get_column_path(path, item) { - Ok(Value { - value: UntaggedValue::Primitive(Primitive::Nothing), - .. - }) => vec![], - Ok(Value { - value: UntaggedValue::Table(rows), - .. - }) => rows.into_iter().map(ReturnSuccess::value).collect(), - Ok(other) => vec![ReturnSuccess::value(other)], - Err(reason) => vec![ReturnSuccess::value( - UntaggedValue::Error(reason).into_untagged_value(), - )], - } -} - -pub fn get_column_path(path: &ColumnPath, obj: &Value) -> Result { - get_data_by_column_path(obj, path, move |obj_source, column_path_tried, error| { - let path_members_span = path.maybe_span().unwrap_or_else(Span::unknown); - - match &obj_source.value { - UntaggedValue::Table(rows) => { - return get_column_path_from_table_error( - rows, - column_path_tried, - &path_members_span, - ); - } - UntaggedValue::Row(columns) => { - if let Some(error) = get_column_from_row_error( - columns, - column_path_tried, - &path_members_span, - obj_source, - ) { - return error; - } - } - _ => {} - } - - if let Some(suggestions) = did_you_mean(obj_source, column_path_tried.as_string()) { - ShellError::labeled_error( - "Unknown column", - format!("did you mean '{}'?", suggestions[0]), - column_path_tried.span.since(path_members_span), - ) - } else { - error - } - }) -} - -pub fn get_column_path_from_table_error( - rows: &[Value], - column_path_tried: &PathMember, - path_members_span: &Span, -) -> ShellError { - match column_path_tried { - PathMember { - unspanned: UnspannedPathMember::String(column), - .. - } => { - let primary_label = format!("There isn't a column named '{}'", &column); - - let suggestions: IndexSet<_> = rows - .iter() - .filter_map(|r| did_you_mean(r, column_path_tried.as_string())) - .map(|s| s[0].to_owned()) - .collect(); - let mut existing_columns: IndexSet<_> = IndexSet::default(); - let mut names: Vec = vec![]; - - for row in rows { - for field in row.data_descriptors() { - if !existing_columns.contains(&field[..]) { - existing_columns.insert(field.clone()); - names.push(field); - } - } - } - - if names.is_empty() { - ShellError::labeled_error_with_secondary( - "Unknown column", - primary_label, - column_path_tried.span, - "Appears to contain rows. Try indexing instead.", - column_path_tried.span.since(path_members_span), - ) - } else { - ShellError::labeled_error_with_secondary( - "Unknown column", - primary_label, - column_path_tried.span, - format!( - "Perhaps you meant '{}'? Columns available: {}", - suggestions - .iter() - .map(|x| x.to_owned()) - .collect::>() - .join(","), - names.join(", ") - ), - column_path_tried.span.since(path_members_span), - ) - } - } - PathMember { - unspanned: UnspannedPathMember::Int(idx), - .. - } => { - let total = rows.len(); - - let secondary_label = if total == 1 { - "The table only has 1 row".to_owned() - } else { - format!("The table only has {} rows (0 to {})", total, total - 1) - }; - - ShellError::labeled_error_with_secondary( - "Row not found", - format!("There isn't a row indexed at {}", idx), - column_path_tried.span, - secondary_label, - column_path_tried.span.since(path_members_span), - ) - } - } -} - -pub fn get_column_from_row_error( - columns: &Dictionary, - column_path_tried: &PathMember, - path_members_span: &Span, - obj_source: &Value, -) -> Option { - match column_path_tried { - PathMember { - unspanned: UnspannedPathMember::String(column), - .. - } => { - let primary_label = format!("There isn't a column named '{}'", &column); - - did_you_mean(obj_source, column_path_tried.as_string()).map(|suggestions| { - ShellError::labeled_error_with_secondary( - "Unknown column", - primary_label, - column_path_tried.span, - format!( - "Perhaps you meant '{}'? Columns available: {}", - suggestions[0], - &obj_source.data_descriptors().join(", ") - ), - column_path_tried.span.since(path_members_span), - ) - }) - } - PathMember { - unspanned: UnspannedPathMember::Int(idx), - .. - } => Some(ShellError::labeled_error_with_secondary( - "No rows available", - format!("A row at '{}' can't be indexed.", &idx), - column_path_tried.span, - format!( - "Appears to contain columns. Columns available: {}", - columns.keys().join(", ") - ), - column_path_tried.span.since(path_members_span), - )), - } -} diff --git a/crates/nu-command/src/commands/filters/group_by.rs b/crates/nu-command/src/commands/filters/group_by.rs deleted file mode 100644 index c764d7b093..0000000000 --- a/crates/nu-command/src/commands/filters/group_by.rs +++ /dev/null @@ -1,325 +0,0 @@ -use crate::prelude::*; -use crate::utils::suggestions::suggestions; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::hir::ExternalRedirection; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tagged; -use nu_value_ext::as_string; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "group-by" - } - - fn signature(&self) -> Signature { - Signature::build("group-by").optional( - "grouper", - SyntaxShape::Any, - "the grouper value to use", - ) - } - - fn usage(&self) -> &str { - "Create a new table grouped." - } - - fn run(&self, args: CommandArgs) -> Result { - group_by(args) - } - - #[allow(clippy::unwrap_used)] - fn examples(&self) -> Vec { - use nu_data::value::date_naive_from_str as date; - - vec![ - Example { - description: "group items by column named \"type\"", - example: r#"ls | group-by type"#, - result: Some(vec![UntaggedValue::row(indexmap! { - "File".to_string() => UntaggedValue::Table(vec![ - UntaggedValue::row(indexmap! { - "name".to_string() => UntaggedValue::string("Andres.txt").into(), - "type".to_string() => UntaggedValue::string("File").into(), - "chickens".to_string() => UntaggedValue::int(10).into(), - "modified".to_string() => date("2019-07-23".tagged_unknown()).unwrap().into(), - }).into(), - UntaggedValue::row(indexmap! { - "name".to_string() => UntaggedValue::string("Darren.txt").into(), - "type".to_string() => UntaggedValue::string("File").into(), - "chickens".to_string() => UntaggedValue::int(20).into(), - "modified".to_string() => date("2019-09-24".tagged_unknown()).unwrap().into(), - }).into(), - ]).into(), - "Dir".to_string() => UntaggedValue::Table(vec![ - UntaggedValue::row(indexmap! { - "name".to_string() => UntaggedValue::string("Jonathan").into(), - "type".to_string() => UntaggedValue::string("Dir").into(), - "chickens".to_string() => UntaggedValue::int(5).into(), - "modified".to_string() => date("2019-07-23".tagged_unknown()).unwrap().into(), - }).into(), - UntaggedValue::row(indexmap! { - "name".to_string() => UntaggedValue::string("Yehuda").into(), - "type".to_string() => UntaggedValue::string("Dir").into(), - "chickens".to_string() => UntaggedValue::int(4).into(), - "modified".to_string() => date("2019-09-24".tagged_unknown()).unwrap().into(), - }).into(), - ]).into(), - }) - .into()]), - }, - Example { - description: "you can also group by raw values by leaving out the argument", - example: "echo [1 3 1 3 2 1 1] | group-by", - result: Some(vec![UntaggedValue::row(indexmap! { - "1".to_string() => UntaggedValue::Table(vec![ - UntaggedValue::int(1).into(), - UntaggedValue::int(1).into(), - UntaggedValue::int(1).into(), - UntaggedValue::int(1).into(), - ]).into(), - - "3".to_string() => UntaggedValue::Table(vec![ - UntaggedValue::int(3).into(), - UntaggedValue::int(3).into(), - ]).into(), - - "2".to_string() => UntaggedValue::Table(vec![ - UntaggedValue::int(2).into(), - ]).into(), - }) - .into()]), - }, - Example { - description: - "use the block form to generate a grouping key when each row gets processed", - example: "echo [1 3 1 3 2 1 1] | group-by { ($it - 1) mod 3 }", - result: Some(vec![UntaggedValue::row(indexmap! { - "0".to_string() => UntaggedValue::Table(vec![ - UntaggedValue::int(1).into(), - UntaggedValue::int(1).into(), - UntaggedValue::int(1).into(), - UntaggedValue::int(1).into(), - - ]).into(), - "2".to_string() => UntaggedValue::Table(vec![ - UntaggedValue::int(3).into(), - UntaggedValue::int(3).into(), - ]).into(), - "1".to_string() => UntaggedValue::Table(vec![ - UntaggedValue::int(2).into(), - ]).into(), - }) - .into()]), - }, - ] - } -} - -enum Grouper { - ByColumn(Option>), - ByBlock, -} - -pub fn group_by(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - let context = Arc::new(args.context.clone()); - - let grouper = args.opt(0)?; - let values: Vec = args.input.collect(); - let mut keys: Vec> = vec![]; - let mut group_strategy = Grouper::ByColumn(None); - - match grouper { - Some(Value { - value: UntaggedValue::Block(block_given), - .. - }) => { - let block = Arc::new(*block_given); - let error_key = "error"; - - for value in &values { - let run = block.clone(); - let context = context.clone(); - - match crate::commands::filters::each::process_row( - run, - context, - value.clone(), - ExternalRedirection::Stdout, - ) { - Ok(mut s) => { - let collection: Vec = s.drain_vec(); - - if collection.len() > 1 { - return Err(ShellError::labeled_error( - "expected one value from the block", - "requires a table with one value for grouping", - &name, - )); - } - - let value = match collection.get(0) { - Some(Value { - value: UntaggedValue::Error(_), - .. - }) - | None => UntaggedValue::string(error_key).into_value(&name), - Some(return_value) => return_value.clone(), - }; - - keys.push(as_string(&value)); - } - Err(_) => { - keys.push(Ok(error_key.into())); - } - } - } - - group_strategy = Grouper::ByBlock; - } - Some(other) => { - group_strategy = Grouper::ByColumn(Some(as_string(&other)?.tagged(&name))); - } - _ => {} - } - - if values.is_empty() { - return Err(ShellError::labeled_error( - "expected table from pipeline", - "requires a table input", - name, - )); - } - - let first = values[0].clone(); - - let name = if first.tag.anchor().is_some() { - first.tag - } else { - name - }; - - let values = UntaggedValue::table(&values).into_value(&name); - - let group_value = match group_strategy { - Grouper::ByBlock => { - let map = keys; - - let block = Box::new(move |idx: usize, row: &Value| match map.get(idx) { - Some(Ok(key)) => Ok(key.clone()), - Some(Err(reason)) => Err(reason.clone()), - None => as_string(row), - }); - - nu_data::utils::group(&values, &Some(block), name) - } - Grouper::ByColumn(column_name) => group(&column_name, &values, &name), - }; - - Ok(OutputStream::one(group_value?)) -} - -pub fn group( - column_name: &Option>, - values: &Value, - tag: impl Into, -) -> Result { - 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)), - } - }); - - nu_data::utils::group(values, &Some(block), &name) - } - Grouper::ByColumn(None) => { - let block = Box::new(move |_, row: &Value| as_string(row)); - - nu_data::utils::group(values, &Some(block), &name) - } - Grouper::ByBlock => Err(ShellError::unimplemented( - "Block not implemented: This should never happen.", - )), - } -} - -#[cfg(test)] -mod tests { - use super::group; - use nu_data::utils::helpers::committers; - use nu_errors::ShellError; - use nu_source::*; - use nu_test_support::value::{date, int, row, string, table}; - - #[test] - fn groups_table_by_date_column() -> Result<(), ShellError> { - let for_key = Some(String::from("date").tagged_unknown()); - let sample = table(&committers()); - - assert_eq!( - group(&for_key, &sample, Tag::unknown())?, - row(indexmap! { - "2019-07-23".into() => table(&[ - row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => date("2019-07-23"), "chickens".into() => int(10) }), - row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => date("2019-07-23"), "chickens".into() => int(5) }), - row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => date("2019-07-23"), "chickens".into() => int(2) }) - ]), - "2019-10-10".into() => table(&[ - row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => date("2019-10-10"), "chickens".into() => int(6) }), - row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => date("2019-10-10"), "chickens".into() => int(15) }), - row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => date("2019-10-10"), "chickens".into() => int(30) }) - ]), - "2019-09-24".into() => table(&[ - row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => date("2019-09-24"), "chickens".into() => int(20) }), - row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => date("2019-09-24"), "chickens".into() => int(4) }), - row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => date("2019-09-24"), "chickens".into() => int(10) }) - ]), - }) - ); - - Ok(()) - } - - #[test] - fn groups_table_by_country_column() -> Result<(), ShellError> { - let for_key = Some(String::from("country").tagged_unknown()); - let sample = table(&committers()); - - assert_eq!( - group(&for_key, &sample, Tag::unknown())?, - row(indexmap! { - "EC".into() => table(&[ - row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => date("2019-07-23"), "chickens".into() => int(10) }), - row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => date("2019-09-24"), "chickens".into() => int(20) }), - row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => date("2019-10-10"), "chickens".into() => int(30) }) - ]), - "NZ".into() => table(&[ - row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => date("2019-07-23"), "chickens".into() => int(5) }), - row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => date("2019-10-10"), "chickens".into() => int(15) }), - row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => date("2019-09-24"), "chickens".into() => int(10) }) - ]), - "US".into() => table(&[ - row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => date("2019-10-10"), "chickens".into() => int(6) }), - row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => date("2019-09-24"), "chickens".into() => int(4) }), - row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => date("2019-07-23"), "chickens".into() => int(2) }), - ]), - }) - ); - - Ok(()) - } -} diff --git a/crates/nu-command/src/commands/filters/group_by_date.rs b/crates/nu-command/src/commands/filters/group_by_date.rs deleted file mode 100644 index 3787f3f1d5..0000000000 --- a/crates/nu-command/src/commands/filters/group_by_date.rs +++ /dev/null @@ -1,133 +0,0 @@ -use crate::prelude::*; -use crate::utils::suggestions::suggestions; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tagged; - -pub struct GroupByDate; - -impl WholeStreamCommand for GroupByDate { - fn name(&self) -> &str { - "group-by date" - } - - fn signature(&self) -> Signature { - Signature::build("group-by date") - .optional( - "column_name", - SyntaxShape::String, - "the name of the column to group by", - ) - .named( - "format", - SyntaxShape::String, - "Specify date and time formatting", - Some('f'), - ) - } - - fn usage(&self) -> &str { - "creates a table grouped by date." - } - - fn run(&self, args: CommandArgs) -> Result { - group_by_date(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Group files by type", - example: "ls | group-by date --format '%d/%m/%Y'", - result: None, - }] - } -} - -enum Grouper { - ByDate(Option>), -} - -enum GroupByColumn { - Name(Option>), -} - -pub fn group_by_date(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - let column_name: Option> = args.opt(0)?; - let format: Option> = args.get_flag("format")?; - - let values: Vec = args.input.collect(); - - if values.is_empty() { - Err(ShellError::labeled_error( - "Expected table from pipeline", - "requires a table input", - name, - )) - } else { - 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) - }; - - let value_result = match (grouper_date, grouper_column) { - (Grouper::ByDate(None), GroupByColumn::Name(None)) => { - let block = Box::new(move |_, row: &Value| row.format("%Y-%m-%d")); - - nu_data::utils::group(&values, &Some(block), &name) - } - (Grouper::ByDate(None), GroupByColumn::Name(Some(column_name))) => { - let block = Box::new(move |_, row: &Value| { - let group_key = row - .get_data_by_key(column_name.borrow_spanned()) - .ok_or_else(|| suggestions(column_name.borrow_tagged(), row)); - - group_key?.format("%Y-%m-%d") - }); - - nu_data::utils::group(&values, &Some(block), &name) - } - (Grouper::ByDate(Some(fmt)), GroupByColumn::Name(None)) => { - let block = Box::new(move |_, row: &Value| row.format(&fmt)); - - nu_data::utils::group(&values, &Some(block), &name) - } - (Grouper::ByDate(Some(fmt)), GroupByColumn::Name(Some(column_name))) => { - let block = Box::new(move |_, row: &Value| { - let group_key = row - .get_data_by_key(column_name.borrow_spanned()) - .ok_or_else(|| suggestions(column_name.borrow_tagged(), row)); - - group_key?.format(&fmt) - }); - - nu_data::utils::group(&values, &Some(block), &name) - } - }; - - Ok(OutputStream::one(value_result?)) - } -} - -#[cfg(test)] -mod tests { - use super::GroupByDate; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(GroupByDate {}) - } -} diff --git a/crates/nu-command/src/commands/filters/headers.rs b/crates/nu-command/src/commands/filters/headers.rs deleted file mode 100644 index 85368173f2..0000000000 --- a/crates/nu-command/src/commands/filters/headers.rs +++ /dev/null @@ -1,119 +0,0 @@ -use crate::prelude::*; - -use indexmap::IndexMap; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::Dictionary; -use nu_protocol::{Primitive, ReturnSuccess, Signature, UntaggedValue, Value}; - -pub struct Headers; - -impl WholeStreamCommand for Headers { - fn name(&self) -> &str { - "headers" - } - - fn signature(&self) -> Signature { - Signature::build("headers") - } - - fn usage(&self) -> &str { - "Use the first row of the table as column names." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - headers(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Create headers from a raw string", - example: r#"echo "a b c|1 2 3" | split row "|" | split column " " | headers"#, - result: None, - }, - Example { - description: "Don't panic on rows with different headers", - example: r#"echo "a b c|1 2 3|1 2 3 4" | split row "|" | split column " " | headers"#, - result: None, - }, - ] - } -} - -pub fn headers(args: CommandArgs) -> Result { - let input = args.input; - let rows: Vec = input.collect(); - - if rows.is_empty() { - return Err(ShellError::untagged_runtime_error( - "Couldn't find headers, was the input a properly formatted, non-empty table?", - )); - } - - //the headers are the first row in the table - let headers: Vec = match &rows[0].value { - UntaggedValue::Row(d) => { - Ok(d.entries - .iter() - .map(|(k, v)| { - match v.as_string() { - Ok(s) => s, - Err(_) => { - //If a cell that should contain a header name is empty, we name the column Column[index] - match d.entries.get_full(k) { - Some((index, _, _)) => format!("Column{}", index), - None => "unknownColumn".to_string(), - } - } - } - }) - .collect()) - } - _ => Err(ShellError::unexpected_eof( - "Could not get headers, is the table empty?", - rows[0].tag.span, - )), - }?; - - Ok(rows - .into_iter() - .skip(1) - .map(move |r| { - //Each row is a dictionary with the headers as keys - match &r.value { - UntaggedValue::Row(d) => { - let mut entries = IndexMap::new(); - for (i, header) in headers.iter().enumerate() { - let value = match d.entries.get_index(i) { - Some((_, value)) => value.clone(), - None => UntaggedValue::Primitive(Primitive::Nothing).into(), - }; - - entries.insert(header.clone(), value); - } - Ok(ReturnSuccess::Value( - UntaggedValue::Row(Dictionary { entries }).into_value(r.tag.clone()), - )) - } - _ => Err(ShellError::unexpected_eof( - "Couldn't iterate through rows, was the input a properly formatted table?", - r.tag.span, - )), - } - }) - .into_action_stream()) -} - -#[cfg(test)] -mod tests { - use super::Headers; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Headers {}) - } -} diff --git a/crates/nu-command/src/commands/filters/insert.rs b/crates/nu-command/src/commands/filters/insert.rs deleted file mode 100644 index d3399bb18d..0000000000 --- a/crates/nu-command/src/commands/filters/insert.rs +++ /dev/null @@ -1,174 +0,0 @@ -use crate::prelude::*; -use nu_engine::run_block; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::hir::ExternalRedirection; -use nu_protocol::{ - ColumnPath, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value, -}; -use nu_value_ext::ValueExt; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "insert" - } - - fn signature(&self) -> Signature { - Signature::build("insert") - .required( - "column", - SyntaxShape::ColumnPath, - "the column name to insert", - ) - .required("value", SyntaxShape::Any, "the value to give the cell(s)") - } - - fn usage(&self) -> &str { - "Insert a new column with a given value." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - insert(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Insert a column with a value", - example: "echo [[author, commits]; ['Andrés', 1]] | insert branches 5", - result: Some(vec![UntaggedValue::row(indexmap! { - "author".to_string() => Value::from("Andrés"), - "commits".to_string() => UntaggedValue::int(1).into(), - "branches".to_string() => UntaggedValue::int(5).into(), - }) - .into()]), - },Example { - description: "Use in block form for more involved insertion logic", - example: "echo [[author, lucky_number]; ['Yehuda', 4]] | insert success { $it.lucky_number * 10 }", - result: Some(vec![UntaggedValue::row(indexmap! { - "author".to_string() => Value::from("Yehuda"), - "lucky_number".to_string() => UntaggedValue::int(4).into(), - "success".to_string() => UntaggedValue::int(40).into(), - }) - .into()]), - }] - } -} - -fn process_row( - context: Arc, - input: Value, - mut value: Arc, - field: Arc, -) -> Result { - let value = Arc::make_mut(&mut value); - - Ok(match value { - Value { - value: UntaggedValue::Block(block), - tag: block_tag, - } => { - let for_block = input.clone(); - let input_stream = vec![Ok(for_block)].into_iter().into_input_stream(); - - context.scope.enter_scope(); - context.scope.add_vars(&block.captured.entries); - if let Some((arg, _)) = block.block.params.positional.first() { - context.scope.add_var(arg.name(), input.clone()); - } - - let result = run_block( - &block.block, - &context, - input_stream, - ExternalRedirection::Stdout, - ); - - context.scope.exit_scope(); - - match result { - Ok(mut stream) => { - let values = stream.drain_vec(); - - let errors = context.get_errors(); - if let Some(error) = errors.first() { - return Err(error.clone()); - } - - let result = if values.len() == 1 { - let value = values - .get(0) - .ok_or_else(|| ShellError::unexpected("No value to insert with."))?; - - Value { - value: value.value.clone(), - tag: input.tag.clone(), - } - } else if values.is_empty() { - UntaggedValue::nothing().into_value(&input.tag) - } else { - UntaggedValue::table(&values).into_value(&input.tag) - }; - - match input { - obj @ Value { - value: UntaggedValue::Row(_), - .. - } => match obj.insert_data_at_column_path(&field, result) { - Ok(v) => ActionStream::one(ReturnSuccess::value(v)), - Err(e) => ActionStream::one(Err(e)), - }, - _ => ActionStream::one(Err(ShellError::labeled_error( - "Unrecognized type in stream", - "original value", - block_tag.clone(), - ))), - } - } - Err(e) => ActionStream::one(Err(e)), - } - } - value => match input { - Value { - value: UntaggedValue::Primitive(Primitive::Nothing), - .. - } => match context - .scope - .get_var("$it") - .unwrap_or_else(|| UntaggedValue::nothing().into_untagged_value()) - .insert_data_at_column_path(&field, value.clone()) - { - Ok(v) => ActionStream::one(ReturnSuccess::value(v)), - Err(e) => ActionStream::one(Err(e)), - }, - _ => match input.insert_data_at_column_path(&field, value.clone()) { - Ok(v) => ActionStream::one(ReturnSuccess::value(v)), - Err(e) => ActionStream::one(Err(e)), - }, - }, - }) -} - -fn insert(args: CommandArgs) -> Result { - let context = Arc::new(args.context.clone()); - let column: ColumnPath = args.req(0)?; - let value: Value = args.req(1)?; - let input = args.input; - - let value = Arc::new(value); - let column = Arc::new(column); - - Ok(input - .flat_map(move |input| { - let context = context.clone(); - let value = value.clone(); - let column = column.clone(); - - match process_row(context, input, value, column) { - Ok(s) => s, - Err(e) => ActionStream::one(Err(e)), - } - }) - .into_action_stream()) -} diff --git a/crates/nu-command/src/commands/filters/keep/command.rs b/crates/nu-command/src/commands/filters/keep/command.rs deleted file mode 100644 index 9d0a9bc69f..0000000000 --- a/crates/nu-command/src/commands/filters/keep/command.rs +++ /dev/null @@ -1,74 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue}; -use nu_source::Tagged; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "keep" - } - - fn signature(&self) -> Signature { - Signature::build("keep").optional( - "rows", - SyntaxShape::Int, - "Starting from the front, the number of rows to keep", - ) - } - - fn usage(&self) -> &str { - "Keep the number of rows only." - } - - fn run(&self, args: CommandArgs) -> Result { - keep(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Keep the first row", - example: "echo [1 2 3] | keep", - result: Some(vec![UntaggedValue::int(1).into()]), - }, - Example { - description: "Keep the first four rows", - example: "echo [1 2 3 4 5] | keep 4", - result: Some(vec![ - UntaggedValue::int(1).into(), - UntaggedValue::int(2).into(), - UntaggedValue::int(3).into(), - UntaggedValue::int(4).into(), - ]), - }, - ] - } -} - -fn keep(args: CommandArgs) -> Result { - let rows: Option> = args.opt(0)?; - - let rows_desired = if let Some(quantity) = rows { - *quantity - } else { - 1 - }; - - Ok(args.input.take(rows_desired).into_output_stream()) -} - -#[cfg(test)] -mod tests { - use super::Command; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Command {}) - } -} diff --git a/crates/nu-command/src/commands/filters/keep/mod.rs b/crates/nu-command/src/commands/filters/keep/mod.rs deleted file mode 100644 index 56051b6dec..0000000000 --- a/crates/nu-command/src/commands/filters/keep/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod command; -mod until; -mod while_; - -pub use command::Command as Keep; -pub use until::SubCommand as KeepUntil; -pub use while_::SubCommand as KeepWhile; diff --git a/crates/nu-command/src/commands/filters/keep/until.rs b/crates/nu-command/src/commands/filters/keep/until.rs deleted file mode 100644 index e5e9d3aa99..0000000000 --- a/crates/nu-command/src/commands/filters/keep/until.rs +++ /dev/null @@ -1,97 +0,0 @@ -use crate::prelude::*; -use nu_engine::evaluate_baseline_expr; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_parser::ParserScope; -use nu_protocol::{ - hir::{CapturedBlock, ClassifiedCommand}, - Signature, SyntaxShape, -}; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "keep until" - } - - fn signature(&self) -> Signature { - Signature::build("keep until") - .required( - "condition", - SyntaxShape::RowCondition, - "The condition that must be met to stop keeping rows", - ) - .filter() - } - - fn usage(&self) -> &str { - "Keeps rows until the condition matches." - } - - fn run(&self, args: CommandArgs) -> Result { - let ctx = Arc::new(args.context.clone()); - let tag = args.call_info.name_tag.clone(); - - let block: CapturedBlock = args.req(0)?; - let condition = { - if block.block.block.len() != 1 { - return Err(ShellError::labeled_error( - "Expected a condition", - "expected a condition", - tag, - )); - } - match block.block.block[0].pipelines.get(0) { - Some(item) => match item.list.get(0) { - Some(ClassifiedCommand::Expr(expr)) => expr.clone(), - _ => { - return Err(ShellError::labeled_error( - "Expected a condition", - "expected a condition", - tag, - )); - } - }, - None => { - return Err(ShellError::labeled_error( - "Expected a condition", - "expected a condition", - tag, - )); - } - } - }; - - Ok(args - .input - .take_while(move |item| { - let condition = condition.clone(); - let ctx = ctx.clone(); - ctx.scope.enter_scope(); - ctx.scope.add_vars(&block.captured.entries); - if let Some((arg, _)) = block.block.params.positional.first() { - ctx.scope.add_var(arg.name(), item.clone()); - } - - let result = evaluate_baseline_expr(&condition, &ctx); - ctx.scope.exit_scope(); - - !matches!(result, Ok(ref v) if v.is_true()) - }) - .into_output_stream()) - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/filters/keep/while_.rs b/crates/nu-command/src/commands/filters/keep/while_.rs deleted file mode 100644 index ad4f98def5..0000000000 --- a/crates/nu-command/src/commands/filters/keep/while_.rs +++ /dev/null @@ -1,100 +0,0 @@ -use crate::prelude::*; -use log::trace; -use nu_engine::evaluate_baseline_expr; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - hir::{CapturedBlock, ClassifiedCommand}, - Signature, SyntaxShape, -}; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "keep while" - } - - fn signature(&self) -> Signature { - Signature::build("keep while") - .required( - "condition", - SyntaxShape::RowCondition, - "The condition that must be met to keep rows", - ) - .filter() - } - - fn usage(&self) -> &str { - "Keeps rows while the condition matches." - } - - fn run(&self, args: CommandArgs) -> Result { - let ctx = Arc::new(args.context.clone()); - let tag = args.call_info.name_tag.clone(); - - let block: CapturedBlock = args.req(0)?; - let condition = { - if block.block.block.len() != 1 { - return Err(ShellError::labeled_error( - "Expected a condition", - "expected a condition", - tag, - )); - } - match block.block.block[0].pipelines.get(0) { - Some(item) => match item.list.get(0) { - Some(ClassifiedCommand::Expr(expr)) => expr.clone(), - _ => { - return Err(ShellError::labeled_error( - "Expected a condition", - "expected a condition", - tag, - )); - } - }, - None => { - return Err(ShellError::labeled_error( - "Expected a condition", - "expected a condition", - tag, - )); - } - } - }; - - Ok(args - .input - .take_while(move |item| { - let condition = condition.clone(); - let ctx = ctx.clone(); - - ctx.scope.enter_scope(); - ctx.scope.add_vars(&block.captured.entries); - if let Some((arg, _)) = block.block.params.positional.first() { - ctx.scope.add_var(arg.name(), item.clone()); - } - trace!("ITEM = {:?}", item); - - let result = evaluate_baseline_expr(&condition, &ctx); - ctx.scope.exit_scope(); - trace!("RESULT = {:?}", result); - - matches!(result, Ok(ref v) if v.is_true()) - }) - .into_output_stream()) - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/filters/last.rs b/crates/nu-command/src/commands/filters/last.rs deleted file mode 100644 index b7a035b34c..0000000000 --- a/crates/nu-command/src/commands/filters/last.rs +++ /dev/null @@ -1,81 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue}; - -pub struct Last; - -impl WholeStreamCommand for Last { - fn name(&self) -> &str { - "last" - } - - fn signature(&self) -> Signature { - Signature::build("last").optional( - "rows", - SyntaxShape::Number, - "starting from the back, the number of rows to return", - ) - } - - fn usage(&self) -> &str { - "Show only the last number of rows." - } - - fn run(&self, args: CommandArgs) -> Result { - last(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Get the last row", - example: "echo [1 2 3] | last", - result: Some(vec![UntaggedValue::int(3).into()]), - }, - Example { - description: "Get the last three rows", - example: "echo [1 2 3 4 5] | last 3", - result: Some(vec![ - UntaggedValue::int(3).into(), - UntaggedValue::int(4).into(), - UntaggedValue::int(5).into(), - ]), - }, - ] - } -} - -fn last(args: CommandArgs) -> Result { - let rows: Option = args.opt(0)?; - let v: Vec<_> = args.input.into_vec(); - - let end_rows_desired = if let Some(quantity) = rows { - quantity - } else { - 1 - }; - - let beginning_rows_to_skip = if end_rows_desired < v.len() { - v.len() - end_rows_desired - } else { - 0 - }; - - let iter = v.into_iter().skip(beginning_rows_to_skip); - - Ok(OutputStream::from_stream(iter)) -} - -#[cfg(test)] -mod tests { - use super::Last; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Last {}) - } -} diff --git a/crates/nu-command/src/commands/filters/length.rs b/crates/nu-command/src/commands/filters/length.rs deleted file mode 100644 index a2abee100d..0000000000 --- a/crates/nu-command/src/commands/filters/length.rs +++ /dev/null @@ -1,108 +0,0 @@ -use crate::prelude::*; - -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, UntaggedValue, Value}; - -pub struct Length; - -impl WholeStreamCommand for Length { - fn name(&self) -> &str { - "length" - } - - fn signature(&self) -> Signature { - Signature::build("length").switch( - "column", - "Calculate number of columns in table", - Some('c'), - ) - } - - fn usage(&self) -> &str { - "Show the total number of rows or items." - } - - fn run(&self, args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let column = args.has_flag("column"); - let input = args.input; - - Ok(CountIterator { - column, - input, - done: false, - tag, - } - .into_output_stream()) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Count the number of entries in a list", - example: "echo [1 2 3 4 5] | length", - result: Some(vec![UntaggedValue::int(5).into()]), - }, - Example { - description: "Count the number of columns in the calendar table", - example: "cal | length -c", - result: None, - }, - ] - } -} - -struct CountIterator { - column: bool, - input: InputStream, - done: bool, - tag: Tag, -} - -impl Iterator for CountIterator { - type Item = Value; - - fn next(&mut self) -> Option { - if self.done { - return None; - } - - self.done = true; - - let length = if self.column { - if let Some(first) = self.input.next() { - match &first.value { - UntaggedValue::Row(dictionary) => dictionary.length(), - _ => { - return Some(Value::error(ShellError::labeled_error( - "Cannot obtain column length", - "cannot obtain column length", - self.tag.clone(), - ))); - } - } - } else { - 0 - } - } else { - let input = &mut self.input; - input.count() - }; - - Some(UntaggedValue::int(length as i64).into_value(self.tag.clone())) - } -} - -#[cfg(test)] -mod tests { - use super::Length; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Length {}) - } -} diff --git a/crates/nu-command/src/commands/filters/merge.rs b/crates/nu-command/src/commands/filters/merge.rs deleted file mode 100644 index 7bc087fc20..0000000000 --- a/crates/nu-command/src/commands/filters/merge.rs +++ /dev/null @@ -1,109 +0,0 @@ -use crate::prelude::*; -use nu_data::value::merge_values; -use nu_engine::run_block; -use nu_engine::WholeStreamCommand; - -use indexmap::IndexMap; -use nu_errors::ShellError; -use nu_protocol::{ - hir::CapturedBlock, hir::ExternalRedirection, ReturnSuccess, Signature, SyntaxShape, - UntaggedValue, Value, -}; -pub struct Merge; - -impl WholeStreamCommand for Merge { - fn name(&self) -> &str { - "merge" - } - - fn signature(&self) -> Signature { - Signature::build("merge").required( - "block", - SyntaxShape::Block, - "the block to run and merge into the table", - ) - } - - fn usage(&self) -> &str { - "Merge a table." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - merge(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Merge a 1-based index column with some ls output", - example: "ls | select name | keep 3 | merge { echo [1 2 3] | wrap index }", - result: None, - }] - } -} - -fn merge(args: CommandArgs) -> Result { - let context = &args.context; - let name_tag = args.call_info.name_tag.clone(); - - let block: CapturedBlock = args.req(0)?; - let input = args.input; - - context.scope.enter_scope(); - context.scope.add_vars(&block.captured.entries); - let result = run_block( - &block.block, - context, - InputStream::empty(), - ExternalRedirection::Stdout, - ); - context.scope.exit_scope(); - - let table: Option> = match result { - Ok(mut stream) => Some(stream.drain_vec()), - Err(err) => { - return Err(err); - } - }; - - let table = table.unwrap_or_else(|| { - vec![Value { - value: UntaggedValue::row(IndexMap::default()), - tag: name_tag, - }] - }); - - Ok(input - .enumerate() - .map(move |(idx, value)| { - let other = table.get(idx); - - match other { - Some(replacement) => match merge_values(&value.value, &replacement.value) { - Ok(merged_value) => ReturnSuccess::value(merged_value.into_value(&value.tag)), - Err(_) => { - let message = format!("The row at {:?} types mismatch", idx); - Err(ShellError::labeled_error( - "Could not merge", - &message, - &value.tag, - )) - } - }, - None => ReturnSuccess::value(value), - } - }) - .into_action_stream()) -} - -#[cfg(test)] -mod tests { - use super::Merge; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Merge {}) - } -} diff --git a/crates/nu-command/src/commands/filters/mod.rs b/crates/nu-command/src/commands/filters/mod.rs deleted file mode 100644 index e44c5f7c7d..0000000000 --- a/crates/nu-command/src/commands/filters/mod.rs +++ /dev/null @@ -1,85 +0,0 @@ -mod all; -mod any; -mod append; -mod collect; -mod compact; -mod default; -mod drop; -mod each; -mod empty; -mod every; -mod first; -mod flatten; -pub(crate) mod get; -mod group_by; -mod group_by_date; -mod headers; -mod insert; -mod keep; -mod last; -mod length; -mod merge; -mod move_; -mod nth; -mod pivot; -mod prepend; -mod range; -mod reduce; -mod reject; -mod rename; -mod reverse; -mod roll; -mod rotate; -mod select; -mod shuffle; -mod skip; -pub(crate) mod sort_by; -mod uniq; -mod update; -mod update_cells; -mod where_; -mod wrap; -mod zip_; - -pub use all::Command as All; -pub use any::Command as Any; -pub use append::Command as Append; -pub use collect::Command as Collect; -pub use compact::Compact; -pub use default::Default; -pub use drop::*; -pub use each::*; -pub use empty::Command as Empty; -pub use every::Every; -pub use first::First; -pub use flatten::Command as Flatten; -pub use get::Command as Get; -pub use group_by::Command as GroupBy; -pub use group_by_date::GroupByDate; -pub use headers::Headers; -pub use insert::Command as Insert; -pub use keep::*; -pub use last::Last; -pub use length::Length; -pub use merge::Merge; -pub use move_::Command as MoveColumn; -pub use nth::Nth; -pub use pivot::Pivot; -pub use prepend::Prepend; -pub use range::Range; -pub use reduce::Reduce; -pub use reject::Reject; -pub use rename::Rename; -pub use reverse::Reverse; -pub use roll::{Roll, RollColumn, RollUp}; -pub use rotate::{Rotate, RotateCounterClockwise}; -pub use select::Command as Select; -pub use shuffle::Shuffle; -pub use skip::{Skip, SkipUntil, SkipWhile}; -pub use sort_by::SortBy; -pub use uniq::Uniq; -pub use update::Command as Update; -pub use update_cells::SubCommand as UpdateCells; -pub use where_::Command as Where; -pub use wrap::Wrap; -pub use zip_::Command as Zip; diff --git a/crates/nu-command/src/commands/filters/move_.rs b/crates/nu-command/src/commands/filters/move_.rs deleted file mode 100644 index a7daf5423a..0000000000 --- a/crates/nu-command/src/commands/filters/move_.rs +++ /dev/null @@ -1,315 +0,0 @@ -use crate::prelude::*; -use nu_data::base::select_fields; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ColumnPath, Signature, SyntaxShape, Value}; -use nu_source::HasFallibleSpan; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "move" - } - - fn signature(&self) -> Signature { - Signature::build("move") - .rest("rest", SyntaxShape::ColumnPath, "the columns to move") - .named( - "after", - SyntaxShape::ColumnPath, - "the column that will precede the columns moved", - None, - ) - .named( - "before", - SyntaxShape::ColumnPath, - "the column that will be next the columns moved", - None, - ) - } - - fn usage(&self) -> &str { - "Move columns." - } - - fn run(&self, args: CommandArgs) -> Result { - operate(args) - } - - fn examples(&self) -> Vec { - use nu_test_support::value::*; - - vec![ - Example { - description: "Move the column \"type\" before the column \"name\"", - example: r#"ls | move type --before name | first"#, - result: Some(vec![row! { - "type".into() => string("File"), - "name".into() => string("Andres.txt"), - "chickens".into() => int(10), - "modified".into() => date("2019-07-23") - }]), - }, - Example { - description: "or move the column \"chickens\" after \"name\"", - example: r#"ls | move chickens --after name | first"#, - result: Some(vec![row! { - "name".into() => string("Andres.txt"), - "chickens".into() => int(10), - "type".into() => string("File"), - "modified".into() => date("2019-07-23") - }]), - }, - Example { - description: "you can selectively move many columns in either direction", - example: r#"ls | move name chickens --after type | first"#, - result: Some(vec![row! { - "type".into() => string("File"), - "name".into() => string("Andres.txt"), - "chickens".into() => int(10), - "modified".into() => date("2019-07-23") - }]), - }, - ] - } -} - -fn operate(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - - let mut columns: Vec = args.rest(0)?; - let before: Option = args.get_flag("before")?; - let after: Option = args.get_flag("after")?; - - if columns.is_empty() { - return Err(ShellError::labeled_error( - "expected columns", - "expected columns", - name, - )); - } - - if columns.iter().any(|c| c.members().len() > 1) { - return Err(ShellError::labeled_error( - "expected columns", - "expected columns", - name, - )); - } - - if vec![&after, &before] - .iter() - .map(|o| if o.is_some() { 1 } else { 0 }) - .sum::() - > 1 - { - return Err(ShellError::labeled_error( - "can't move column(s)", - "pick exactly one (before, after)", - name, - )); - } - - if let Some(after) = after { - let member = columns.remove(0); - - Ok(args - .input - .map(move |item| { - let member = vec![member.clone()]; - let column_paths = vec![&member, &columns] - .into_iter() - .flatten() - .collect::>(); - - let after_span = after.maybe_span().unwrap_or_else(Span::unknown); - - if after.members().len() == 1 { - let keys = column_paths - .iter() - .filter_map(|c| c.last()) - .map(|c| c.as_string()) - .collect::>(); - - if let Some(column) = after.last() { - if !keys.contains(&column.as_string()) { - move_after(&item, &keys, &after) - } else { - let msg = - format!("can't move column {} after itself", column.as_string()); - Err(ShellError::labeled_error( - "can't move column", - msg, - after_span, - )) - } - } else { - Err(ShellError::labeled_error( - "expected column", - "expected column", - after_span, - )) - } - } else { - Err(ShellError::labeled_error( - "expected column", - "expected column", - after_span, - )) - } - }) - .into_input_stream()) - } else if let Some(before) = before { - let member = columns.remove(0); - - Ok(args - .input - .map(move |item| { - let member = vec![member.clone()]; - let column_paths = vec![&member, &columns] - .into_iter() - .flatten() - .collect::>(); - - let before_span = before.maybe_span().unwrap_or_else(Span::unknown); - - if before.members().len() == 1 { - let keys = column_paths - .iter() - .filter_map(|c| c.last()) - .map(|c| c.as_string()) - .collect::>(); - - if let Some(column) = before.last() { - if !keys.contains(&column.as_string()) { - move_before(&item, &keys, &before) - } else { - let msg = - format!("can't move column {} before itself", column.as_string()); - Err(ShellError::labeled_error( - "can't move column", - msg, - before_span, - )) - } - } else { - Err(ShellError::labeled_error( - "expected column", - "expected column", - before_span, - )) - } - } else { - Err(ShellError::labeled_error( - "expected column", - "expected column", - before_span, - )) - } - }) - .into_input_stream()) - } else { - Err(ShellError::labeled_error( - "no columns given", - "no columns given", - name, - )) - } -} - -fn move_after(table: &Value, columns: &[String], from: &ColumnPath) -> Result { - let from_fields = from.maybe_span().unwrap_or_else(Span::unknown); - let from = if let Some((last, _)) = from.split_last() { - last.as_string() - } else { - return Err(ShellError::labeled_error( - "unknown column", - "unknown column", - from_fields, - )); - }; - - let columns_moved = table.data_descriptors().into_iter().map(|name| { - if columns.contains(&name) { - None - } else { - Some(name) - } - }); - - let mut reordered_columns = vec![]; - let mut insert = false; - let mut inserted = false; - - for name in columns_moved { - if let Some(name) = name { - reordered_columns.push(Some(name.clone())); - - if !inserted && name == from { - insert = true; - } - } else { - reordered_columns.push(None); - } - - if insert { - for column in columns { - reordered_columns.push(Some(column.clone())); - } - inserted = true; - } - } - - Ok(select_fields( - table, - &reordered_columns.into_iter().flatten().collect::>(), - &table.tag, - )) -} - -fn move_before(table: &Value, columns: &[String], from: &ColumnPath) -> Result { - let from_fields = from.maybe_span().unwrap_or_else(Span::unknown); - let from = if let Some((last, _)) = from.split_last() { - last.as_string() - } else { - return Err(ShellError::labeled_error( - "unknown column", - "unknown column", - from_fields, - )); - }; - - let columns_moved = table.data_descriptors().into_iter().map(|name| { - if columns.contains(&name) { - None - } else { - Some(name) - } - }); - - let mut reordered_columns = vec![]; - let mut inserted = false; - - for name in columns_moved { - if let Some(name) = name { - if !inserted && name == from { - for column in columns { - reordered_columns.push(Some(column.clone())); - } - - inserted = true; - } - - reordered_columns.push(Some(name.clone())); - } else { - reordered_columns.push(None); - } - } - - Ok(select_fields( - table, - &reordered_columns.into_iter().flatten().collect::>(), - &table.tag, - )) -} diff --git a/crates/nu-command/src/commands/filters/nth.rs b/crates/nu-command/src/commands/filters/nth.rs deleted file mode 100644 index 510477dac9..0000000000 --- a/crates/nu-command/src/commands/filters/nth.rs +++ /dev/null @@ -1,114 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, Value}; -use nu_source::Tagged; - -pub struct Nth; - -impl WholeStreamCommand for Nth { - fn name(&self) -> &str { - "nth" - } - - fn signature(&self) -> Signature { - Signature::build("nth") - .required( - "row number", - SyntaxShape::Int, - "the number of the row to return", - ) - .rest("rest", SyntaxShape::Any, "Optionally return more rows") - .switch("skip", "Skip the rows instead of selecting them", Some('s')) - } - - fn usage(&self) -> &str { - "Return or skip only the selected rows." - } - - fn run(&self, args: CommandArgs) -> Result { - nth(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Get the second row", - example: "echo [first second third] | nth 1", - result: Some(vec![Value::from("second")]), - }, - Example { - description: "Get the first and third rows", - example: "echo [first second third] | nth 0 2", - result: Some(vec![Value::from("first"), Value::from("third")]), - }, - Example { - description: "Skip the first and third rows", - example: "echo [first second third] | nth --skip 0 2", - result: Some(vec![Value::from("second")]), - }, - ] - } -} - -fn nth(args: CommandArgs) -> Result { - let row_number: Tagged = args.req(0)?; - let and_rows: Vec> = args.rest(1)?; - let skip = args.has_flag("skip"); - let input = args.input; - - let mut rows: Vec<_> = and_rows.into_iter().map(|x| x.item as usize).collect(); - rows.push(row_number.item as usize); - rows.sort_unstable(); - - Ok(NthIterator { - input, - rows, - skip, - current: 0, - } - .into_output_stream()) -} - -struct NthIterator { - input: InputStream, - rows: Vec, - skip: bool, - current: usize, -} - -impl Iterator for NthIterator { - type Item = Value; - - fn next(&mut self) -> Option { - loop { - if !self.skip { - if let Some(row) = self.rows.get(0) { - if self.current == *row { - self.rows.remove(0); - self.current += 1; - return self.input.next(); - } else { - self.current += 1; - let _ = self.input.next(); - continue; - } - } else { - return None; - } - } else if let Some(row) = self.rows.get(0) { - if self.current == *row { - self.rows.remove(0); - self.current += 1; - let _ = self.input.next(); - continue; - } else { - self.current += 1; - return self.input.next(); - } - } else { - return self.input.next(); - } - } - } -} diff --git a/crates/nu-command/src/commands/filters/pivot.rs b/crates/nu-command/src/commands/filters/pivot.rs deleted file mode 100644 index 2ae659e21b..0000000000 --- a/crates/nu-command/src/commands/filters/pivot.rs +++ /dev/null @@ -1,164 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - merge_descriptors, ReturnSuccess, Signature, SyntaxShape, TaggedDictBuilder, UntaggedValue, -}; -use nu_source::{SpannedItem, Tagged}; -use nu_value_ext::ValueExt; - -pub struct Pivot; - -#[derive(Deserialize)] -pub struct PivotArgs { - rest: Vec>, - #[serde(rename(deserialize = "header-row"))] - header_row: bool, - #[serde(rename(deserialize = "ignore-titles"))] - ignore_titles: bool, -} - -impl WholeStreamCommand for Pivot { - fn name(&self) -> &str { - "pivot" - } - - fn signature(&self) -> Signature { - Signature::build("pivot") - .switch( - "header-row", - "treat the first row as column names", - Some('r'), - ) - .switch( - "ignore-titles", - "don't pivot the column names into values", - Some('i'), - ) - .rest( - "rest", - SyntaxShape::String, - "the names to give columns once pivoted", - ) - } - - fn usage(&self) -> &str { - "Pivots the table contents so rows become columns and columns become rows." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - pivot(args) - } -} - -pub fn pivot(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - //let (args, input): (PivotArgs, _) = args.process()?; - let pivot_args = PivotArgs { - header_row: args.has_flag("header-row"), - ignore_titles: args.has_flag("ignore-titles"), - rest: args.rest(0)?, - }; - let input = args.input.into_vec(); - let args = pivot_args; - - let descs = merge_descriptors(&input); - - let mut headers: Vec = vec![]; - - if !args.rest.is_empty() && args.header_row { - return Err(ShellError::labeled_error( - "Can not provide header names and use header row", - "using header row", - name, - )); - } - - if args.header_row { - for i in input.clone() { - if let Some(desc) = descs.get(0) { - match &i.get_data_by_key(desc[..].spanned_unknown()) { - Some(x) => { - if let Ok(s) = x.as_string() { - headers.push(s.to_string()); - } else { - return Err(ShellError::labeled_error( - "Header row needs string headers", - "used non-string headers", - name, - )); - } - } - _ => { - return Err(ShellError::labeled_error( - "Header row is incomplete and can't be used", - "using incomplete header row", - name, - )); - } - } - } else { - return Err(ShellError::labeled_error( - "Header row is incomplete and can't be used", - "using incomplete header row", - name, - )); - } - } - } else { - for i in 0..=input.len() { - if let Some(name) = args.rest.get(i) { - headers.push(name.to_string()) - } else { - headers.push(format!("Column{}", i)); - } - } - } - - let descs: Vec<_> = if args.header_row { - descs.into_iter().skip(1).collect() - } else { - descs - }; - - Ok((descs.into_iter().map(move |desc| { - let mut column_num: usize = 0; - let mut dict = TaggedDictBuilder::new(&name); - - if !args.ignore_titles && !args.header_row { - dict.insert_untagged( - headers[column_num].clone(), - UntaggedValue::string(desc.clone()), - ); - column_num += 1 - } - - for i in input.clone() { - match &i.get_data_by_key(desc[..].spanned_unknown()) { - Some(x) => { - dict.insert_value(headers[column_num].clone(), x.clone()); - } - _ => { - dict.insert_untagged(headers[column_num].clone(), UntaggedValue::nothing()); - } - } - column_num += 1; - } - - ReturnSuccess::value(dict.into_value()) - })) - .into_action_stream()) -} - -#[cfg(test)] -mod tests { - use super::Pivot; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Pivot {}) - } -} diff --git a/crates/nu-command/src/commands/filters/prepend.rs b/crates/nu-command/src/commands/filters/prepend.rs deleted file mode 100644 index 8dd98a68d6..0000000000 --- a/crates/nu-command/src/commands/filters/prepend.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value}; - -pub struct Prepend; - -impl WholeStreamCommand for Prepend { - fn name(&self) -> &str { - "prepend" - } - - fn signature(&self) -> Signature { - Signature::build("prepend").required( - "row value", - SyntaxShape::Any, - "the value of the row to prepend to the table", - ) - } - - fn usage(&self) -> &str { - "Prepend the given row to the front of the table." - } - - fn run(&self, args: CommandArgs) -> Result { - prepend(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Add something to the beginning of a list or table", - example: "echo [2 3 4] | prepend 1", - result: Some(vec![ - UntaggedValue::int(1).into(), - UntaggedValue::int(2).into(), - UntaggedValue::int(3).into(), - UntaggedValue::int(4).into(), - ]), - }] - } -} - -fn prepend(args: CommandArgs) -> Result { - let row: Value = args.req(0)?; - let input = args.input; - - let bos = vec![row].into_iter(); - - Ok(bos.chain(input).into_output_stream()) -} - -#[cfg(test)] -mod tests { - use super::Prepend; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Prepend {}) - } -} diff --git a/crates/nu-command/src/commands/filters/range.rs b/crates/nu-command/src/commands/filters/range.rs deleted file mode 100644 index 93feadb7c8..0000000000 --- a/crates/nu-command/src/commands/filters/range.rs +++ /dev/null @@ -1,114 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value}; - -struct RangeArgs { - range: nu_protocol::Range, -} - -pub struct Range; - -impl WholeStreamCommand for Range { - fn name(&self) -> &str { - "range" - } - - fn signature(&self) -> Signature { - Signature::build("range").required( - "rows", - SyntaxShape::Range, - "range of rows to return: Eg) 4..7 (=> from 4 to 7)", - ) - } - - fn usage(&self) -> &str { - "Return only the selected rows." - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Return rows 1 through 3", - example: "echo [1 2 3 4 5] | range 1..3", - result: Some(vec![ - UntaggedValue::int(2).into(), - UntaggedValue::int(3).into(), - UntaggedValue::int(4).into(), - ]), - }, - Example { - description: "Return the third row from the end, through to the end", - example: "echo [1 2 3 4 5] | range (-3..)", - result: Some(vec![ - UntaggedValue::int(3).into(), - UntaggedValue::int(4).into(), - UntaggedValue::int(5).into(), - ]), - }, - ] - } - - fn run(&self, args: CommandArgs) -> Result { - range(args) - } -} - -fn range(args: CommandArgs) -> Result { - let cmd_args = RangeArgs { - range: args.req(0)?, - }; - - let from_raw = cmd_args.range.min_i64()?; - let to_raw = cmd_args.range.max_i64()?; - // only collect the input if we have any negative indices - if from_raw < 0 || to_raw < 0 { - let input = args.input.into_vec(); - let input_size = input.len() as i64; - - let from = if from_raw < 0 { - (input_size + from_raw) as usize - } else { - from_raw as usize - }; - - let to = if to_raw < 0 { - (input_size + to_raw) as usize - } else if to_raw > input.len() as i64 { - input.len() - } else { - to_raw as usize - }; - - if from > to { - Ok(OutputStream::one(Value::nothing())) - } else { - Ok(OutputStream::from(input[from..to].to_vec())) - } - } else { - let from = from_raw as usize; - let to = to_raw as usize; - if from > to { - Ok(OutputStream::one(Value::nothing())) - } else { - Ok(args - .input - .skip(from) - .take(to - from + 1) - .into_output_stream()) - } - } -} - -#[cfg(test)] -mod tests { - use super::Range; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Range {}) - } -} diff --git a/crates/nu-command/src/commands/filters/reduce.rs b/crates/nu-command/src/commands/filters/reduce.rs deleted file mode 100644 index bacb0601b4..0000000000 --- a/crates/nu-command/src/commands/filters/reduce.rs +++ /dev/null @@ -1,223 +0,0 @@ -use super::each; -use crate::prelude::*; -use nu_engine::run_block; -use nu_engine::WholeStreamCommand; -use nu_engine::{CommandArgs, Example}; -use nu_errors::ShellError; -use nu_parser::ParserScope; -use nu_protocol::{ - hir::CapturedBlock, hir::ExternalRedirection, Signature, SyntaxShape, UntaggedValue, Value, -}; -use nu_stream::ActionStream; - -pub struct Reduce; - -#[derive(Deserialize)] -pub struct ReduceArgs { - block: CapturedBlock, - fold: Option, - numbered: bool, -} - -impl WholeStreamCommand for Reduce { - fn name(&self) -> &str { - "reduce" - } - - fn signature(&self) -> Signature { - Signature::build("reduce") - .named( - "fold", - SyntaxShape::Any, - "reduce with initial value", - Some('f'), - ) - .required("block", SyntaxShape::Block, "reducing function") - .switch( - "numbered", - "returned a numbered item ($it.index and $it.item)", - Some('n'), - ) - } - - fn usage(&self) -> &str { - "Aggregate a list table to a single value using an accumulator block." - } - - fn extra_usage(&self) -> &str { - "Block must be (A, A) -> A unless --fold is selected, in which case it may be A, B -> A." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - reduce(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Simple summation (equivalent to math sum)", - example: "echo 1 2 3 4 | reduce { $acc + $it }", - result: Some(vec![UntaggedValue::int(10).into()]), - }, - Example { - description: "Summation from starting value using fold", - example: "echo 1 2 3 4 | reduce -f (-1) { $acc + $it }", - result: Some(vec![UntaggedValue::int(9).into()]), - }, - Example { - description: "Folding with rows", - example: " | reduce -f 1.6 { $acc * (echo $it.a | str to-int) + (echo $it.b | str to-int) }", - result: None, - }, - Example { - description: "Numbered reduce to find index of longest word", - example: "echo one longest three bar | reduce -n { if ($it.item | str length) > ($acc.item | str length) {echo $it} {echo $acc}} | get index", - result: None, - }, - ] - } -} - -fn process_row( - block: Arc, - context: &EvaluationContext, - row: Value, -) -> Result { - let row_clone = row.clone(); - let input_stream = vec![Ok(row_clone)].into_iter().into_input_stream(); - - context.scope.enter_scope(); - context.scope.add_vars(&block.captured.entries); - - if let Some((arg, _)) = block.block.params.positional.first() { - context.scope.add_var(arg.name(), row); - } else { - context.scope.add_var("$it", row); - } - - let result = run_block( - &block.block, - context, - input_stream, - ExternalRedirection::Stdout, - ); - context.scope.exit_scope(); - - result -} - -fn reduce(args: CommandArgs) -> Result { - let span = args.call_info.name_tag.span; - let context = Arc::new(args.context.clone()); - let reduce_args = ReduceArgs { - block: args.req(0)?, - fold: args.get_flag("fold")?, - numbered: args.has_flag("numbered"), - }; - let mut input = args.input; - - let block = Arc::new(reduce_args.block); - let (ioffset, start) = if !input.is_empty() { - match reduce_args.fold { - None => { - let first = input.next().expect("non-empty stream"); - - (1, first) - } - Some(acc) => (0, acc), - } - } else { - return Err(ShellError::labeled_error( - "Expected input", - "needs input", - span, - )); - }; - - if reduce_args.numbered { - // process_row returns Result, so we must fold with one - let initial = Ok(InputStream::one(each::make_indexed_item( - ioffset - 1, - start, - ))); - - Ok(input - .enumerate() - .fold(initial, move |acc, input| { - let context = context.clone(); - let block = Arc::clone(&block); - let row = each::make_indexed_item(input.0 + ioffset, input.1); - - let values = acc?.drain_vec(); - - let f = if values.len() == 1 { - let value = values - .get(0) - .ok_or_else(|| ShellError::unexpected("No value to update with"))?; - value.clone() - } else if values.is_empty() { - UntaggedValue::nothing().into_untagged_value() - } else { - UntaggedValue::table(&values).into_untagged_value() - }; - - context.scope.enter_scope(); - context.scope.add_var("$acc", f); - let result = process_row(block, &context, row); - context.scope.exit_scope(); - - // we make sure that result is an indexed item - result.and_then(|mut acc| { - let values = acc.drain_vec(); - let value = values - .get(0) - .ok_or_else(|| ShellError::unexpected("No value to update with"))?; - Ok(InputStream::one(match value.value { - UntaggedValue::Primitive(_) => each::make_indexed_item(0, value.clone()), - _ => value.clone(), - })) - }) - })? - .into_action_stream()) - } else { - let initial = Ok(InputStream::one(start)); - Ok(input - .fold(initial, move |acc, row| { - let block = Arc::clone(&block); - let context = context.clone(); - - let values = acc?.drain_vec(); - - let f = if values.len() == 1 { - let value = values - .get(0) - .ok_or_else(|| ShellError::unexpected("No value to update with"))?; - value.clone() - } else if values.is_empty() { - UntaggedValue::nothing().into_untagged_value() - } else { - UntaggedValue::table(&values).into_untagged_value() - }; - - context.scope.enter_scope(); - context.scope.add_var("$acc", f); - let result = process_row(block, &context, row); - context.scope.exit_scope(); - result - })? - .into_action_stream()) - } -} - -#[cfg(test)] -mod tests { - use super::Reduce; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Reduce {}) - } -} diff --git a/crates/nu-command/src/commands/filters/reject.rs b/crates/nu-command/src/commands/filters/reject.rs deleted file mode 100644 index 0e91aba1d1..0000000000 --- a/crates/nu-command/src/commands/filters/reject.rs +++ /dev/null @@ -1,71 +0,0 @@ -use crate::prelude::*; -use nu_data::base::reject_fields; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ReturnSuccess, Signature, SyntaxShape}; -use nu_source::Tagged; - -pub struct Reject; - -impl WholeStreamCommand for Reject { - fn name(&self) -> &str { - "reject" - } - - fn signature(&self) -> Signature { - Signature::build("reject").rest( - "rest", - SyntaxShape::String, - "the names of columns to remove", - ) - } - - fn usage(&self) -> &str { - "Remove the given columns from the table. If you want to remove rows, try 'drop'." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - reject(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Lists the files in a directory without showing the modified column", - example: "ls | reject modified", - result: None, - }] - } -} - -fn reject(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - let fields: Vec> = args.rest(0)?; - - if fields.is_empty() { - return Err(ShellError::labeled_error( - "Reject requires fields", - "needs parameter", - name, - )); - } - - let fields: Vec<_> = fields.iter().map(|f| f.item.clone()).collect(); - - Ok(args - .input - .map(move |item| ReturnSuccess::value(reject_fields(&item, &fields, &item.tag))) - .into_action_stream()) -} - -#[cfg(test)] -mod tests { - use super::Reject; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Reject {}) - } -} diff --git a/crates/nu-command/src/commands/filters/rename.rs b/crates/nu-command/src/commands/filters/rename.rs deleted file mode 100644 index d6ffbdb323..0000000000 --- a/crates/nu-command/src/commands/filters/rename.rs +++ /dev/null @@ -1,120 +0,0 @@ -use crate::prelude::*; -use indexmap::indexmap; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tagged; - -pub struct Rename; - -impl WholeStreamCommand for Rename { - fn name(&self) -> &str { - "rename" - } - - fn signature(&self) -> Signature { - Signature::build("rename") - .required( - "column_name", - SyntaxShape::String, - "the new name for the first column", - ) - .rest( - "rest", - SyntaxShape::String, - "the new name for additional columns", - ) - } - - fn usage(&self) -> &str { - "Creates a new table with columns renamed." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - rename(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Rename a column", - example: "echo [[a, b]; [1, 2]] | rename my_column", - result: Some(vec![UntaggedValue::row(indexmap! { - "my_column".to_string() => UntaggedValue::int(1).into(), - "b".to_string() => UntaggedValue::int(2).into(), - }) - .into()]), - }, - Example { - description: "Rename many columns", - example: "echo [[a, b, c]; [1, 2, 3]] | rename eggs ham bacon", - result: Some(vec![UntaggedValue::row(indexmap! { - "eggs".to_string() => UntaggedValue::int(1).into(), - "ham".to_string() => UntaggedValue::int(2).into(), - "bacon".to_string() => UntaggedValue::int(3).into(), - }) - .into()]), - }, - ] - } -} - -pub fn rename(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - let column_name: Tagged = args.req(0)?; - let rest: Vec> = args.rest(1)?; - let input = args.input; - - let mut new_column_names = vec![vec![column_name]]; - new_column_names.push(rest); - - let new_column_names = new_column_names.into_iter().flatten().collect::>(); - - Ok(input - .map(move |item| { - if let Value { - value: UntaggedValue::Row(row), - tag, - } = item - { - let mut renamed_row = IndexMap::new(); - - for (idx, (key, value)) in row.entries.iter().enumerate() { - let key = if idx < new_column_names.len() { - &new_column_names[idx].item - } else { - key - }; - - renamed_row.insert(key.clone(), value.clone()); - } - - let out = UntaggedValue::Row(renamed_row.into()).into_value(tag); - - ReturnSuccess::value(out) - } else { - ReturnSuccess::value( - UntaggedValue::Error(ShellError::labeled_error( - "no column names available", - "can't rename", - &name, - )) - .into_untagged_value(), - ) - } - }) - .into_action_stream()) -} - -#[cfg(test)] -mod tests { - use super::Rename; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Rename {}) - } -} diff --git a/crates/nu-command/src/commands/filters/reverse.rs b/crates/nu-command/src/commands/filters/reverse.rs deleted file mode 100644 index dbd7ee939c..0000000000 --- a/crates/nu-command/src/commands/filters/reverse.rs +++ /dev/null @@ -1,59 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ReturnSuccess, Signature, UntaggedValue}; - -pub struct Reverse; - -impl WholeStreamCommand for Reverse { - fn name(&self) -> &str { - "reverse" - } - - fn signature(&self) -> Signature { - Signature::build("reverse") - } - - fn usage(&self) -> &str { - "Reverses the table." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - reverse(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Sort list of numbers in descending file size", - example: "echo [3 1 2 19 0] | reverse", - result: Some(vec![ - UntaggedValue::int(0).into(), - UntaggedValue::int(19).into(), - UntaggedValue::int(2).into(), - UntaggedValue::int(1).into(), - UntaggedValue::int(3).into(), - ]), - }] - } -} - -#[allow(clippy::needless_collect)] -fn reverse(args: CommandArgs) -> Result { - // Clippy warning should be ignored - // This collect is needed to apply rev - let input = args.input.collect::>(); - Ok((input.into_iter().rev().map(ReturnSuccess::value)).into_action_stream()) -} - -#[cfg(test)] -mod tests { - use super::Reverse; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Reverse {}) - } -} diff --git a/crates/nu-command/src/commands/filters/roll/column.rs b/crates/nu-command/src/commands/filters/roll/column.rs deleted file mode 100644 index 6650babd00..0000000000 --- a/crates/nu-command/src/commands/filters/roll/column.rs +++ /dev/null @@ -1,111 +0,0 @@ -use crate::prelude::*; -use nu_data::base::select_fields; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, TaggedDictBuilder, UntaggedValue, Value}; -use nu_source::Tagged; - -use super::support::{rotate, Direction}; - -pub struct SubCommand; - -pub struct Arguments { - by: Option>, - opposite: bool, - cells_only: bool, -} - -impl Arguments { - fn direction(&self) -> Direction { - if self.opposite { - return Direction::Left; - } - - Direction::Right - } - - fn move_headers(&self) -> bool { - !self.cells_only - } -} - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "roll column" - } - - fn signature(&self) -> Signature { - Signature::build("roll column") - .optional("by", SyntaxShape::Int, "the number of times to roll") - .switch("opposite", "roll in the opposite direction", Some('o')) - .switch("cells-only", "only roll the cells", Some('c')) - } - - fn usage(&self) -> &str { - "Rolls the table columns" - } - - fn run(&self, args: CommandArgs) -> Result { - roll(args) - } -} - -pub fn roll(args: CommandArgs) -> Result { - let options = Arguments { - by: args.opt(0)?, - opposite: args.has_flag("opposite"), - cells_only: args.has_flag("cells-only"), - }; - - Ok(args - .input - .flat_map(move |value| { - let tag = value.tag(); - - roll_by(value, &options) - .unwrap_or_else(|| vec![UntaggedValue::nothing().into_value(tag)]) - }) - .into_output_stream()) -} - -fn roll_by(value: Value, options: &Arguments) -> Option> { - let tag = value.tag(); - let direction = options.direction(); - - if value.is_row() { - let columns = value.data_descriptors(); - if options.move_headers() { - if let Some(fields) = rotate(columns, &options.by, direction) { - return Some(vec![select_fields(&value, &fields, &tag)]); - } - } else { - let values_rotated = rotate( - value - .row_entries() - .map(|(_, value)| value.clone()) - .collect::>(), - &options.by, - direction, - ); - - if let Some(ref values) = values_rotated { - let mut out = TaggedDictBuilder::new(&tag); - - for (k, v) in columns.iter().zip(values) { - out.insert_value(k, v.clone()); - } - - return Some(vec![out.into_value()]); - } - } - None - } else if value.is_table() { - rotate( - value.table_entries().cloned().collect(), - &options.by, - direction, - ) - } else { - Some(vec![value]) - } -} diff --git a/crates/nu-command/src/commands/filters/roll/command.rs b/crates/nu-command/src/commands/filters/roll/command.rs deleted file mode 100644 index 7cda6350b9..0000000000 --- a/crates/nu-command/src/commands/filters/roll/command.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tagged; - -use super::support::{rotate, Direction}; - -pub struct Command; - -pub struct Arguments { - by: Option>, -} - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "roll" - } - - fn signature(&self) -> Signature { - Signature::build("roll").optional("by", SyntaxShape::Int, "the number of times to roll") - } - - fn usage(&self) -> &str { - "Rolls the table rows." - } - - fn run(&self, args: CommandArgs) -> Result { - roll(args) - } -} - -pub fn roll(mut args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - - let options = Arguments { by: args.opt(0)? }; - - let values = args.input.drain_vec(); - - Ok(roll_down(values, &options) - .unwrap_or_else(|| vec![UntaggedValue::nothing().into_value(&name)]) - .into_iter() - .into_output_stream()) -} - -fn roll_down(values: Vec, Arguments { by: ref n }: &Arguments) -> Option> { - rotate(values, n, Direction::Down) -} diff --git a/crates/nu-command/src/commands/filters/roll/mod.rs b/crates/nu-command/src/commands/filters/roll/mod.rs deleted file mode 100644 index a74a70df8b..0000000000 --- a/crates/nu-command/src/commands/filters/roll/mod.rs +++ /dev/null @@ -1,42 +0,0 @@ -mod column; -mod command; -mod up; - -pub use column::SubCommand as RollColumn; -pub use command::Command as Roll; -pub use up::SubCommand as RollUp; - -mod support { - - pub enum Direction { - Left, - Right, - Down, - Up, - } - - pub fn rotate( - mut collection: Vec, - n: &Option>, - direction: Direction, - ) -> Option> { - if collection.is_empty() { - return None; - } - - let values = collection.as_mut_slice(); - - let rotations = if let Some(n) = n { - n.item as usize % values.len() - } else { - 1 - }; - - match direction { - Direction::Up | Direction::Right => values.rotate_left(rotations), - Direction::Down | Direction::Left => values.rotate_right(rotations), - } - - Some(values.to_vec()) - } -} diff --git a/crates/nu-command/src/commands/filters/roll/up.rs b/crates/nu-command/src/commands/filters/roll/up.rs deleted file mode 100644 index 47d26c05b0..0000000000 --- a/crates/nu-command/src/commands/filters/roll/up.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tagged; - -use super::support::{rotate, Direction}; - -pub struct SubCommand; - -#[derive(Deserialize)] -pub struct Arguments { - by: Option>, -} - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "roll up" - } - - fn signature(&self) -> Signature { - Signature::build("roll up").optional("by", SyntaxShape::Int, "the number of times to roll") - } - - fn usage(&self) -> &str { - "Rolls the table rows" - } - - fn run(&self, args: CommandArgs) -> Result { - roll(args) - } -} - -pub fn roll(mut args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - - let options = Arguments { by: args.opt(0)? }; - - let values = args.input.drain_vec(); - - Ok(roll_up(values, &options) - .unwrap_or_else(|| vec![UntaggedValue::nothing().into_value(&name)]) - .into_iter() - .into_output_stream()) -} - -fn roll_up(values: Vec, Arguments { by: ref n }: &Arguments) -> Option> { - rotate(values, n, Direction::Up) -} diff --git a/crates/nu-command/src/commands/filters/rotate/command.rs b/crates/nu-command/src/commands/filters/rotate/command.rs deleted file mode 100644 index 2d2516c365..0000000000 --- a/crates/nu-command/src/commands/filters/rotate/command.rs +++ /dev/null @@ -1,113 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - merge_descriptors, ColumnPath, ReturnSuccess, Signature, SyntaxShape, TaggedDictBuilder, - UntaggedValue, -}; -use nu_source::{SpannedItem, Tagged}; -use nu_value_ext::ValueExt; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "rotate" - } - - fn signature(&self) -> Signature { - Signature::build("rotate").rest( - "rest", - SyntaxShape::String, - "the names to give columns once rotated", - ) - } - - fn usage(&self) -> &str { - "Rotates the table by 90 degrees clockwise." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - rotate(args) - } -} - -pub fn rotate(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - let rest: Vec> = args.rest(0)?; - let input = args.input.into_vec(); - - let total_rows = input.len(); - let descs = merge_descriptors(&input); - let total_descriptors = descs.len(); - - let descs = descs.into_iter().rev().collect::>(); - - if total_rows == 0 { - return Ok(ActionStream::empty()); - } - - let mut headers: Vec = vec![]; - - for i in 0..=total_rows { - headers.push(format!("Column{}", i)); - } - - let first = input[0].clone(); - - let name = if first.tag.anchor().is_some() { - first.tag - } else { - name - }; - - let values = - UntaggedValue::table(&input.into_iter().rev().collect::>()).into_value(&name); - - let values = nu_data::utils::group( - &values, - &Some(Box::new(move |row_number: usize, _| { - Ok(match headers.get(row_number) { - Some(name) => name.clone(), - None => String::new(), - }) - })), - &name, - )?; - - Ok(((0..total_descriptors) - .map(move |row_number| { - let mut row = TaggedDictBuilder::new(&name); - - for (current_numbered_column, (column_name, _)) in values.row_entries().enumerate() { - let raw_column_path = - format!("{}.0.{}", column_name, descs[row_number]).spanned_unknown(); - let path = ColumnPath::build(&raw_column_path); - - match &values.get_data_by_column_path(&path, Box::new(move |_, _, error| error)) { - Ok(x) => { - row.insert_value( - rest.get(current_numbered_column) - .map(|c| c.item.clone()) - .unwrap_or_else(|| column_name.to_string()), - x.clone(), - ); - } - Err(_) => {} - } - } - - row.insert_value( - rest.get(total_rows) - .map(|c| c.item.clone()) - .unwrap_or_else(|| format!("Column{}", total_rows)), - UntaggedValue::string(&descs[row_number]).into_untagged_value(), - ); - - ReturnSuccess::value(row.into_value()) - }) - .rev() - .collect::>()) - .into_iter() - .into_action_stream()) -} diff --git a/crates/nu-command/src/commands/filters/rotate/counter_clockwise.rs b/crates/nu-command/src/commands/filters/rotate/counter_clockwise.rs deleted file mode 100644 index dc95cb3988..0000000000 --- a/crates/nu-command/src/commands/filters/rotate/counter_clockwise.rs +++ /dev/null @@ -1,109 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - merge_descriptors, ColumnPath, ReturnSuccess, Signature, SyntaxShape, TaggedDictBuilder, - UntaggedValue, -}; -use nu_source::{SpannedItem, Tagged}; -use nu_value_ext::ValueExt; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "rotate counter-clockwise" - } - - fn signature(&self) -> Signature { - Signature::build("rotate counter-clockwise").rest( - "rest", - SyntaxShape::String, - "the names to give columns once rotated", - ) - } - - fn usage(&self) -> &str { - "Rotates the table by 90 degrees counter clockwise." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - rotate(args) - } -} - -pub fn rotate(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - let rest: Vec> = args.rest(0)?; - - let input = args.input.into_vec(); - let descs = merge_descriptors(&input); - let total_rows = input.len(); - - if total_rows == 0 { - return Ok(ActionStream::empty()); - } - - let mut headers: Vec = vec![]; - for i in 0..=total_rows { - headers.push(format!("Column{}", i + 1)); - } - - let first = input[0].clone(); - - let name = if first.tag.anchor().is_some() { - first.tag - } else { - name - }; - - let values = UntaggedValue::table(&input).into_value(&name); - - let values = nu_data::utils::group( - &values, - &Some(Box::new(move |row_number: usize, _| { - Ok(match headers.get(row_number) { - Some(name) => name.clone(), - None => String::new(), - }) - })), - &name, - )?; - - Ok(((0..descs.len()) - .rev() - .map(move |row_number| { - let mut row = TaggedDictBuilder::new(&name); - - row.insert_value( - rest.get(0) - .map(|c| c.item.clone()) - .unwrap_or_else(|| String::from("Column0")), - UntaggedValue::string(descs.get(row_number).unwrap_or(&String::new())) - .into_untagged_value(), - ); - - for (current_numbered_column, (column_name, _)) in values.row_entries().enumerate() { - let raw_column_path = - format!("{}.0.{}", column_name, &descs[row_number]).spanned_unknown(); - let path = ColumnPath::build(&raw_column_path); - - match &values.get_data_by_column_path(&path, Box::new(move |_, _, error| error)) { - Ok(x) => { - row.insert_value( - rest.get(current_numbered_column + 1) - .map(|c| c.item.clone()) - .unwrap_or_else(|| column_name.to_string()), - x.clone(), - ); - } - Err(_) => {} - } - } - - ReturnSuccess::value(row.into_value()) - }) - .collect::>()) - .into_iter() - .into_action_stream()) -} diff --git a/crates/nu-command/src/commands/filters/rotate/mod.rs b/crates/nu-command/src/commands/filters/rotate/mod.rs deleted file mode 100644 index 16b4d8c06e..0000000000 --- a/crates/nu-command/src/commands/filters/rotate/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod command; -mod counter_clockwise; - -pub use command::Command as Rotate; -pub use counter_clockwise::SubCommand as RotateCounterClockwise; diff --git a/crates/nu-command/src/commands/filters/select.rs b/crates/nu-command/src/commands/filters/select.rs deleted file mode 100644 index 41e1ff1261..0000000000 --- a/crates/nu-command/src/commands/filters/select.rs +++ /dev/null @@ -1,275 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - ColumnPath, PathMember, Primitive, Signature, SyntaxShape, TaggedDictBuilder, - UnspannedPathMember, UntaggedValue, Value, -}; -use nu_value_ext::{as_string, get_data_by_column_path}; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "select" - } - - fn signature(&self) -> Signature { - Signature::build("select") - .named( - "columns", - SyntaxShape::Table, - "Optionally operate by column path", - Some('c'), - ) - .rest( - "rest", - SyntaxShape::ColumnPath, - "the columns to select from the table", - ) - } - - fn usage(&self) -> &str { - "Down-select table to only these columns." - } - - fn run(&self, args: CommandArgs) -> Result { - let mut columns = args.rest(0)?; - columns.extend(column_paths_from_args(&args)?); - let input = args.input; - let name = args.call_info.name_tag; - select(name, columns, input) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Select just the name column", - example: "ls | select name", - result: None, - }, - Example { - description: "Select the name and size columns", - example: "ls | select name size", - result: None, - }, - Example { - description: "Select columns dynamically", - example: "[[a b]; [1 2]] | select -c [a]", - result: Some(vec![UntaggedValue::row(indexmap! { - "a".to_string() => UntaggedValue::int(1).into(), - }) - .into()]), - }, - ] - } -} - -fn column_paths_from_args(args: &CommandArgs) -> Result, ShellError> { - let column_paths: Option> = args.get_flag("columns")?; - let has_columns = column_paths.is_some(); - let column_paths = match column_paths { - Some(cols) => { - let mut c = Vec::new(); - for col in cols { - let colpath = ColumnPath::build(&col.convert_to_string().spanned_unknown()); - if !colpath.is_empty() { - c.push(colpath) - } - } - c - } - None => Vec::new(), - }; - - if has_columns && column_paths.is_empty() { - let colval: Option = args.get_flag("columns")?; - let colspan = match colval { - Some(v) => v.tag.span, - None => Span::unknown(), - }; - return Err(ShellError::labeled_error( - "Requires a list of columns", - "must be a list of columns", - colspan, - )); - } - - Ok(column_paths) -} - -fn select( - name: Tag, - columns: Vec, - input: InputStream, -) -> Result { - if columns.is_empty() { - return Err(ShellError::labeled_error( - "Select requires columns to select", - "needs parameter", - name, - )); - } - - let mut bring_back: indexmap::IndexMap> = indexmap::IndexMap::new(); - - for value in input { - for path in &columns { - let fetcher = get_data_by_column_path( - &value, - path, - move |obj_source, path_member_tried, error| { - if let PathMember { - unspanned: UnspannedPathMember::String(column), - .. - } = path_member_tried - { - return ShellError::labeled_error_with_secondary( - "No data to fetch.", - format!("Couldn't select column \"{}\"", column), - path_member_tried.span, - "How about exploring it with \"get\"? Check the input is appropriate originating from here", - obj_source.tag.span); - } - - error - }, - ); - - let field = path.clone(); - let key = as_string( - &UntaggedValue::Primitive(Primitive::ColumnPath(field.clone())) - .into_untagged_value(), - )?; - - match fetcher { - Ok(results) => match results.value { - UntaggedValue::Table(records) => { - for x in records { - let mut out = TaggedDictBuilder::new(name.clone()); - out.insert_untagged(&key, x.value.clone()); - let group = bring_back.entry(key.clone()).or_insert(vec![]); - group.push(out.into_value()); - } - } - x => { - let mut out = TaggedDictBuilder::new(name.clone()); - out.insert_untagged(&key, x.clone()); - let group = bring_back.entry(key.clone()).or_insert(vec![]); - group.push(out.into_value()); - } - }, - Err(reason) => { - // At the moment, we can't add switches, named flags - // and the like while already using .rest since it - // breaks the parser. - // - // We allow flexibility for now and skip the error - // if a given column isn't present. - let strict: Option = None; - - if strict.is_some() { - return Err(reason); - } - - // No value for column 'key' found, insert nothing to make sure all rows contain all keys. - bring_back - .entry(key.clone()) - .or_insert(vec![]) - .push(UntaggedValue::nothing().into()); - } - } - } - } - - let mut max = 0; - - if let Some(max_column) = bring_back.values().max() { - max = max_column.len(); - } - - let keys = bring_back.keys().cloned().collect::>(); - - Ok(((0..max).map(move |current| { - let mut out = TaggedDictBuilder::new(name.clone()); - - for k in &keys { - let new_key = k.replace(".", "_"); - let nothing = UntaggedValue::Primitive(Primitive::Nothing).into_untagged_value(); - let subsets = bring_back.get(k); - - match subsets { - Some(set) => match set.get(current) { - Some(row) => out.insert_untagged(new_key, row.get_data(k).borrow().clone()), - None => out.insert_untagged(new_key, nothing.clone()), - }, - None => out.insert_untagged(new_key, nothing.clone()), - } - } - - out.into_value() - })) - .into_output_stream()) -} - -#[cfg(test)] -mod tests { - use nu_protocol::ColumnPath; - use nu_source::Span; - use nu_source::SpannedItem; - use nu_source::Tag; - use nu_stream::InputStream; - use nu_test_support::value::nothing; - use nu_test_support::value::row; - use nu_test_support::value::string; - - use super::select; - use super::Command; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Command {}) - } - - #[test] - fn select_using_sparse_table() { - // Create a sparse table with 3 rows: - // col_foo | col_bar - // ----------------- - // foo | - // | bar - // foo | - let input = vec![ - row(indexmap! {"col_foo".into() => string("foo")}), - row(indexmap! {"col_bar".into() => string("bar")}), - row(indexmap! {"col_foo".into() => string("foo")}), - ]; - - let expected = vec![ - row( - indexmap! {"col_none".into() => nothing(), "col_foo".into() => string("foo"), "col_bar".into() => nothing()}, - ), - row( - indexmap! {"col_none".into() => nothing(), "col_foo".into() => nothing(), "col_bar".into() => string("bar")}, - ), - row( - indexmap! {"col_none".into() => nothing(), "col_foo".into() => string("foo"), "col_bar".into() => nothing()}, - ), - ]; - - let actual = select( - Tag::unknown(), - vec![ - ColumnPath::build(&"col_none".to_string().spanned(Span::unknown())), - ColumnPath::build(&"col_foo".to_string().spanned(Span::unknown())), - ColumnPath::build(&"col_bar".to_string().spanned(Span::unknown())), - ], - input.into(), - ); - - assert_eq!(Ok(expected), actual.map(InputStream::into_vec)); - } -} diff --git a/crates/nu-command/src/commands/filters/shuffle.rs b/crates/nu-command/src/commands/filters/shuffle.rs deleted file mode 100644 index 970f422700..0000000000 --- a/crates/nu-command/src/commands/filters/shuffle.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ReturnSuccess, Value}; - -use rand::seq::SliceRandom; -use rand::thread_rng; - -pub struct Shuffle; - -impl WholeStreamCommand for Shuffle { - fn name(&self) -> &str { - "shuffle" - } - - fn usage(&self) -> &str { - "Shuffle rows randomly." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - Ok(shuffle(args)) - } -} - -fn shuffle(args: CommandArgs) -> ActionStream { - let input = args.input; - let mut values: Vec = input.collect(); - - values.shuffle(&mut thread_rng()); - - values - .into_iter() - .map(ReturnSuccess::value) - .into_action_stream() -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::Shuffle; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Shuffle {}) - } -} diff --git a/crates/nu-command/src/commands/filters/skip/command.rs b/crates/nu-command/src/commands/filters/skip/command.rs deleted file mode 100644 index aa3be0d712..0000000000 --- a/crates/nu-command/src/commands/filters/skip/command.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue}; -use nu_source::Tagged; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "skip" - } - - fn signature(&self) -> Signature { - Signature::build("skip").optional("rows", SyntaxShape::Int, "How many rows to skip") - } - - fn usage(&self) -> &str { - "Skip some number of rows." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - skip(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Skip the first 5 rows", - example: "echo [1 2 3 4 5 6 7] | skip 5", - result: Some(vec![ - UntaggedValue::int(6).into(), - UntaggedValue::int(7).into(), - ]), - }] - } -} - -fn skip(args: CommandArgs) -> Result { - let rows: Option> = args.opt(0)?; - let input = args.input; - - let rows_desired = if let Some(quantity) = rows { - *quantity - } else { - 1 - }; - - Ok(input.skip(rows_desired).into_action_stream()) -} - -#[cfg(test)] -mod tests { - use super::Command; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Command {}) - } -} diff --git a/crates/nu-command/src/commands/filters/skip/mod.rs b/crates/nu-command/src/commands/filters/skip/mod.rs deleted file mode 100644 index f384eabbd1..0000000000 --- a/crates/nu-command/src/commands/filters/skip/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod command; -mod until; -mod while_; - -pub use command::Command as Skip; -pub use until::SubCommand as SkipUntil; -pub use while_::SubCommand as SkipWhile; diff --git a/crates/nu-command/src/commands/filters/skip/until.rs b/crates/nu-command/src/commands/filters/skip/until.rs deleted file mode 100644 index 33a347940f..0000000000 --- a/crates/nu-command/src/commands/filters/skip/until.rs +++ /dev/null @@ -1,100 +0,0 @@ -use crate::prelude::*; -use log::trace; -use nu_engine::evaluate_baseline_expr; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - hir::{CapturedBlock, ClassifiedCommand}, - Signature, SyntaxShape, -}; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "skip until" - } - - fn signature(&self) -> Signature { - Signature::build("skip until") - .required( - "condition", - SyntaxShape::RowCondition, - "The condition that must be met to stop skipping", - ) - .filter() - } - - fn usage(&self) -> &str { - "Skips rows until the condition matches." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - let ctx = Arc::new(args.context.clone()); - let tag = args.call_info.name_tag.clone(); - - let block: CapturedBlock = args.req(0)?; - let condition = { - if block.block.block.len() != 1 { - return Err(ShellError::labeled_error( - "Expected a condition", - "expected a condition", - tag, - )); - } - match block.block.block[0].pipelines.get(0) { - Some(item) => match item.list.get(0) { - Some(ClassifiedCommand::Expr(expr)) => expr.clone(), - _ => { - return Err(ShellError::labeled_error( - "Expected a condition", - "expected a condition", - tag, - )); - } - }, - None => { - return Err(ShellError::labeled_error( - "Expected a condition", - "expected a condition", - tag, - )); - } - } - }; - - Ok(args - .input - .skip_while(move |item| { - let condition = condition.clone(); - let ctx = ctx.clone(); - - ctx.scope.enter_scope(); - ctx.scope.add_vars(&block.captured.entries); - if let Some((arg, _)) = block.block.params.positional.first() { - ctx.scope.add_var(arg.name(), item.clone()); - } - trace!("ITEM = {:?}", item); - - let result = evaluate_baseline_expr(&condition, &ctx); - ctx.scope.exit_scope(); - trace!("RESULT = {:?}", result); - - !matches!(result, Ok(ref v) if v.is_true()) - }) - .into_action_stream()) - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/filters/skip/while_.rs b/crates/nu-command/src/commands/filters/skip/while_.rs deleted file mode 100644 index ed8b355296..0000000000 --- a/crates/nu-command/src/commands/filters/skip/while_.rs +++ /dev/null @@ -1,101 +0,0 @@ -use crate::prelude::*; -use log::trace; -use nu_engine::evaluate_baseline_expr; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - hir::{CapturedBlock, ClassifiedCommand}, - Signature, SyntaxShape, -}; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "skip while" - } - - fn signature(&self) -> Signature { - Signature::build("skip while") - .required( - "condition", - SyntaxShape::RowCondition, - "The condition that must be met to continue skipping", - ) - .filter() - } - - fn usage(&self) -> &str { - "Skips rows while the condition matches." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - let ctx = Arc::new(args.context.clone()); - let tag = args.call_info.name_tag.clone(); - - let block: CapturedBlock = args.req(0)?; - let condition = { - if block.block.block.len() != 1 { - return Err(ShellError::labeled_error( - "Expected a condition", - "expected a condition", - tag, - )); - } - match block.block.block[0].pipelines.get(0) { - Some(item) => match item.list.get(0) { - Some(ClassifiedCommand::Expr(expr)) => expr.clone(), - _ => { - return Err(ShellError::labeled_error( - "Expected a condition", - "expected a condition", - tag, - )); - } - }, - None => { - return Err(ShellError::labeled_error( - "Expected a condition", - "expected a condition", - tag, - )); - } - } - }; - - Ok(args - .input - .skip_while(move |item| { - let item = item.clone(); - let condition = condition.clone(); - let ctx = ctx.clone(); - - ctx.scope.enter_scope(); - ctx.scope.add_vars(&block.captured.entries); - if let Some((arg, _)) = block.block.params.positional.first() { - ctx.scope.add_var(arg.name(), item.clone()); - } - trace!("ITEM = {:?}", item); - - let result = evaluate_baseline_expr(&condition, &ctx); - ctx.scope.exit_scope(); - trace!("RESULT = {:?}", result); - - matches!(result, Ok(ref v) if v.is_true()) - }) - .into_action_stream()) - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/filters/sort_by.rs b/crates/nu-command/src/commands/filters/sort_by.rs deleted file mode 100644 index 30b6869c40..0000000000 --- a/crates/nu-command/src/commands/filters/sort_by.rs +++ /dev/null @@ -1,226 +0,0 @@ -use crate::prelude::*; -use nu_data::base::coerce_compare; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tagged; -use nu_value_ext::ValueExt; - -pub struct SortBy; - -impl WholeStreamCommand for SortBy { - fn name(&self) -> &str { - "sort-by" - } - - fn signature(&self) -> Signature { - Signature::build("sort-by") - .switch( - "insensitive", - "Sort string-based columns case-insensitively", - Some('i'), - ) - .switch("reverse", "Sort in reverse order", Some('r')) - .rest("rest", SyntaxShape::String, "the column(s) to sort by") - } - - fn usage(&self) -> &str { - "Sort by the given columns, in increasing order." - } - - fn run(&self, args: CommandArgs) -> Result { - sort_by(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Sort list by increasing value", - example: "echo [4 2 3 1] | sort-by", - result: Some(vec![ - UntaggedValue::int(1).into(), - UntaggedValue::int(2).into(), - UntaggedValue::int(3).into(), - UntaggedValue::int(4).into(), - ]), - }, - Example { - description: "Sort list by decreasing value", - example: "echo [2 3 4 1] | sort-by -r", - result: Some(vec![ - UntaggedValue::int(4).into(), - UntaggedValue::int(3).into(), - UntaggedValue::int(2).into(), - UntaggedValue::int(1).into(), - ]), - }, - Example { - description: "Sort output by increasing file size", - example: "ls | sort-by size", - result: None, - }, - Example { - description: "Sort output by type, and then by file size for each type", - example: "ls | sort-by type size", - result: None, - }, - Example { - description: "Sort strings (case-sensitive)", - example: "echo [airplane Truck Car] | sort-by", - result: Some(vec![ - UntaggedValue::string("Car").into(), - UntaggedValue::string("Truck").into(), - UntaggedValue::string("airplane").into(), - ]), - }, - Example { - description: "Sort strings (reversed case-sensitive)", - example: "echo [airplane Truck Car] | sort-by -r", - result: Some(vec![ - UntaggedValue::string("airplane").into(), - UntaggedValue::string("Truck").into(), - UntaggedValue::string("Car").into(), - ]), - }, - Example { - description: "Sort strings (case-insensitive)", - example: "echo [airplane Truck Car] | sort-by -i", - result: Some(vec![ - UntaggedValue::string("airplane").into(), - UntaggedValue::string("Car").into(), - UntaggedValue::string("Truck").into(), - ]), - }, - Example { - description: "Sort strings (reversed case-insensitive)", - example: "echo [airplane Truck Car] | sort-by -i -r", - result: Some(vec![ - UntaggedValue::string("Truck").into(), - UntaggedValue::string("Car").into(), - UntaggedValue::string("airplane").into(), - ]), - }, - ] - } -} - -fn sort_by(mut args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let rest: Vec> = args.rest(0)?; - let insensitive = args.has_flag("insensitive"); - let reverse = args.has_flag("reverse"); - let mut vec = args.input.drain_vec(); - - sort(&mut vec, &rest, &tag, insensitive)?; - - if reverse { - vec.reverse() - } - - Ok(vec.into_iter().into_output_stream()) -} - -pub fn sort( - vec: &mut [Value], - keys: &[Tagged], - tag: impl Into, - insensitive: bool, -) -> Result<(), ShellError> { - let tag = tag.into(); - - if vec.is_empty() { - return Err(ShellError::labeled_error( - "no values to work with", - "no values to work with", - tag, - )); - } - - for sort_arg in keys { - let match_test = &vec[0].get_data_by_key(sort_arg.borrow_spanned()); - if match_test.is_none() { - return Err(ShellError::labeled_error( - "Can not find column to sort by", - "invalid column", - sort_arg.borrow_spanned().span, - )); - } - } - - match &vec[0] { - Value { - value: UntaggedValue::Primitive(_), - .. - } => { - let should_sort_case_insensitively = insensitive && vec.iter().all(|x| x.is_string()); - - if let Some(values) = vec - .windows(2) - .map(|elem| coerce_compare(&elem[0], &elem[1])) - .find(|elem| elem.is_err()) - { - let (type_1, type_2) = values - .err() - .expect("An error occurred in the checking of types"); - return Err(ShellError::labeled_error( - "Not all values can be compared", - format!( - "Unable to sort values, as \"{}\" cannot compare against \"{}\"", - type_1, type_2 - ), - tag, - )); - } - - vec.sort_by(|a, b| { - if should_sort_case_insensitively { - let lowercase_a_string = a.expect_string().to_ascii_lowercase(); - let lowercase_b_string = b.expect_string().to_ascii_lowercase(); - - lowercase_a_string.cmp(&lowercase_b_string) - } else { - coerce_compare(a, b).expect("Unimplemented BUG: What about primitives that don't have an order defined?").compare() - } - }); - } - _ => { - let calc_key = |item: &Value| { - keys.iter() - .map(|f| { - let mut value_option = item.get_data_by_key(f.borrow_spanned()); - - if insensitive { - if let Some(value) = &value_option { - if let Ok(string_value) = value.as_string() { - value_option = Some( - UntaggedValue::string(string_value.to_ascii_lowercase()) - .into_value(value.tag.clone()), - ) - } - } - } - - value_option - }) - .collect::>>() - }; - vec.sort_by_cached_key(calc_key); - } - }; - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SortBy; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SortBy {}) - } -} diff --git a/crates/nu-command/src/commands/filters/uniq.rs b/crates/nu-command/src/commands/filters/uniq.rs deleted file mode 100644 index 16cf98a767..0000000000 --- a/crates/nu-command/src/commands/filters/uniq.rs +++ /dev/null @@ -1,200 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, UntaggedValue}; - -pub struct Uniq; - -impl WholeStreamCommand for Uniq { - fn name(&self) -> &str { - "uniq" - } - - fn signature(&self) -> Signature { - Signature::build("uniq") - .switch("count", "Count the unique rows", Some('c')) - .switch( - "repeated", - "Count the rows that has more than one value", - Some('d'), - ) - .switch( - "ignore-case", - "Ignore differences in case when comparing", - Some('i'), - ) - .switch("unique", "Only return unique values", Some('u')) - } - - fn usage(&self) -> &str { - "Return the unique rows." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - uniq(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Remove duplicate rows of a list/table", - example: "echo [2 3 3 4] | uniq", - result: Some(vec![ - UntaggedValue::int(2).into(), - UntaggedValue::int(3).into(), - UntaggedValue::int(4).into(), - ]), - }, - Example { - description: "Only print duplicate lines, one for each group", - example: "echo [1 2 2] | uniq -d", - result: Some(vec![UntaggedValue::int(2).into()]), - }, - Example { - description: "Only print unique lines lines", - example: "echo [1 2 2] | uniq -u", - result: Some(vec![UntaggedValue::int(1).into()]), - }, - Example { - description: "Ignore differences in case when comparing", - example: "echo ['hello' 'goodbye' 'Hello'] | uniq -i", - result: Some(vec![ - UntaggedValue::string("hello").into(), - UntaggedValue::string("goodbye").into(), - ]), - }, - Example { - description: "Remove duplicate rows and show counts of a list/table", - example: "echo [1 2 2] | uniq -c", - result: Some(vec![ - UntaggedValue::row(indexmap! { - "value".to_string() => UntaggedValue::int(1).into(), - "count".to_string() => UntaggedValue::int(1).into(), - }) - .into(), - UntaggedValue::row(indexmap! { - "value".to_string() => UntaggedValue::int(2).into(), - "count".to_string() => UntaggedValue::int(2).into(), - }) - .into(), - ]), - }, - ] - } -} - -fn to_lowercase(value: nu_protocol::Value) -> nu_protocol::Value { - use nu_protocol::value::StringExt; - - if value.is_string() { - value - .value - .expect_string() - .to_lowercase() - .to_string_value(value.tag) - } else { - value - } -} - -fn uniq(args: CommandArgs) -> Result { - let should_show_count = args.has_flag("count"); - let show_repeated = args.has_flag("repeated"); - let ignore_case = args.has_flag("ignore-case"); - let only_uniques = args.has_flag("unique"); - let input = args.input; - let uniq_values = { - let mut counter = IndexMap::::new(); - for line in input.into_vec() { - let item = if ignore_case { - to_lowercase(line) - } else { - line - }; - *counter.entry(item).or_insert(0) += 1; - } - counter - }; - - let mut values = if show_repeated { - uniq_values.into_iter().filter(|i| i.1 > 1).collect::<_>() - } else { - uniq_values - }; - - if only_uniques { - values = values.into_iter().filter(|i| i.1 == 1).collect::<_>(); - } - - let mut values_vec_deque = VecDeque::new(); - - if should_show_count { - for item in values { - use nu_protocol::Value; - let value = { - match item.0.value { - UntaggedValue::Row(mut row) => { - row.entries.insert( - "count".to_string(), - UntaggedValue::int(item.1 as i64).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 as i64).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, - )) - } - #[cfg(feature = "dataframe")] - UntaggedValue::DataFrame(_) | UntaggedValue::FrameStruct(_) => { - return Err(ShellError::labeled_error( - "uniq -c cannot operate on data structs", - "source", - item.0.tag.span, - )) - } - UntaggedValue::Error(_) | UntaggedValue::Block(_) => item.0, - } - }; - values_vec_deque.push_back(value); - } - } else { - for item in values { - values_vec_deque.push_back(item.0); - } - } - - Ok(values_vec_deque.into_iter().into_action_stream()) -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::Uniq; - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Uniq {}) - } -} diff --git a/crates/nu-command/src/commands/filters/update.rs b/crates/nu-command/src/commands/filters/update.rs deleted file mode 100644 index a3bf26a90e..0000000000 --- a/crates/nu-command/src/commands/filters/update.rs +++ /dev/null @@ -1,196 +0,0 @@ -use crate::prelude::*; -use nu_engine::run_block; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::hir::ExternalRedirection; -use nu_protocol::{ - ColumnPath, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value, -}; -use nu_source::HasFallibleSpan; -use nu_value_ext::ValueExt; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "update" - } - - fn signature(&self) -> Signature { - Signature::build("update") - .required( - "field", - SyntaxShape::ColumnPath, - "the name of the column to update", - ) - .required( - "replacement value", - SyntaxShape::Any, - "the new value to give the cell(s)", - ) - } - - fn usage(&self) -> &str { - "Update an existing column to have a new value." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - update(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Update a column value", - example: "echo [[name, stars]; ['nu', 5]] | update name 'Nushell'", - result: Some(vec![UntaggedValue::row(indexmap! { - "name".to_string() => Value::from("Nushell"), - "stars".to_string() => UntaggedValue::int(5).into(), - }) - .into()]), - },Example { - description: "Use in block form for more involved updating logic", - example: "echo [[project, authors]; ['nu', ['Andrés', 'Jonathan', 'Yehuda']]] | update authors { get authors | str collect ',' }", - result: Some(vec![UntaggedValue::row(indexmap! { - "project".to_string() => Value::from("nu"), - "authors".to_string() => Value::from("Andrés,Jonathan,Yehuda"), - }) - .into()]), - }] - } -} - -fn process_row( - context: Arc, - input: Value, - mut replacement: Arc, - field: Arc, - tag: Arc, -) -> Result { - let replacement = Arc::make_mut(&mut replacement); - - Ok(match replacement { - Value { - value: UntaggedValue::Block(captured_block), - tag: block_tag, - } => { - let for_block = input.clone(); - let input_stream = vec![Ok(for_block)].into_iter().into_input_stream(); - - context.scope.enter_scope(); - if let Some((arg, _)) = captured_block.block.params.positional.first() { - context.scope.add_var(arg.name(), input.clone()); - } - - context.scope.add_vars(&captured_block.captured.entries); - - let result = run_block( - &captured_block.block, - &context, - input_stream, - ExternalRedirection::Stdout, - ); - - context.scope.exit_scope(); - - match result { - Ok(mut stream) => { - let values = stream.drain_vec(); - - let errors = context.get_errors(); - if let Some(error) = errors.first() { - return Err(error.clone()); - } - - let result = if values.len() == 1 { - let value = values - .get(0) - .ok_or_else(|| ShellError::unexpected("No value to update with."))?; - - Value { - value: value.value.clone(), - tag: input.tag.clone(), - } - } else if values.is_empty() { - UntaggedValue::nothing().into_value(&input.tag) - } else { - UntaggedValue::table(&values).into_value(&input.tag) - }; - - match input { - obj @ Value { - value: UntaggedValue::Row(_), - .. - } => match obj.replace_data_at_column_path(&field, result) { - Some(v) => ActionStream::one(ReturnSuccess::value(v)), - None => ActionStream::one(Err(ShellError::labeled_error( - "update could not find place to insert column", - "column name", - obj.tag, - ))), - }, - _ => ActionStream::one(Err(ShellError::labeled_error( - "Unrecognized type in stream", - "original value", - block_tag.clone(), - ))), - } - } - Err(e) => ActionStream::one(Err(e)), - } - } - replacement => match input { - Value { - value: UntaggedValue::Primitive(Primitive::Nothing), - .. - } => match context - .scope - .get_var("$it") - .unwrap_or_else(|| UntaggedValue::nothing().into_untagged_value()) - .replace_data_at_column_path(&field, replacement.clone()) - { - Some(v) => ActionStream::one(ReturnSuccess::value(v)), - None => ActionStream::one(Err(ShellError::labeled_error( - "update could not find place to insert column", - "column name", - field.maybe_span().unwrap_or(tag.span), - ))), - }, - Value { value: _, ref tag } => { - match input.replace_data_at_column_path(&field, replacement.clone()) { - Some(v) => ActionStream::one(ReturnSuccess::value(v)), - None => ActionStream::one(Err(ShellError::labeled_error( - "update could not find place to insert column", - "column name", - field.maybe_span().unwrap_or(tag.span), - ))), - } - } - }, - }) -} - -fn update(args: CommandArgs) -> Result { - let name_tag = Arc::new(args.call_info.name_tag.clone()); - let context = Arc::new(args.context.clone()); - - let field: ColumnPath = args.req(0)?; - let replacement: Value = args.req(1)?; - let input = args.input; - - let replacement = Arc::new(replacement); - let field = Arc::new(field); - - Ok(input - .flat_map(move |input| { - let tag = name_tag.clone(); - let context = context.clone(); - let replacement = replacement.clone(); - let field = field.clone(); - - match process_row(context, input, replacement, field, tag) { - Ok(s) => s, - Err(e) => ActionStream::one(Err(e)), - } - }) - .into_action_stream()) -} diff --git a/crates/nu-command/src/commands/filters/update_cells.rs b/crates/nu-command/src/commands/filters/update_cells.rs deleted file mode 100644 index c3de869750..0000000000 --- a/crates/nu-command/src/commands/filters/update_cells.rs +++ /dev/null @@ -1,211 +0,0 @@ -use crate::prelude::*; -use nu_engine::run_block; -use nu_engine::WholeStreamCommand; - -use nu_errors::ShellError; -use nu_protocol::{ - hir::{CapturedBlock, ExternalRedirection}, - Signature, SyntaxShape, TaggedDictBuilder, UntaggedValue, Value, -}; -use std::collections::HashSet; -use std::iter::FromIterator; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "update cells" - } - - fn signature(&self) -> Signature { - Signature::build("update cells") - .required( - "block", - SyntaxShape::Block, - "the block to run an update for each cell", - ) - .named( - "columns", - SyntaxShape::Table, - "list of columns to update", - Some('c'), - ) - } - - fn usage(&self) -> &str { - "Update the table cells." - } - - fn run(&self, args: CommandArgs) -> Result { - update_cells(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Update the zero value cells to empty strings.", - example: r#"[ - [2021-04-16, 2021-06-10, 2021-09-18, 2021-10-15, 2021-11-16, 2021-11-17, 2021-11-18]; - [ 37, 0, 0, 0, 37, 0, 0] -] | update cells {|value| - if ($value | into int) == 0 { - "" - } { - $value - } -}"#, - result: Some(vec![UntaggedValue::row(indexmap! { - "2021-04-16".to_string() => UntaggedValue::int(37).into(), - "2021-06-10".to_string() => Value::from(""), - "2021-09-18".to_string() => Value::from(""), - "2021-10-15".to_string() => Value::from(""), - "2021-11-16".to_string() => UntaggedValue::int(37).into(), - "2021-11-17".to_string() => Value::from(""), - "2021-11-18".to_string() => Value::from(""), - }) - .into()]), - }, - Example { - description: "Update the zero value cells to empty strings in 2 last columns.", - example: r#"[ - [2021-04-16, 2021-06-10, 2021-09-18, 2021-10-15, 2021-11-16, 2021-11-17, 2021-11-18]; - [ 37, 0, 0, 0, 37, 0, 0] -] | update cells -c ["2021-11-18", "2021-11-17"] {|value| - if ($value | into int) == 0 { - "" - } { - $value - } -}"#, - result: Some(vec![UntaggedValue::row(indexmap! { - "2021-04-16".to_string() => UntaggedValue::int(37).into(), - "2021-06-10".to_string() => UntaggedValue::int(0).into(), - "2021-09-18".to_string() => UntaggedValue::int(0).into(), - "2021-10-15".to_string() => UntaggedValue::int(0).into(), - "2021-11-16".to_string() => UntaggedValue::int(37).into(), - "2021-11-17".to_string() => Value::from(""), - "2021-11-18".to_string() => Value::from(""), - }) - .into()]), - }, - ] - } -} - -fn update_cells(args: CommandArgs) -> Result { - let context = Arc::new(args.context.clone()); - let external_redirection = args.call_info.args.external_redirection; - - let block: CapturedBlock = args.req(0)?; - let block = Arc::new(block); - - let columns = args - .get_flag("columns")? - .map(|x: Value| HashSet::from_iter(x.table_entries().map(|val| val.convert_to_string()))); - let columns = Arc::new(columns); - - Ok(args - .input - .flat_map(move |input| { - let block = block.clone(); - let context = context.clone(); - - if input.is_row() { - OutputStream::one(process_cells( - block, - columns.clone(), - context, - input, - external_redirection, - )) - } else { - match process_input(block, context, input, external_redirection) { - Ok(s) => s, - Err(e) => OutputStream::one(Value::error(e)), - } - } - }) - .into_output_stream()) -} - -pub fn process_input( - captured_block: Arc, - context: Arc, - input: Value, - external_redirection: ExternalRedirection, -) -> Result { - let input_clone = input.clone(); - // When we process a row, we need to know whether the block wants to have the contents of the row as - // a parameter to the block (so it gets assigned to a variable that can be used inside the block) or - // if it wants the contents as as an input stream - - let input_stream = if !captured_block.block.params.positional.is_empty() { - InputStream::empty() - } else { - vec![Ok(input_clone)].into_iter().into_input_stream() - }; - - context.scope.enter_scope(); - context.scope.add_vars(&captured_block.captured.entries); - - if let Some((arg, _)) = captured_block.block.params.positional.first() { - context.scope.add_var(arg.name(), input); - } else { - context.scope.add_var("$it", input); - } - - let result = run_block( - &captured_block.block, - &context, - input_stream, - external_redirection, - ); - - context.scope.exit_scope(); - - result -} - -pub fn process_cells( - captured_block: Arc, - columns: Arc>>, - context: Arc, - input: Value, - external_redirection: ExternalRedirection, -) -> Value { - TaggedDictBuilder::build(input.tag(), |row| { - input.row_entries().for_each(|(column, cell_value)| { - match &*columns { - Some(col) if !col.contains(column) => { - row.insert_value(column, cell_value.clone()); - return; - } - _ => {} - }; - let cell_processed = process_input( - captured_block.clone(), - context.clone(), - cell_value.clone(), - external_redirection, - ) - .map(|it| it.into_vec()) - .map_err(Value::error); - - match cell_processed { - Ok(value) => { - match value.get(0) { - Some(one) => { - row.insert_value(column, one.clone()); - } - None => { - row.insert_untagged(column, UntaggedValue::nothing()); - } - }; - } - Err(reason) => { - row.insert_value(column, reason); - } - } - }); - }) -} diff --git a/crates/nu-command/src/commands/filters/where_.rs b/crates/nu-command/src/commands/filters/where_.rs deleted file mode 100644 index e081f1d808..0000000000 --- a/crates/nu-command/src/commands/filters/where_.rs +++ /dev/null @@ -1,154 +0,0 @@ -use crate::prelude::*; -use nu_engine::evaluate_baseline_expr; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - hir::CapturedBlock, - hir::{ClassifiedCommand, SpannedExpression}, - Signature, SyntaxShape, Value, -}; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "where" - } - - fn signature(&self) -> Signature { - Signature::build("where").required( - "condition", - SyntaxShape::RowCondition, - "the condition that must match", - ) - } - - fn usage(&self) -> &str { - "Filter table to match the condition." - } - - fn run(&self, args: CommandArgs) -> Result { - where_command(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "List all files in the current directory with sizes greater than 2kb", - example: "ls | where size > 2kb", - result: None, - }, - Example { - description: "List only the files in the current directory", - example: "ls | where type == File", - result: None, - }, - Example { - description: "List all files with names that contain \"Car\"", - example: "ls | where name =~ \"Car\"", - result: None, - }, - Example { - description: "List all files that were modified in the last two weeks", - example: "ls | where modified <= 2wk", - result: None, - }, - ] - } -} -fn where_command(args: CommandArgs) -> Result { - let context = Arc::new(args.context.clone()); - let tag = args.call_info.name_tag.clone(); - - let block: CapturedBlock = args.req(0)?; - - let condition = { - if block.block.block.len() != 1 { - return Err(ShellError::labeled_error( - "Expected a condition", - "expected a condition", - tag, - )); - } - match block.block.block[0].pipelines.get(0) { - Some(item) => match item.list.get(0) { - Some(ClassifiedCommand::Expr(expr)) => expr.clone(), - _ => { - return Err(ShellError::labeled_error( - "Expected a condition", - "expected a condition", - tag, - )); - } - }, - None => { - return Err(ShellError::labeled_error( - "Expected a condition", - "expected a condition", - tag, - )); - } - } - }; - - Ok(WhereIterator { - condition, - context, - input: args.input, - block, - } - .into_output_stream()) -} - -#[cfg(test)] -mod tests { - use super::Command; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Command {}) - } -} - -struct WhereIterator { - condition: Box, - context: Arc, - input: InputStream, - block: CapturedBlock, -} - -impl Iterator for WhereIterator { - type Item = Value; - - fn next(&mut self) -> Option { - for x in &mut self.input { - self.context.scope.enter_scope(); - self.context.scope.add_vars(&self.block.captured.entries); - - if let Some((arg, _)) = self.block.block.params.positional.first() { - self.context.scope.add_var(arg.name(), x.clone()); - } - - //FIXME: should we use the scope that's brought in as well? - let condition = evaluate_baseline_expr(&self.condition, &self.context); - self.context.scope.exit_scope(); - - match condition { - Ok(condition) => match condition.as_bool() { - Ok(b) => { - if b { - return Some(x); - } - } - Err(e) => return Some(Value::error(e)), - }, - Err(e) => return Some(Value::error(e)), - } - } - - None - } -} diff --git a/crates/nu-command/src/commands/filters/wrap.rs b/crates/nu-command/src/commands/filters/wrap.rs deleted file mode 100644 index 90f42334d1..0000000000 --- a/crates/nu-command/src/commands/filters/wrap.rs +++ /dev/null @@ -1,135 +0,0 @@ -use crate::prelude::*; -use indexmap::{indexmap, IndexMap}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tagged; - -const DEFAULT_COLUMN_NAME: &str = "Column"; - -pub struct Wrap; - -impl WholeStreamCommand for Wrap { - fn name(&self) -> &str { - "wrap" - } - - fn signature(&self) -> Signature { - Signature::build("wrap").optional( - "column", - SyntaxShape::String, - "the name of the new column", - ) - } - - fn usage(&self) -> &str { - "Wraps the given data in a table." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - wrap(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Wrap a list into a table with the default column name", - example: "echo [1 2 3] | wrap", - result: Some(vec![ - UntaggedValue::row(indexmap! { - DEFAULT_COLUMN_NAME.to_string() => UntaggedValue::int(1).into(), - }) - .into(), - UntaggedValue::row(indexmap! { - DEFAULT_COLUMN_NAME.to_string() => UntaggedValue::int(2).into(), - }) - .into(), - UntaggedValue::row(indexmap! { - DEFAULT_COLUMN_NAME.to_string() => UntaggedValue::int(3).into(), - }) - .into(), - ]), - }, - Example { - description: "Wrap a list into a table with a given column name", - example: "echo [1 2 3] | wrap MyColumn", - result: Some(vec![ - UntaggedValue::row(indexmap! { - "MyColumn".to_string() => UntaggedValue::int(1).into(), - }) - .into(), - UntaggedValue::row(indexmap! { - "MyColumn".to_string() => UntaggedValue::int(2).into(), - }) - .into(), - UntaggedValue::row(indexmap! { - "MyColumn".to_string() => UntaggedValue::int(3).into(), - }) - .into(), - ]), - }, - ] - } -} - -fn wrap(args: CommandArgs) -> Result { - let column: Option> = args.opt(0)?; - - let mut result_table = vec![]; - let mut are_all_rows = true; - - for value in args.input { - match value { - Value { - value: UntaggedValue::Row(_), - .. - } => { - result_table.push(value); - } - _ => { - are_all_rows = false; - - let mut index_map = IndexMap::new(); - index_map.insert( - match &column { - Some(key) => key.item.clone(), - None => DEFAULT_COLUMN_NAME.to_string(), - }, - value, - ); - - result_table.push(UntaggedValue::row(index_map).into_value(Tag::unknown())); - } - } - } - - if are_all_rows { - let mut index_map = IndexMap::new(); - index_map.insert( - match &column { - Some(key) => key.item.clone(), - None => DEFAULT_COLUMN_NAME.to_string(), - }, - UntaggedValue::table(&result_table).into_value(Tag::unknown()), - ); - - let row = UntaggedValue::row(index_map).into_untagged_value(); - - Ok(ActionStream::one(ReturnSuccess::value(row))) - } else { - Ok((result_table.into_iter().map(ReturnSuccess::value)).into_action_stream()) - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::Wrap; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Wrap {}) - } -} diff --git a/crates/nu-command/src/commands/filters/zip_.rs b/crates/nu-command/src/commands/filters/zip_.rs deleted file mode 100644 index 8ce5af6dca..0000000000 --- a/crates/nu-command/src/commands/filters/zip_.rs +++ /dev/null @@ -1,168 +0,0 @@ -use crate::prelude::*; -use nu_engine::run_block; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::did_you_mean; -use nu_protocol::TaggedDictBuilder; -use nu_protocol::{ - hir::CapturedBlock, hir::ExternalRedirection, ColumnPath, PathMember, Signature, SyntaxShape, - UnspannedPathMember, UntaggedValue, Value, -}; -use nu_value_ext::get_data_by_column_path; - -use nu_source::HasFallibleSpan; -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "zip" - } - - fn signature(&self) -> Signature { - Signature::build("zip").required( - "block", - SyntaxShape::Block, - "the block to run and zip into the table", - ) - } - - fn usage(&self) -> &str { - "Zip two tables." - } - - fn run(&self, args: CommandArgs) -> Result { - command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Zip two lists", - example: "[0 2 4 6 8] | zip { [1 3 5 7 9] } | each { $it }", - result: None, - }, - Example { - description: "Zip two tables", - example: "[[symbol]; ['('] ['['] ['{']] | zip { [[symbol]; [')'] [']'] ['}']] } | each { get symbol | $'($in.0)nushell($in.1)' }", - result: Some(vec![ - Value::from("(nushell)"), - Value::from("[nushell]"), - Value::from("{nushell}") - ]) - }] - } -} - -fn command(args: CommandArgs) -> Result { - let context = &args.context; - let name_tag = args.call_info.name_tag.clone(); - - let block: CapturedBlock = args.req(0)?; - let block_span = &block.block.span.clone(); - let input = args.input; - - context.scope.enter_scope(); - context.scope.add_vars(&block.captured.entries); - let result = run_block( - &block.block, - context, - InputStream::empty(), - ExternalRedirection::Stdout, - ); - context.scope.exit_scope(); - - Ok(OutputStream::from_stream(zip( - input, - result, - name_tag, - *block_span, - )?)) -} - -fn zip<'a>( - l: impl Iterator + 'a + Sync + Send, - r: Result, - command_tag: Tag, - secondary_command_span: Span, -) -> Result + 'a + Sync + Send>, ShellError> { - Ok(Box::new(l.zip(r?).map(move |(s1, s2)| match (s1, s2) { - ( - left_row @ Value { - value: UntaggedValue::Row(_), - .. - }, - mut right_row @ Value { - value: UntaggedValue::Row(_), - .. - }, - ) => { - let mut zipped_row = TaggedDictBuilder::new(left_row.tag()); - - right_row.tag = Tag::new(right_row.tag.anchor(), secondary_command_span); - - for column in left_row.data_descriptors() { - let path = ColumnPath::build(&(column.to_string()).spanned(right_row.tag.span)); - zipped_row.insert_value(column, zip_row(&path, &left_row, &right_row)); - } - - zipped_row.into_value() - } - (s1, s2) => { - let mut name_tag = command_tag.clone(); - name_tag.anchor = s1.tag.anchor(); - UntaggedValue::table(&vec![s1, s2]).into_value(&name_tag) - } - }))) -} - -fn zip_row(path: &ColumnPath, left: &Value, right: &Value) -> UntaggedValue { - UntaggedValue::table(&vec![ - get_column(path, left) - .unwrap_or_else(|err| UntaggedValue::Error(err).into_untagged_value()), - get_column(path, right) - .unwrap_or_else(|err| UntaggedValue::Error(err).into_untagged_value()), - ]) -} - -pub fn get_column(path: &ColumnPath, value: &Value) -> Result { - get_data_by_column_path(value, path, move |obj_source, column_path_tried, error| { - let path_members_span = path.maybe_span().unwrap_or_else(Span::unknown); - - if obj_source.is_row() { - if let Some(error) = error_message(column_path_tried, &path_members_span, obj_source) { - return error; - } - } - - error - }) -} - -fn error_message( - column_tried: &PathMember, - path_members_span: &Span, - obj_source: &Value, -) -> Option { - match column_tried { - PathMember { - unspanned: UnspannedPathMember::String(column), - .. - } => { - let primary_label = format!("There isn't a column named '{}' from this table", &column); - - did_you_mean(obj_source, column_tried.as_string()).map(|suggestions| { - ShellError::labeled_error_with_secondary( - "Unknown column", - primary_label, - obj_source.tag.span, - format!( - "Perhaps you meant '{}'? Columns available: {}", - suggestions[0], - &obj_source.data_descriptors().join(", ") - ), - column_tried.span.since(path_members_span), - ) - }) - } - _ => None, - } -} diff --git a/crates/nu-command/src/commands/formats/from/command.rs b/crates/nu-command/src/commands/formats/from/command.rs deleted file mode 100644 index 5d6d68af9d..0000000000 --- a/crates/nu-command/src/commands/formats/from/command.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, UntaggedValue}; - -pub struct From; - -impl WholeStreamCommand for From { - fn name(&self) -> &str { - "from" - } - - fn signature(&self) -> Signature { - Signature::build("from") - } - - fn usage(&self) -> &str { - "Parse content (string or binary) as a table (input format based on subcommand, like csv, ini, json, toml)." - } - - fn run(&self, args: CommandArgs) -> Result { - Ok(OutputStream::one( - UntaggedValue::string(get_full_help(&From, args.scope())).into_value(Tag::unknown()), - )) - } -} - -#[cfg(test)] -mod tests { - use super::From; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(From {}) - } -} diff --git a/crates/nu-command/src/commands/formats/from/csv.rs b/crates/nu-command/src/commands/formats/from/csv.rs deleted file mode 100644 index 2c27bf8b11..0000000000 --- a/crates/nu-command/src/commands/formats/from/csv.rs +++ /dev/null @@ -1,107 +0,0 @@ -use super::delimited::from_delimited_data; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Primitive, Signature, SyntaxShape, UntaggedValue, Value}; - -pub struct FromCsv; - -impl WholeStreamCommand for FromCsv { - fn name(&self) -> &str { - "from csv" - } - - fn signature(&self) -> Signature { - Signature::build("from csv") - .named( - "separator", - SyntaxShape::String, - "a character to separate columns, defaults to ','", - Some('s'), - ) - .switch( - "noheaders", - "don't treat the first row as column names", - Some('n'), - ) - } - - fn usage(&self) -> &str { - "Parse text as .csv and create table." - } - - fn run(&self, args: CommandArgs) -> Result { - from_csv(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Convert comma-separated data to a table", - example: "open data.txt | from csv", - result: None, - }, - Example { - description: "Convert comma-separated data to a table, ignoring headers", - example: "open data.txt | from csv --noheaders", - result: None, - }, - Example { - description: "Convert comma-separated data to a table, ignoring headers", - example: "open data.txt | from csv -n", - result: None, - }, - Example { - description: "Convert semicolon-separated data to a table", - example: "open data.txt | from csv --separator ';'", - result: None, - }, - ] - } -} - -fn from_csv(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - - let noheaders = args.has_flag("noheaders"); - let separator: Option = args.get_flag("separator")?; - let input = args.input; - - let sep = match separator { - Some(Value { - value: UntaggedValue::Primitive(Primitive::String(s)), - tag, - .. - }) => { - if s == r"\t" { - '\t' - } else { - let vec_s: Vec = s.chars().collect(); - if vec_s.len() != 1 { - return Err(ShellError::labeled_error( - "Expected a single separator char from --separator", - "requires a single character string input", - tag, - )); - }; - vec_s[0] - } - } - _ => ',', - }; - - from_delimited_data(noheaders, sep, "CSV", input, name) -} - -#[cfg(test)] -mod tests { - use super::FromCsv; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(FromCsv {}) - } -} diff --git a/crates/nu-command/src/commands/formats/from/delimited.rs b/crates/nu-command/src/commands/formats/from/delimited.rs deleted file mode 100644 index 28b5df27e4..0000000000 --- a/crates/nu-command/src/commands/formats/from/delimited.rs +++ /dev/null @@ -1,112 +0,0 @@ -use crate::prelude::*; -use csv::{ErrorKind, ReaderBuilder}; -use nu_errors::ShellError; -use nu_protocol::{TaggedDictBuilder, UntaggedValue, Value}; - -fn from_delimited_string_to_value( - s: String, - noheaders: bool, - separator: char, - tag: impl Into, -) -> Result { - let mut reader = ReaderBuilder::new() - .has_headers(!noheaders) - .delimiter(separator as u8) - .from_reader(s.as_bytes()); - let tag = tag.into(); - let span = tag.span; - - let headers = if noheaders { - (1..=reader.headers()?.len()) - .map(|i| format!("Column{}", i)) - .collect::>() - } else { - reader.headers()?.iter().map(String::from).collect() - }; - - let mut rows = vec![]; - for row in reader.records() { - let mut tagged_row = TaggedDictBuilder::new(&tag); - for (value, header) in row?.iter().zip(&headers) { - if let Ok(i) = value.parse::() { - tagged_row.insert_value(header, UntaggedValue::int(i).into_value(&tag)) - } else if let Ok(f) = value.parse::() { - tagged_row.insert_value( - header, - UntaggedValue::decimal_from_float(f, span).into_value(&tag), - ) - } else { - tagged_row.insert_value(header, UntaggedValue::string(value).into_value(&tag)) - } - } - rows.push(tagged_row.into_value()); - } - - Ok(UntaggedValue::Table(rows).into_value(&tag)) -} - -pub fn from_delimited_data( - noheaders: bool, - sep: char, - format_name: &'static str, - input: InputStream, - name: Tag, -) -> Result { - let name_tag = name; - let concat_string = input.collect_string(name_tag.clone())?; - let sample_lines = concat_string.item.lines().take(3).collect_vec().join("\n"); - - match from_delimited_string_to_value(concat_string.item, noheaders, sep, name_tag.clone()) { - Ok(x) => match x { - Value { - value: UntaggedValue::Table(list), - .. - } => Ok(list.into_iter().into_output_stream()), - x => Ok(OutputStream::one(x)), - }, - Err(err) => { - let line_one = match pretty_csv_error(err) { - Some(pretty) => format!( - "Could not parse as {} split by '{}' ({})", - format_name, sep, pretty - ), - None => format!("Could not parse as {} split by '{}'", format_name, sep), - }; - let line_two = format!( - "input cannot be parsed as {} split by '{}'. Input's first lines:\n{}", - format_name, sep, sample_lines - ); - - Err(ShellError::labeled_error_with_secondary( - line_one, - line_two, - name_tag, - "value originates from here", - concat_string.tag, - )) - } - } -} - -fn pretty_csv_error(err: csv::Error) -> Option { - match err.kind() { - ErrorKind::UnequalLengths { - pos, - expected_len, - len, - } => { - if let Some(pos) = pos { - Some(format!( - "Line {}: expected {} fields, found {}", - pos.line(), - expected_len, - len - )) - } else { - Some(format!("Expected {} fields, found {}", expected_len, len)) - } - } - ErrorKind::Seek => Some("Internal error while parsing csv".to_string()), - _ => None, - } -} diff --git a/crates/nu-command/src/commands/formats/from/eml.rs b/crates/nu-command/src/commands/formats/from/eml.rs deleted file mode 100644 index 68bbee84c2..0000000000 --- a/crates/nu-command/src/commands/formats/from/eml.rs +++ /dev/null @@ -1,125 +0,0 @@ -use crate::prelude::*; -use ::eml_parser::eml::*; -use ::eml_parser::EmlParser; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, TaggedDictBuilder, UntaggedValue}; -use nu_source::Tagged; - -pub struct FromEml; - -const DEFAULT_BODY_PREVIEW: usize = 50; - -impl WholeStreamCommand for FromEml { - fn name(&self) -> &str { - "from eml" - } - - fn signature(&self) -> Signature { - Signature::build("from eml").named( - "preview-body", - SyntaxShape::Int, - "How many bytes of the body to preview", - Some('b'), - ) - } - - fn usage(&self) -> &str { - "Parse text as .eml and create table." - } - - fn run(&self, args: CommandArgs) -> Result { - from_eml(args) - } -} - -fn emailaddress_to_value(tag: &Tag, email_address: &EmailAddress) -> TaggedDictBuilder { - let mut dict = TaggedDictBuilder::with_capacity(tag, 2); - let (n, a) = match email_address { - EmailAddress::AddressOnly { address } => { - (UntaggedValue::nothing(), UntaggedValue::string(address)) - } - EmailAddress::NameAndEmailAddress { name, address } => { - (UntaggedValue::string(name), UntaggedValue::string(address)) - } - }; - - dict.insert_untagged("Name", n); - dict.insert_untagged("Address", a); - - dict -} - -fn headerfieldvalue_to_value(tag: &Tag, value: &HeaderFieldValue) -> UntaggedValue { - use HeaderFieldValue::*; - - match value { - SingleEmailAddress(address) => emailaddress_to_value(tag, address).into_untagged_value(), - MultipleEmailAddresses(addresses) => UntaggedValue::Table( - addresses - .iter() - .map(|a| emailaddress_to_value(tag, a).into_value()) - .collect(), - ), - Unstructured(s) => UntaggedValue::string(s), - Empty => UntaggedValue::nothing(), - } -} - -fn from_eml(args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let preview_body: Option> = args.get_flag("preview-body")?; - - let value = args.input.collect_string(tag.clone())?; - - let body_preview = preview_body.map(|b| b.item).unwrap_or(DEFAULT_BODY_PREVIEW); - - let eml = EmlParser::from_string(value.item) - .with_body_preview(body_preview) - .parse() - .map_err(|_| { - ShellError::labeled_error( - "Could not parse .eml file", - "could not parse .eml file", - &tag, - ) - })?; - - let mut dict = TaggedDictBuilder::new(&tag); - - if let Some(subj) = eml.subject { - dict.insert_untagged("Subject", UntaggedValue::string(subj)); - } - - if let Some(from) = eml.from { - dict.insert_untagged("From", headerfieldvalue_to_value(&tag, &from)); - } - - if let Some(to) = eml.to { - dict.insert_untagged("To", headerfieldvalue_to_value(&tag, &to)); - } - - for HeaderField { name, value } in &eml.headers { - dict.insert_untagged(name, headerfieldvalue_to_value(&tag, value)); - } - - if let Some(body) = eml.body { - dict.insert_untagged("Body", UntaggedValue::string(body)); - } - - Ok(OutputStream::one(dict.into_value())) -} - -#[cfg(test)] -mod tests { - use super::FromEml; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(FromEml {}) - } -} diff --git a/crates/nu-command/src/commands/formats/from/ics.rs b/crates/nu-command/src/commands/formats/from/ics.rs deleted file mode 100644 index 2770cb14ea..0000000000 --- a/crates/nu-command/src/commands/formats/from/ics.rs +++ /dev/null @@ -1,250 +0,0 @@ -extern crate ical; -use crate::prelude::*; -use ical::parser::ical::component::*; -use ical::property::Property; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Primitive, Signature, TaggedDictBuilder, UntaggedValue, Value}; -use std::io::BufReader; - -pub struct FromIcs; - -impl WholeStreamCommand for FromIcs { - fn name(&self) -> &str { - "from ics" - } - - fn signature(&self) -> Signature { - Signature::build("from ics") - } - - fn usage(&self) -> &str { - "Parse text as .ics and create table." - } - - fn run(&self, args: CommandArgs) -> Result { - from_ics(args) - } -} - -fn from_ics(args: CommandArgs) -> Result { - let tag = args.name_tag(); - let input = args.input; - - let input_string = input.collect_string(tag.clone())?.item; - let input_bytes = input_string.as_bytes(); - let buf_reader = BufReader::new(input_bytes); - let parser = ical::IcalParser::new(buf_reader); - - // TODO: it should be possible to make this a stream, but the some of the lifetime requirements make this tricky. - // Pre-computing for now - let mut output = vec![]; - - for calendar in parser { - match calendar { - Ok(c) => output.push(calendar_to_value(c, tag.clone())), - Err(_) => output.push(Value::error(ShellError::labeled_error( - "Could not parse as .ics", - "input cannot be parsed as .ics", - tag.clone(), - ))), - } - } - - Ok(output.into_iter().into_output_stream()) -} - -fn calendar_to_value(calendar: IcalCalendar, tag: Tag) -> Value { - let mut row = TaggedDictBuilder::new(tag.clone()); - - row.insert_untagged( - "properties", - properties_to_value(calendar.properties, tag.clone()), - ); - row.insert_untagged("events", events_to_value(calendar.events, tag.clone())); - row.insert_untagged("alarms", alarms_to_value(calendar.alarms, tag.clone())); - row.insert_untagged("to-Dos", todos_to_value(calendar.todos, tag.clone())); - row.insert_untagged( - "journals", - journals_to_value(calendar.journals, tag.clone()), - ); - row.insert_untagged( - "free-busys", - free_busys_to_value(calendar.free_busys, tag.clone()), - ); - row.insert_untagged("timezones", timezones_to_value(calendar.timezones, tag)); - - row.into_value() -} - -fn events_to_value(events: Vec, tag: Tag) -> UntaggedValue { - UntaggedValue::table( - &events - .into_iter() - .map(|event| { - let mut row = TaggedDictBuilder::new(tag.clone()); - row.insert_untagged( - "properties", - properties_to_value(event.properties, tag.clone()), - ); - row.insert_untagged("alarms", alarms_to_value(event.alarms, tag.clone())); - row.into_value() - }) - .collect::>(), - ) -} - -fn alarms_to_value(alarms: Vec, tag: Tag) -> UntaggedValue { - UntaggedValue::table( - &alarms - .into_iter() - .map(|alarm| { - let mut row = TaggedDictBuilder::new(tag.clone()); - row.insert_untagged( - "properties", - properties_to_value(alarm.properties, tag.clone()), - ); - row.into_value() - }) - .collect::>(), - ) -} - -fn todos_to_value(todos: Vec, tag: Tag) -> UntaggedValue { - UntaggedValue::table( - &todos - .into_iter() - .map(|todo| { - let mut row = TaggedDictBuilder::new(tag.clone()); - row.insert_untagged( - "properties", - properties_to_value(todo.properties, tag.clone()), - ); - row.insert_untagged("alarms", alarms_to_value(todo.alarms, tag.clone())); - row.into_value() - }) - .collect::>(), - ) -} - -fn journals_to_value(journals: Vec, tag: Tag) -> UntaggedValue { - UntaggedValue::table( - &journals - .into_iter() - .map(|journal| { - let mut row = TaggedDictBuilder::new(tag.clone()); - row.insert_untagged( - "properties", - properties_to_value(journal.properties, tag.clone()), - ); - row.into_value() - }) - .collect::>(), - ) -} - -fn free_busys_to_value(free_busys: Vec, tag: Tag) -> UntaggedValue { - UntaggedValue::table( - &free_busys - .into_iter() - .map(|free_busy| { - let mut row = TaggedDictBuilder::new(tag.clone()); - row.insert_untagged( - "properties", - properties_to_value(free_busy.properties, tag.clone()), - ); - row.into_value() - }) - .collect::>(), - ) -} - -fn timezones_to_value(timezones: Vec, tag: Tag) -> UntaggedValue { - UntaggedValue::table( - &timezones - .into_iter() - .map(|timezone| { - let mut row = TaggedDictBuilder::new(tag.clone()); - row.insert_untagged( - "properties", - properties_to_value(timezone.properties, tag.clone()), - ); - row.insert_untagged( - "transitions", - timezone_transitions_to_value(timezone.transitions, tag.clone()), - ); - row.into_value() - }) - .collect::>(), - ) -} - -fn timezone_transitions_to_value( - transitions: Vec, - tag: Tag, -) -> UntaggedValue { - UntaggedValue::table( - &transitions - .into_iter() - .map(|transition| { - let mut row = TaggedDictBuilder::new(tag.clone()); - row.insert_untagged( - "properties", - properties_to_value(transition.properties, tag.clone()), - ); - row.into_value() - }) - .collect::>(), - ) -} - -fn properties_to_value(properties: Vec, tag: Tag) -> UntaggedValue { - UntaggedValue::table( - &properties - .into_iter() - .map(|prop| { - let mut row = TaggedDictBuilder::new(tag.clone()); - - let name = UntaggedValue::string(prop.name); - let value = match prop.value { - Some(val) => UntaggedValue::string(val), - None => UntaggedValue::Primitive(Primitive::Nothing), - }; - let params = match prop.params { - Some(param_list) => params_to_value(param_list, tag.clone()).into(), - None => UntaggedValue::Primitive(Primitive::Nothing), - }; - - row.insert_untagged("name", name); - row.insert_untagged("value", value); - row.insert_untagged("params", params); - row.into_value() - }) - .collect::>(), - ) -} - -fn params_to_value(params: Vec<(String, Vec)>, tag: Tag) -> Value { - let mut row = TaggedDictBuilder::new(tag); - - for (param_name, param_values) in params { - let values: Vec = param_values.into_iter().map(|val| val.into()).collect(); - let values = UntaggedValue::table(&values); - row.insert_untagged(param_name, values); - } - - row.into_value() -} - -#[cfg(test)] -mod tests { - use super::FromIcs; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(FromIcs {}) - } -} diff --git a/crates/nu-command/src/commands/formats/from/ini.rs b/crates/nu-command/src/commands/formats/from/ini.rs deleted file mode 100644 index 950f8f0642..0000000000 --- a/crates/nu-command/src/commands/formats/from/ini.rs +++ /dev/null @@ -1,86 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, UntaggedValue, Value}; -use std::collections::HashMap; - -#[derive(Debug, thiserror::Error)] -pub enum DeserializationError { - #[error("Failed to parse input as INI")] - Ini(#[from] serde_ini::de::Error), - - #[error("Failed to convert to a nushell value")] - Nu(#[from] nu_serde::Error), -} - -pub struct FromIni; - -impl WholeStreamCommand for FromIni { - fn name(&self) -> &str { - "from ini" - } - - fn signature(&self) -> Signature { - Signature::build("from ini") - } - - fn usage(&self) -> &str { - "Parse text as .ini and create table" - } - - fn run(&self, args: CommandArgs) -> Result { - from_ini(args) - } -} - -pub fn from_ini_string_to_value( - s: String, - tag: impl Into, -) -> Result { - let v: HashMap> = serde_ini::from_str(&s)?; - - Ok(nu_serde::to_value(v, tag)?) -} - -fn from_ini(args: CommandArgs) -> Result { - let tag = args.name_tag(); - let input = args.input; - let concat_string = input.collect_string(tag.clone())?; - - match from_ini_string_to_value(concat_string.item, tag.clone()) { - Ok(x) => match x { - Value { - value: UntaggedValue::Table(list), - .. - } => Ok(list.into_iter().into_output_stream()), - x => Ok(OutputStream::one(x)), - }, - Err(DeserializationError::Ini(e)) => Err(ShellError::labeled_error_with_secondary( - format!("Could not parse as INI: {}", e), - "input cannot be parsed as INI", - &tag, - "value originates from here", - concat_string.tag, - )), - Err(DeserializationError::Nu(e)) => Err(ShellError::labeled_error_with_secondary( - format!("Could not convert to nushell value: {}", e), - "input cannot be converted to nushell", - &tag, - "value originates from here", - concat_string.tag, - )), - } -} - -#[cfg(test)] -mod tests { - use super::FromIni; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(FromIni {}) - } -} diff --git a/crates/nu-command/src/commands/formats/from/json.rs b/crates/nu-command/src/commands/formats/from/json.rs deleted file mode 100644 index 7d0c3f65d9..0000000000 --- a/crates/nu-command/src/commands/formats/from/json.rs +++ /dev/null @@ -1,149 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, UntaggedValue, Value}; - -#[derive(Debug, thiserror::Error)] -pub enum DeserializationError { - #[error("Failed to parse input as JSON")] - Json(#[from] nu_json::Error), - - #[error("Failed to convert JSON to a nushell value")] - Nu(#[from] Box), -} - -pub struct FromJson; - -impl WholeStreamCommand for FromJson { - fn name(&self) -> &str { - "from json" - } - - fn signature(&self) -> Signature { - Signature::build("from json").switch( - "objects", - "treat each line as a separate value", - Some('o'), - ) - } - - fn usage(&self) -> &str { - "Parse text as .json and create table." - } - - fn run(&self, args: CommandArgs) -> Result { - from_json(args) - } -} - -pub fn from_json_string_to_value( - s: String, - tag: impl Into, -) -> Result { - let v: nu_json::Value = nu_json::from_str(&s)?; - - Ok(nu_serde::to_value(v, tag).map_err(Box::new)?) -} - -fn from_json(args: CommandArgs) -> Result { - let name_tag = args.call_info.name_tag.clone(); - - let objects = args.has_flag("objects"); - - let concat_string = args.input.collect_string(name_tag.clone())?; - - if objects { - #[allow(clippy::needless_collect)] - let lines: Vec<_> = concat_string.item.lines().map(|x| x.to_string()).collect(); - Ok(lines - .into_iter() - .filter_map(move |json_str| { - if json_str.is_empty() { - return None; - } - - match from_json_string_to_value(json_str, &name_tag) { - Ok(x) => Some(x), - Err(DeserializationError::Nu(e)) => { - let mut message = "Could not convert JSON to nushell value (".to_string(); - message.push_str(&e.to_string()); - message.push(')'); - Some(Value::error(ShellError::labeled_error_with_secondary( - message, - "input cannot be converted to nushell values", - name_tag.clone(), - "value originates from here", - concat_string.tag.clone(), - ))) - } - Err(DeserializationError::Json(e)) => { - let mut message = "Could not parse as JSON (".to_string(); - message.push_str(&e.to_string()); - message.push(')'); - - Some(Value::error(ShellError::labeled_error_with_secondary( - message, - "input cannot be parsed as JSON", - name_tag.clone(), - "value originates from here", - concat_string.tag.clone(), - ))) - } - } - }) - .into_output_stream()) - } else { - match from_json_string_to_value(concat_string.item, name_tag.clone()) { - Ok(x) => match x { - Value { - value: UntaggedValue::Table(list), - .. - } => Ok(list.into_iter().into_output_stream()), - - x => Ok(OutputStream::one(x)), - }, - Err(DeserializationError::Json(e)) => { - let mut message = "Could not parse as JSON (".to_string(); - message.push_str(&e.to_string()); - message.push(')'); - - Ok(OutputStream::one(Value::error( - ShellError::labeled_error_with_secondary( - message, - "input cannot be parsed as JSON", - name_tag, - "value originates from here", - concat_string.tag, - ), - ))) - } - Err(DeserializationError::Nu(e)) => { - let mut message = "Could not convert JSON to nushell value (".to_string(); - message.push_str(&e.to_string()); - message.push(')'); - Ok(OutputStream::one(Value::error( - ShellError::labeled_error_with_secondary( - message, - "input cannot be converted to nushell values", - name_tag, - "value originates from here", - concat_string.tag, - ), - ))) - } - } - } -} - -#[cfg(test)] -mod tests { - use super::FromJson; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(FromJson {}) - } -} diff --git a/crates/nu-command/src/commands/formats/from/mod.rs b/crates/nu-command/src/commands/formats/from/mod.rs deleted file mode 100644 index fe91fdb639..0000000000 --- a/crates/nu-command/src/commands/formats/from/mod.rs +++ /dev/null @@ -1,32 +0,0 @@ -mod command; -pub(crate) mod csv; -mod delimited; -mod eml; -mod ics; -mod ini; -mod json; -mod ods; -mod ssv; -pub(crate) mod toml; -mod tsv; -pub(crate) mod url; -mod vcf; -mod xlsx; -mod xml; -mod yaml; - -pub use self::csv::FromCsv; -pub use self::toml::FromToml; -pub use self::url::FromUrl; -pub use command::From; -pub use eml::FromEml; -pub use ics::FromIcs; -pub use ini::FromIni; -pub use json::FromJson; -pub use ods::FromOds; -pub use ssv::FromSsv; -pub use tsv::FromTsv; -pub use vcf::FromVcf; -pub use xlsx::FromXlsx; -pub use xml::FromXml; -pub use yaml::{FromYaml, FromYml}; diff --git a/crates/nu-command/src/commands/formats/from/ods.rs b/crates/nu-command/src/commands/formats/from/ods.rs deleted file mode 100644 index 5187d5f082..0000000000 --- a/crates/nu-command/src/commands/formats/from/ods.rs +++ /dev/null @@ -1,120 +0,0 @@ -use crate::prelude::*; -use calamine::*; -use nu_data::TaggedListBuilder; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Primitive, Signature, SyntaxShape, TaggedDictBuilder, UntaggedValue, Value}; -use std::io::Cursor; - -pub struct FromOds; - -impl WholeStreamCommand for FromOds { - fn name(&self) -> &str { - "from ods" - } - - fn signature(&self) -> Signature { - Signature::build("from ods").named( - "sheets", - SyntaxShape::Table, - "Only convert specified sheets", - Some('s'), - ) - } - - fn usage(&self) -> &str { - "Parse OpenDocument Spreadsheet(.ods) data and create table." - } - - fn run(&self, args: CommandArgs) -> Result { - from_ods(args) - } -} - -// Adapted from crates/nu-command/src/commands/dataframe/utils.rs -fn convert_columns(columns: &[Value]) -> Result, ShellError> { - let res = columns - .iter() - .map(|value| match &value.value { - UntaggedValue::Primitive(Primitive::String(s)) => Ok(s.clone()), - _ => Err(ShellError::labeled_error( - "Incorrect column format", - "Only string as column name", - &value.tag, - )), - }) - .collect::, _>>()?; - - Ok(res) -} - -fn from_ods(args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let span = tag.span; - - let mut sel_sheets = vec![]; - - if let Some(columns) = args.get_flag::>("sheets")? { - sel_sheets = convert_columns(columns.as_slice())?; - } - - let bytes = args.input.collect_binary(tag.clone())?; - let buf: Cursor> = Cursor::new(bytes.item); - let mut ods = Ods::<_>::new(buf).map_err(|_| { - ShellError::labeled_error("Could not load ods file", "could not load ods file", &tag) - })?; - - let mut dict = TaggedDictBuilder::new(&tag); - - let mut sheet_names = ods.sheet_names().to_owned(); - if !sel_sheets.is_empty() { - sheet_names.retain(|e| sel_sheets.contains(e)); - } - - for sheet_name in &sheet_names { - let mut sheet_output = TaggedListBuilder::new(&tag); - - if let Some(Ok(current_sheet)) = ods.worksheet_range(sheet_name) { - for row in current_sheet.rows() { - let mut row_output = TaggedDictBuilder::new(&tag); - for (i, cell) in row.iter().enumerate() { - let value = match cell { - DataType::Empty => UntaggedValue::nothing(), - DataType::String(s) => UntaggedValue::string(s), - DataType::Float(f) => UntaggedValue::decimal_from_float(*f, span), - DataType::Int(i) => UntaggedValue::int(*i), - DataType::Bool(b) => UntaggedValue::boolean(*b), - _ => UntaggedValue::nothing(), - }; - - row_output.insert_untagged(&format!("Column{}", i), value); - } - - sheet_output.push_untagged(row_output.into_untagged_value()); - } - - dict.insert_untagged(sheet_name, sheet_output.into_untagged_value()); - } else { - return Err(ShellError::labeled_error( - "Could not load sheet", - "could not load sheet", - &tag, - )); - } - } - - Ok(OutputStream::one(dict.into_value())) -} - -#[cfg(test)] -mod tests { - use super::FromOds; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(FromOds {}) - } -} diff --git a/crates/nu-command/src/commands/formats/from/ssv.rs b/crates/nu-command/src/commands/formats/from/ssv.rs deleted file mode 100644 index 2ede8a8e4a..0000000000 --- a/crates/nu-command/src/commands/formats/from/ssv.rs +++ /dev/null @@ -1,480 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Primitive, Signature, SyntaxShape, TaggedDictBuilder, UntaggedValue, Value}; -use nu_source::Tagged; - -pub struct FromSsv; - -const STRING_REPRESENTATION: &str = "from ssv"; -const DEFAULT_MINIMUM_SPACES: usize = 2; - -impl WholeStreamCommand for FromSsv { - fn name(&self) -> &str { - STRING_REPRESENTATION - } - - fn signature(&self) -> Signature { - Signature::build(STRING_REPRESENTATION) - .switch( - "noheaders", - "don't treat the first row as column names", - Some('n'), - ) - .switch("aligned-columns", "assume columns are aligned", Some('a')) - .named( - "minimum-spaces", - SyntaxShape::Int, - "the minimum spaces to separate columns", - Some('m'), - ) - } - - fn usage(&self) -> &str { - "Parse text as space-separated values and create a table. The default minimum number of spaces counted as a separator is 2." - } - - fn run(&self, args: CommandArgs) -> Result { - from_ssv(args) - } -} - -enum HeaderOptions<'a> { - WithHeaders(&'a str), - WithoutHeaders, -} - -fn parse_aligned_columns<'a>( - lines: impl Iterator, - headers: HeaderOptions, - separator: &str, -) -> Vec> { - fn construct<'a>( - lines: impl Iterator, - headers: Vec<(String, usize)>, - ) -> Vec> { - lines - .map(|l| { - headers - .iter() - .enumerate() - .map(|(i, (header_name, start_position))| { - let val = match headers.get(i + 1) { - Some((_, end)) => { - if *end < l.len() { - l.get(*start_position..*end) - } else { - l.get(*start_position..) - } - } - None => l.get(*start_position..), - } - .unwrap_or("") - .trim() - .into(); - (header_name.clone(), val) - }) - .collect() - }) - .collect() - } - - let find_indices = |line: &str| { - let values = line - .split(&separator) - .map(str::trim) - .filter(|s| !s.is_empty()); - values - .fold( - (0, vec![]), - |(current_pos, mut indices), value| match line[current_pos..].find(value) { - None => (current_pos, indices), - Some(index) => { - let absolute_index = current_pos + index; - indices.push(absolute_index); - (absolute_index + value.len(), indices) - } - }, - ) - .1 - }; - - let parse_with_headers = |lines, headers_raw: &str| { - let indices = find_indices(headers_raw); - let headers = headers_raw - .split(&separator) - .map(str::trim) - .filter(|s| !s.is_empty()) - .map(String::from) - .zip(indices); - - let columns = headers.collect::>(); - - construct(lines, columns) - }; - - let parse_without_headers = |ls: Vec<&str>| { - let mut indices = ls - .iter() - .flat_map(|s| find_indices(*s)) - .collect::>(); - - indices.sort_unstable(); - indices.dedup(); - - let headers: Vec<(String, usize)> = indices - .iter() - .enumerate() - .map(|(i, position)| (format!("Column{}", i + 1), *position)) - .collect(); - - construct(ls.iter().map(|s| s.to_owned()), headers) - }; - - match headers { - HeaderOptions::WithHeaders(headers_raw) => parse_with_headers(lines, headers_raw), - HeaderOptions::WithoutHeaders => parse_without_headers(lines.collect()), - } -} - -fn parse_separated_columns<'a>( - lines: impl Iterator, - headers: HeaderOptions, - separator: &str, -) -> Vec> { - fn collect<'a>( - headers: Vec, - rows: impl Iterator, - separator: &str, - ) -> Vec> { - rows.map(|r| { - headers - .iter() - .zip(r.split(separator).map(str::trim).filter(|s| !s.is_empty())) - .map(|(a, b)| (a.to_owned(), b.to_owned())) - .collect() - }) - .collect() - } - - let parse_with_headers = |lines, headers_raw: &str| { - let headers = headers_raw - .split(&separator) - .map(str::trim) - .map(str::to_owned) - .filter(|s| !s.is_empty()) - .collect(); - collect(headers, lines, separator) - }; - - let parse_without_headers = |ls: Vec<&str>| { - let num_columns = ls.iter().map(|r| r.len()).max().unwrap_or(0); - - let headers = (1..=num_columns) - .map(|i| format!("Column{}", i)) - .collect::>(); - collect(headers, ls.into_iter(), separator) - }; - - match headers { - HeaderOptions::WithHeaders(headers_raw) => parse_with_headers(lines, headers_raw), - HeaderOptions::WithoutHeaders => parse_without_headers(lines.collect()), - } -} - -fn string_to_table( - s: &str, - noheaders: bool, - aligned_columns: bool, - split_at: usize, -) -> Vec> { - let mut lines = s.lines().filter(|l| !l.trim().is_empty()); - let separator = " ".repeat(std::cmp::max(split_at, 1)); - - let (ls, header_options) = if noheaders { - (lines, HeaderOptions::WithoutHeaders) - } else { - match lines.next() { - Some(header) => (lines, HeaderOptions::WithHeaders(header)), - None => return vec![], - } - }; - - let f = if aligned_columns { - parse_aligned_columns - } else { - parse_separated_columns - }; - - f(ls, header_options, &separator) -} - -fn from_ssv_string_to_value( - s: &str, - noheaders: bool, - aligned_columns: bool, - split_at: usize, - tag: impl Into, -) -> Value { - let tag = tag.into(); - let rows = string_to_table(s, noheaders, aligned_columns, split_at) - .iter() - .map(|row| { - let mut tagged_dict = TaggedDictBuilder::new(&tag); - for (col, entry) in row { - tagged_dict.insert_value( - col, - UntaggedValue::Primitive(Primitive::String(String::from(entry))) - .into_value(&tag), - ) - } - tagged_dict.into_value() - }) - .collect(); - - UntaggedValue::Table(rows).into_value(&tag) -} - -fn from_ssv(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - - let noheaders = args.has_flag("noheaders"); - let aligned_columns = args.has_flag("aligned-columns"); - let minimum_spaces: Option> = args.get_flag("minimum-spaces")?; - - let concat_string = args.input.collect_string(name.clone())?; - let split_at = match minimum_spaces { - Some(number) => number.item, - None => DEFAULT_MINIMUM_SPACES, - }; - - Ok( - match from_ssv_string_to_value( - &concat_string.item, - noheaders, - aligned_columns, - split_at, - name, - ) { - Value { - value: UntaggedValue::Table(list), - .. - } => list.into_iter().into_output_stream(), - x => OutputStream::one(x), - }, - ) -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::*; - - fn owned(x: &str, y: &str) -> (String, String) { - (String::from(x), String::from(y)) - } - - #[test] - fn it_trims_empty_and_whitespace_only_lines() { - let input = r#" - - a b - - 1 2 - - 3 4 - "#; - let result = string_to_table(input, false, true, 1); - assert_eq!( - result, - vec![ - vec![owned("a", "1"), owned("b", "2")], - vec![owned("a", "3"), owned("b", "4")] - ] - ); - } - - #[test] - fn it_deals_with_single_column_input() { - let input = r#" - a - 1 - 2 - "#; - let result = string_to_table(input, false, true, 1); - assert_eq!(result, vec![vec![owned("a", "1")], vec![owned("a", "2")]]); - } - - #[test] - fn it_uses_first_row_as_data_when_noheaders() { - let input = r#" - a b - 1 2 - 3 4 - "#; - let result = string_to_table(input, true, true, 1); - assert_eq!( - result, - vec![ - vec![owned("Column1", "a"), owned("Column2", "b")], - vec![owned("Column1", "1"), owned("Column2", "2")], - vec![owned("Column1", "3"), owned("Column2", "4")] - ] - ); - } - - #[test] - fn it_allows_a_predefined_number_of_spaces() { - let input = r#" - column a column b - entry 1 entry number 2 - 3 four - "#; - - let result = string_to_table(input, false, true, 3); - assert_eq!( - result, - vec![ - vec![ - owned("column a", "entry 1"), - owned("column b", "entry number 2") - ], - vec![owned("column a", "3"), owned("column b", "four")] - ] - ); - } - - #[test] - fn it_trims_remaining_separator_space() { - let input = r#" - colA colB colC - val1 val2 val3 - "#; - - let trimmed = |s: &str| s.trim() == s; - - let result = string_to_table(input, false, true, 2); - assert!(result - .iter() - .all(|row| row.iter().all(|(a, b)| trimmed(a) && trimmed(b)))); - } - - #[test] - fn it_keeps_empty_columns() { - let input = r#" - colA col B col C - val2 val3 - val4 val 5 val 6 - val7 val8 - "#; - - let result = string_to_table(input, false, true, 2); - assert_eq!( - result, - vec![ - vec![ - owned("colA", ""), - owned("col B", "val2"), - owned("col C", "val3") - ], - vec![ - owned("colA", "val4"), - owned("col B", "val 5"), - owned("col C", "val 6") - ], - vec![ - owned("colA", "val7"), - owned("col B", ""), - owned("col C", "val8") - ], - ] - ); - } - - #[test] - fn it_can_produce_an_empty_stream_for_header_only_input() { - let input = "colA col B"; - - let result = string_to_table(input, false, true, 2); - let expected: Vec> = vec![]; - assert_eq!(expected, result); - } - - #[test] - fn it_uses_the_full_final_column() { - let input = r#" - colA col B - val1 val2 trailing value that should be included - "#; - - let result = string_to_table(input, false, true, 2); - assert_eq!( - result, - vec![vec![ - owned("colA", "val1"), - owned("col B", "val2 trailing value that should be included"), - ]] - ); - } - - #[test] - fn it_handles_empty_values_when_noheaders_and_aligned_columns() { - let input = r#" - a multi-word value b d - 1 3-3 4 - last - "#; - - let result = string_to_table(input, true, true, 2); - assert_eq!( - result, - vec![ - vec![ - owned("Column1", "a multi-word value"), - owned("Column2", "b"), - owned("Column3", ""), - owned("Column4", "d"), - owned("Column5", "") - ], - vec![ - owned("Column1", "1"), - owned("Column2", ""), - owned("Column3", "3-3"), - owned("Column4", "4"), - owned("Column5", "") - ], - vec![ - owned("Column1", ""), - owned("Column2", ""), - owned("Column3", ""), - owned("Column4", ""), - owned("Column5", "last") - ], - ] - ); - } - - #[test] - fn input_is_parsed_correctly_if_either_option_works() { - let input = r#" - docker-registry docker-registry=default docker-registry=default 172.30.78.158 5000/TCP - kubernetes component=apiserver,provider=kubernetes 172.30.0.2 443/TCP - kubernetes-ro component=apiserver,provider=kubernetes 172.30.0.1 80/TCP - "#; - - let aligned_columns_noheaders = string_to_table(input, true, true, 2); - let separator_noheaders = string_to_table(input, true, false, 2); - let aligned_columns_with_headers = string_to_table(input, false, true, 2); - let separator_with_headers = string_to_table(input, false, false, 2); - assert_eq!(aligned_columns_noheaders, separator_noheaders); - assert_eq!(aligned_columns_with_headers, separator_with_headers); - } - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use super::FromSsv; - use crate::examples::test as test_examples; - - test_examples(FromSsv {}) - } -} diff --git a/crates/nu-command/src/commands/formats/from/toml.rs b/crates/nu-command/src/commands/formats/from/toml.rs deleted file mode 100644 index f65cd3d98f..0000000000 --- a/crates/nu-command/src/commands/formats/from/toml.rs +++ /dev/null @@ -1,82 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, UntaggedValue, Value}; - -#[derive(Debug, thiserror::Error)] -pub enum DeserializationError { - #[error("Failed to parse input as TOML")] - Toml(#[from] toml::de::Error), - - #[error("Failed to convert to a nushell value")] - Nu(#[from] Box), -} - -pub struct FromToml; - -impl WholeStreamCommand for FromToml { - fn name(&self) -> &str { - "from toml" - } - - fn signature(&self) -> Signature { - Signature::build("from toml") - } - - fn usage(&self) -> &str { - "Parse text as .toml and create table." - } - - fn run(&self, args: CommandArgs) -> Result { - from_toml(args) - } -} - -pub fn from_toml_string_to_value( - s: String, - tag: impl Into, -) -> Result { - let v: toml::Value = s.parse::()?; - - Ok(nu_serde::to_value(v, tag).map_err(Box::new)?) -} - -pub fn from_toml(args: CommandArgs) -> Result { - let tag = args.name_tag(); - let input = args.input; - - let concat_string = input.collect_string(tag.clone())?; - Ok( - match from_toml_string_to_value(concat_string.item, tag.clone()) { - Ok(x) => match x { - Value { - value: UntaggedValue::Table(list), - .. - } => list.into_iter().into_output_stream(), - x => OutputStream::one(x), - }, - Err(_) => { - return Err(ShellError::labeled_error_with_secondary( - "Could not parse as TOML", - "input cannot be parsed as TOML", - &tag, - "value originates from here", - concat_string.tag, - )) - } - }, - ) -} - -#[cfg(test)] -mod tests { - use super::FromToml; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(FromToml {}) - } -} diff --git a/crates/nu-command/src/commands/formats/from/tsv.rs b/crates/nu-command/src/commands/formats/from/tsv.rs deleted file mode 100644 index 4c9c8b6104..0000000000 --- a/crates/nu-command/src/commands/formats/from/tsv.rs +++ /dev/null @@ -1,50 +0,0 @@ -use super::delimited::from_delimited_data; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::Signature; - -pub struct FromTsv; - -impl WholeStreamCommand for FromTsv { - fn name(&self) -> &str { - "from tsv" - } - - fn signature(&self) -> Signature { - Signature::build("from tsv").switch( - "noheaders", - "don't treat the first row as column names", - Some('n'), - ) - } - - fn usage(&self) -> &str { - "Parse text as .tsv and create table." - } - - fn run(&self, args: CommandArgs) -> Result { - from_tsv(args) - } -} - -fn from_tsv(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - let noheaders = args.has_flag("noheaders"); - let input = args.input; - - from_delimited_data(noheaders, '\t', "TSV", input, name) -} - -#[cfg(test)] -mod tests { - use super::FromTsv; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(FromTsv {}) - } -} diff --git a/crates/nu-command/src/commands/formats/from/url.rs b/crates/nu-command/src/commands/formats/from/url.rs deleted file mode 100644 index f3c9e9bace..0000000000 --- a/crates/nu-command/src/commands/formats/from/url.rs +++ /dev/null @@ -1,65 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, TaggedDictBuilder, UntaggedValue}; - -pub struct FromUrl; - -impl WholeStreamCommand for FromUrl { - fn name(&self) -> &str { - "from url" - } - - fn signature(&self) -> Signature { - Signature::build("from url") - } - - fn usage(&self) -> &str { - "Parse url-encoded string as a table." - } - - fn run(&self, args: CommandArgs) -> Result { - from_url(args) - } -} - -fn from_url(args: CommandArgs) -> Result { - let tag = args.name_tag(); - let input = args.input; - - let concat_string = input.collect_string(tag.clone())?; - - let result = serde_urlencoded::from_str::>(&concat_string.item); - - match result { - Ok(result) => { - let mut row = TaggedDictBuilder::new(tag); - - for (k, v) in result { - row.insert_untagged(k, UntaggedValue::string(v)); - } - - Ok(OutputStream::one(row.into_value())) - } - _ => Err(ShellError::labeled_error_with_secondary( - "String not compatible with url-encoding", - "input not url-encoded", - tag, - "value originates from here", - concat_string.tag, - )), - } -} - -#[cfg(test)] -mod tests { - use super::FromUrl; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(FromUrl {}) - } -} diff --git a/crates/nu-command/src/commands/formats/from/vcf.rs b/crates/nu-command/src/commands/formats/from/vcf.rs deleted file mode 100644 index f5b7d1f249..0000000000 --- a/crates/nu-command/src/commands/formats/from/vcf.rs +++ /dev/null @@ -1,106 +0,0 @@ -use crate::prelude::*; -use ical::parser::vcard::component::*; -use ical::property::Property; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Primitive, Signature, TaggedDictBuilder, UntaggedValue, Value}; - -pub struct FromVcf; - -impl WholeStreamCommand for FromVcf { - fn name(&self) -> &str { - "from vcf" - } - - fn signature(&self) -> Signature { - Signature::build("from vcf") - } - - fn usage(&self) -> &str { - "Parse text as .vcf and create table." - } - - fn run(&self, args: CommandArgs) -> Result { - from_vcf(args) - } -} - -fn from_vcf(args: CommandArgs) -> Result { - let tag = args.name_tag(); - let input = args.input; - - let input_string = input.collect_string(tag.clone())?.item; - let input_bytes = input_string.into_bytes(); - let cursor = std::io::Cursor::new(input_bytes); - let parser = ical::VcardParser::new(cursor); - - let iter = parser.map(move |contact| match contact { - Ok(c) => contact_to_value(c, tag.clone()), - Err(_) => Value::error(ShellError::labeled_error( - "Could not parse as .vcf", - "input cannot be parsed as .vcf", - tag.clone(), - )), - }); - - let collected: Vec<_> = iter.collect(); - - Ok(collected.into_iter().into_output_stream()) -} - -fn contact_to_value(contact: VcardContact, tag: Tag) -> Value { - let mut row = TaggedDictBuilder::new(tag.clone()); - row.insert_untagged("properties", properties_to_value(contact.properties, tag)); - row.into_value() -} - -fn properties_to_value(properties: Vec, tag: Tag) -> UntaggedValue { - UntaggedValue::table( - &properties - .into_iter() - .map(|prop| { - let mut row = TaggedDictBuilder::new(tag.clone()); - - let name = UntaggedValue::string(prop.name); - let value = match prop.value { - Some(val) => UntaggedValue::string(val), - None => UntaggedValue::Primitive(Primitive::Nothing), - }; - let params = match prop.params { - Some(param_list) => params_to_value(param_list, tag.clone()).into(), - None => UntaggedValue::Primitive(Primitive::Nothing), - }; - - row.insert_untagged("name", name); - row.insert_untagged("value", value); - row.insert_untagged("params", params); - row.into_value() - }) - .collect::>(), - ) -} - -fn params_to_value(params: Vec<(String, Vec)>, tag: Tag) -> Value { - let mut row = TaggedDictBuilder::new(tag); - - for (param_name, param_values) in params { - let values: Vec = param_values.into_iter().map(|val| val.into()).collect(); - let values = UntaggedValue::table(&values); - row.insert_untagged(param_name, values); - } - - row.into_value() -} - -#[cfg(test)] -mod tests { - use super::FromVcf; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(FromVcf {}) - } -} diff --git a/crates/nu-command/src/commands/formats/from/xlsx.rs b/crates/nu-command/src/commands/formats/from/xlsx.rs deleted file mode 100644 index 0bf2c0d6bb..0000000000 --- a/crates/nu-command/src/commands/formats/from/xlsx.rs +++ /dev/null @@ -1,127 +0,0 @@ -use crate::prelude::*; -use calamine::*; -use nu_data::TaggedListBuilder; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Primitive, Signature, SyntaxShape, TaggedDictBuilder, UntaggedValue, Value}; -use std::io::Cursor; - -pub struct FromXlsx; - -impl WholeStreamCommand for FromXlsx { - fn name(&self) -> &str { - "from xlsx" - } - - fn signature(&self) -> Signature { - Signature::build("from xlsx") - .switch( - "noheaders", - "don't treat the first row as column names", - Some('n'), - ) - .named( - "sheets", - SyntaxShape::Table, - "Only convert specified sheets", - Some('s'), - ) - } - - fn usage(&self) -> &str { - "Parse binary Excel(.xlsx) data and create table." - } - - fn run(&self, args: CommandArgs) -> Result { - from_xlsx(args) - } -} - -// Adapted from crates/nu-command/src/commands/dataframe/utils.rs -fn convert_columns(columns: &[Value]) -> Result, ShellError> { - let res = columns - .iter() - .map(|value| match &value.value { - UntaggedValue::Primitive(Primitive::String(s)) => Ok(s.clone()), - _ => Err(ShellError::labeled_error( - "Incorrect column format", - "Only string as column name", - &value.tag, - )), - }) - .collect::, _>>()?; - - Ok(res) -} - -fn from_xlsx(args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let span = tag.span; - - let mut sel_sheets = vec![]; - - if let Some(columns) = args.get_flag::>("sheets")? { - sel_sheets = convert_columns(columns.as_slice())?; - } - - let value = args.input.collect_binary(tag.clone())?; - - let buf: Cursor> = Cursor::new(value.item); - let mut xls = Xlsx::<_>::new(buf).map_err(|_| { - ShellError::labeled_error("Could not load xlsx file", "could not load xlsx file", &tag) - })?; - - let mut dict = TaggedDictBuilder::new(&tag); - - let mut sheet_names = xls.sheet_names().to_owned(); - if !sel_sheets.is_empty() { - sheet_names.retain(|e| sel_sheets.contains(e)); - } - - for sheet_name in &sheet_names { - let mut sheet_output = TaggedListBuilder::new(&tag); - - if let Some(Ok(current_sheet)) = xls.worksheet_range(sheet_name) { - for row in current_sheet.rows() { - let mut row_output = TaggedDictBuilder::new(&tag); - for (i, cell) in row.iter().enumerate() { - let value = match cell { - DataType::Empty => UntaggedValue::nothing(), - DataType::String(s) => UntaggedValue::string(s), - DataType::Float(f) => UntaggedValue::decimal_from_float(*f, span), - DataType::Int(i) => UntaggedValue::int(*i), - DataType::Bool(b) => UntaggedValue::boolean(*b), - _ => UntaggedValue::nothing(), - }; - - row_output.insert_untagged(&format!("Column{}", i), value); - } - - sheet_output.push_untagged(row_output.into_untagged_value()); - } - - dict.insert_untagged(sheet_name, sheet_output.into_untagged_value()); - } else { - return Err(ShellError::labeled_error( - "Could not load sheet", - "could not load sheet", - &tag, - )); - } - } - - Ok(OutputStream::one(dict.into_value())) -} - -#[cfg(test)] -mod tests { - use super::FromXlsx; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(FromXlsx {}) - } -} diff --git a/crates/nu-command/src/commands/formats/from/xml.rs b/crates/nu-command/src/commands/formats/from/xml.rs deleted file mode 100644 index 34a94c6fc3..0000000000 --- a/crates/nu-command/src/commands/formats/from/xml.rs +++ /dev/null @@ -1,302 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Primitive, Signature, TaggedDictBuilder, UntaggedValue, Value}; - -pub struct FromXml; - -impl WholeStreamCommand for FromXml { - fn name(&self) -> &str { - "from xml" - } - - fn signature(&self) -> Signature { - Signature::build("from xml") - } - - fn usage(&self) -> &str { - "Parse text as .xml and create table." - } - - fn run(&self, args: CommandArgs) -> Result { - from_xml(args) - } -} - -fn from_attributes_to_value(attributes: &[roxmltree::Attribute], tag: impl Into) -> Value { - let tag = tag.into(); - - let mut collected = TaggedDictBuilder::new(tag); - for a in attributes { - collected.insert_untagged(String::from(a.name()), UntaggedValue::string(a.value())); - } - - collected.into_value() -} - -fn from_node_to_value(n: &roxmltree::Node, tag: impl Into) -> Value { - let tag = tag.into(); - - if n.is_element() { - let name = n.tag_name().name().trim().to_string(); - - let mut children_values = vec![]; - for c in n.children() { - children_values.push(from_node_to_value(&c, &tag)); - } - - let children_values: Vec = children_values - .into_iter() - .filter(|x| match x { - Value { - value: UntaggedValue::Primitive(Primitive::String(f)), - .. - } => { - !f.trim().is_empty() // non-whitespace characters? - } - _ => true, - }) - .collect(); - - let mut collected = TaggedDictBuilder::new(&tag); - - let attribute_value: Value = from_attributes_to_value(n.attributes(), &tag); - - let mut row = TaggedDictBuilder::new(&tag); - row.insert_untagged( - String::from("children"), - UntaggedValue::Table(children_values), - ); - row.insert_untagged(String::from("attributes"), attribute_value); - collected.insert_untagged(name, row.into_value()); - - collected.into_value() - } else if n.is_comment() { - UntaggedValue::string("").into_value(tag) - } else if n.is_pi() { - UntaggedValue::string("").into_value(tag) - } else if n.is_text() { - match n.text() { - Some(text) => UntaggedValue::string(text).into_value(tag), - None => UntaggedValue::string("").into_value(tag), - } - } else { - UntaggedValue::string("").into_value(tag) - } -} - -fn from_document_to_value(d: &roxmltree::Document, tag: impl Into) -> Value { - from_node_to_value(&d.root_element(), tag) -} - -pub fn from_xml_string_to_value(s: String, tag: impl Into) -> Result { - let parsed = roxmltree::Document::parse(&s)?; - Ok(from_document_to_value(&parsed, tag)) -} - -fn from_xml(args: CommandArgs) -> Result { - let tag = args.name_tag(); - let input = args.input; - - let concat_string = input.collect_string(tag.clone())?; - - Ok( - match from_xml_string_to_value(concat_string.item, tag.clone()) { - Ok(x) => match x { - Value { - value: UntaggedValue::Table(list), - .. - } => list.into_iter().into_output_stream(), - x => OutputStream::one(x), - }, - Err(_) => { - return Err(ShellError::labeled_error_with_secondary( - "Could not parse as XML", - "input cannot be parsed as XML", - &tag, - "value originates from here", - &concat_string.tag, - )) - } - }, - ) -} - -#[cfg(test)] -mod tests { - use super::*; - - use indexmap::IndexMap; - use nu_protocol::{UntaggedValue, Value}; - - fn string(input: impl Into) -> Value { - UntaggedValue::string(input.into()).into_untagged_value() - } - - fn row(entries: IndexMap) -> Value { - UntaggedValue::row(entries).into_untagged_value() - } - - fn table(list: &[Value]) -> Value { - UntaggedValue::table(list).into_untagged_value() - } - - fn parse(xml: &str) -> Result { - from_xml_string_to_value(xml.to_string(), Tag::unknown()) - } - - #[test] - fn parses_empty_element() -> Result<(), roxmltree::Error> { - let source = ""; - - assert_eq!( - parse(source)?, - row(indexmap! { - "nu".into() => row(indexmap! { - "children".into() => table(&[]), - "attributes".into() => row(indexmap! {}) - }) - }) - ); - - Ok(()) - } - - #[test] - fn parses_element_with_text() -> Result<(), roxmltree::Error> { - let source = "La era de los tres caballeros"; - - assert_eq!( - parse(source)?, - row(indexmap! { - "nu".into() => row(indexmap! { - "children".into() => table(&[string("La era de los tres caballeros")]), - "attributes".into() => row(indexmap! {}) - }) - }) - ); - - Ok(()) - } - - #[test] - fn parses_element_with_elements() -> Result<(), roxmltree::Error> { - let source = "\ - - Andrés - Jonathan - Yehuda -"; - - assert_eq!( - parse(source)?, - row(indexmap! { - "nu".into() => row(indexmap! { - "children".into() => table(&[ - row(indexmap! { - "dev".into() => row(indexmap! { - "children".into() => table(&[string("Andrés")]), - "attributes".into() => row(indexmap! {}) - }) - }), - row(indexmap! { - "dev".into() => row(indexmap! { - "children".into() => table(&[string("Jonathan")]), - "attributes".into() => row(indexmap! {}) - }) - }), - row(indexmap! { - "dev".into() => row(indexmap! { - "children".into() => table(&[string("Yehuda")]), - "attributes".into() => row(indexmap! {}) - }) - }) - ]), - "attributes".into() => row(indexmap! {}) - }) - }) - ); - - Ok(()) - } - - #[test] - fn parses_element_with_attribute() -> Result<(), roxmltree::Error> { - let source = "\ - -"; - - assert_eq!( - parse(source)?, - row(indexmap! { - "nu".into() => row(indexmap! { - "children".into() => table(&[]), - "attributes".into() => row(indexmap! { - "version".into() => string("2.0") - }) - }) - }) - ); - - Ok(()) - } - - #[test] - fn parses_element_with_attribute_and_element() -> Result<(), roxmltree::Error> { - let source = "\ - - 2.0 -"; - - assert_eq!( - parse(source)?, - row(indexmap! { - "nu".into() => row(indexmap! { - "children".into() => table(&[ - row(indexmap! { - "version".into() => row(indexmap! { - "children".into() => table(&[string("2.0")]), - "attributes".into() => row(indexmap! {}) - }) - }) - ]), - "attributes".into() => row(indexmap! { - "version".into() => string("2.0") - }) - }) - }) - ); - - Ok(()) - } - - #[test] - fn parses_element_with_multiple_attributes() -> Result<(), roxmltree::Error> { - let source = "\ - -"; - - assert_eq!( - parse(source)?, - row(indexmap! { - "nu".into() => row(indexmap! { - "children".into() => table(&[]), - "attributes".into() => row(indexmap! { - "version".into() => string("2.0"), - "age".into() => string("25") - }) - }) - }) - ); - - Ok(()) - } - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use super::FromXml; - use crate::examples::test as test_examples; - - test_examples(FromXml {}) - } -} diff --git a/crates/nu-command/src/commands/formats/from/yaml.rs b/crates/nu-command/src/commands/formats/from/yaml.rs deleted file mode 100644 index b371074a10..0000000000 --- a/crates/nu-command/src/commands/formats/from/yaml.rs +++ /dev/null @@ -1,258 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Primitive, Signature, TaggedDictBuilder, UntaggedValue, Value}; - -pub struct FromYaml; - -impl WholeStreamCommand for FromYaml { - fn name(&self) -> &str { - "from yaml" - } - - fn signature(&self) -> Signature { - Signature::build("from yaml") - } - - fn usage(&self) -> &str { - "Parse text as .yaml/.yml and create table." - } - - fn run(&self, args: CommandArgs) -> Result { - from_yaml(args) - } -} - -pub struct FromYml; - -impl WholeStreamCommand for FromYml { - fn name(&self) -> &str { - "from yml" - } - - fn signature(&self) -> Signature { - Signature::build("from yml") - } - - fn usage(&self) -> &str { - "Parse text as .yaml/.yml and create table." - } - - fn run(&self, args: CommandArgs) -> Result { - from_yaml(args) - } -} - -fn convert_yaml_value_to_nu_value( - v: &serde_yaml::Value, - tag: impl Into, -) -> Result { - let tag = tag.into(); - let span = tag.span; - - 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(err_not_compatible_number)?).into_value(tag) - } - serde_yaml::Value::Number(n) if n.is_f64() => { - UntaggedValue::decimal_from_float(n.as_f64().ok_or(err_not_compatible_number)?, span) - .into_value(tag) - } - serde_yaml::Value::String(s) => UntaggedValue::string(s).into_value(tag), - serde_yaml::Value::Sequence(a) => { - let result: Result, ShellError> = a - .iter() - .map(|x| convert_yaml_value_to_nu_value(x, &tag)) - .collect(); - UntaggedValue::Table(result?).into_value(tag) - } - serde_yaml::Value::Mapping(t) => { - let mut collected = TaggedDictBuilder::new(&tag); - - for (k, v) in t { - // 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)?); - } - // 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); - } - } - } - - collected.into_value() - } - serde_yaml::Value::Null => UntaggedValue::Primitive(Primitive::Nothing).into_value(tag), - x => unimplemented!("Unsupported yaml case: {:?}", x), - }) -} - -pub fn from_yaml_string_to_value(s: String, tag: impl Into) -> Result { - let tag = tag.into(); - - let mut documents = vec![]; - - for document in serde_yaml::Deserializer::from_str(&s) { - let v: serde_yaml::Value = serde_yaml::Value::deserialize(document).map_err(|x| { - ShellError::labeled_error( - format!("Could not load yaml: {}", x), - "could not load yaml from text", - &tag, - ) - })?; - - documents.push(convert_yaml_value_to_nu_value(&v, tag.clone())?); - } - - match documents.len() { - 0 => Ok(UntaggedValue::nothing().into_value(tag)), - 1 => Ok(documents.remove(0)), - _ => Ok(UntaggedValue::Table(documents).into_value(tag)), - } -} - -fn from_yaml(args: CommandArgs) -> Result { - let tag = args.name_tag(); - let input = args.input; - - let concat_string = input.collect_string(tag.clone())?; - - match from_yaml_string_to_value(concat_string.item, tag.clone()) { - Ok(x) => match x { - Value { - value: UntaggedValue::Table(list), - .. - } => Ok(list.into_iter().into_output_stream()), - x => Ok(OutputStream::one(x)), - }, - Err(_) => Err(ShellError::labeled_error_with_secondary( - "Could not parse as YAML", - "input cannot be parsed as YAML", - &tag, - "value originates from here", - &concat_string.tag, - )), - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::*; - use nu_protocol::row; - use nu_test_support::value::int; - use nu_test_support::value::row; - use nu_test_support::value::string; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - 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 { - 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); - } - } - } - - #[test] - fn test_empty_yaml() { - let input = ""; - - let expected = UntaggedValue::nothing().into_value(Tag::default()); - - let actual = from_yaml_string_to_value(input.to_owned(), Tag::default()); - - assert_eq!(Ok(expected), actual); - } - - #[test] - fn test_single_doc_yaml() { - let input = "k: 107\nj: '106'\n"; - - let expected = - UntaggedValue::row(indexmap! {"k".into() => int(107), "j".into() => string("106") }) - .into_value(Tag::default()); - - let actual = from_yaml_string_to_value(input.to_owned(), Tag::default()); - - assert_eq!(Ok(expected), actual); - } - - #[test] - fn test_multidoc_yaml() { - let input = "---\nk: 107\n---\nj: '106'\n"; - - let expected = UntaggedValue::table(&[ - row(indexmap! {"k".into() => int(107) }), - row(indexmap! {"j".into() => string("106") }), - ]) - .into_value(Tag::default()); - - let actual = from_yaml_string_to_value(input.to_owned(), Tag::default()); - - assert_eq!(Ok(expected), actual); - } -} diff --git a/crates/nu-command/src/commands/formats/to/command.rs b/crates/nu-command/src/commands/formats/to/command.rs deleted file mode 100644 index 75573f0984..0000000000 --- a/crates/nu-command/src/commands/formats/to/command.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, UntaggedValue}; - -#[derive(Clone)] -pub struct To; - -impl WholeStreamCommand for To { - fn name(&self) -> &str { - "to" - } - - fn signature(&self) -> Signature { - Signature::build("to") - } - - fn usage(&self) -> &str { - "Convert table into an output format (based on subcommand, like csv, html, json, yaml)." - } - - fn run(&self, args: CommandArgs) -> Result { - Ok(OutputStream::one( - UntaggedValue::string(get_full_help(&To, args.scope())).into_value(Tag::unknown()), - )) - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::To; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(To {}) - } -} diff --git a/crates/nu-command/src/commands/formats/to/csv.rs b/crates/nu-command/src/commands/formats/to/csv.rs deleted file mode 100644 index b7069c051d..0000000000 --- a/crates/nu-command/src/commands/formats/to/csv.rs +++ /dev/null @@ -1,80 +0,0 @@ -use crate::commands::formats::to::delimited::to_delimited_data; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Primitive, Signature, SyntaxShape, UntaggedValue, Value}; - -pub struct ToCsv; - -impl WholeStreamCommand for ToCsv { - fn name(&self) -> &str { - "to csv" - } - - fn signature(&self) -> Signature { - Signature::build("to csv") - .named( - "separator", - SyntaxShape::String, - "a character to separate columns, defaults to ','", - Some('s'), - ) - .switch( - "noheaders", - "do not output the columns names as the first row", - Some('n'), - ) - } - - fn usage(&self) -> &str { - "Convert table into .csv text " - } - - fn run(&self, args: CommandArgs) -> Result { - to_csv(args) - } -} - -fn to_csv(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - let separator: Option = args.get_flag("separator")?; - let noheaders = args.has_flag("noheaders"); - let input = args.input; - let sep = match separator { - Some(Value { - value: UntaggedValue::Primitive(Primitive::String(s)), - tag, - .. - }) => { - if s == r"\t" { - '\t' - } else { - let vec_s: Vec = s.chars().collect(); - if vec_s.len() != 1 { - return Err(ShellError::labeled_error( - "Expected a single separator char from --separator", - "requires a single character string input", - tag, - )); - }; - vec_s[0] - } - } - _ => ',', - }; - - to_delimited_data(noheaders, sep, "CSV", input, name) -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::ToCsv; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(ToCsv {}) - } -} diff --git a/crates/nu-command/src/commands/formats/to/delimited.rs b/crates/nu-command/src/commands/formats/to/delimited.rs deleted file mode 100644 index 2b48d61f17..0000000000 --- a/crates/nu-command/src/commands/formats/to/delimited.rs +++ /dev/null @@ -1,220 +0,0 @@ -use crate::prelude::*; -use csv::WriterBuilder; -use indexmap::{indexset, IndexSet}; -use nu_errors::ShellError; -use nu_protocol::{Primitive, UntaggedValue, Value}; -use nu_source::Spanned; -use nu_value_ext::{as_string, ValueExt}; - -fn from_value_to_delimited_string( - tagged_value: &Value, - separator: char, -) -> Result { - let v = &tagged_value.value; - - match v { - UntaggedValue::Row(o) => { - let mut wtr = WriterBuilder::new() - .delimiter(separator as u8) - .from_writer(vec![]); - let mut fields: VecDeque = VecDeque::new(); - let mut values: VecDeque = VecDeque::new(); - - for (k, v) in &o.entries { - fields.push_back(k.clone()); - - values.push_back(to_string_tagged_value(v)?); - } - - wtr.write_record(fields).expect("can not write."); - wtr.write_record(values).expect("can not write."); - - let v = String::from_utf8(wtr.into_inner().map_err(|_| { - ShellError::labeled_error( - "Could not convert record", - "original value", - &tagged_value.tag, - ) - })?) - .map_err(|_| { - ShellError::labeled_error( - "Could not convert record", - "original value", - &tagged_value.tag, - ) - })?; - Ok(v) - } - UntaggedValue::Table(list) => { - let mut wtr = WriterBuilder::new() - .delimiter(separator as u8) - .from_writer(vec![]); - - let merged_descriptors = merge_descriptors(list); - - if merged_descriptors.is_empty() { - wtr.write_record( - list.iter() - .map(|ele| to_string_tagged_value(ele).unwrap_or_else(|_| String::new())) - .collect::>(), - ) - .expect("can not write"); - } else { - wtr.write_record(merged_descriptors.iter().map(|item| &item.item[..])) - .expect("can not write."); - - for l in list { - let mut row = vec![]; - for desc in &merged_descriptors { - row.push(match l.get_data_by_key(desc.borrow_spanned()) { - Some(s) => to_string_tagged_value(&s)?, - None => String::new(), - }); - } - wtr.write_record(&row).expect("can not write"); - } - } - let v = String::from_utf8(wtr.into_inner().map_err(|_| { - ShellError::labeled_error( - "Could not convert record", - "original value", - &tagged_value.tag, - ) - })?) - .map_err(|_| { - ShellError::labeled_error( - "Could not convert record", - "original value", - &tagged_value.tag, - ) - })?; - Ok(v) - } - _ => to_string_tagged_value(tagged_value), - } -} - -// NOTE: could this be useful more widely and implemented on Value ? -pub fn clone_tagged_value(v: &Value) -> Value { - match &v.value { - UntaggedValue::Primitive(Primitive::String(s)) => { - UntaggedValue::Primitive(Primitive::String(s.clone())) - } - UntaggedValue::Primitive(Primitive::Nothing) => { - UntaggedValue::Primitive(Primitive::Nothing) - } - UntaggedValue::Primitive(Primitive::Boolean(b)) => { - UntaggedValue::Primitive(Primitive::Boolean(*b)) - } - UntaggedValue::Primitive(Primitive::Decimal(f)) => { - UntaggedValue::Primitive(Primitive::Decimal(f.clone())) - } - UntaggedValue::Primitive(Primitive::Int(i)) => UntaggedValue::Primitive(Primitive::Int(*i)), - UntaggedValue::Primitive(Primitive::FilePath(x)) => { - UntaggedValue::Primitive(Primitive::FilePath(x.clone())) - } - UntaggedValue::Primitive(Primitive::Filesize(b)) => { - UntaggedValue::Primitive(Primitive::Filesize(*b)) - } - UntaggedValue::Primitive(Primitive::Date(d)) => { - UntaggedValue::Primitive(Primitive::Date(*d)) - } - UntaggedValue::Row(o) => UntaggedValue::Row(o.clone()), - UntaggedValue::Table(l) => UntaggedValue::Table(l.clone()), - UntaggedValue::Block(_) => UntaggedValue::Primitive(Primitive::Nothing), - _ => UntaggedValue::Primitive(Primitive::Nothing), - } - .into_value(v.tag.clone()) -} - -// NOTE: could this be useful more widely and implemented on Value ? -fn to_string_tagged_value(v: &Value) -> Result { - match &v.value { - UntaggedValue::Primitive( - Primitive::String(_) - | Primitive::Filesize(_) - | Primitive::Boolean(_) - | Primitive::Decimal(_) - | Primitive::FilePath(_) - | Primitive::Int(_) - | Primitive::BigInt(_), - ) => as_string(v), - UntaggedValue::Primitive(Primitive::Date(d)) => Ok(d.to_string()), - UntaggedValue::Primitive(Primitive::Nothing) => Ok(String::new()), - UntaggedValue::Table(_) => Ok(String::from("[Table]")), - UntaggedValue::Row(_) => Ok(String::from("[Row]")), - _ => Err(ShellError::labeled_error( - "Unexpected value", - "", - v.tag.clone(), - )), - } -} - -fn merge_descriptors(values: &[Value]) -> Vec> { - let mut ret: Vec> = vec![]; - let mut seen: IndexSet = indexset! {}; - for value in values { - for desc in value.data_descriptors() { - if !seen.contains(&desc) { - seen.insert(desc.clone()); - ret.push(desc.spanned(value.tag.span)); - } - } - } - ret -} - -pub fn to_delimited_data( - noheaders: bool, - sep: char, - format_name: &'static str, - input: InputStream, - name: Tag, -) -> Result { - let name_tag = name; - let name_span = name_tag.span; - - let input: Vec = input.collect(); - - let to_process_input = match input.len() { - x if x > 1 => { - let tag = input[0].tag.clone(); - vec![Value { - value: UntaggedValue::Table(input), - tag, - }] - } - 1 => input, - _ => vec![], - }; - - Ok((to_process_input.into_iter().map(move |value| { - match from_value_to_delimited_string(&clone_tagged_value(&value), sep) { - Ok(mut x) => { - if noheaders { - if let Some(second_line) = x.find('\n') { - let start = second_line + 1; - x.replace_range(0..start, ""); - } - } - UntaggedValue::Primitive(Primitive::String(x)).into_value(&name_tag) - } - Err(_) => { - let expected = format!( - "Expected a table with {}-compatible structure from pipeline", - format_name - ); - let requires = format!("requires {}-compatible input", format_name); - Value::error(ShellError::labeled_error_with_secondary( - expected, - requires, - name_span, - "originates from here".to_string(), - value.tag.span, - )) - } - } - })) - .into_output_stream()) -} diff --git a/crates/nu-command/src/commands/formats/to/html.rs b/crates/nu-command/src/commands/formats/to/html.rs deleted file mode 100644 index 991f17e69c..0000000000 --- a/crates/nu-command/src/commands/formats/to/html.rs +++ /dev/null @@ -1,743 +0,0 @@ -use crate::prelude::*; - -use nu_data::value::format_leaf; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Primitive, Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::{AnchorLocation, Tagged}; -use regex::Regex; -use rust_embed::RustEmbed; -use serde::{Deserialize, Serialize}; -use std::borrow::Cow; -use std::collections::HashMap; -use std::error::Error; -use std::fmt::Write; - -#[derive(Serialize, Deserialize, Debug)] -pub struct HtmlThemes { - themes: Vec, -} - -#[allow(non_snake_case)] -#[derive(Serialize, Deserialize, Debug)] -pub struct HtmlTheme { - name: String, - black: String, - red: String, - green: String, - yellow: String, - blue: String, - purple: String, - cyan: String, - white: String, - brightBlack: String, - brightRed: String, - brightGreen: String, - brightYellow: String, - brightBlue: String, - brightPurple: String, - brightCyan: String, - brightWhite: String, - background: String, - foreground: String, -} - -impl Default for HtmlThemes { - fn default() -> Self { - HtmlThemes { - themes: vec![HtmlTheme::default()], - } - } -} - -impl Default for HtmlTheme { - fn default() -> Self { - HtmlTheme { - name: "nu_default".to_string(), - black: "black".to_string(), - red: "red".to_string(), - green: "green".to_string(), - yellow: "#717100".to_string(), - blue: "blue".to_string(), - purple: "#c800c8".to_string(), - cyan: "#037979".to_string(), - white: "white".to_string(), - brightBlack: "black".to_string(), - brightRed: "red".to_string(), - brightGreen: "green".to_string(), - brightYellow: "#717100".to_string(), - brightBlue: "blue".to_string(), - brightPurple: "#c800c8".to_string(), - brightCyan: "#037979".to_string(), - brightWhite: "white".to_string(), - background: "white".to_string(), - foreground: "black".to_string(), - } - } -} - -#[derive(RustEmbed)] -#[folder = "assets/"] -struct Assets; - -pub struct ToHtml; - -impl WholeStreamCommand for ToHtml { - fn name(&self) -> &str { - "to html" - } - - fn signature(&self) -> Signature { - Signature::build("to html") - .switch("html_color", "change ansi colors to html colors", Some('c')) - .switch("no_color", "remove all ansi colors in output", Some('n')) - .switch( - "dark", - "indicate your background color is a darker color", - Some('d'), - ) - .switch( - "partial", - "only output the html for the content itself", - Some('p'), - ) - .named( - "theme", - SyntaxShape::String, - "the name of the theme to use (github, blulocolight, ...)", - Some('t'), - ) - .switch("list", "list the names of all available themes", Some('l')) - } - - fn usage(&self) -> &str { - "Convert table into simple HTML" - } - - fn run(&self, args: CommandArgs) -> Result { - to_html(args) - } -} - -fn get_theme_from_asset_file( - is_dark: bool, - theme: &Option>, - theme_tag: &Tag, -) -> Result, ShellError> { - let theme_name = match theme { - Some(s) => s.as_str(), - None => "default", // There is no theme named "default" so this will be HtmlTheme::default(), which is "nu_default". - }; - - // 228 themes come from - // https://github.com/mbadolato/iTerm2-Color-Schemes/tree/master/windowsterminal - // we should find a hit on any name in there - let asset = get_asset_by_name_as_html_themes("228_themes.zip", "228_themes.json"); - - // If asset doesn't work, make sure to return the default theme - let asset = match asset { - Ok(a) => a, - _ => HtmlThemes::default(), - }; - - // Find the theme by theme name - let th = asset - .themes - .iter() - .find(|&n| n.name.to_lowercase() == theme_name.to_lowercase()); // case insensitive search - - // If no theme is found by the name provided, ensure we return the default theme - let default_theme = HtmlTheme::default(); - let th = match th { - Some(t) => t, - None => &default_theme, - }; - - // this just means no theme was passed in - if th.name.to_lowercase().eq(&"nu_default".to_string()) - // this means there was a theme passed in - && theme.is_some() - { - return Err(ShellError::labeled_error( - "Error finding theme name", - "Error finding theme name", - theme_tag.span, - )); - } - - Ok(convert_html_theme_to_hash_map(is_dark, th)) -} - -#[allow(unused_variables)] -fn get_asset_by_name_as_html_themes( - zip_name: &str, - json_name: &str, -) -> Result> { - match Assets::get(zip_name) { - Some(content) => { - let asset: Vec = match content { - Cow::Borrowed(bytes) => bytes.into(), - Cow::Owned(bytes) => bytes, - }; - let reader = std::io::Cursor::new(asset); - #[cfg(feature = "zip")] - { - use std::io::Read; - let mut archive = zip::ZipArchive::new(reader)?; - let mut zip_file = archive.by_name(json_name)?; - let mut contents = String::new(); - zip_file.read_to_string(&mut contents)?; - Ok(serde_json::from_str(&contents)?) - } - #[cfg(not(feature = "zip"))] - { - let th = HtmlThemes::default(); - Ok(th) - } - } - None => { - let th = HtmlThemes::default(); - Ok(th) - } - } -} - -fn convert_html_theme_to_hash_map( - is_dark: bool, - theme: &HtmlTheme, -) -> HashMap<&'static str, String> { - let mut hm: HashMap<&str, String> = HashMap::new(); - - hm.insert("bold_black", theme.brightBlack[..].to_string()); - hm.insert("bold_red", theme.brightRed[..].to_string()); - hm.insert("bold_green", theme.brightGreen[..].to_string()); - hm.insert("bold_yellow", theme.brightYellow[..].to_string()); - hm.insert("bold_blue", theme.brightBlue[..].to_string()); - hm.insert("bold_magenta", theme.brightPurple[..].to_string()); - hm.insert("bold_cyan", theme.brightCyan[..].to_string()); - hm.insert("bold_white", theme.brightWhite[..].to_string()); - - hm.insert("black", theme.black[..].to_string()); - hm.insert("red", theme.red[..].to_string()); - hm.insert("green", theme.green[..].to_string()); - hm.insert("yellow", theme.yellow[..].to_string()); - hm.insert("blue", theme.blue[..].to_string()); - hm.insert("magenta", theme.purple[..].to_string()); - hm.insert("cyan", theme.cyan[..].to_string()); - hm.insert("white", theme.white[..].to_string()); - - // Try to make theme work with light or dark but - // flipping the foreground and background but leave - // the other colors the same. - if is_dark { - hm.insert("background", theme.black[..].to_string()); - hm.insert("foreground", theme.white[..].to_string()); - } else { - hm.insert("background", theme.white[..].to_string()); - hm.insert("foreground", theme.black[..].to_string()); - } - - hm -} - -fn get_list_of_theme_names() -> Vec { - let asset = get_asset_by_name_as_html_themes("228_themes.zip", "228_themes.json"); - - // If asset doesn't work, make sure to return the default theme - let html_themes = match asset { - Ok(a) => a, - _ => HtmlThemes::default(), - }; - - let theme_names: Vec = html_themes.themes.iter().map(|n| n.name.clone()).collect(); - - theme_names -} - -fn to_html(args: CommandArgs) -> Result { - let name_tag = args.call_info.name_tag.clone(); - let html_color = args.has_flag("html_color"); - let no_color = args.has_flag("no_color"); - let dark = args.has_flag("dark"); - let partial = args.has_flag("partial"); - let list = args.has_flag("list"); - let theme: Option> = args.get_flag("theme")?; - - let input: Vec = args.input.collect(); - let headers = nu_protocol::merge_descriptors(&input); - let headers = Some(headers) - .filter(|headers| !headers.is_empty() && (headers.len() > 1 || !headers[0].is_empty())); - let mut output_string = String::new(); - let mut regex_hm: HashMap = HashMap::new(); - - if list { - // Get the list of theme names - let theme_names = get_list_of_theme_names(); - - // Put that list into the output string - for s in &theme_names { - writeln!(&mut output_string, "{}", s).unwrap(); - } - - output_string.push_str("\nScreenshots of themes can be found here:\n"); - output_string.push_str("https://github.com/mbadolato/iTerm2-Color-Schemes\n"); - } else { - let theme_tag = match &theme { - Some(v) => &v.tag, - None => &name_tag, - }; - - let color_hm = get_theme_from_asset_file(dark, &theme, theme_tag); - let color_hm = match color_hm { - Ok(c) => c, - _ => { - return Err(ShellError::labeled_error( - "Error finding theme name", - "Error finding theme name", - theme_tag.span, - )) - } - }; - - // change the color of the page - if !partial { - write!( - &mut output_string, - r"", - color_hm - .get("background") - .expect("Error getting background color"), - color_hm - .get("foreground") - .expect("Error getting foreground color") - ) - .unwrap(); - } else { - write!( - &mut output_string, - "
", - color_hm - .get("background") - .expect("Error getting background color"), - color_hm - .get("foreground") - .expect("Error getting foreground color") - ) - .unwrap(); - } - - let inner_value = match input.len() { - 0 => String::default(), - 1 => match headers { - Some(headers) => html_table(input, headers), - None => { - let value = &input[0]; - html_value(value) - } - }, - _ => match headers { - Some(headers) => html_table(input, headers), - None => html_list(input), - }, - }; - - output_string.push_str(&inner_value); - - if !partial { - output_string.push_str(""); - } else { - output_string.push_str("
") - } - - // Check to see if we want to remove all color or change ansi to html colors - if html_color { - setup_html_color_regexes(&mut regex_hm, &color_hm); - output_string = run_regexes(®ex_hm, &output_string); - } else if no_color { - setup_no_color_regexes(&mut regex_hm); - output_string = run_regexes(®ex_hm, &output_string); - } - } - Ok(OutputStream::one( - UntaggedValue::string(output_string).into_value(name_tag), - )) -} - -fn html_list(list: Vec) -> String { - let mut output_string = String::new(); - output_string.push_str("
    "); - for value in list { - output_string.push_str("
  1. "); - output_string.push_str(&html_value(&value)); - output_string.push_str("
  2. "); - } - output_string.push_str("
"); - output_string -} - -fn html_table(table: Vec, headers: Vec) -> String { - let mut output_string = String::new(); - // Add grid lines to html - // let mut output_string = ""); - - output_string.push_str("
"); - - output_string.push_str(""); - for header in &headers { - output_string.push_str(""); - } - output_string.push_str(""); - - for row in table { - if let UntaggedValue::Row(row) = row.value { - output_string.push_str(""); - for header in &headers { - let data = row.get_data(header); - output_string.push_str(""); - } - output_string.push_str(""); - } - } - output_string.push_str("
"); - output_string.push_str(&htmlescape::encode_minimal(header)); - output_string.push_str("
"); - output_string.push_str(&html_value(data.borrow())); - output_string.push_str("
"); - - output_string -} - -fn html_value(value: &Value) -> String { - let mut output_string = String::new(); - match &value.value { - UntaggedValue::Primitive(Primitive::Binary(b)) => { - // This might be a bit much, but it's fun :) - match &value.tag.anchor { - Some(AnchorLocation::Url(f)) | Some(AnchorLocation::File(f)) => { - let extension = f.split('.').last().map(String::from); - match extension { - Some(s) - if ["png", "jpg", "bmp", "gif", "tiff", "jpeg"] - .contains(&s.to_lowercase().as_str()) => - { - output_string.push_str(""); - } - _ => { - let output = nu_pretty_hex::pretty_hex(&b); - - output_string.push_str("
");
-                            output_string.push_str(&output);
-                            output_string.push_str("
"); - } - } - } - _ => { - let output = nu_pretty_hex::pretty_hex(&b); - - output_string.push_str("
");
-                    output_string.push_str(&output);
-                    output_string.push_str("
"); - } - } - } - UntaggedValue::Primitive(Primitive::String(ref b)) => { - // This might be a bit much, but it's fun :) - match &value.tag.anchor { - Some(AnchorLocation::Url(f)) | Some(AnchorLocation::File(f)) => { - let extension = f.split('.').last().map(String::from); - match extension { - Some(s) if s.to_lowercase() == "svg" => { - output_string.push_str(""); - return output_string; - } - _ => {} - } - } - _ => {} - } - output_string.push_str( - &htmlescape::encode_minimal(&format_leaf(&value.value).plain_string(100_000)) - .replace("\n", "
"), - ); - } - other => output_string.push_str( - &htmlescape::encode_minimal(&format_leaf(other).plain_string(100_000)) - .replace("\n", "
"), - ), - } - output_string -} - -fn setup_html_color_regexes( - hash: &mut HashMap, - color_hm: &HashMap<&str, String>, -) { - // All the bold colors - hash.insert( - 0, - ( - r"(?P\[0m)(?P[[:alnum:][:space:][:punct:]]*)", - // Reset the text color, normal weight font - format!( - r"$word", - color_hm - .get("foreground") - .expect("Error getting reset text color") - ), - ), - ); - hash.insert( - 1, - ( - // Bold Black - r"(?P\[1;30m)(?P[[:alnum:][:space:][:punct:]]*)", - format!( - r"$word", - color_hm - .get("foreground") - .expect("Error getting bold black text color") - ), - ), - ); - hash.insert( - 2, - ( - // Bold Red - r"(?P
\[1;31m)(?P[[:alnum:][:space:][:punct:]]*)", - format!( - r"$word", - color_hm - .get("bold_red") - .expect("Error getting bold red text color"), - ), - ), - ); - hash.insert( - 3, - ( - // Bold Green - r"(?P\[1;32m)(?P[[:alnum:][:space:][:punct:]]*)", - format!( - r"$word", - color_hm - .get("bold_green") - .expect("Error getting bold green text color"), - ), - ), - ); - hash.insert( - 4, - ( - // Bold Yellow - r"(?P\[1;33m)(?P[[:alnum:][:space:][:punct:]]*)", - format!( - r"$word", - color_hm - .get("bold_yellow") - .expect("Error getting bold yellow text color"), - ), - ), - ); - hash.insert( - 5, - ( - // Bold Blue - r"(?P\[1;34m)(?P[[:alnum:][:space:][:punct:]]*)", - format!( - r"$word", - color_hm - .get("bold_blue") - .expect("Error getting bold blue text color"), - ), - ), - ); - hash.insert( - 6, - ( - // Bold Magenta - r"(?P\[1;35m)(?P[[:alnum:][:space:][:punct:]]*)", - format!( - r"$word", - color_hm - .get("bold_magenta") - .expect("Error getting bold magenta text color"), - ), - ), - ); - hash.insert( - 7, - ( - // Bold Cyan - r"(?P\[1;36m)(?P[[:alnum:][:space:][:punct:]]*)", - format!( - r"$word", - color_hm - .get("bold_cyan") - .expect("Error getting bold cyan text color"), - ), - ), - ); - hash.insert( - 8, - ( - // Bold White - // Let's change this to black since the html background - // is white. White on white = no bueno. - r"(?P\[1;37m)(?P[[:alnum:][:space:][:punct:]]*)", - format!( - r"$word", - color_hm - .get("foreground") - .expect("Error getting bold bold white text color"), - ), - ), - ); - // All the normal colors - hash.insert( - 9, - ( - // Black - r"(?P\[30m)(?P[[:alnum:][:space:][:punct:]]*)", - format!( - r"$word", - color_hm - .get("foreground") - .expect("Error getting black text color"), - ), - ), - ); - hash.insert( - 10, - ( - // Red - r"(?P\[31m)(?P[[:alnum:][:space:][:punct:]]*)", - format!( - r"$word", - color_hm.get("red").expect("Error getting red text color"), - ), - ), - ); - hash.insert( - 11, - ( - // Green - r"(?P\[32m)(?P[[:alnum:][:space:][:punct:]]*)", - format!( - r"$word", - color_hm - .get("green") - .expect("Error getting green text color"), - ), - ), - ); - hash.insert( - 12, - ( - // Yellow - r"(?P\[33m)(?P[[:alnum:][:space:][:punct:]]*)", - format!( - r"$word", - color_hm - .get("yellow") - .expect("Error getting yellow text color"), - ), - ), - ); - hash.insert( - 13, - ( - // Blue - r"(?P\[34m)(?P[[:alnum:][:space:][:punct:]]*)", - format!( - r"$word", - color_hm.get("blue").expect("Error getting blue text color"), - ), - ), - ); - hash.insert( - 14, - ( - // Magenta - r"(?P\[35m)(?P[[:alnum:][:space:][:punct:]]*)", - format!( - r"$word", - color_hm - .get("magenta") - .expect("Error getting magenta text color"), - ), - ), - ); - hash.insert( - 15, - ( - // Cyan - r"(?P\[36m)(?P[[:alnum:][:space:][:punct:]]*)", - format!( - r"$word", - color_hm.get("cyan").expect("Error getting cyan text color"), - ), - ), - ); - hash.insert( - 16, - ( - // White - // Let's change this to black since the html background - // is white. White on white = no bueno. - r"(?P\[37m)(?P[[:alnum:][:space:][:punct:]]*)", - format!( - r"$word", - color_hm - .get("foreground") - .expect("Error getting white text color"), - ), - ), - ); -} - -fn setup_no_color_regexes(hash: &mut HashMap) { - // We can just use one regex here because we're just removing ansi sequences - // and not replacing them with html colors. - // attribution: https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python - hash.insert( - 0, - ( - r"(?:\x1B[@-Z\\-_]|[\x80-\x9A\x9C-\x9F]|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~])", - r"$name_group_doesnt_exist".to_string(), - ), - ); -} - -fn run_regexes(hash: &HashMap, contents: &str) -> String { - let mut working_string = contents.to_owned(); - let hash_count: u32 = hash.len() as u32; - for n in 0..hash_count { - let value = hash.get(&n).expect("error getting hash at index"); - //println!("{},{}", value.0, value.1); - let re = Regex::new(value.0).expect("problem with color regex"); - let after = re.replace_all(&working_string, &value.1[..]).to_string(); - working_string = after.clone(); - } - working_string -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::*; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(ToHtml {}) - } -} diff --git a/crates/nu-command/src/commands/formats/to/json.rs b/crates/nu-command/src/commands/formats/to/json.rs deleted file mode 100644 index 43f769ec8b..0000000000 --- a/crates/nu-command/src/commands/formats/to/json.rs +++ /dev/null @@ -1,256 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::{CoerceInto, ShellError}; -use nu_protocol::{Primitive, Signature, SyntaxShape, UnspannedPathMember, UntaggedValue, Value}; -use serde::Serialize; -use serde_json::json; - -pub struct ToJson; - -impl WholeStreamCommand for ToJson { - fn name(&self) -> &str { - "to json" - } - - fn signature(&self) -> Signature { - Signature::build("to json").named( - "pretty", - SyntaxShape::Int, - "Formats the JSON text with the provided indentation setting", - Some('p'), - ) - } - - fn usage(&self) -> &str { - "Converts table data into JSON text." - } - - fn run(&self, args: CommandArgs) -> Result { - to_json(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: - "Outputs an unformatted JSON string representing the contents of this table", - example: "echo [1 2 3] | to json", - result: Some(vec![Value::from("[1,2,3]")]), - }, - Example { - description: - "Outputs a formatted JSON string representing the contents of this table with an indentation setting of 2 spaces", - example: "echo [1 2 3] | to json --pretty 2", - result: Some(vec![Value::from("[\n 1,\n 2,\n 3\n]")]), - }, - ] - } -} - -pub fn value_to_json_value(v: &Value) -> Result { - Ok(match &v.value { - UntaggedValue::Primitive(Primitive::Boolean(b)) => serde_json::Value::Bool(*b), - UntaggedValue::Primitive(Primitive::Filesize(b)) => serde_json::Value::Number( - serde_json::Number::from(b.to_u64().expect("What about really big numbers")), - ), - UntaggedValue::Primitive(Primitive::Duration(i)) => { - serde_json::Value::String(i.to_string()) - } - UntaggedValue::Primitive(Primitive::Date(d)) => serde_json::Value::String(d.to_string()), - UntaggedValue::Primitive(Primitive::EndOfStream) => serde_json::Value::Null, - UntaggedValue::Primitive(Primitive::BeginningOfStream) => serde_json::Value::Null, - UntaggedValue::Primitive(Primitive::Decimal(f)) => { - if let Some(f) = f.to_f64() { - if let Some(num) = serde_json::Number::from_f64( - f.to_f64().expect("TODO: What about really big decimals?"), - ) { - serde_json::Value::Number(num) - } else { - return Err(ShellError::labeled_error( - "Could not convert value to decimal number", - "could not convert to decimal", - &v.tag, - )); - } - } else { - return Err(ShellError::labeled_error( - "Could not convert value to decimal number", - "could not convert to decimal", - &v.tag, - )); - } - } - - UntaggedValue::Primitive(Primitive::Int(i)) => { - serde_json::Value::Number(serde_json::Number::from(*i)) - } - UntaggedValue::Primitive(Primitive::BigInt(i)) => { - serde_json::Value::Number(serde_json::Number::from(CoerceInto::::coerce_into( - i.tagged(&v.tag), - "converting to JSON number", - )?)) - } - UntaggedValue::Primitive(Primitive::Nothing) => serde_json::Value::Null, - UntaggedValue::Primitive(Primitive::GlobPattern(s)) => serde_json::Value::String(s.clone()), - UntaggedValue::Primitive(Primitive::String(s)) => serde_json::Value::String(s.clone()), - UntaggedValue::Primitive(Primitive::ColumnPath(path)) => serde_json::Value::Array( - path.iter() - .map(|x| match &x.unspanned { - UnspannedPathMember::String(string) => { - Ok(serde_json::Value::String(string.clone())) - } - UnspannedPathMember::Int(int) => { - Ok(serde_json::Value::Number(serde_json::Number::from(*int))) - } - }) - .collect::, ShellError>>()?, - ), - UntaggedValue::Primitive(Primitive::FilePath(s)) => { - serde_json::Value::String(s.display().to_string()) - } - - UntaggedValue::Table(l) => serde_json::Value::Array(json_list(l)?), - UntaggedValue::Error(e) => return Err(e.clone()), - UntaggedValue::Block(_) | UntaggedValue::Primitive(Primitive::Range(_)) => { - serde_json::Value::Null - } - #[cfg(feature = "dataframe")] - UntaggedValue::DataFrame(_) | UntaggedValue::FrameStruct(_) => serde_json::Value::Null, - UntaggedValue::Primitive(Primitive::Binary(b)) => serde_json::Value::Array( - b.iter() - .map(|x| { - serde_json::Number::from_f64(*x as f64).ok_or_else(|| { - ShellError::labeled_error( - "Can not convert number from floating point", - "can not convert to number", - &v.tag, - ) - }) - }) - .collect::, ShellError>>()? - .into_iter() - .map(serde_json::Value::Number) - .collect(), - ), - UntaggedValue::Row(o) => { - let mut m = serde_json::Map::new(); - for (k, v) in &o.entries { - m.insert(k.clone(), value_to_json_value(v)?); - } - serde_json::Value::Object(m) - } - }) -} - -fn json_list(input: &[Value]) -> Result, ShellError> { - let mut out = vec![]; - - for value in input { - out.push(value_to_json_value(value)?); - } - - Ok(out) -} - -fn to_json(args: CommandArgs) -> Result { - let name_tag = args.call_info.name_tag.clone(); - let pretty: Option = args.get_flag("pretty")?; - - let name_span = name_tag.span; - let input: Vec = args.input.collect(); - - let to_process_input = match input.len() { - x if x > 1 => { - let tag = input[0].tag.clone(); - vec![Value { - value: UntaggedValue::Table(input), - tag, - }] - } - 1 => input, - _ => vec![], - }; - - Ok((to_process_input - .into_iter() - .map(move |value| match value_to_json_value(&value) { - Ok(json_value) => { - let value_span = value.tag.span; - - match serde_json::to_string(&json_value) { - Ok(mut serde_json_string) => { - if let Some(pretty_value) = &pretty { - let mut pretty_format_failed = true; - - if let Ok(pretty_u64) = pretty_value.as_u64() { - if let Ok(serde_json_value) = - serde_json::from_str::(&serde_json_string) - { - let indentation_string = " ".repeat(pretty_u64 as usize); - let serde_formatter = - serde_json::ser::PrettyFormatter::with_indent( - indentation_string.as_bytes(), - ); - let serde_buffer = Vec::new(); - let mut serde_serializer = - serde_json::Serializer::with_formatter( - serde_buffer, - serde_formatter, - ); - let serde_json_object = json!(serde_json_value); - - if let Ok(()) = - serde_json_object.serialize(&mut serde_serializer) - { - if let Ok(ser_json_string) = - String::from_utf8(serde_serializer.into_inner()) - { - pretty_format_failed = false; - serde_json_string = ser_json_string - } - } - } - } - - if pretty_format_failed { - return Value::error(ShellError::labeled_error( - "Pretty formatting failed", - "failed", - pretty_value.tag(), - )); - } - } - - UntaggedValue::Primitive(Primitive::String(serde_json_string)) - .into_value(&value.tag) - } - _ => Value::error(ShellError::labeled_error_with_secondary( - "Expected a table with JSON-compatible structure.tag() from pipeline", - "requires JSON-compatible input", - name_span, - "originates from here".to_string(), - value_span, - )), - } - } - _ => Value::error(ShellError::labeled_error( - "Expected a table with JSON-compatible structure from pipeline", - "requires JSON-compatible input", - &name_tag, - )), - })) - .into_output_stream()) -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::ToJson; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(ToJson {}) - } -} diff --git a/crates/nu-command/src/commands/formats/to/md.rs b/crates/nu-command/src/commands/formats/to/md.rs deleted file mode 100644 index f29fe145d3..0000000000 --- a/crates/nu-command/src/commands/formats/to/md.rs +++ /dev/null @@ -1,380 +0,0 @@ -use crate::prelude::*; - -use nu_data::value::format_leaf; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, UntaggedValue, Value}; - -pub struct Command; - -#[derive(Deserialize)] -pub struct Arguments { - pretty: bool, - #[serde(rename = "per-element")] - per_element: bool, -} - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "to md" - } - - fn signature(&self) -> Signature { - Signature::build("to md") - .switch( - "pretty", - "Formats the Markdown table to vertically align items", - Some('p'), - ) - .switch( - "per-element", - "treat each row as markdown syntax element", - Some('e'), - ) - } - - fn usage(&self) -> &str { - "Convert table into simple Markdown" - } - - fn run(&self, args: CommandArgs) -> Result { - to_md(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Outputs an unformatted table markdown string (default)", - example: "ls | to md", - result: Some(vec![Value::from(one(r#" - |name|type|chickens|modified| - |-|-|-|-| - |Andres.txt|File|10|2 years ago| - |Jonathan|Dir|5|2 years ago| - |Darren.txt|File|20|2 years ago| - |Yehuda|Dir|4|2 years ago| - "#))]), - }, - Example { - description: "Optionally, output a formatted markdown string", - example: "ls | to md --pretty", - result: Some(vec![Value::from(one(r#" - | name | type | chickens | modified | - | ---------- | ---- | -------- | ----------- | - | Andres.txt | File | 10 | 2 years ago | - | Jonathan | Dir | 5 | 2 years ago | - | Darren.txt | File | 20 | 2 years ago | - | Yehuda | Dir | 4 | 2 years ago | - "#))]), - }, - Example { - description: "Treat each row as a markdown element", - example: "echo [[H1]; [\"Welcome to Nushell\"]] | append (ls | first 2) | to md --per-element --pretty", - result: Some(vec![Value::from(one(r#" - # Welcome to Nushell - | name | type | chickens | modified | - | ---------- | ---- | -------- | ----------- | - | Andres.txt | File | 10 | 2 years ago | - | Jonathan | Dir | 5 | 2 years ago | - "#))]), - } - ] - } -} - -fn to_md(args: CommandArgs) -> Result { - let name_tag = args.call_info.name_tag.clone(); - let arguments = Arguments { - per_element: args.has_flag("per-element"), - pretty: args.has_flag("pretty"), - }; - - let input: Vec = args.input.collect(); - - Ok(OutputStream::one( - UntaggedValue::string(process(&input, arguments)).into_value(if input.is_empty() { - name_tag - } else { - input[0].tag() - }), - )) -} - -fn process( - input: &[Value], - Arguments { - pretty, - per_element, - }: Arguments, -) -> String { - if per_element { - input - .iter() - .map(|v| match &v.value { - UntaggedValue::Table(values) => table(values, pretty), - _ => fragment(v, pretty), - }) - .collect::() - } else { - table(input, pretty) - } -} - -fn fragment(input: &Value, pretty: bool) -> String { - let headers = input.data_descriptors(); - let mut out = String::new(); - - if headers.len() == 1 { - let markup = match (&headers[0]).to_ascii_lowercase().as_ref() { - "h1" => "# ".to_string(), - "h2" => "## ".to_string(), - "h3" => "### ".to_string(), - "blockquote" => "> ".to_string(), - - _ => return table(&[input.clone()], pretty), - }; - - out.push_str(&markup); - out.push_str(&format_leaf(input.get_data(&headers[0]).borrow()).plain_string(100_000)); - } else if input.is_row() { - let string = match input.row_entries().next() { - Some(value) => value.1.as_string().unwrap_or_default(), - None => String::from(""), - }; - - out = format_leaf(&UntaggedValue::from(string)).plain_string(100_000) - } else { - out = format_leaf(&input.value).plain_string(100_000) - } - - out.push('\n'); - out -} - -fn collect_headers(headers: &[String]) -> (Vec, Vec) { - let mut escaped_headers: Vec = Vec::new(); - let mut column_widths: Vec = Vec::new(); - - if !headers.is_empty() && (headers.len() > 1 || !headers[0].is_empty()) { - for header in headers { - let escaped_header_string = htmlescape::encode_minimal(header); - column_widths.push(escaped_header_string.len()); - escaped_headers.push(escaped_header_string); - } - } else { - column_widths = vec![0; headers.len()] - } - - (escaped_headers, column_widths) -} - -fn table(input: &[Value], pretty: bool) -> String { - let headers = nu_protocol::merge_descriptors(input); - - let (escaped_headers, mut column_widths) = collect_headers(&headers); - - let mut escaped_rows: Vec> = Vec::new(); - - for row in input { - let mut escaped_row: Vec = Vec::new(); - - match row.value.clone() { - UntaggedValue::Row(row) => { - for i in 0..headers.len() { - let data = row.get_data(&headers[i]); - let value_string = format_leaf(data.borrow()).plain_string(100_000); - let new_column_width = value_string.len(); - - escaped_row.push(value_string); - - if column_widths[i] < new_column_width { - column_widths[i] = new_column_width; - } - } - } - p => { - let value_string = - htmlescape::encode_minimal(&format_leaf(&p).plain_string(100_000)); - escaped_row.push(value_string); - } - } - - escaped_rows.push(escaped_row); - } - - let output_string = if (column_widths.is_empty() || column_widths.iter().all(|x| *x == 0)) - && escaped_rows.is_empty() - { - String::from("") - } else { - get_output_string(&escaped_headers, &escaped_rows, &column_widths, pretty) - .trim() - .to_string() - }; - - output_string -} - -fn get_output_string( - headers: &[String], - rows: &[Vec], - column_widths: &[usize], - pretty: bool, -) -> String { - let mut output_string = String::new(); - - if !headers.is_empty() { - output_string.push('|'); - - for i in 0..headers.len() { - if pretty { - output_string.push(' '); - output_string.push_str(&get_padded_string( - headers[i].clone(), - column_widths[i], - ' ', - )); - output_string.push(' '); - } else { - output_string.push_str(&headers[i]); - } - - output_string.push('|'); - } - - output_string.push_str("\n|"); - - #[allow(clippy::needless_range_loop)] - for i in 0..headers.len() { - if pretty { - output_string.push(' '); - output_string.push_str(&get_padded_string( - String::from("-"), - column_widths[i], - '-', - )); - output_string.push(' '); - } else { - output_string.push('-'); - } - - output_string.push('|'); - } - - output_string.push('\n'); - } - - for row in rows { - if !headers.is_empty() { - output_string.push('|'); - } - - for i in 0..row.len() { - if pretty { - output_string.push(' '); - output_string.push_str(&get_padded_string(row[i].clone(), column_widths[i], ' ')); - output_string.push(' '); - } else { - output_string.push_str(&row[i]); - } - - if !headers.is_empty() { - output_string.push('|'); - } - } - - output_string.push('\n'); - } - - output_string -} - -fn get_padded_string(text: String, desired_length: usize, padding_character: char) -> String { - let repeat_length = if text.len() > desired_length { - 0 - } else { - desired_length - text.len() - }; - - format!( - "{}{}", - text, - padding_character.to_string().repeat(repeat_length) - ) -} - -fn one(string: &str) -> String { - string - .lines() - .skip(1) - .map(|line| line.trim()) - .collect::>() - .join("\n") - .trim_end() - .to_string() -} - -#[cfg(test)] -mod tests { - use super::{fragment, one, table}; - use nu_protocol::{row, Value}; - - #[test] - fn render_h1() { - let value = row! {"H1".into() => Value::from("Ecuador")}; - - assert_eq!(fragment(&value, false), "# Ecuador\n"); - } - - #[test] - fn render_h2() { - let value = row! {"H2".into() => Value::from("Ecuador")}; - - assert_eq!(fragment(&value, false), "## Ecuador\n"); - } - - #[test] - fn render_h3() { - let value = row! {"H3".into() => Value::from("Ecuador")}; - - assert_eq!(fragment(&value, false), "### Ecuador\n"); - } - - #[test] - fn render_blockquote() { - let value = row! {"BLOCKQUOTE".into() => Value::from("Ecuador")}; - - assert_eq!(fragment(&value, false), "> Ecuador\n"); - } - - #[test] - fn render_table() { - let value = vec![ - row! { "country".into() => Value::from("Ecuador")}, - row! { "country".into() => Value::from("New Zealand")}, - row! { "country".into() => Value::from("USA")}, - ]; - - assert_eq!( - table(&value, false), - one(r#" - |country| - |-| - |Ecuador| - |New Zealand| - |USA| - "#) - ); - - assert_eq!( - table(&value, true), - one(r#" - | country | - | ----------- | - | Ecuador | - | New Zealand | - | USA | - "#) - ); - } -} diff --git a/crates/nu-command/src/commands/formats/to/mod.rs b/crates/nu-command/src/commands/formats/to/mod.rs deleted file mode 100644 index e6e1ff7fc3..0000000000 --- a/crates/nu-command/src/commands/formats/to/mod.rs +++ /dev/null @@ -1,22 +0,0 @@ -mod command; -pub(crate) mod csv; -pub(crate) mod delimited; -mod html; -mod json; -mod md; -pub(crate) mod toml; -mod tsv; -pub(crate) mod url; -mod xml; -mod yaml; - -pub use self::csv::ToCsv; -pub use self::toml::ToToml; -pub use self::url::ToUrl; -pub use command::To; -pub use html::ToHtml; -pub use json::ToJson; -pub use md::Command as ToMarkdown; -pub use tsv::ToTsv; -pub use xml::ToXml; -pub use yaml::ToYaml; diff --git a/crates/nu-command/src/commands/formats/to/toml.rs b/crates/nu-command/src/commands/formats/to/toml.rs deleted file mode 100644 index 1610e35c66..0000000000 --- a/crates/nu-command/src/commands/formats/to/toml.rs +++ /dev/null @@ -1,242 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::{CoerceInto, ShellError}; -use nu_protocol::{Primitive, Signature, UnspannedPathMember, UntaggedValue, Value}; - -pub struct ToToml; - -impl WholeStreamCommand for ToToml { - fn name(&self) -> &str { - "to toml" - } - - fn signature(&self) -> Signature { - Signature::build("to toml") - } - - fn usage(&self) -> &str { - "Convert table into .toml text" - } - - fn run(&self, args: CommandArgs) -> Result { - to_toml(args) - } -} - -// Helper method to recursively convert nu_protocol::Value -> toml::Value -// This shouldn't be called at the top-level -fn helper(v: &Value) -> Result { - Ok(match &v.value { - UntaggedValue::Primitive(Primitive::Boolean(b)) => toml::Value::Boolean(*b), - UntaggedValue::Primitive(Primitive::Filesize(b)) => { - if let Some(value) = b.to_i64() { - toml::Value::Integer(value) - } else { - return Err(ShellError::labeled_error( - "Value too large to write to toml", - "value too large for toml", - v.tag.span, - )); - } - } - UntaggedValue::Primitive(Primitive::Duration(i)) => toml::Value::String(i.to_string()), - UntaggedValue::Primitive(Primitive::Date(d)) => toml::Value::String(d.to_string()), - UntaggedValue::Primitive(Primitive::EndOfStream) => { - toml::Value::String("".to_string()) - } - UntaggedValue::Primitive(Primitive::BeginningOfStream) => { - toml::Value::String("".to_string()) - } - UntaggedValue::Primitive(Primitive::Decimal(f)) => { - toml::Value::Float(f.tagged(&v.tag).coerce_into("converting to TOML float")?) - } - UntaggedValue::Primitive(Primitive::Int(i)) => toml::Value::Integer(*i), - UntaggedValue::Primitive(Primitive::BigInt(i)) => { - toml::Value::Integer(i.tagged(&v.tag).coerce_into("converting to TOML integer")?) - } - UntaggedValue::Primitive(Primitive::Nothing) => { - toml::Value::String("".to_string()) - } - UntaggedValue::Primitive(Primitive::GlobPattern(s)) => toml::Value::String(s.clone()), - UntaggedValue::Primitive(Primitive::String(s)) => toml::Value::String(s.clone()), - UntaggedValue::Primitive(Primitive::FilePath(s)) => { - toml::Value::String(s.display().to_string()) - } - UntaggedValue::Primitive(Primitive::ColumnPath(path)) => toml::Value::Array( - path.iter() - .map(|x| match &x.unspanned { - UnspannedPathMember::String(string) => Ok(toml::Value::String(string.clone())), - UnspannedPathMember::Int(int) => Ok(toml::Value::Integer(*int)), - }) - .collect::, ShellError>>()?, - ), - UntaggedValue::Table(l) => toml::Value::Array(collect_values(l)?), - UntaggedValue::Error(e) => return Err(e.clone()), - UntaggedValue::Block(_) => toml::Value::String("".to_string()), - #[cfg(feature = "dataframe")] - UntaggedValue::DataFrame(_) | UntaggedValue::FrameStruct(_) => { - toml::Value::String("".to_string()) - } - UntaggedValue::Primitive(Primitive::Range(_)) => toml::Value::String("".to_string()), - UntaggedValue::Primitive(Primitive::Binary(b)) => { - toml::Value::Array(b.iter().map(|x| toml::Value::Integer(*x as i64)).collect()) - } - UntaggedValue::Row(o) => { - let mut m = toml::map::Map::new(); - for (k, v) in &o.entries { - m.insert(k.clone(), helper(v)?); - } - toml::Value::Table(m) - } - }) -} - -/// Converts a nu_protocol::Value into a toml::Value -/// Will return a Shell Error, if the Nu Value is not a valid top-level TOML Value -pub fn value_to_toml_value(v: &Value) -> Result { - match &v.value { - UntaggedValue::Row(o) => { - let mut m = toml::map::Map::new(); - for (k, v) in &o.entries { - m.insert(k.clone(), helper(v)?); - } - Ok(toml::Value::Table(m)) - } - UntaggedValue::Primitive(Primitive::String(s)) => { - // Attempt to de-serialize the String - toml::de::from_str(s).map_err(|_| { - ShellError::labeled_error( - format!("{:?} unable to de-serialize string to TOML", s), - "invalid TOML", - v.tag(), - ) - }) - } - _ => Err(ShellError::labeled_error( - format!("{:?} is not a valid top-level TOML", v.value), - "invalid TOML", - v.tag(), - )), - } -} - -fn collect_values(input: &[Value]) -> Result, ShellError> { - let mut out = vec![]; - - for value in input { - out.push(helper(value)?); - } - - Ok(out) -} - -fn to_toml(args: CommandArgs) -> Result { - let name_tag = args.name_tag(); - let name_span = name_tag.span; - let input: Vec = args.input.collect(); - - let to_process_input = match input.len() { - x if x > 1 => { - let tag = input[0].tag.clone(); - vec![Value { - value: UntaggedValue::Table(input), - tag, - }] - } - 1 => input, - _ => vec![], - }; - - Ok((to_process_input.into_iter().map(move |value| { - let value_span = value.tag.span; - match value_to_toml_value(&value) { - Ok(toml_value) => match toml::to_string(&toml_value) { - Ok(x) => UntaggedValue::Primitive(Primitive::String(x)).into_value(&name_tag), - - _ => Value::error(ShellError::labeled_error_with_secondary( - "Expected a table with TOML-compatible structure.tag() from pipeline", - "requires TOML-compatible input", - name_span, - "originates from here".to_string(), - value_span, - )), - }, - _ => Value::error(ShellError::labeled_error( - "Expected a table with TOML-compatible structure from pipeline", - "requires TOML-compatible input", - &name_tag, - )), - } - })) - .into_output_stream()) -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::*; - use nu_protocol::Dictionary; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(ToToml {}) - } - - #[test] - fn test_value_to_toml_value() { - // - // Positive Tests - // - - // Dictionary -> What we do in "crates/nu-cli/src/data/config.rs" to write the config file - let mut m = indexmap::IndexMap::new(); - m.insert("rust".to_owned(), Value::from("editor")); - m.insert("is".to_owned(), Value::nothing()); - m.insert( - "features".to_owned(), - UntaggedValue::Table(vec![ - UntaggedValue::string("hello").into_untagged_value(), - UntaggedValue::string("array").into_untagged_value(), - ]) - .into_untagged_value(), - ); - let tv = value_to_toml_value(&UntaggedValue::Row(Dictionary::new(m)).into_untagged_value()) - .expect("Expected Ok from valid TOML dictionary"); - assert_eq!( - tv.get("features"), - Some(&toml::Value::Array(vec![ - toml::Value::String("hello".to_owned()), - toml::Value::String("array".to_owned()) - ])) - ); - // TOML string - let tv = value_to_toml_value(&Value::from( - r#" - title = "TOML Example" - - [owner] - name = "Tom Preston-Werner" - dob = 1979-05-27T07:32:00-08:00 # First class dates - - [dependencies] - rustyline = "4.1.0" - sysinfo = "0.8.4" - chrono = { version = "0.4.6", features = ["serde"] } - "#, - )) - .expect("Expected Ok from valid TOML string"); - assert_eq!( - tv.get("title").unwrap(), - &toml::Value::String("TOML Example".to_owned()) - ); - // - // Negative Tests - // - value_to_toml_value(&Value::from("not_valid")) - .expect_err("Expected non-valid toml (String) to cause error!"); - value_to_toml_value(&UntaggedValue::Table(vec![Value::from("1")]).into_untagged_value()) - .expect_err("Expected non-valid toml (Table) to cause error!"); - } -} diff --git a/crates/nu-command/src/commands/formats/to/tsv.rs b/crates/nu-command/src/commands/formats/to/tsv.rs deleted file mode 100644 index f721bea56b..0000000000 --- a/crates/nu-command/src/commands/formats/to/tsv.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::commands::formats::to::delimited::to_delimited_data; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::Signature; - -pub struct ToTsv; - -impl WholeStreamCommand for ToTsv { - fn name(&self) -> &str { - "to tsv" - } - - fn signature(&self) -> Signature { - Signature::build("to tsv").switch( - "noheaders", - "do not output the column names as the first row", - Some('n'), - ) - } - - fn usage(&self) -> &str { - "Convert table into .tsv text" - } - - fn run(&self, args: CommandArgs) -> Result { - to_tsv(args) - } -} - -fn to_tsv(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - let noheaders = args.has_flag("noheaders"); - - to_delimited_data(noheaders, '\t', "TSV", args.input, name) -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::ToTsv; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(ToTsv {}) - } -} diff --git a/crates/nu-command/src/commands/formats/to/url.rs b/crates/nu-command/src/commands/formats/to/url.rs deleted file mode 100644 index e968f5c97a..0000000000 --- a/crates/nu-command/src/commands/formats/to/url.rs +++ /dev/null @@ -1,85 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, UntaggedValue, Value}; - -pub struct ToUrl; - -impl WholeStreamCommand for ToUrl { - fn name(&self) -> &str { - "to url" - } - - fn signature(&self) -> Signature { - Signature::build("to url") - } - - fn usage(&self) -> &str { - "Convert table into url-encoded text" - } - - fn run(&self, args: CommandArgs) -> Result { - to_url(args) - } -} - -fn to_url(args: CommandArgs) -> Result { - let tag = args.name_tag(); - let input = args.input; - - Ok(input - .map(move |value| match value { - Value { - value: UntaggedValue::Row(row), - .. - } => { - let mut row_vec = vec![]; - for (k, v) in row.entries { - match v.as_string() { - Ok(s) => { - row_vec.push((k.clone(), s.to_string())); - } - _ => { - return Value::error(ShellError::labeled_error_with_secondary( - "Expected table with string values", - "requires table with strings", - &tag, - "value originates from here", - v.tag, - )); - } - } - } - - match serde_urlencoded::to_string(row_vec) { - Ok(s) => UntaggedValue::string(s).into_value(&tag), - _ => Value::error(ShellError::labeled_error( - "Failed to convert to url-encoded", - "cannot url-encode", - &tag, - )), - } - } - Value { tag: value_tag, .. } => Value::error(ShellError::labeled_error_with_secondary( - "Expected a table from pipeline", - "requires table input", - &tag, - "value originates from here", - value_tag.span, - )), - }) - .into_output_stream()) -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::ToUrl; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(ToUrl {}) - } -} diff --git a/crates/nu-command/src/commands/formats/to/xml.rs b/crates/nu-command/src/commands/formats/to/xml.rs deleted file mode 100644 index 28fe8d0c20..0000000000 --- a/crates/nu-command/src/commands/formats/to/xml.rs +++ /dev/null @@ -1,196 +0,0 @@ -use crate::prelude::*; -use indexmap::IndexMap; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Primitive, Signature, SyntaxShape, UntaggedValue, Value}; -use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; -use std::collections::HashSet; -use std::io::Cursor; -use std::io::Write; - -pub struct ToXml; - -impl WholeStreamCommand for ToXml { - fn name(&self) -> &str { - "to xml" - } - - fn signature(&self) -> Signature { - Signature::build("to xml").named( - "pretty", - SyntaxShape::Int, - "Formats the XML text with the provided indentation setting", - Some('p'), - ) - } - - fn usage(&self) -> &str { - "Convert table into .xml text" - } - - fn run(&self, args: CommandArgs) -> Result { - to_xml(args) - } -} - -pub fn add_attributes<'a>( - element: &mut quick_xml::events::BytesStart<'a>, - attributes: &'a IndexMap, -) { - for (k, v) in attributes { - element.push_attribute((k.as_str(), v.as_str())); - } -} - -pub fn get_attributes(row: &Value) -> Option> { - if let UntaggedValue::Row(r) = &row.value { - if let Some(v) = r.entries.get("attributes") { - if let UntaggedValue::Row(a) = &v.value { - let mut h = IndexMap::new(); - for (k, v) in &a.entries { - h.insert(k.clone(), v.convert_to_string()); - } - return Some(h); - } - } - } - None -} - -pub fn get_children(row: &Value) -> Option<&Vec> { - if let UntaggedValue::Row(r) = &row.value { - if let Some(v) = r.entries.get("children") { - if let UntaggedValue::Table(t) = &v.value { - return Some(t); - } - } - } - None -} - -pub fn is_xml_row(row: &Value) -> bool { - if let UntaggedValue::Row(r) = &row.value { - let keys: HashSet<&String> = r.keys().collect(); - let children: String = "children".to_string(); - let attributes: String = "attributes".to_string(); - return keys.contains(&children) && keys.contains(&attributes) && keys.len() == 2; - } - false -} - -pub fn write_xml_events( - current: &Value, - writer: &mut quick_xml::Writer, -) -> Result<(), ShellError> { - match ¤t.value { - UntaggedValue::Row(o) => { - for (k, v) in &o.entries { - let mut e = BytesStart::owned(k.as_bytes(), k.len()); - if !is_xml_row(v) { - return Err(ShellError::labeled_error( - "Expected a row with 'children' and 'attributes' columns", - "missing 'children' and 'attributes' columns ", - ¤t.tag, - )); - } - let a = get_attributes(v); - if let Some(ref a) = a { - add_attributes(&mut e, a); - } - writer - .write_event(Event::Start(e)) - .expect("Couldn't open XML node"); - let c = get_children(v); - if let Some(c) = c { - for v in c { - write_xml_events(v, writer)?; - } - } - writer - .write_event(Event::End(BytesEnd::borrowed(k.as_bytes()))) - .expect("Couldn't close XML node"); - } - } - UntaggedValue::Table(t) => { - for v in t { - write_xml_events(v, writer)?; - } - } - _ => { - let s = current.convert_to_string(); - writer - .write_event(Event::Text(BytesText::from_plain_str(s.as_str()))) - .expect("Couldn't write XML text"); - } - } - Ok(()) -} - -fn to_xml(args: CommandArgs) -> Result { - let name_tag = args.call_info.name_tag.clone(); - let name_span = name_tag.span; - let pretty: Option = args.get_flag("pretty")?; - let input: Vec = args.input.collect(); - - let to_process_input = match input.len() { - x if x > 1 => { - let tag = input[0].tag.clone(); - vec![Value { - value: UntaggedValue::Table(input), - tag, - }] - } - 1 => input, - _ => vec![], - }; - - Ok((to_process_input.into_iter().map(move |value| { - let mut w = pretty.as_ref().map_or_else( - || quick_xml::Writer::new(Cursor::new(Vec::new())), - |p| { - quick_xml::Writer::new_with_indent( - Cursor::new(Vec::new()), - b' ', - p.value.expect_int() as usize, - ) - }, - ); - - let value_span = value.tag.span; - - match write_xml_events(&value, &mut w) { - Ok(_) => { - let b = w.into_inner().into_inner(); - let s = if let Ok(s) = String::from_utf8(b) { - s - } else { - return Value::error(ShellError::untagged_runtime_error( - "Could not convert a string to utf-8", - )); - }; - UntaggedValue::Primitive(Primitive::String(s)).into_value(&name_tag) - } - Err(_) => Value::error(ShellError::labeled_error_with_secondary( - "Expected a table with XML-compatible structure from pipeline", - "requires XML-compatible input", - name_span, - "originates from here".to_string(), - value_span, - )), - } - })) - .into_output_stream()) -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::ToXml; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(ToXml {}) - } -} diff --git a/crates/nu-command/src/commands/formats/to/yaml.rs b/crates/nu-command/src/commands/formats/to/yaml.rs deleted file mode 100644 index 44f65d29f6..0000000000 --- a/crates/nu-command/src/commands/formats/to/yaml.rs +++ /dev/null @@ -1,172 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::{CoerceInto, ShellError}; -use nu_protocol::{Primitive, Signature, UnspannedPathMember, UntaggedValue, Value}; - -pub struct ToYaml; - -impl WholeStreamCommand for ToYaml { - fn name(&self) -> &str { - "to yaml" - } - - fn signature(&self) -> Signature { - Signature::build("to yaml") - } - - fn usage(&self) -> &str { - "Convert table into .yaml/.yml text" - } - - fn run(&self, args: CommandArgs) -> Result { - to_yaml(args) - } -} - -pub fn value_to_yaml_value(v: &Value) -> Result { - Ok(match &v.value { - UntaggedValue::Primitive(Primitive::Boolean(b)) => serde_yaml::Value::Bool(*b), - UntaggedValue::Primitive(Primitive::Filesize(b)) => { - serde_yaml::Value::Number(serde_yaml::Number::from(b.to_f64().ok_or_else(|| { - ShellError::labeled_error( - "Could not convert to bytes", - "could not convert to bytes", - &v.tag, - ) - })?)) - } - UntaggedValue::Primitive(Primitive::Duration(i)) => { - serde_yaml::Value::String(i.to_string()) - } - UntaggedValue::Primitive(Primitive::Date(d)) => serde_yaml::Value::String(d.to_string()), - UntaggedValue::Primitive(Primitive::EndOfStream) => serde_yaml::Value::Null, - UntaggedValue::Primitive(Primitive::BeginningOfStream) => serde_yaml::Value::Null, - UntaggedValue::Primitive(Primitive::Decimal(f)) => { - serde_yaml::Value::Number(serde_yaml::Number::from(f.to_f64().ok_or_else(|| { - ShellError::labeled_error( - "Could not convert to decimal", - "could not convert to decimal", - &v.tag, - ) - })?)) - } - UntaggedValue::Primitive(Primitive::Int(i)) => { - serde_yaml::Value::Number(serde_yaml::Number::from(*i)) - } - UntaggedValue::Primitive(Primitive::BigInt(i)) => { - serde_yaml::Value::Number(serde_yaml::Number::from(CoerceInto::::coerce_into( - i.tagged(&v.tag), - "converting to YAML number", - )?)) - } - UntaggedValue::Primitive(Primitive::Nothing) => serde_yaml::Value::Null, - UntaggedValue::Primitive(Primitive::GlobPattern(s)) => serde_yaml::Value::String(s.clone()), - UntaggedValue::Primitive(Primitive::String(s)) => serde_yaml::Value::String(s.clone()), - UntaggedValue::Primitive(Primitive::ColumnPath(path)) => { - let mut out = vec![]; - - for member in path { - match &member.unspanned { - UnspannedPathMember::String(string) => { - out.push(serde_yaml::Value::String(string.clone())) - } - UnspannedPathMember::Int(int) => { - out.push(serde_yaml::Value::Number(serde_yaml::Number::from(*int))) - } - } - } - - serde_yaml::Value::Sequence(out) - } - UntaggedValue::Primitive(Primitive::FilePath(s)) => { - serde_yaml::Value::String(s.display().to_string()) - } - - UntaggedValue::Table(l) => { - let mut out = vec![]; - - for value in l { - out.push(value_to_yaml_value(value)?); - } - - serde_yaml::Value::Sequence(out) - } - UntaggedValue::Error(e) => return Err(e.clone()), - UntaggedValue::Block(_) | UntaggedValue::Primitive(Primitive::Range(_)) => { - serde_yaml::Value::Null - } - #[cfg(feature = "dataframe")] - UntaggedValue::DataFrame(_) | UntaggedValue::FrameStruct(_) => serde_yaml::Value::Null, - UntaggedValue::Primitive(Primitive::Binary(b)) => serde_yaml::Value::Sequence( - b.iter() - .map(|x| serde_yaml::Value::Number(serde_yaml::Number::from(*x))) - .collect(), - ), - UntaggedValue::Row(o) => { - let mut m = serde_yaml::Mapping::new(); - for (k, v) in &o.entries { - m.insert( - serde_yaml::Value::String(k.clone()), - value_to_yaml_value(v)?, - ); - } - serde_yaml::Value::Mapping(m) - } - }) -} - -fn to_yaml(args: CommandArgs) -> Result { - let name_tag = args.name_tag(); - let name_span = name_tag.span; - - let input: Vec = args.input.collect(); - - let to_process_input = match input.len() { - x if x > 1 => { - let tag = input[0].tag.clone(); - vec![Value { - value: UntaggedValue::Table(input), - tag, - }] - } - 1 => input, - _ => vec![], - }; - - Ok((to_process_input.into_iter().map(move |value| { - let value_span = value.tag.span; - - match value_to_yaml_value(&value) { - Ok(yaml_value) => match serde_yaml::to_string(&yaml_value) { - Ok(x) => UntaggedValue::Primitive(Primitive::String(x)).into_value(&name_tag), - - _ => Value::error(ShellError::labeled_error_with_secondary( - "Expected a table with YAML-compatible structure from pipeline", - "requires YAML-compatible input", - name_span, - "originates from here".to_string(), - value_span, - )), - }, - _ => Value::error(ShellError::labeled_error( - "Expected a table with YAML-compatible structure from pipeline", - "requires YAML-compatible input", - &name_tag, - )), - } - })) - .into_output_stream()) -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::ToYaml; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(ToYaml {}) - } -} diff --git a/crates/nu-command/src/commands/generators/cal.rs b/crates/nu-command/src/commands/generators/cal.rs deleted file mode 100644 index 12195a4d1e..0000000000 --- a/crates/nu-command/src/commands/generators/cal.rs +++ /dev/null @@ -1,338 +0,0 @@ -use crate::prelude::*; -use chrono::{Datelike, Local, NaiveDate}; -use indexmap::IndexMap; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Dictionary, Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tagged; - -pub struct Cal; - -impl WholeStreamCommand for Cal { - fn name(&self) -> &str { - "cal" - } - - fn signature(&self) -> Signature { - Signature::build("cal") - .switch("year", "Display the year column", Some('y')) - .switch("quarter", "Display the quarter column", Some('q')) - .switch("month", "Display the month column", Some('m')) - .named( - "full-year", - SyntaxShape::Int, - "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", - None, - ) - } - - fn usage(&self) -> &str { - "Display a calendar." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - cal(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "This month's calendar", - example: "cal", - result: None, - }, - Example { - description: "The calendar for all of 2012", - 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, - }, - ] - } -} - -pub fn cal(args: CommandArgs) -> Result { - let mut calendar_vec_deque = VecDeque::new(); - let tag = args.call_info.name_tag.clone(); - - let (current_year, current_month, current_day) = get_current_date(); - - let mut selected_year: i32 = current_year; - let mut current_day_option: Option = Some(current_day); - - let full_year_value: Option = args.get_flag("full-year")?; - let month_range = if let Some(full_year_value) = full_year_value { - selected_year = full_year_value as i32; - - if selected_year != current_year { - current_day_option = None - } - - (1, 12) - } else { - (current_month, current_month) - }; - - add_months_of_year_to_table( - &args, - &mut calendar_vec_deque, - &tag, - selected_year, - month_range, - current_month, - current_day_option, - )?; - - Ok(calendar_vec_deque.into_iter().into_action_stream()) -} - -fn get_invalid_year_shell_error(year_tag: &Tag) -> ShellError { - ShellError::labeled_error("The year is invalid", "invalid year", year_tag) -} - -struct MonthHelper { - 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 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, - 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(), - }) - } - - 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 - if selected_month == 12 { - selected_year += 1; - selected_month = 1; - } else { - selected_month += 1; - }; - - let next_month_naive_date = - NaiveDate::from_ymd_opt(selected_year, selected_month, 1).ok_or(())?; - - Ok(next_month_naive_date.pred().day()) - } -} - -fn get_current_date() -> (i32, u32, u32) { - let local_now_date = Local::now().date(); - - let current_year: i32 = local_now_date.year(); - let current_month: u32 = local_now_date.month(); - let current_day: u32 = local_now_date.day(); - - (current_year, current_month, current_day) -} - -fn add_months_of_year_to_table( - args: &CommandArgs, - calendar_vec_deque: &mut VecDeque, - tag: &Tag, - selected_year: i32, - (start_month, end_month): (u32, u32), - current_month: u32, - current_day_option: Option, -) -> Result<(), ShellError> { - for month_number in start_month..=end_month { - let mut new_current_day_option: Option = None; - - if let Some(current_day) = current_day_option { - if month_number == current_month { - new_current_day_option = Some(current_day) - } - } - - let add_month_to_table_result = add_month_to_table( - args, - calendar_vec_deque, - tag, - selected_year, - month_number, - new_current_day_option, - ); - - add_month_to_table_result? - } - - Ok(()) -} - -fn add_month_to_table( - args: &CommandArgs, - calendar_vec_deque: &mut VecDeque, - tag: &Tag, - selected_year: i32, - current_month: u32, - current_day_option: Option, -) -> Result<(), ShellError> { - let month_helper_result = MonthHelper::new(selected_year, current_month); - - let full_year_value: Option> = args.get_flag("full-year")?; - - let month_helper = match month_helper_result { - Ok(month_helper) => month_helper, - Err(()) => match full_year_value { - Some(full_year_value) => { - return Err(get_invalid_year_shell_error(&full_year_value.tag())) - } - None => { - return Err(ShellError::labeled_error( - "Issue parsing command", - "invalid command", - tag, - )) - } - }, - }; - - let mut days_of_the_week = [ - "sunday", - "monday", - "tuesday", - "wednesday", - "thursday", - "friday", - "saturday", - ]; - - let mut week_start_day = days_of_the_week[0].to_string(); - - if let Some(day) = args.get_flag::>("week-start")? { - if days_of_the_week.contains(&day.item.as_str()) { - week_start_day = day.item; - } else { - return Err(ShellError::labeled_error( - "The specified week start day is invalid", - "invalid week start day", - day.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_flag("year"); - let should_show_quarter_column = args.has_flag("quarter"); - let should_show_month_column = args.has_flag("month"); - let should_show_month_names = args.has_flag("month-names"); - - while day_number <= day_limit { - let mut indexmap = IndexMap::new(); - - if should_show_year_column { - indexmap.insert( - "year".to_string(), - UntaggedValue::int(month_helper.selected_year).into_value(tag), - ); - } - - if should_show_quarter_column { - indexmap.insert( - "quarter".to_string(), - UntaggedValue::int(month_helper.quarter_number).into_value(tag), - ); - } - - if should_show_month_column || should_show_month_names { - let month_value = if should_show_month_names { - UntaggedValue::string(month_helper.month_name.clone()).into_value(tag) - } else { - UntaggedValue::int(month_helper.selected_month).into_value(tag) - }; - - indexmap.insert("month".to_string(), month_value); - } - - for day in &days_of_the_week { - let should_add_day_number_to_table = - (day_number > total_start_offset) && (day_number <= day_limit); - - let mut value = UntaggedValue::nothing().into_value(tag); - - if should_add_day_number_to_table { - let adjusted_day_number = day_number - total_start_offset; - - value = UntaggedValue::int(adjusted_day_number).into_value(tag); - - if let Some(current_day) = current_day_option { - if current_day == adjusted_day_number { - // TODO: Update the value here with a color when color support is added - // This colors the current day - } - } - } - - indexmap.insert((*day).to_string(), value); - - day_number += 1; - } - - calendar_vec_deque - .push_back(UntaggedValue::Row(Dictionary::from(indexmap)).into_value(tag)); - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::Cal; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Cal {}) - } -} diff --git a/crates/nu-command/src/commands/generators/date/command.rs b/crates/nu-command/src/commands/generators/date/command.rs deleted file mode 100644 index 1e736eb10b..0000000000 --- a/crates/nu-command/src/commands/generators/date/command.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, UntaggedValue}; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "date" - } - - fn signature(&self) -> Signature { - Signature::build("date") - } - - fn usage(&self) -> &str { - "Apply date function." - } - - fn run(&self, args: CommandArgs) -> Result { - Ok(OutputStream::one( - UntaggedValue::string(get_full_help(&Command, args.scope())).into_value(Tag::unknown()), - )) - } -} - -#[cfg(test)] -mod tests { - use super::Command; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Command {}) - } -} diff --git a/crates/nu-command/src/commands/generators/date/format.rs b/crates/nu-command/src/commands/generators/date/format.rs deleted file mode 100644 index 73c486180f..0000000000 --- a/crates/nu-command/src/commands/generators/date/format.rs +++ /dev/null @@ -1,108 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Dictionary, Primitive, Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tagged; -use std::fmt::{self, write}; - -pub struct Date; - -impl WholeStreamCommand for Date { - fn name(&self) -> &str { - "date format" - } - - fn signature(&self) -> Signature { - Signature::build("date format") - .required("format", SyntaxShape::String, "strftime format") - .switch("table", "print date in a table", Some('t')) - } - - fn usage(&self) -> &str { - "Format a given date using the given format string." - } - - fn run(&self, args: CommandArgs) -> Result { - format(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Format the current date", - example: "date now | date format '%Y.%m.%d_%H %M %S,%z'", - result: None, - }, - Example { - description: "Format the current date and print in a table", - example: "date now | date format -t '%Y-%m-%d_%H:%M:%S %z'", - result: None, - }, - ] - } -} - -pub fn format(args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let format: Tagged = args.req(0)?; - let table: Option = args.get_flag("table")?; - - let input = if args.input.is_empty() { - InputStream::one(super::now::date_now(&tag)) - } else { - args.input - }; - - Ok(input - .map(move |value| match value { - Value { - value: UntaggedValue::Primitive(Primitive::Date(dt)), - .. - } => { - let mut output = String::new(); - if let Err(fmt::Error) = - write(&mut output, format_args!("{}", dt.format(&format.item))) - { - Err(ShellError::labeled_error( - "The date format is invalid", - "invalid strftime format", - &format.tag, - )) - } else { - let value = if table.is_some() { - let mut indexmap = IndexMap::new(); - indexmap.insert( - "formatted".to_string(), - UntaggedValue::string(&output).into_value(&tag), - ); - - UntaggedValue::Row(Dictionary::from(indexmap)).into_value(&tag) - } else { - UntaggedValue::string(&output).into_value(&tag) - }; - - Ok(value) - } - } - _ => Err(ShellError::labeled_error( - "Expected a date from pipeline", - "requires date input", - &tag, - )), - }) - .into_input_stream()) -} - -#[cfg(test)] -mod tests { - use super::Date; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Date {}) - } -} diff --git a/crates/nu-command/src/commands/generators/date/humanize.rs b/crates/nu-command/src/commands/generators/date/humanize.rs deleted file mode 100644 index af440f9819..0000000000 --- a/crates/nu-command/src/commands/generators/date/humanize.rs +++ /dev/null @@ -1,81 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Dictionary, Primitive, Signature, UntaggedValue, Value}; - -pub struct Date; - -impl WholeStreamCommand for Date { - fn name(&self) -> &str { - "date humanize" - } - - fn signature(&self) -> Signature { - Signature::build("date humanize").switch("table", "print date in a table", Some('t')) - } - - fn usage(&self) -> &str { - "Print a 'humanized' format for the date, relative to now." - } - - fn run(&self, args: CommandArgs) -> Result { - humanize(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Humanize the current date", - example: "date now | date humanize", - result: None, - }] - } -} - -pub fn humanize(args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let table: Option = args.get_flag("table")?; - let input = args.input; - - Ok(input - .map(move |value| match value { - Value { - value: UntaggedValue::Primitive(dt @ Primitive::Date(_)), - .. - } => { - let output = nu_protocol::format_primitive(&dt, None); - let value = if table.is_some() { - let mut indexmap = IndexMap::new(); - indexmap.insert( - "formatted".to_string(), - UntaggedValue::string(&output).into_value(&tag), - ); - - UntaggedValue::Row(Dictionary::from(indexmap)).into_value(&tag) - } else { - UntaggedValue::string(&output).into_value(&tag) - }; - - Ok(value) - } - _ => Err(ShellError::labeled_error( - "Expected a date from pipeline", - "requires date input", - &tag, - )), - }) - .into_input_stream()) -} - -#[cfg(test)] -mod tests { - use super::Date; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Date {}) - } -} diff --git a/crates/nu-command/src/commands/generators/date/list_timezone.rs b/crates/nu-command/src/commands/generators/date/list_timezone.rs deleted file mode 100644 index af7fc94ad6..0000000000 --- a/crates/nu-command/src/commands/generators/date/list_timezone.rs +++ /dev/null @@ -1,71 +0,0 @@ -use crate::prelude::*; -use chrono_tz::TZ_VARIANTS; -use indexmap::IndexMap; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Dictionary, Signature, UntaggedValue}; - -pub struct Date; - -impl WholeStreamCommand for Date { - fn name(&self) -> &str { - "date list-timezone" - } - - fn signature(&self) -> Signature { - Signature::build("date list-timezone") - } - - fn usage(&self) -> &str { - "List supported time zones." - } - - fn run(&self, args: CommandArgs) -> Result { - list_timezone(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "List all supported time zones", - example: "date list-timezone", - result: None, - }, - Example { - description: "List all supported European time zones", - example: "date list-timezone | where timezone =~ Europe", - result: None, - }, - ] - } -} - -fn list_timezone(args: CommandArgs) -> Result { - let tag = args.call_info.name_tag; - - let list = TZ_VARIANTS.iter().map(move |tz| { - let mut entries = IndexMap::new(); - - entries.insert( - "timezone".to_string(), - UntaggedValue::string(tz.name()).into_value(&tag), - ); - - Ok(UntaggedValue::Row(Dictionary { entries }).into_value(&tag)) - }); - - Ok(list.into_iter().into_input_stream()) -} - -#[cfg(test)] -mod tests { - use super::Date; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Date {}) - } -} diff --git a/crates/nu-command/src/commands/generators/date/mod.rs b/crates/nu-command/src/commands/generators/date/mod.rs deleted file mode 100644 index c98e62ef29..0000000000 --- a/crates/nu-command/src/commands/generators/date/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -pub mod command; -pub mod format; -pub mod humanize; -pub mod list_timezone; -pub mod now; -pub mod to_table; -pub mod to_timezone; - -mod parser; - -pub use command::Command as Date; -pub use format::Date as DateFormat; -pub use humanize::Date as DateHumanize; -pub use list_timezone::Date as DateListTimeZone; -pub use now::Date as DateNow; -pub use to_table::Date as DateToTable; -pub use to_timezone::Date as DateToTimeZone; diff --git a/crates/nu-command/src/commands/generators/date/now.rs b/crates/nu-command/src/commands/generators/date/now.rs deleted file mode 100644 index fda7b596a4..0000000000 --- a/crates/nu-command/src/commands/generators/date/now.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::prelude::*; -use chrono::{DateTime, Local}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, UntaggedValue, Value}; - -pub struct Date; - -impl WholeStreamCommand for Date { - fn name(&self) -> &str { - "date now" - } - - fn signature(&self) -> Signature { - Signature::build("date now") - } - - fn usage(&self) -> &str { - "Get the current date." - } - - fn run(&self, args: CommandArgs) -> Result { - now(args) - } -} - -pub fn date_now(tag: &Tag) -> Value { - let now: DateTime = Local::now(); - - UntaggedValue::date(now.with_timezone(now.offset())).into_value(tag) -} - -pub fn now(args: CommandArgs) -> Result { - let value = date_now(&args.call_info.name_tag); - - Ok(OutputStream::one(value)) -} - -#[cfg(test)] -mod tests { - use super::Date; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Date {}) - } -} diff --git a/crates/nu-command/src/commands/generators/date/to_table.rs b/crates/nu-command/src/commands/generators/date/to_table.rs deleted file mode 100644 index 10163451f8..0000000000 --- a/crates/nu-command/src/commands/generators/date/to_table.rs +++ /dev/null @@ -1,107 +0,0 @@ -use crate::prelude::*; -use chrono::{Datelike, Timelike}; -use indexmap::IndexMap; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Dictionary, Primitive, Signature, UntaggedValue, Value}; - -pub struct Date; - -impl WholeStreamCommand for Date { - fn name(&self) -> &str { - "date to-table" - } - - fn signature(&self) -> Signature { - Signature::build("date to-table") - } - - fn usage(&self) -> &str { - "Print the date in a structured table." - } - - fn run(&self, args: CommandArgs) -> Result { - to_table(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Print the current date in a table", - example: "date now | date to-table", - result: None, - }] - } -} - -fn to_table(args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let input = if args.input.is_empty() { - InputStream::one(super::now::date_now(&tag)) - } else { - args.input - }; - - Ok(input - .map(move |value| match value { - Value { - value: UntaggedValue::Primitive(Primitive::Date(dt)), - .. - } => { - let mut indexmap = IndexMap::new(); - - indexmap.insert( - "year".to_string(), - UntaggedValue::int(dt.year()).into_value(&tag), - ); - indexmap.insert( - "month".to_string(), - UntaggedValue::int(dt.month()).into_value(&tag), - ); - indexmap.insert( - "day".to_string(), - UntaggedValue::int(dt.day()).into_value(&tag), - ); - indexmap.insert( - "hour".to_string(), - UntaggedValue::int(dt.hour()).into_value(&tag), - ); - indexmap.insert( - "minute".to_string(), - UntaggedValue::int(dt.minute()).into_value(&tag), - ); - indexmap.insert( - "second".to_string(), - UntaggedValue::int(dt.second()).into_value(&tag), - ); - - let tz = dt.offset(); - indexmap.insert( - "timezone".to_string(), - UntaggedValue::string(tz.to_string()).into_value(&tag), - ); - - let value = UntaggedValue::Row(Dictionary::from(indexmap)).into_value(&tag); - - Ok(value) - } - _ => Err(ShellError::labeled_error( - "Expected a date from pipeline", - "requires date input", - &tag, - )), - }) - .into_input_stream()) -} - -#[cfg(test)] -mod tests { - use super::Date; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Date {}) - } -} diff --git a/crates/nu-command/src/commands/generators/date/to_timezone.rs b/crates/nu-command/src/commands/generators/date/to_timezone.rs deleted file mode 100644 index 6e1994ba71..0000000000 --- a/crates/nu-command/src/commands/generators/date/to_timezone.rs +++ /dev/null @@ -1,107 +0,0 @@ -use super::parser::{datetime_in_timezone, ParseErrorKind}; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Primitive, Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tagged; - -pub struct Date; - -impl WholeStreamCommand for Date { - fn name(&self) -> &str { - "date to-timezone" - } - - fn signature(&self) -> Signature { - Signature::build("date to-timezone").required( - "time zone", - SyntaxShape::String, - "time zone description", - ) - } - - fn usage(&self) -> &str { - "Convert a date to a given time zone." - } - - fn extra_usage(&self) -> &str { - "Use 'date list-timezone' to list all supported time zones." - } - - fn run(&self, args: CommandArgs) -> Result { - to_timezone(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Get the current date in UTC+05:00", - example: "date now | date to-timezone +0500", - result: None, - }, - Example { - description: "Get the current local date", - example: "date now | date to-timezone local", - result: None, - }, - Example { - description: "Get the current date in Hawaii", - example: "date now | date to-timezone US/Hawaii", - result: None, - }, - ] - } -} - -fn to_timezone(args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let timezone: Tagged = args.req(0)?; - - Ok(args - .input - .map(move |value| match value { - Value { - value: UntaggedValue::Primitive(Primitive::Date(dt)), - .. - } => match datetime_in_timezone(&dt, &timezone.item) { - Ok(dt) => { - let value = UntaggedValue::date(dt).into_value(&tag); - - Ok(value) - } - Err(e) => Err(ShellError::labeled_error( - error_message(e), - "invalid time zone", - &timezone.tag, - )), - }, - _ => Err(ShellError::labeled_error( - "Expected a date from pipeline", - "requires date input", - &tag, - )), - }) - .into_input_stream()) -} - -fn error_message(err: ParseErrorKind) -> &'static str { - match err { - ParseErrorKind::Invalid => "The time zone description is invalid", - ParseErrorKind::OutOfRange => "The time zone offset is out of range", - ParseErrorKind::TooShort => "The format of the time zone is invalid", - } -} - -#[cfg(test)] -mod tests { - use super::Date; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Date {}) - } -} diff --git a/crates/nu-command/src/commands/generators/for_in.rs b/crates/nu-command/src/commands/generators/for_in.rs deleted file mode 100644 index ef455c6109..0000000000 --- a/crates/nu-command/src/commands/generators/for_in.rs +++ /dev/null @@ -1,167 +0,0 @@ -use crate::prelude::*; -use nu_engine::{evaluate_baseline_expr, run_block}; -use nu_engine::{FromValue, WholeStreamCommand}; - -use nu_errors::ShellError; -use nu_protocol::{ - hir::{CapturedBlock, ExternalRedirection}, - Signature, SyntaxShape, TaggedDictBuilder, UntaggedValue, Value, -}; - -pub struct ForIn; - -impl WholeStreamCommand for ForIn { - fn name(&self) -> &str { - "for" - } - - fn signature(&self) -> Signature { - Signature::build("for") - .required("var", SyntaxShape::String, "the name of the variable") - .required("in", SyntaxShape::String, "the word 'in'") - .required("value", SyntaxShape::Any, "the value we want to iterate") - .required("block", SyntaxShape::Block, "the block to run on each item") - .switch( - "numbered", - "returned a numbered item ($it.index and $it.item)", - Some('n'), - ) - } - - fn usage(&self) -> &str { - "Run a block on each row of the table." - } - - fn run(&self, args: CommandArgs) -> Result { - for_in(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Echo the square of each integer", - example: "for x in [1 2 3] { $x * $x }", - result: Some(vec![ - UntaggedValue::int(1).into(), - UntaggedValue::int(4).into(), - UntaggedValue::int(9).into(), - ]), - }, - Example { - description: "Work with elements of a range", - example: "for $x in 1..3 { $x }", - result: Some(vec![ - UntaggedValue::int(1).into(), - UntaggedValue::int(2).into(), - UntaggedValue::int(3).into(), - ]), - }, - Example { - description: "Number each item and echo a message", - example: "for $it in ['bob' 'fred'] --numbered { $\"($it.index) is ($it.item)\" }", - result: Some(vec![Value::from("0 is bob"), Value::from("1 is fred")]), - }, - ] - } -} - -pub fn process_row( - captured_block: &CapturedBlock, - context: &EvaluationContext, - input: Value, - var_name: &str, - external_redirection: ExternalRedirection, -) -> Result { - let input_clone = input.clone(); - // When we process a row, we need to know whether the block wants to have the contents of the row as - // a parameter to the block (so it gets assigned to a variable that can be used inside the block) or - // if it wants the contents as as an input stream - - let input_stream = if !captured_block.block.params.positional.is_empty() { - InputStream::empty() - } else { - vec![Ok(input_clone)].into_iter().into_input_stream() - }; - - context.scope.enter_scope(); - context.scope.add_vars(&captured_block.captured.entries); - - context.scope.add_var(var_name, input); - - let result = run_block( - &captured_block.block, - context, - input_stream, - external_redirection, - ); - - context.scope.exit_scope(); - - result -} - -pub(crate) fn make_indexed_item(index: usize, item: Value) -> Value { - let mut dict = TaggedDictBuilder::new(item.tag()); - dict.insert_untagged("index", UntaggedValue::int(index as i64)); - dict.insert_value("item", item); - - dict.into_value() -} - -fn for_in(args: CommandArgs) -> Result { - let context = args.context.clone(); - let external_redirection = args.call_info.args.external_redirection; - // - let numbered: bool = args.call_info.switch_present("numbered"); - let positional = args - .call_info - .args - .positional - .expect("Internal error: type checker should require args"); - - let var_name = positional[0].var_name()?; - let rhs = evaluate_baseline_expr(&positional[2], &context)?; - - let block: CapturedBlock = - FromValue::from_value(&evaluate_baseline_expr(&positional[3], &context)?)?; - - let input = crate::commands::core_commands::echo::expand_value_to_stream(rhs); - - if numbered { - Ok(input - .enumerate() - .flat_map(move |input| { - let row = make_indexed_item(input.0, input.1); - - match process_row(&block, &context, row, &var_name, external_redirection) { - Ok(s) => s, - Err(e) => OutputStream::one(Value::error(e)), - } - }) - .into_output_stream()) - } else { - Ok(input - .flat_map(move |input| { - let block = block.clone(); - - match process_row(&block, &context, input, &var_name, external_redirection) { - Ok(s) => s, - Err(e) => OutputStream::one(Value::error(e)), - } - }) - .into_output_stream()) - } -} - -#[cfg(test)] -mod tests { - use super::ForIn; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(ForIn {}) - } -} diff --git a/crates/nu-command/src/commands/generators/hash_/base64_.rs b/crates/nu-command/src/commands/generators/hash_/base64_.rs deleted file mode 100644 index a623bc3a69..0000000000 --- a/crates/nu-command/src/commands/generators/hash_/base64_.rs +++ /dev/null @@ -1,288 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::ShellTypeName; -use nu_protocol::{ColumnPath, Primitive, Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::{Tag, Tagged}; - -use base64::{decode_config, encode_config}; - -#[derive(Clone)] -pub struct Base64Config { - pub character_set: String, - pub action_type: ActionType, -} - -#[derive(Clone, Copy, PartialEq)] -pub enum ActionType { - Encode, - Decode, -} -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "hash base64" - } - - fn signature(&self) -> Signature { - Signature::build("hash base64") - .named( - "character_set", - SyntaxShape::String, - "specify the character rules for encoding the input.\n\ - \tValid values are 'standard', 'standard-no-padding', 'url-safe', 'url-safe-no-padding',\ - 'binhex', 'bcrypt', 'crypt'", - Some('c'), - ) - .switch( - "encode", - "encode the input as base64. This is the default behavior if not specified.", - Some('e') - ) - .switch( - "decode", - "decode the input from base64", - Some('d')) - .rest( -"rest", - SyntaxShape::ColumnPath, - "optionally base64 encode / decode data by column paths", - ) - } - - fn usage(&self) -> &str { - "base64 encode or decode a value" - } - - fn run(&self, args: CommandArgs) -> Result { - operate(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Base64 encode a string with default settings", - example: "echo 'username:password' | hash base64", - result: Some(vec![ - UntaggedValue::string("dXNlcm5hbWU6cGFzc3dvcmQ=").into_untagged_value() - ]), - }, - Example { - description: "Base64 encode a string with the binhex character set", - example: "echo 'username:password' | hash base64 --character_set binhex --encode", - result: Some(vec![ - UntaggedValue::string("F@0NEPjJD97kE'&bEhFZEP3").into_untagged_value() - ]), - }, - Example { - description: "Base64 decode a value", - example: "echo 'dXNlcm5hbWU6cGFzc3dvcmQ=' | hash base64 --decode", - result: Some(vec![ - UntaggedValue::string("username:password").into_untagged_value() - ]), - }, - ] - } -} - -fn operate(args: CommandArgs) -> Result { - let name_tag = args.call_info.name_tag.clone(); - - let encode = args.has_flag("encode"); - let decode = args.has_flag("decode"); - let character_set: Option> = args.get_flag("character_set")?; - let column_paths: Vec = args.rest(0)?; - - if encode && decode { - return Err(ShellError::labeled_error( - "only one of --decode and --encode flags can be used", - "conflicting flags", - name_tag, - )); - } - - // Default the action to be encoding if no flags are specified. - let action_type = if decode { - ActionType::Decode - } else { - ActionType::Encode - }; - - // Default the character set to standard if the argument is not specified. - let character_set = match character_set { - Some(inner_tag) => inner_tag.item().to_string(), - None => "standard".to_string(), - }; - - let encoding_config = Base64Config { - character_set, - action_type, - }; - - Ok(args - .input - .map(move |v| { - if column_paths.is_empty() { - action(&v, &encoding_config, v.tag()) - } else { - let mut ret = v; - - for path in &column_paths { - let config = encoding_config.clone(); - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| action(old, &config, old.tag())), - )?; - } - - Ok(ret) - } - }) - .into_input_stream()) -} - -fn action( - input: &Value, - base64_config: &Base64Config, - tag: impl Into, -) -> Result { - match &input.value { - UntaggedValue::Primitive(Primitive::String(s)) => { - let base64_config_enum: base64::Config = if &base64_config.character_set == "standard" { - base64::STANDARD - } else if &base64_config.character_set == "standard-no-padding" { - base64::STANDARD_NO_PAD - } else if &base64_config.character_set == "url-safe" { - base64::URL_SAFE - } else if &base64_config.character_set == "url-safe-no-padding" { - base64::URL_SAFE_NO_PAD - } else if &base64_config.character_set == "binhex" { - base64::BINHEX - } else if &base64_config.character_set == "bcrypt" { - base64::BCRYPT - } else if &base64_config.character_set == "crypt" { - base64::CRYPT - } else { - return Err(ShellError::labeled_error( - "value is not an accepted character set", - format!( - "{} is not a valid character-set.\nPlease use `help hash base64` to see a list of valid character sets.", - &base64_config.character_set - ), - tag.into().span, - )); - }; - - match base64_config.action_type { - ActionType::Encode => Ok(UntaggedValue::string(encode_config( - &s, - base64_config_enum, - )) - .into_value(tag)), - ActionType::Decode => { - let decode_result = decode_config(&s, base64_config_enum); - - match decode_result { - Ok(decoded_value) => Ok(UntaggedValue::string( - std::string::String::from_utf8_lossy(&decoded_value), - ) - .into_value(tag)), - Err(_) => Err(ShellError::labeled_error( - "value could not be base64 decoded", - format!( - "invalid base64 input for character set {}", - &base64_config.character_set - ), - tag.into().span, - )), - } - } - } - } - other => { - let got = format!("got {}", other.type_name()); - Err(ShellError::labeled_error( - "value is not string", - got, - tag.into().span, - )) - } - } -} - -#[cfg(test)] -mod tests { - use super::{action, ActionType, Base64Config}; - use nu_protocol::UntaggedValue; - use nu_source::Tag; - use nu_test_support::value::string; - - #[test] - fn base64_encode_standard() { - let word = string("username:password"); - let expected = UntaggedValue::string("dXNlcm5hbWU6cGFzc3dvcmQ=").into_untagged_value(); - - let actual = action( - &word, - &Base64Config { - character_set: "standard".to_string(), - action_type: ActionType::Encode, - }, - Tag::unknown(), - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn base64_encode_standard_no_padding() { - let word = string("username:password"); - let expected = UntaggedValue::string("dXNlcm5hbWU6cGFzc3dvcmQ").into_untagged_value(); - - let actual = action( - &word, - &Base64Config { - character_set: "standard-no-padding".to_string(), - action_type: ActionType::Encode, - }, - Tag::unknown(), - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn base64_encode_url_safe() { - let word = string("this is for url"); - let expected = UntaggedValue::string("dGhpcyBpcyBmb3IgdXJs").into_untagged_value(); - - let actual = action( - &word, - &Base64Config { - character_set: "url-safe".to_string(), - action_type: ActionType::Encode, - }, - Tag::unknown(), - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn base64_decode_binhex() { - let word = string("A5\"KC9jRB@IIF'8bF!"); - let expected = UntaggedValue::string("a binhex test").into_untagged_value(); - - let actual = action( - &word, - &Base64Config { - character_set: "binhex".to_string(), - action_type: ActionType::Decode, - }, - Tag::unknown(), - ) - .unwrap(); - assert_eq!(actual, expected); - } -} diff --git a/crates/nu-command/src/commands/generators/hash_/command.rs b/crates/nu-command/src/commands/generators/hash_/command.rs deleted file mode 100644 index 90ea809629..0000000000 --- a/crates/nu-command/src/commands/generators/hash_/command.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue}; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "hash" - } - - fn signature(&self) -> Signature { - Signature::build("hash").rest( - "rest", - SyntaxShape::ColumnPath, - "optionally convert by column paths", - ) - } - - fn usage(&self) -> &str { - "Apply hash function." - } - - fn run(&self, args: CommandArgs) -> Result { - Ok(OutputStream::one( - UntaggedValue::string(get_full_help(&Command, args.scope())).into_value(Tag::unknown()), - )) - } -} - -#[cfg(test)] -mod tests { - use super::Command; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Command {}) - } -} diff --git a/crates/nu-command/src/commands/generators/hash_/generic_digest.rs b/crates/nu-command/src/commands/generators/hash_/generic_digest.rs deleted file mode 100644 index bc0542e5f9..0000000000 --- a/crates/nu-command/src/commands/generators/hash_/generic_digest.rs +++ /dev/null @@ -1,103 +0,0 @@ -use std::marker::PhantomData; - -use crate::prelude::*; -use nu_errors::ShellError; -use nu_protocol::{ColumnPath, Primitive, SyntaxShape, UntaggedValue, Value}; -use nu_protocol::{ShellTypeName, Signature}; -use nu_source::Tag; - -pub trait HashDigest: digest::Digest { - fn name() -> &'static str; - fn examples() -> Vec; -} - -pub struct SubCommand { - name_string: String, - usage_string: String, - phantom: PhantomData, -} - -impl Default for SubCommand { - fn default() -> Self { - Self { - name_string: format!("hash {}", D::name()), - usage_string: format!("{} encode a value", D::name()), - phantom: PhantomData, - } - } -} - -impl WholeStreamCommand for SubCommand -where - D: HashDigest + Send + Sync, - digest::Output: core::fmt::LowerHex, -{ - fn name(&self) -> &str { - &self.name_string - } - - fn signature(&self) -> Signature { - Signature::build(self.name()).rest( - "rest", - SyntaxShape::ColumnPath, - format!("optionally {} encode data by column paths", D::name()), - ) - } - - fn usage(&self) -> &str { - &self.usage_string - } - - fn examples(&self) -> Vec { - D::examples() - } - - fn run(&self, args: CommandArgs) -> Result { - let column_paths: Vec = args.rest(0)?; - - Ok(args - .input - .map(move |v| { - if column_paths.is_empty() { - action::(&v, v.tag()) - } else { - let mut ret = v; - - for path in &column_paths { - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| action::(old, old.tag())), - )?; - } - - Ok(ret) - } - }) - .into_input_stream()) - } -} - -pub fn action(input: &Value, tag: Tag) -> Result -where - D: HashDigest, - digest::Output: core::fmt::LowerHex, -{ - match &input.value { - UntaggedValue::Primitive(Primitive::String(s)) => { - let digest_result = D::digest(s.as_bytes()); - Ok(UntaggedValue::string(&format!("{:x}", digest_result)).into_value(tag)) - } - UntaggedValue::Primitive(Primitive::Binary(bytes)) => { - let digest_result = D::digest(bytes); - Ok(UntaggedValue::string(&format!("{:x}", digest_result)).into_value(tag)) - } - other => { - let got = format!("got {}", other.type_name()); - Err(ShellError::labeled_error( - format!("value is not supported for hashing as {}", D::name()), - got, - tag.span, - )) - } - } -} diff --git a/crates/nu-command/src/commands/generators/hash_/md5_.rs b/crates/nu-command/src/commands/generators/hash_/md5_.rs deleted file mode 100644 index 0559e7dfc6..0000000000 --- a/crates/nu-command/src/commands/generators/hash_/md5_.rs +++ /dev/null @@ -1,65 +0,0 @@ -use crate::prelude::*; -use md5::Md5; -use nu_protocol::UntaggedValue; - -use super::generic_digest::{self, HashDigest}; - -pub type SubCommand = generic_digest::SubCommand; - -impl HashDigest for Md5 { - fn name() -> &'static str { - "md5" - } - - fn examples() -> Vec { - vec![ - Example { - description: "md5 encode a string", - example: "echo 'abcdefghijklmnopqrstuvwxyz' | hash md5", - result: Some(vec![UntaggedValue::string( - "c3fcd3d76192e4007dfb496cca67e13b", - ) - .into_untagged_value()]), - }, - Example { - description: "md5 encode a file", - example: "open ./nu_0_24_1_windows.zip | hash md5", - result: Some(vec![UntaggedValue::string( - "dcf30f2836a1a99fc55cf72e28272606", - ) - .into_untagged_value()]), - }, - ] - } -} - -#[cfg(test)] -mod tests { - use md5::Md5; - use nu_protocol::{Primitive, UntaggedValue}; - use nu_source::Tag; - use nu_test_support::value::string; - - use crate::commands::generators::hash_::generic_digest::action; - - #[test] - fn md5_encode_string() { - let word = string("abcdefghijklmnopqrstuvwxyz"); - let expected = - UntaggedValue::string("c3fcd3d76192e4007dfb496cca67e13b").into_untagged_value(); - - let actual = action::(&word, Tag::unknown()).unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn md5_encode_bytes() { - let bytes = vec![0xC0, 0xFF, 0xEE]; - let binary = UntaggedValue::Primitive(Primitive::Binary(bytes)).into_untagged_value(); - let expected = - UntaggedValue::string("5f80e231382769b0102b1164cf722d83").into_untagged_value(); - - let actual = action::(&binary, Tag::unknown()).unwrap(); - assert_eq!(actual, expected); - } -} diff --git a/crates/nu-command/src/commands/generators/hash_/mod.rs b/crates/nu-command/src/commands/generators/hash_/mod.rs deleted file mode 100644 index 0a76ae094a..0000000000 --- a/crates/nu-command/src/commands/generators/hash_/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -mod base64_; -mod command; -mod generic_digest; -mod md5_; -mod sha256_; - -pub use base64_::SubCommand as HashBase64; -pub use command::Command as Hash; -pub use md5_::SubCommand as HashMd5; -pub use sha256_::SubCommand as HashSha256; diff --git a/crates/nu-command/src/commands/generators/hash_/sha256_.rs b/crates/nu-command/src/commands/generators/hash_/sha256_.rs deleted file mode 100644 index 4028dfdb42..0000000000 --- a/crates/nu-command/src/commands/generators/hash_/sha256_.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::prelude::*; -use nu_protocol::UntaggedValue; -use sha2::Sha256; - -use super::generic_digest::{self, HashDigest}; - -pub type SubCommand = generic_digest::SubCommand; - -impl HashDigest for Sha256 { - fn name() -> &'static str { - "sha256" - } - - fn examples() -> Vec { - vec![ - Example { - description: "sha256 encode a string", - example: "echo 'abcdefghijklmnopqrstuvwxyz' | hash sha256", - result: Some(vec![UntaggedValue::string( - "71c480df93d6ae2f1efad1447c66c9525e316218cf51fc8d9ed832f2daf18b73", - ) - .into_untagged_value()]), - }, - Example { - description: "sha256 encode a file", - example: "open ./nu_0_24_1_windows.zip | hash sha256", - result: Some(vec![UntaggedValue::string( - "c47a10dc272b1221f0380a2ae0f7d7fa830b3e378f2f5309bbf13f61ad211913", - ) - .into_untagged_value()]), - }, - ] - } -} - -#[cfg(test)] -mod tests { - use nu_protocol::{Primitive, UntaggedValue}; - use nu_source::Tag; - use nu_test_support::value::string; - use sha2::Sha256; - - use crate::commands::generators::hash_::generic_digest::action; - - #[test] - fn md5_encode_string() { - let word = string("abcdefghijklmnopqrstuvwxyz"); - let expected = UntaggedValue::string( - "71c480df93d6ae2f1efad1447c66c9525e316218cf51fc8d9ed832f2daf18b73", - ) - .into_untagged_value(); - - let actual = action::(&word, Tag::unknown()).unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn md5_encode_bytes() { - let bytes = vec![0xC0, 0xFF, 0xEE]; - let binary = UntaggedValue::Primitive(Primitive::Binary(bytes)).into_untagged_value(); - let expected = UntaggedValue::string( - "c47a10dc272b1221f0380a2ae0f7d7fa830b3e378f2f5309bbf13f61ad211913", - ) - .into_untagged_value(); - - let actual = action::(&binary, Tag::unknown()).unwrap(); - assert_eq!(actual, expected); - } -} diff --git a/crates/nu-command/src/commands/generators/mod.rs b/crates/nu-command/src/commands/generators/mod.rs deleted file mode 100644 index 21e5102bfb..0000000000 --- a/crates/nu-command/src/commands/generators/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -mod cal; -mod date; -mod for_in; -mod hash_; -mod seq; -mod seq_dates; - -pub use cal::Cal; -pub use date::*; -pub use for_in::ForIn; -pub use hash_::*; -pub use seq::Seq; -pub use seq_dates::SeqDates; diff --git a/crates/nu-command/src/commands/generators/seq.rs b/crates/nu-command/src/commands/generators/seq.rs deleted file mode 100644 index a0eb3fb958..0000000000 --- a/crates/nu-command/src/commands/generators/seq.rs +++ /dev/null @@ -1,368 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::value::{DecimalExt, I64Ext, StrExt}; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tagged; -use std::cmp; - -pub struct Seq; - -impl WholeStreamCommand for Seq { - fn name(&self) -> &str { - "seq" - } - - fn signature(&self) -> Signature { - Signature::build("seq") - .rest("rest", SyntaxShape::Number, "sequence values") - .named( - "separator", - SyntaxShape::String, - "separator character (defaults to \\n)", - Some('s'), - ) - .named( - "terminator", - SyntaxShape::String, - "terminator character (defaults to \\n)", - Some('t'), - ) - .switch( - "widths", - "equalize widths of all numbers by padding with zeros", - Some('w'), - ) - } - - fn usage(&self) -> &str { - "Print sequences of numbers." - } - - fn run(&self, args: CommandArgs) -> Result { - seq(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "sequence 1 to 10 with newline separator", - example: "seq 1 10", - result: Some(vec![ - UntaggedValue::int(1).into(), - UntaggedValue::int(2).into(), - UntaggedValue::int(3).into(), - UntaggedValue::int(4).into(), - UntaggedValue::int(5).into(), - UntaggedValue::int(6).into(), - UntaggedValue::int(7).into(), - UntaggedValue::int(8).into(), - UntaggedValue::int(9).into(), - UntaggedValue::int(10).into(), - ]), - }, - Example { - description: "sequence 1.0 to 2.0 by 0.1s with newline separator", - example: "seq 1.0 0.1 2.0", - result: Some(vec![ - UntaggedValue::decimal_from_float(1.0000, Span::default()).into(), - UntaggedValue::decimal_from_float(1.1000, Span::default()).into(), - UntaggedValue::decimal_from_float(1.2000, Span::default()).into(), - UntaggedValue::decimal_from_float(1.3000, Span::default()).into(), - UntaggedValue::decimal_from_float(1.4000, Span::default()).into(), - UntaggedValue::decimal_from_float(1.5000, Span::default()).into(), - UntaggedValue::decimal_from_float(1.6000, Span::default()).into(), - UntaggedValue::decimal_from_float(1.7000, Span::default()).into(), - UntaggedValue::decimal_from_float(1.8000, Span::default()).into(), - UntaggedValue::decimal_from_float(1.9000, Span::default()).into(), - UntaggedValue::decimal_from_float(2.0000, Span::default()).into(), - ]), - }, - Example { - description: "sequence 1 to 10 with pipe separator", - example: "seq -s '|' 1 10", - result: Some(vec![Value::from("1|2|3|4|5|6|7|8|9|10")]), - }, - Example { - description: "sequence 1 to 10 with pipe separator padded with 0", - example: "seq -s '|' -w 1 10", - result: Some(vec![Value::from("01|02|03|04|05|06|07|08|09|10")]), - }, - Example { - description: "sequence 1 to 10 with pipe separator padded by 2s", - example: "seq -s ' | ' -w 1 2 10", - result: Some(vec![Value::from("01 | 03 | 05 | 07 | 09")]), - }, - ] - } -} - -fn seq(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - - let rest_nums: Vec> = args.rest(0)?; - let separator: Option> = args.get_flag("separator")?; - let terminator: Option> = args.get_flag("terminator")?; - let widths = args.has_flag("widths"); - - if rest_nums.is_empty() { - return Err(ShellError::labeled_error( - "seq requires some parameters", - "needs parameter", - name, - )); - } - - let sep: String = match separator { - Some(s) => { - if s.item == r"\t" { - '\t'.to_string() - } else if s.item == r"\n" { - '\n'.to_string() - } else if s.item == r"\r" { - '\r'.to_string() - } else { - let vec_s: Vec = s.chars().collect(); - if vec_s.is_empty() { - return Err(ShellError::labeled_error( - "Expected a single separator char from --separator", - "requires a single character string input", - &s.tag, - )); - }; - vec_s.iter().collect() - } - } - _ => '\n'.to_string(), - }; - - let term: String = match terminator { - Some(t) => { - if t.item == r"\t" { - '\t'.to_string() - } else if t.item == r"\n" { - '\n'.to_string() - } else if t.item == r"\r" { - '\r'.to_string() - } else { - let vec_t: Vec = t.chars().collect(); - if vec_t.is_empty() { - return Err(ShellError::labeled_error( - "Expected a single terminator char from --terminator", - "requires a single character string input", - &t.tag, - )); - }; - vec_t.iter().collect() - } - } - _ => '\n'.to_string(), - }; - - let rest_nums: Vec = rest_nums.iter().map(|n| n.item.to_string()).collect(); - - run_seq(sep, Some(term), widths, rest_nums) -} - -#[cfg(test)] -mod tests { - use super::Seq; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Seq {}) - } -} - -fn parse_float(mut s: &str) -> Result { - if s.starts_with('+') { - s = &s[1..]; - } - match s.parse() { - Ok(n) => Ok(n), - Err(e) => Err(format!( - "seq: invalid floating point argument `{}`: {}", - s, e - )), - } -} - -fn escape_sequences(s: &str) -> String { - s.replace("\\n", "\n").replace("\\t", "\t") -} - -pub fn run_seq( - sep: String, - termy: Option, - widths: bool, - free: Vec, -) -> Result { - let mut largest_dec = 0; - let mut padding = 0; - let first = if free.len() > 1 { - let slice = &free[0][..]; - let len = slice.len(); - let dec = slice.find('.').unwrap_or(len); - largest_dec = len - dec; - padding = dec; - match parse_float(slice) { - Ok(n) => n, - Err(s) => { - return Err(ShellError::labeled_error( - s, - "error parsing float", - Tag::unknown(), - )); - } - } - } else { - 1.0 - }; - let step = if free.len() > 2 { - let slice = &free[1][..]; - let len = slice.len(); - let dec = slice.find('.').unwrap_or(len); - largest_dec = cmp::max(largest_dec, len - dec); - padding = cmp::max(padding, dec); - match parse_float(slice) { - Ok(n) => n, - Err(s) => { - return Err(ShellError::labeled_error( - s, - "error parsing float", - Tag::unknown(), - )); - } - } - } else { - 1.0 - }; - let last = { - let slice = &free[free.len() - 1][..]; - padding = cmp::max(padding, slice.find('.').unwrap_or_else(|| slice.len())); - match parse_float(slice) { - Ok(n) => n, - Err(s) => { - return Err(ShellError::labeled_error( - s, - "error parsing float", - Tag::unknown(), - )); - } - } - }; - if largest_dec > 0 { - largest_dec -= 1; - } - let separator = escape_sequences(&sep[..]); - let terminator = match termy { - Some(term) => escape_sequences(&term[..]), - None => separator.clone(), - }; - Ok(print_seq( - first, - step, - last, - largest_dec, - separator, - terminator, - widths, - padding, - )) -} - -fn done_printing(next: f64, step: f64, last: f64) -> bool { - if step >= 0f64 { - next > last - } else { - next < last - } -} - -#[allow(clippy::too_many_arguments)] -fn print_seq( - first: f64, - step: f64, - last: f64, - largest_dec: usize, - separator: String, - terminator: String, - pad: bool, - padding: usize, -) -> OutputStream { - let mut i = 0isize; - let mut value = first + i as f64 * step; - // for string output - let mut ret_str = "".to_owned(); - // for number output - let mut ret_num = vec![]; - // If the separator and terminator are line endings we can convert to numbers - let use_num = - (separator == "\n" || separator == "\r") && (terminator == "\n" || terminator == "\r"); - - while !done_printing(value, step, last) { - if use_num { - ret_num.push(value); - } else { - // formatting for string output with potential padding - let istr = format!("{:.*}", largest_dec, value); - let ilen = istr.len(); - let before_dec = istr.find('.').unwrap_or(ilen); - if pad && before_dec < padding { - for _ in 0..(padding - before_dec) { - ret_str.push('0'); - } - } - ret_str.push_str(&istr); - } - i += 1; - value = first + i as f64 * step; - if !done_printing(value, step, last) { - ret_str.push_str(&separator); - } - } - - if !use_num && ((first >= last && step < 0f64) || (first <= last && step > 0f64)) { - ret_str.push_str(&terminator); - } - - if use_num { - // we'd like to keep the datatype the same for the output, so check - // and see if any of the output is really decimals, and if it is - // we'll make the entire output decimals - let contains_decimals = vec_contains_decimals(&ret_num); - let rows: Vec = ret_num - .iter() - .map(|v| { - if contains_decimals { - v.to_value_create_tag() - } else { - let vi64 = *v as i64; - vi64.to_value_create_tag() - } - }) - .collect(); - (rows.into_iter()).into_output_stream() - } else { - let rows: Vec = ret_str - .lines() - .map(|v| v.to_str_value_create_tag()) - .collect(); - (rows.into_iter()).into_output_stream() - } -} - -fn vec_contains_decimals(array: &[f64]) -> bool { - let mut found_decimal = false; - for x in array { - if x.fract() != 0.0 { - found_decimal = true; - break; - } - } - - found_decimal -} diff --git a/crates/nu-command/src/commands/generators/seq_dates.rs b/crates/nu-command/src/commands/generators/seq_dates.rs deleted file mode 100644 index 184aae7c90..0000000000 --- a/crates/nu-command/src/commands/generators/seq_dates.rs +++ /dev/null @@ -1,353 +0,0 @@ -use crate::prelude::*; -use chrono::naive::NaiveDate; -use chrono::{Duration, Local}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{value::I64Ext, value::StrExt, value::StringExt}; -use nu_protocol::{ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tagged; - -pub struct SeqDates; - -impl WholeStreamCommand for SeqDates { - fn name(&self) -> &str { - "seq date" - } - - fn signature(&self) -> Signature { - Signature::build("seq date") - .named( - "separator", - SyntaxShape::String, - "separator character (defaults to \\n)", - Some('s'), - ) - .named( - "output-format", - SyntaxShape::String, - "prints dates in this format (defaults to %Y-%m-%d)", - Some('o'), - ) - .named( - "input-format", - SyntaxShape::String, - "give argument dates in this format (defaults to %Y-%m-%d)", - Some('i'), - ) - .named( - "begin-date", - SyntaxShape::String, - "beginning date range", - Some('b'), - ) - .named("end-date", SyntaxShape::String, "ending date", Some('e')) - .named( - "increment", - SyntaxShape::Int, - "increment dates by this number", - Some('n'), - ) - .named( - "days", - SyntaxShape::Int, - "number of days to print", - Some('d'), - ) - .switch("reverse", "print dates in reverse", Some('r')) - } - - fn usage(&self) -> &str { - "print sequences of dates" - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - seq_dates(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "print the next 10 days in YYYY-MM-DD format with newline separator", - example: "seq date --days 10", - result: None, - }, - Example { - description: "print the previous 10 days in YYYY-MM-DD format with newline separator", - example: "seq date --days 10 -r", - result: None, - }, - Example { - description: "print the previous 10 days starting today in MM/DD/YYYY format with newline separator", - example: "seq date --days 10 -o '%m/%d/%Y' -r", - result: None, - }, - Example { - description: "print the first 10 days in January, 2020", - example: "seq date -b '2020-01-01' -e '2020-01-10'", - result: Some(vec![ - UntaggedValue::string("2020-01-01").into(), - UntaggedValue::string("2020-01-02").into(), - UntaggedValue::string("2020-01-03").into(), - UntaggedValue::string("2020-01-04").into(), - UntaggedValue::string("2020-01-05").into(), - UntaggedValue::string("2020-01-06").into(), - UntaggedValue::string("2020-01-07").into(), - UntaggedValue::string("2020-01-08").into(), - UntaggedValue::string("2020-01-09").into(), - UntaggedValue::string("2020-01-10").into(), - ]), - }, - Example { - description: "print every fifth day between January 1st 2020 and January 31st 2020", - example: "seq date -b '2020-01-01' -e '2020-01-31' -n 5", - result: Some(vec![ - UntaggedValue::string("2020-01-01").into(), - UntaggedValue::string("2020-01-06").into(), - UntaggedValue::string("2020-01-11").into(), - UntaggedValue::string("2020-01-16").into(), - UntaggedValue::string("2020-01-21").into(), - UntaggedValue::string("2020-01-26").into(), - UntaggedValue::string("2020-01-31").into(), - ]), - }, - Example { - description: "starting on May 5th, 2020, print the next 10 days in your locale's date format, colon separated", - example: "seq date -o %x -s ':' -d 10 -b '2020-05-01'", - result: None, - }, - ] - } -} - -fn seq_dates(args: CommandArgs) -> Result { - let _name = args.call_info.name_tag.clone(); - - let separator: Option> = args.get_flag("separator")?; - let output_format: Option> = args.get_flag("output-format")?; - let input_format: Option> = args.get_flag("input-format")?; - let begin_date: Option> = args.get_flag("begin-date")?; - let end_date: Option> = args.get_flag("end-date")?; - let increment: Option> = args.get_flag("increment")?; - let days: Option> = args.get_flag("days")?; - let reverse = args.has_flag("reverse"); - - let sep: String = match separator { - Some(s) => { - if s.item == r"\t" { - '\t'.to_string() - } else if s.item == r"\n" { - '\n'.to_string() - } else if s.item == r"\r" { - '\r'.to_string() - } else { - let vec_s: Vec = s.chars().collect(); - if vec_s.is_empty() { - return Err(ShellError::labeled_error( - "Expected a single separator char from --separator", - "requires a single character string input", - &s.tag, - )); - }; - vec_s.iter().collect() - } - } - _ => '\n'.to_string(), - }; - - let outformat = match output_format { - Some(s) => Some(s.item.to_string_value(s.tag)), - _ => None, - }; - - let informat = match input_format { - Some(s) => Some(s.item.to_string_value(s.tag)), - _ => None, - }; - - let begin = match begin_date { - Some(s) => Some(s.item), - _ => None, - }; - - let end = match end_date { - Some(s) => Some(s.item), - _ => None, - }; - - let inc = match increment { - Some(i) => { - let clone = i.clone(); - i.to_value(clone.tag) - } - _ => (1_i64).to_value_create_tag(), - }; - - let day_count: Option = match days { - Some(i) => Some(i.item.to_value(i.tag)), - _ => None, - }; - - let mut rev = false; - if reverse { - rev = reverse; - } - - run_seq_dates(sep, outformat, informat, begin, end, inc, day_count, rev) -} - -pub fn parse_date_string(s: &str, format: &str) -> Result { - let d = match NaiveDate::parse_from_str(s, format) { - Ok(d) => d, - Err(_) => return Err("Failed to parse date."), - }; - Ok(d) -} - -#[allow(clippy::too_many_arguments)] -pub fn run_seq_dates( - separator: String, - output_format: Option, - input_format: Option, - beginning_date: Option, - ending_date: Option, - increment: Value, - day_count: Option, - reverse: bool, -) -> Result { - let today = Local::today().naive_local(); - let mut step_size: i64 = increment - .as_i64() - .expect("unable to change increment to i64"); - - if step_size == 0 { - return Err(ShellError::labeled_error( - "increment cannot be 0", - "increment cannot be 0", - increment.tag, - )); - } - - let in_format = match input_format { - Some(i) => i.as_string().map_err(|e| { - ShellError::labeled_error( - e.to_string(), - "error with input_format as_string", - i.tag.span, - ) - })?, - None => "%Y-%m-%d".to_string(), - }; - - let out_format = match output_format { - Some(o) => o.as_string().map_err(|e| { - ShellError::labeled_error( - e.to_string(), - "error with output_format as_string", - o.tag.span, - ) - })?, - None => "%Y-%m-%d".to_string(), - }; - - let start_date = match beginning_date { - Some(d) => match parse_date_string(&d, &in_format) { - Ok(nd) => nd, - Err(e) => { - return Err(ShellError::labeled_error( - e, - "Failed to parse date", - Tag::unknown(), - )) - } - }, - _ => today, - }; - - let mut end_date = match ending_date { - Some(d) => match parse_date_string(&d, &in_format) { - Ok(nd) => nd, - Err(e) => { - return Err(ShellError::labeled_error( - e, - "Failed to parse date", - Tag::unknown(), - )) - } - }, - _ => today, - }; - - let mut days_to_output = match day_count { - Some(d) => d.as_i64()?, - None => 0i64, - }; - - // Make the signs opposite if we're created dates in reverse direction - if reverse { - step_size *= -1; - days_to_output *= -1; - } - - if days_to_output != 0 { - end_date = match start_date.checked_add_signed(Duration::days(days_to_output)) { - Some(date) => date, - None => { - return Err(ShellError::labeled_error( - "integer value too large", - "integer value too large", - Tag::unknown(), - )); - } - } - } - - // conceptually counting down with a positive step or counting up with a negative step - // makes no sense, attempt to do what one means by inverting the signs in those cases. - if (start_date > end_date) && (step_size > 0) || (start_date < end_date) && step_size < 0 { - step_size = -step_size; - } - - let is_out_of_range = - |next| (step_size > 0 && next > end_date) || (step_size < 0 && next < end_date); - - let mut next = start_date; - if is_out_of_range(next) { - return Err(ShellError::labeled_error( - "date is out of range", - "date is out of range", - Tag::unknown(), - )); - } - - let mut ret_str = String::from(""); - loop { - ret_str.push_str(&next.format(&out_format).to_string()); - // TODO: check this value is good - next += Duration::days(step_size); - - if is_out_of_range(next) { - break; - } - - ret_str.push_str(&separator); - } - - let rows: Vec = ret_str - .lines() - .map(|v| v.to_str_value_create_tag()) - .collect(); - Ok((rows.into_iter().map(ReturnSuccess::value)).into_action_stream()) -} - -#[cfg(test)] -mod tests { - use super::SeqDates; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SeqDates {}) - } -} diff --git a/crates/nu-command/src/commands/math/abs.rs b/crates/nu-command/src/commands/math/abs.rs deleted file mode 100644 index e257bca1fc..0000000000 --- a/crates/nu-command/src/commands/math/abs.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Primitive, Signature, UntaggedValue, Value}; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "math abs" - } - - fn signature(&self) -> Signature { - Signature::build("math abs") - } - - fn usage(&self) -> &str { - "Returns absolute values of a list of numbers" - } - - fn run(&self, args: CommandArgs) -> Result { - let mapped = args.input.map(move |val| match val.value { - UntaggedValue::Primitive(Primitive::Int(val)) => UntaggedValue::int(val.abs()).into(), - UntaggedValue::Primitive(Primitive::BigInt(val)) => { - UntaggedValue::big_int(val.magnitude().clone()).into() - } - UntaggedValue::Primitive(Primitive::Decimal(val)) => { - UntaggedValue::decimal(val.abs()).into() - } - UntaggedValue::Primitive(Primitive::Duration(val)) => { - UntaggedValue::duration(val).into() - } - other => abs_default(other), - }); - Ok(mapped.into_output_stream()) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Get absolute of each value in a list of numbers", - example: "echo [-50 -100.0 25] | math abs", - result: Some(vec![ - UntaggedValue::int(50).into(), - UntaggedValue::decimal_from_float(100.0, Span::default()).into(), - UntaggedValue::int(25).into(), - ]), - }] - } -} - -fn abs_default(_: UntaggedValue) -> Value { - UntaggedValue::Error(ShellError::unexpected( - "Only numerical values are supported", - )) - .into() -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/math/avg.rs b/crates/nu-command/src/commands/math/avg.rs deleted file mode 100644 index 5a93219ae4..0000000000 --- a/crates/nu-command/src/commands/math/avg.rs +++ /dev/null @@ -1,179 +0,0 @@ -use crate::prelude::*; - -use crate::commands::math::reducers::{reducer_for, Reduce}; -use crate::commands::math::utils::run_with_function; -use nu_engine::WholeStreamCommand; - -use nu_errors::ShellError; -use nu_protocol::{hir::Operator, Primitive, Signature, UntaggedValue, Value}; - -use bigdecimal::FromPrimitive; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "math avg" - } - - fn signature(&self) -> Signature { - Signature::build("math avg") - } - - fn usage(&self) -> &str { - "Finds the average of a list of numbers or tables" - } - - fn run(&self, args: CommandArgs) -> Result { - run_with_function(args, average) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Get the average of a list of numbers", - example: "echo [-50 100.0 25] | math avg", - result: Some(vec![UntaggedValue::decimal_from_float( - 25.0, - Span::unknown(), - ) - .into()]), - }] - } -} - -fn to_byte(value: &Value) -> Option { - match &value.value { - UntaggedValue::Primitive(Primitive::Int(num)) => { - Some(UntaggedValue::Primitive(Primitive::Filesize(*num as u64)).into_untagged_value()) - } - _ => None, - } -} - -pub fn average(values: &[Value], name: &Tag) -> Result { - let sum = reducer_for(Reduce::Summation); - - let number = BigDecimal::from_usize(values.len()).ok_or_else(|| { - ShellError::labeled_error("nothing to average", "nothing to average", &name.span) - })?; - - let total_rows = UntaggedValue::decimal(number); - - let are_bytes = values - .get(0) - .ok_or_else(|| { - ShellError::unexpected("Cannot perform aggregate math operation on empty data") - })? - .is_filesize(); - - let total = if are_bytes { - to_byte(&sum( - UntaggedValue::int(0).into_untagged_value(), - values - .to_vec() - .iter() - .map(|v| match v { - Value { - value: UntaggedValue::Primitive(Primitive::Filesize(num)), - .. - } => UntaggedValue::int(*num as i64).into_untagged_value(), - other => other.clone(), - }) - .collect::>(), - )?) - .ok_or_else(|| { - ShellError::labeled_error( - "could not convert to big decimal", - "could not convert to big decimal", - &name.span, - ) - }) - } else { - sum(UntaggedValue::int(0).into_untagged_value(), values.to_vec()) - }?; - - match total { - Value { - value: UntaggedValue::Primitive(Primitive::Filesize(num)), - .. - } => { - let left = UntaggedValue::from(Primitive::Int(num as i64)); - let result = nu_data::value::compute_values(Operator::Divide, &left, &total_rows); - - match result { - Ok(UntaggedValue::Primitive(Primitive::Decimal(result))) => match result.to_u64() { - Some(number) => Ok(UntaggedValue::filesize(number).into_value(name)), - None => Err(ShellError::labeled_error( - "could not calculate average of non-integer or unrelated types", - "source", - name, - )), - }, - Ok(_) => Err(ShellError::labeled_error( - "could not calculate average of non-integer 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(Primitive::Duration(duration)), - .. - } => { - let left = UntaggedValue::from(Primitive::Duration(duration)); - let result = nu_data::value::compute_values(Operator::Divide, &left, &total_rows); - - match result { - Ok(UntaggedValue::Primitive(Primitive::Duration(result))) => { - Ok(UntaggedValue::duration(result).into_value(name)) - } - Ok(_) => Err(ShellError::labeled_error( - "could not calculate average of non-integer 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 = nu_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 average of non-integer or unrelated types", - "source", - name, - )), - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/math/ceil.rs b/crates/nu-command/src/commands/math/ceil.rs deleted file mode 100644 index fe54c44347..0000000000 --- a/crates/nu-command/src/commands/math/ceil.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::commands::math::utils::run_with_numerical_functions_on_stream; -use crate::prelude::*; -use bigdecimal::One; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, UntaggedValue, Value}; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "math ceil" - } - - fn signature(&self) -> Signature { - Signature::build("math ceil") - } - - fn usage(&self) -> &str { - "Applies the ceil function to a list of numbers" - } - - fn run(&self, args: CommandArgs) -> Result { - let input = args.input; - - run_with_numerical_functions_on_stream( - input, - ceil_int, - ceil_big_int, - ceil_big_decimal, - ceil_default, - ) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Apply the ceil function to a list of numbers", - example: "echo [1.5 2.3 -3.1] | math ceil", - result: Some(vec![ - UntaggedValue::int(2).into(), - UntaggedValue::int(3).into(), - UntaggedValue::int(-3).into(), - ]), - }] - } -} - -fn ceil_int(val: i64) -> Value { - UntaggedValue::int(val).into() -} - -fn ceil_big_int(val: BigInt) -> Value { - UntaggedValue::big_int(val).into() -} - -fn ceil_big_decimal(val: BigDecimal) -> Value { - let mut maybe_ceiled = val.round(0); - if maybe_ceiled < val { - maybe_ceiled += BigDecimal::one(); - } - let ceiling = maybe_ceiled.to_i64(); - - match ceiling { - Some(x) => UntaggedValue::int(x).into(), - None => UntaggedValue::Error(ShellError::untagged_runtime_error( - "Value too big to ceiling to an 64-bit integer", - )) - .into(), - } -} - -fn ceil_default(_: UntaggedValue) -> Value { - UntaggedValue::Error(ShellError::unexpected( - "Only numerical values are supported", - )) - .into() -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/math/command.rs b/crates/nu-command/src/commands/math/command.rs deleted file mode 100644 index 84604745f7..0000000000 --- a/crates/nu-command/src/commands/math/command.rs +++ /dev/null @@ -1,199 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, UntaggedValue}; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "math" - } - - fn signature(&self) -> Signature { - Signature::build("math") - } - - fn usage(&self) -> &str { - "Use mathematical functions as aggregate functions on a list of numbers or tables." - } - - fn run(&self, args: CommandArgs) -> Result { - Ok(OutputStream::one( - UntaggedValue::string(get_full_help(&Command, args.scope())).into_value(Tag::unknown()), - )) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::commands::math::{ - avg::average, max::maximum, median::median, min::minimum, mode::mode, stddev::stddev, - sum::summation, utils::calculate, utils::MathFunction, variance::variance, - }; - use nu_protocol::{row, Value}; - use nu_test_support::value::{decimal, decimal_from_float, int, table}; - use std::str::FromStr; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Command {}) - } - - #[test] - fn test_math_functions() { - struct TestCase { - description: &'static str, - values: Vec, - expected_err: Option, - // Order is: average, minimum, maximum, median, summation - expected_res: Vec>, - } - let tt: Vec = vec![ - TestCase { - description: "Empty data should throw an error", - values: Vec::new(), - expected_err: Some(ShellError::unexpected("Expected data")), - expected_res: Vec::new(), - }, - TestCase { - description: "Single value", - values: vec![int(10)], - expected_err: None, - expected_res: vec![ - Ok(decimal_from_float(10.0)), - Ok(int(10)), - Ok(int(10)), - Ok(int(10)), - Ok(table(&[int(10)])), - Ok(decimal_from_float(0.0)), - Ok(int(10)), - Ok(decimal_from_float(0.0)), - ], - }, - TestCase { - description: "Multiple Values", - values: vec![int(10), int(20), int(30)], - expected_err: None, - expected_res: vec![ - Ok(decimal_from_float(20.0)), - Ok(int(10)), - Ok(int(30)), - Ok(int(20)), - Ok(table(&[int(10), int(20), int(30)])), - Ok(decimal(BigDecimal::from_str("8.164965809277260327324280249019637973219824935522233761442308557503201258191050088466198110348800783").expect("Could not convert to decimal from string"))), - Ok(int(60)), - Ok(decimal(BigDecimal::from_str("66.66666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666667").expect("Could not convert to decimal from string"))), - ], - }, - TestCase { - description: "Mixed Values", - values: vec![int(10), decimal_from_float(26.5), decimal_from_float(26.5)], - expected_err: None, - expected_res: vec![ - Ok(decimal_from_float(21.0)), - Ok(int(10)), - Ok(decimal_from_float(26.5)), - Ok(decimal_from_float(26.5)), - Ok(table(&[decimal_from_float(26.5)])), - Ok(decimal(BigDecimal::from_str("7.77817459305202276840928798315333943213319531457321440247173855894902863154158871367713143880202865").expect("Could not convert to decimal from string"))), - Ok(decimal_from_float(63.0)), - Ok(decimal_from_float(60.5)), - ], - }, - TestCase { - description: "Negative Values", - values: vec![int(-14), int(-11), int(10)], - expected_err: None, - expected_res: vec![ - Ok(decimal_from_float(-5.0)), - Ok(int(-14)), - Ok(int(10)), - Ok(int(-11)), - Ok(table(&[int(-14), int(-11), int(10)])), - Ok(decimal(BigDecimal::from_str("10.67707825203131121081152396559571062628228776946058011397810604284900898365140801704064843595778374").expect("Could not convert to decimal from string"))), - Ok(int(-15)), - Ok(decimal_from_float(114.0)), - ], - }, - TestCase { - description: "Mixed Negative Values", - values: vec![decimal_from_float(-13.5), decimal_from_float(-11.5), int(10)], - expected_err: None, - expected_res: vec![ - Ok(decimal_from_float(-5.0)), - Ok(decimal_from_float(-13.5)), - Ok(int(10)), - Ok(decimal_from_float(-11.5)), - Ok(table(&[decimal_from_float(-13.5), decimal_from_float(-11.5), int(10)])), - Ok(decimal(BigDecimal::from_str("10.63798226482196513098036125801342585449179971588207816421068645273754903468375890632981926875247027").expect("Could not convert to decimal from string"))), - Ok(decimal_from_float(-15.0)), - Ok(decimal(BigDecimal::from_str("113.1666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666667").expect("Could not convert to decimal from string"))), - ], - }, - TestCase { - description: "Tables Or Rows", - values: vec![ - row!["col1".to_owned() => int(1), "col2".to_owned() => int(5)], - row!["col1".to_owned() => int(2), "col2".to_owned() => int(6)], - row!["col1".to_owned() => int(3), "col2".to_owned() => int(7)], - row!["col1".to_owned() => int(4), "col2".to_owned() => int(8)], - ], - expected_err: None, - expected_res: vec![ - Ok(row!["col1".to_owned() => decimal_from_float(2.5), "col2".to_owned() => decimal_from_float(6.5)]), - Ok(row!["col1".to_owned() => int(1), "col2".to_owned() => int(5)]), - Ok(row!["col1".to_owned() => int(4), "col2".to_owned() => int(8)]), - Ok(row!["col1".to_owned() => decimal_from_float(2.5), "col2".to_owned() => decimal_from_float(6.5)]), - Ok(row![ - "col1".to_owned() => table(&[int(1), int(2), int(3), int(4)]), - "col2".to_owned() => table(&[int(5), int(6), int(7), int(8)]) - ]), - Ok(row![ - "col1".to_owned() => decimal(BigDecimal::from_str("1.118033988749894848204586834365638117720309179805762862135448622705260462818902449707207204189391137").expect("Could not convert to decimal from string")), - "col2".to_owned() => decimal(BigDecimal::from_str("1.118033988749894848204586834365638117720309179805762862135448622705260462818902449707207204189391137").expect("Could not convert to decimal from string")) - ]), - Ok(row!["col1".to_owned() => int(10), "col2".to_owned() => int(26)]), - Ok(row!["col1".to_owned() => decimal_from_float(1.25), "col2".to_owned() => decimal_from_float(1.25)]), - ], - }, - // TODO-Uncomment once Issue: https://github.com/nushell/nushell/issues/1883 is resolved - // TestCase { - // description: "Invalid Mixed Values", - // values: vec![int(10), decimal(26.5), decimal(26.5), string("math")], - // expected_err: Some(ShellError::unimplemented("something")), - // expected_res: vec![], - // }, - ]; - let test_tag = Tag::unknown(); - for tc in &tt { - let tc: &TestCase = tc; // Just for type annotations - let math_functions: Vec = vec![ - average, minimum, maximum, median, mode, stddev, summation, variance, - ]; - let results = math_functions - .into_iter() - .map(|mf| calculate(&tc.values, &test_tag, mf)) - .collect_vec(); - - if tc.expected_err.is_some() { - assert!( - results.iter().all(|r| r.is_err()), - "Expected all functions to error for test-case: {}", - tc.description, - ); - } else { - for (i, res) in results.into_iter().enumerate() { - assert_eq!( - res, tc.expected_res[i], - "math function {} failed on test-case {}", - i, tc.description - ); - } - } - } - } -} diff --git a/crates/nu-command/src/commands/math/eval.rs b/crates/nu-command/src/commands/math/eval.rs deleted file mode 100644 index 679358fa15..0000000000 --- a/crates/nu-command/src/commands/math/eval.rs +++ /dev/null @@ -1,117 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Primitive, Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tagged; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "math eval" - } - - fn usage(&self) -> &str { - "Evaluate a math expression into a number" - } - - fn signature(&self) -> Signature { - Signature::build("math eval").desc(self.usage()).optional( - "math expression", - SyntaxShape::String, - "the math expression to evaluate", - ) - } - - fn run(&self, args: CommandArgs) -> Result { - eval(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Evalulate math in the pipeline", - example: "echo '10 / 4' | math eval", - result: Some(vec![UntaggedValue::decimal_from_float( - 2.5, - Span::unknown(), - ) - .into()]), - }] - } -} - -pub fn eval(args: CommandArgs) -> Result { - let expression: Option> = args.opt(0)?; - let name = args.call_info.name_tag.clone(); - let input = args.input; - - if let Some(string) = expression { - match parse(&string, &string.tag) { - Ok(value) => Ok(OutputStream::one(value)), - Err(err) => Err(ShellError::labeled_error( - "Math evaluation error", - err, - &string.tag.span, - )), - } - } else { - let mapped: Result, _> = input - .map(move |x| { - if let Some(Tagged { - tag, - item: expression, - }) = &expression - { - UntaggedValue::string(expression).into_value(tag) - } else { - x - } - }) - .map(move |input| { - if let Ok(string) = input.as_string() { - match parse(&string, &input.tag) { - Ok(value) => Ok(value), - Err(err) => Err(ShellError::labeled_error( - "Math evaluation error", - err, - &input.tag.span, - )), - } - } else { - Err(ShellError::labeled_error( - "Expected a string from pipeline", - "requires string input", - name.clone(), - )) - } - }) - .collect(); - match mapped { - Ok(values) => Ok(OutputStream::from(values)), - Err(e) => Err(e), - } - } -} - -pub fn parse>(math_expression: &str, tag: T) -> Result { - let mut ctx = meval::Context::new(); - ctx.var("tau", std::f64::consts::TAU); - match meval::eval_str_with_context(math_expression, &ctx) { - Ok(num) if num.is_infinite() || num.is_nan() => Err("cannot represent result".to_string()), - Ok(num) => Ok(UntaggedValue::from(Primitive::from(num)).into_value(tag)), - Err(error) => Err(error.to_string().to_lowercase()), - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/math/floor.rs b/crates/nu-command/src/commands/math/floor.rs deleted file mode 100644 index 3ec9ebf210..0000000000 --- a/crates/nu-command/src/commands/math/floor.rs +++ /dev/null @@ -1,83 +0,0 @@ -use crate::commands::math::utils::run_with_numerical_functions_on_stream; -use crate::prelude::*; -use bigdecimal::One; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, UntaggedValue, Value}; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "math floor" - } - - fn signature(&self) -> Signature { - Signature::build("math floor") - } - - fn usage(&self) -> &str { - "Applies the floor function to a list of numbers" - } - - fn run(&self, args: CommandArgs) -> Result { - let input = args.input; - - run_with_numerical_functions_on_stream( - input, - floor_int, - floor_big_int, - floor_big_decimal, - floor_default, - ) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Apply the floor function to a list of numbers", - example: "echo [1.5 2.3 -3.1] | math floor", - result: Some(vec![ - UntaggedValue::big_int(1).into(), - UntaggedValue::big_int(2).into(), - UntaggedValue::big_int(-4).into(), - ]), - }] - } -} - -fn floor_int(val: i64) -> Value { - UntaggedValue::int(val).into() -} - -fn floor_big_int(val: BigInt) -> Value { - UntaggedValue::big_int(val).into() -} - -fn floor_big_decimal(val: BigDecimal) -> Value { - let mut maybe_floored = val.round(0); - if maybe_floored > val { - maybe_floored -= BigDecimal::one(); - } - let (floored, _) = maybe_floored.into_bigint_and_exponent(); - UntaggedValue::big_int(floored).into() -} - -fn floor_default(_: UntaggedValue) -> Value { - UntaggedValue::Error(ShellError::unexpected( - "Only numerical values are supported", - )) - .into() -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/math/max.rs b/crates/nu-command/src/commands/math/max.rs deleted file mode 100644 index 0facfafcc0..0000000000 --- a/crates/nu-command/src/commands/math/max.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::commands::math::reducers::{reducer_for, Reduce}; -use crate::commands::math::utils::run_with_function; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, UntaggedValue, Value}; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "math max" - } - - fn signature(&self) -> Signature { - Signature::build("math max") - } - - fn usage(&self) -> &str { - "Finds the maximum within a list of numbers or tables" - } - - fn run(&self, args: CommandArgs) -> Result { - run_with_function(args, maximum) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Find the maximum of list of numbers", - example: "echo [-50 100 25] | math max", - result: Some(vec![UntaggedValue::int(100).into()]), - }] - } -} - -pub fn maximum(values: &[Value], _name: &Tag) -> Result { - let max_func = reducer_for(Reduce::Maximum); - max_func(Value::nothing(), values.to_vec()) -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/math/median.rs b/crates/nu-command/src/commands/math/median.rs deleted file mode 100644 index 9cc735a7df..0000000000 --- a/crates/nu-command/src/commands/math/median.rs +++ /dev/null @@ -1,185 +0,0 @@ -use crate::commands::math::reducers::{reducer_for, Reduce}; -use crate::commands::math::utils::run_with_function; -use crate::prelude::*; -use bigdecimal::FromPrimitive; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{hir::Operator, Primitive, Signature, UntaggedValue, Value}; - -pub struct SubCommand; - -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" - } - - fn run(&self, args: CommandArgs) -> Result { - run_with_function(args, median) - } - - 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_from_float( - 10.5, - Span::unknown(), - ) - .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::filters::sort_by::sort(&mut sorted, &[], name, false)?; - - 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::nothing(), values.to_vec())?; - - match total { - Value { - value: UntaggedValue::Primitive(Primitive::Filesize(num)), - .. - } => { - let left = UntaggedValue::from(Primitive::Int(num as i64)); - let result = nu_data::value::compute_values(Operator::Divide, &left, &total_rows); - - match result { - Ok(UntaggedValue::Primitive(Primitive::Decimal(result))) => { - let (bi, _) = result.as_bigint_and_exponent(); - let number = bi.to_u64(); - match number { - Some(number) => Ok(UntaggedValue::filesize(number).into_value(name)), - None => Err(ShellError::labeled_error( - "Can't convert to filesize", - "can't convert to filesize", - 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 = nu_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::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/math/min.rs b/crates/nu-command/src/commands/math/min.rs deleted file mode 100644 index 2ed64a0c44..0000000000 --- a/crates/nu-command/src/commands/math/min.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::commands::math::reducers::{reducer_for, Reduce}; -use crate::commands::math::utils::run_with_function; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, UntaggedValue, Value}; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "math min" - } - - fn signature(&self) -> Signature { - Signature::build("math min") - } - - fn usage(&self) -> &str { - "Finds the minimum within a list of numbers or tables" - } - - fn run(&self, args: CommandArgs) -> Result { - run_with_function(args, minimum) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Get the minimum of a list of numbers", - example: "echo [-50 100 25] | math min", - result: Some(vec![UntaggedValue::int(-50).into()]), - }] - } -} - -pub fn minimum(values: &[Value], _name: &Tag) -> Result { - let min_func = reducer_for(Reduce::Minimum); - min_func(Value::nothing(), values.to_vec()) -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/math/mod.rs b/crates/nu-command/src/commands/math/mod.rs deleted file mode 100644 index 66ca5f1e13..0000000000 --- a/crates/nu-command/src/commands/math/mod.rs +++ /dev/null @@ -1,36 +0,0 @@ -pub mod abs; -pub mod avg; -pub mod ceil; -pub mod command; -pub mod eval; -pub mod floor; -pub mod max; -pub mod median; -pub mod min; -pub mod mode; -pub mod product; -pub mod round; -pub mod sqrt; -pub mod stddev; -pub mod sum; -pub mod variance; - -mod reducers; -mod utils; - -pub use abs::SubCommand as MathAbs; -pub use avg::SubCommand as MathAverage; -pub use ceil::SubCommand as MathCeil; -pub use command::Command as Math; -pub use eval::SubCommand as MathEval; -pub use floor::SubCommand as MathFloor; -pub use max::SubCommand as MathMaximum; -pub use median::SubCommand as MathMedian; -pub use min::SubCommand as MathMinimum; -pub use mode::SubCommand as MathMode; -pub use product::SubCommand as MathProduct; -pub use round::SubCommand as MathRound; -pub use sqrt::SubCommand as MathSqrt; -pub use stddev::SubCommand as MathStddev; -pub use sum::SubCommand as MathSummation; -pub use variance::SubCommand as MathVariance; diff --git a/crates/nu-command/src/commands/math/mode.rs b/crates/nu-command/src/commands/math/mode.rs deleted file mode 100644 index 61c7ea50c4..0000000000 --- a/crates/nu-command/src/commands/math/mode.rs +++ /dev/null @@ -1,77 +0,0 @@ -use crate::commands::math::utils::run_with_function; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, UntaggedValue, Value}; -use std::cmp::Ordering; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "math mode" - } - - fn signature(&self) -> Signature { - Signature::build("math mode") - } - - fn usage(&self) -> &str { - "Gets the most frequent element(s) from a list of numbers or tables" - } - - fn run(&self, args: CommandArgs) -> Result { - run_with_function(args, mode) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Get the mode(s) of a list of numbers", - example: "echo [3 3 9 12 12 15] | math mode", - result: Some(vec![ - UntaggedValue::int(3).into_untagged_value(), - UntaggedValue::int(12).into_untagged_value(), - ]), - }] - } -} - -pub fn mode(values: &[Value], name: &Tag) -> Result { - let mut frequency_map = std::collections::HashMap::new(); - for v in values { - let counter = frequency_map.entry(v.value.clone()).or_insert(0); - *counter += 1; - } - - let mut max_freq = -1; - let mut modes = Vec::::new(); - for (value, frequency) in &frequency_map { - match max_freq.cmp(frequency) { - Ordering::Less => { - max_freq = *frequency; - modes.clear(); - modes.push(value.clone().into_value(name)); - } - Ordering::Equal => { - modes.push(value.clone().into_value(name)); - } - Ordering::Greater => (), - } - } - - crate::commands::filters::sort_by::sort(&mut modes, &[], name, false)?; - Ok(UntaggedValue::Table(modes).into_value(name)) -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/math/product.rs b/crates/nu-command/src/commands/math/product.rs deleted file mode 100644 index 880eb29201..0000000000 --- a/crates/nu-command/src/commands/math/product.rs +++ /dev/null @@ -1,103 +0,0 @@ -use crate::commands::math::reducers::{reducer_for, Reduce}; -use crate::commands::math::utils::run_with_function; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Primitive, Signature, UntaggedValue, Value}; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "math product" - } - - fn signature(&self) -> Signature { - Signature::build("math product") - } - - fn usage(&self) -> &str { - "Finds the product of a list of numbers or tables" - } - - fn run(&self, args: CommandArgs) -> Result { - run_with_function(args, product) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Get the product of a list of numbers", - example: "echo [2 3 3 4] | math product", - result: Some(vec![UntaggedValue::int(72).into()]), - }] - } -} - -fn to_byte(value: &Value) -> Option { - match &value.value { - UntaggedValue::Primitive(Primitive::Int(num)) => { - Some(UntaggedValue::Primitive(Primitive::Filesize(*num as u64)).into_untagged_value()) - } - _ => None, - } -} - -/// Calculate product of given values -pub fn product(values: &[Value], name: &Tag) -> Result { - let prod = reducer_for(Reduce::Product); - - let first = values.get(0).ok_or_else(|| { - ShellError::unexpected("Cannot perform aggregate math operation on empty data") - })?; - - match first { - v if v.is_filesize() => to_byte(&prod( - UntaggedValue::int(1).into_untagged_value(), - values - .iter() - .map(|v| match v { - Value { - value: UntaggedValue::Primitive(Primitive::Filesize(num)), - .. - } => UntaggedValue::int(*num as i64).into_untagged_value(), - other => other.clone(), - }) - .collect::>(), - )?) - .ok_or_else(|| { - ShellError::labeled_error( - "could not convert to decimal", - "could not convert to decimal", - &name.span, - ) - }), - - v if v.is_none() => prod( - UntaggedValue::int(1).into_untagged_value(), - values - .iter() - .map(|v| match v { - Value { - value: UntaggedValue::Primitive(Primitive::Nothing), - .. - } => UntaggedValue::int(1).into_untagged_value(), - other => other.clone(), - }) - .collect::>(), - ), - _ => prod(UntaggedValue::int(1).into_untagged_value(), values.to_vec()), - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/math/reducers.rs b/crates/nu-command/src/commands/math/reducers.rs deleted file mode 100644 index fdf183172e..0000000000 --- a/crates/nu-command/src/commands/math/reducers.rs +++ /dev/null @@ -1,183 +0,0 @@ -use nu_data::value::{compare_values, compute_values}; -use nu_errors::ShellError; -use nu_protocol::hir::Operator; -use nu_protocol::{UntaggedValue, Value}; -use nu_source::{SpannedItem, Tag}; - -// Re-usable error messages -const ERR_EMPTY_DATA: &str = "Cannot perform aggregate math operation on empty data"; - -fn formula( - acc_begin: Value, - calculator: Box) -> Result + Send + Sync + 'static>, -) -> Box) -> Result + Send + Sync + 'static> { - Box::new(move |acc, datax| -> Result { - let result = match compute_values(Operator::Multiply, &acc, &acc_begin) { - Ok(v) => v.into_untagged_value(), - Err((left_type, right_type)) => { - return Err(ShellError::coerce_error( - left_type.spanned_unknown(), - right_type.spanned_unknown(), - )) - } - }; - - match calculator(datax) { - Ok(total) => Ok(match compute_values(Operator::Plus, &result, &total) { - Ok(v) => v.into_untagged_value(), - Err((left_type, right_type)) => { - return Err(ShellError::coerce_error( - left_type.spanned_unknown(), - right_type.spanned_unknown(), - )) - } - }), - Err(reason) => Err(reason), - } - }) -} - -pub fn reducer_for( - command: Reduce, -) -> Box) -> Result + Send + Sync + 'static> { - match command { - Reduce::Default => Box::new(formula( - UntaggedValue::int(0).into_untagged_value(), - Box::new(sum), - )), - Reduce::Summation => Box::new(|_, values| sum(values)), - Reduce::Minimum => Box::new(|_, values| min(values)), - Reduce::Maximum => Box::new(|_, values| max(values)), - Reduce::Product => Box::new(|_, values| product(values)), - } -} - -#[allow(dead_code)] -pub enum Reduce { - Summation, - Minimum, - Maximum, - Product, - Default, -} - -pub fn sum(data: Vec) -> Result { - let first_value = data - .get(0) - .ok_or_else(|| ShellError::unexpected(ERR_EMPTY_DATA))?; - - // Generate the initial accumulator value, of the correct type for - // the incoming data, this will be used in conjunction with the - // sum aggregator. Currently this is only handling, filesize, - // and other types are defaulting to an integer. - let mut acc = if first_value.is_filesize() { - UntaggedValue::filesize(0u64).into_untagged_value() - } else { - UntaggedValue::int(0).into_untagged_value() - }; - - for value in data { - match value.value { - UntaggedValue::Primitive(_) => { - acc = match compute_values(Operator::Plus, &acc, &value) { - Ok(v) => v.into_untagged_value(), - Err((left_type, right_type)) => { - return Err(ShellError::coerce_error( - left_type.spanned_unknown(), - right_type.spanned_unknown(), - )) - } - }; - } - _ => { - return Err(ShellError::labeled_error( - "Attempted to compute the sum of a value that cannot be summed.", - "value appears here", - value.tag.span, - )) - } - } - } - Ok(acc) -} - -pub fn max(data: Vec) -> Result { - let mut biggest = data - .first() - .ok_or_else(|| ShellError::unexpected(ERR_EMPTY_DATA))? - .value - .clone(); - - for value in &data { - if let Ok(greater_than) = compare_values(Operator::GreaterThan, &value.value, &biggest) { - if greater_than { - biggest = value.value.clone(); - } - } else { - return Err(ShellError::unexpected(format!( - "Could not compare\nleft: {:?}\nright: {:?}", - biggest, value.value - ))); - } - } - Ok(Value { - value: biggest, - tag: Tag::unknown(), - }) -} - -pub fn min(data: Vec) -> Result { - let mut smallest = data - .first() - .ok_or_else(|| ShellError::unexpected(ERR_EMPTY_DATA))? - .value - .clone(); - - for value in &data { - if let Ok(greater_than) = compare_values(Operator::LessThan, &value.value, &smallest) { - if greater_than { - smallest = value.value.clone(); - } - } else { - return Err(ShellError::unexpected(format!( - "Could not compare\nleft: {:?}\nright: {:?}", - smallest, value.value - ))); - } - } - Ok(Value { - value: smallest, - tag: Tag::unknown(), - }) -} - -pub fn product(data: Vec) -> Result { - if data.is_empty() { - return Err(ShellError::unexpected(ERR_EMPTY_DATA)); - } - - let mut prod = UntaggedValue::int(1).into_untagged_value(); - for value in data { - match value.value { - UntaggedValue::Primitive(_) => { - prod = match compute_values(Operator::Multiply, &prod, &value) { - Ok(v) => v.into_untagged_value(), - Err((left_type, right_type)) => { - return Err(ShellError::coerce_error( - left_type.spanned_unknown(), - right_type.spanned_unknown(), - )) - } - }; - } - _ => { - return Err(ShellError::labeled_error( - "Attempted to compute the product of a value that cannot be multiplied.", - "value appears here", - value.tag.span, - )) - } - } - } - Ok(prod) -} diff --git a/crates/nu-command/src/commands/math/round.rs b/crates/nu-command/src/commands/math/round.rs deleted file mode 100644 index 79eb922343..0000000000 --- a/crates/nu-command/src/commands/math/round.rs +++ /dev/null @@ -1,112 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Primitive, Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tagged; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "math round" - } - - fn signature(&self) -> Signature { - Signature::build("math round").named( - "precision", - SyntaxShape::Number, - "digits of precision", - Some('p'), - ) - } - - fn usage(&self) -> &str { - "Applies the round function to a list of numbers" - } - - fn run(&self, args: CommandArgs) -> Result { - operate(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Apply the round function to a list of numbers", - example: "echo [1.5 2.3 -3.1] | math round", - result: Some(vec![ - UntaggedValue::int(2).into(), - UntaggedValue::int(2).into(), - UntaggedValue::int(-3).into(), - ]), - }, - Example { - description: "Apply the round function with precision specified", - example: "echo [1.555 2.333 -3.111] | math round -p 2", - result: Some(vec![ - UntaggedValue::decimal_from_float(1.56, Span::default()).into(), - UntaggedValue::decimal_from_float(2.33, Span::default()).into(), - UntaggedValue::decimal_from_float(-3.11, Span::default()).into(), - ]), - }, - ] - } -} - -fn operate(args: CommandArgs) -> Result { - let precision: Option> = args.get_flag("precision")?; - let input = args.input; - let precision = if let Some(precision) = precision { - precision.item - } else { - 0 - }; - let mapped = input.map(move |val| match val.value { - UntaggedValue::Primitive(Primitive::BigInt(val)) => round_big_int(val), - UntaggedValue::Primitive(Primitive::Decimal(val)) => { - round_big_decimal(val, precision.into()) - } - UntaggedValue::Primitive(Primitive::Int(val)) => UntaggedValue::int(val).into(), - other => round_default(other), - }); - Ok(mapped.into_output_stream()) -} - -fn round_big_int(val: BigInt) -> Value { - UntaggedValue::big_int(val).into() -} - -fn round_big_decimal(val: BigDecimal, precision: i64) -> Value { - if precision > 0 { - UntaggedValue::decimal(val.with_scale(precision + 1).round(precision)).into() - } else { - let rounded = val.with_scale(precision + 1).round(precision).to_i64(); - - match rounded { - Some(x) => UntaggedValue::int(x).into(), - None => UntaggedValue::Error(ShellError::untagged_runtime_error( - "Number too larger to round to 64-bit int", - )) - .into(), - } - } -} - -fn round_default(_: UntaggedValue) -> Value { - UntaggedValue::Error(ShellError::unexpected( - "Only numerical values are supported", - )) - .into() -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/math/sqrt.rs b/crates/nu-command/src/commands/math/sqrt.rs deleted file mode 100644 index ef0815d43a..0000000000 --- a/crates/nu-command/src/commands/math/sqrt.rs +++ /dev/null @@ -1,82 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Primitive, Signature, UntaggedValue, Value}; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "math sqrt" - } - - fn signature(&self) -> Signature { - Signature::build("math sqrt") - } - - fn usage(&self) -> &str { - "Applies the square root function to a list of numbers" - } - - fn run(&self, args: CommandArgs) -> Result { - Ok(operate(args)) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Apply the square root function to a list of numbers", - example: "echo [9 16] | math sqrt", - result: Some(vec![ - UntaggedValue::int(3).into(), - UntaggedValue::int(4).into(), - ]), - }] - } -} - -fn operate(args: CommandArgs) -> OutputStream { - let mapped = args.input.map(move |val| match val.value { - UntaggedValue::Primitive(Primitive::Int(val)) => sqrt_big_decimal(BigDecimal::from(val)), - UntaggedValue::Primitive(Primitive::Decimal(val)) => sqrt_big_decimal(val), - other => sqrt_default(other), - }); - mapped.into_output_stream() -} - -fn sqrt_big_decimal(val: BigDecimal) -> Value { - let squared = val.sqrt(); - match squared { - None => UntaggedValue::Error(ShellError::untagged_runtime_error( - "Can't square root a negative number", - )) - .into(), - Some(val) if !val.is_integer() => UntaggedValue::decimal(val.normalized()).into(), - Some(val) => match val.to_i64() { - Some(x) => UntaggedValue::int(x).into(), - None => UntaggedValue::Error(ShellError::untagged_runtime_error( - "Value too large to convert to 64-bit integer", - )) - .into(), - }, - } -} - -fn sqrt_default(_: UntaggedValue) -> Value { - UntaggedValue::Error(ShellError::unexpected( - "Only numerical values are supported", - )) - .into() -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/math/stddev.rs b/crates/nu-command/src/commands/math/stddev.rs deleted file mode 100644 index 02f0ce8243..0000000000 --- a/crates/nu-command/src/commands/math/stddev.rs +++ /dev/null @@ -1,139 +0,0 @@ -use super::variance::compute_variance as variance; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Dictionary, Primitive, Signature, UntaggedValue, Value}; -use std::str::FromStr; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "math stddev" - } - - fn signature(&self) -> Signature { - Signature::build("math stddev").switch( - "sample", - "calculate sample standard deviation", - Some('s'), - ) - } - - fn usage(&self) -> &str { - "Finds the stddev of a list of numbers or tables" - } - - fn run(&self, mut args: CommandArgs) -> Result { - let sample: bool = args.has_flag("sample"); - let values: Vec = args.input.drain_vec(); - let name = args.call_info.name_tag; - - let n = if sample { - values.len() - 1 - } else { - values.len() - }; - - let res = if values.iter().all(|v| v.is_primitive()) { - compute_stddev(&values, n, &name) - } else { - // If we are not dealing with Primitives, then perhaps we are dealing with a table - // Create a key for each column name - let mut column_values = IndexMap::new(); - for value in values { - if let UntaggedValue::Row(row_dict) = &value.value { - for (key, value) in &row_dict.entries { - column_values - .entry(key.clone()) - .and_modify(|v: &mut Vec| v.push(value.clone())) - .or_insert(vec![value.clone()]); - } - } - } - // The mathematical function operates over the columns of the table - let mut column_totals = IndexMap::new(); - for (col_name, col_vals) in column_values { - if let Ok(out) = compute_stddev(&col_vals, n, &name) { - column_totals.insert(col_name, out); - } - } - - if column_totals.keys().len() == 0 { - return Err(ShellError::labeled_error( - "Attempted to compute values that can't be operated on", - "value appears here", - name.span, - )); - } - - Ok(UntaggedValue::Row(Dictionary { - entries: column_totals, - }) - .into_untagged_value()) - }?; - - if res.value.is_table() { - Ok(OutputStream::from( - res.table_entries().cloned().collect::>(), - )) - } else { - Ok(OutputStream::one(res)) - } - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Get the stddev of a list of numbers", - example: "echo [1 2 3 4 5] | math stddev", - result: Some(vec![UntaggedValue::decimal(BigDecimal::from_str("1.414213562373095048801688724209698078569671875376948073176679737990732478462107038850387534327641573").expect("Could not convert to decimal from string")).into()]), - }, - Example { - description: "Get the sample stddev of a list of numbers", - example: "echo [1 2 3 4 5] | math stddev -s", - result: Some(vec![UntaggedValue::decimal(BigDecimal::from_str("1.581138830084189665999446772216359266859777569662608413428752426396297219319619110672124054189650148").expect("Could not convert to decimal from string")).into()]), - }, - ] - } -} - -#[cfg(test)] -pub fn stddev(values: &[Value], name: &Tag) -> Result { - compute_stddev(values, values.len(), name) -} - -pub fn compute_stddev(values: &[Value], n: usize, name: &Tag) -> Result { - let variance = variance(values, n, name)?.as_primitive()?; - let sqrt_var = match variance { - Primitive::Decimal(var) => var.sqrt(), - _ => { - return Err(ShellError::labeled_error( - "Could not take square root of variance", - "error occurred here", - name.span, - )) - } - }; - match sqrt_var { - Some(stddev) => Ok(UntaggedValue::from(Primitive::Decimal(stddev)).into_value(name)), - None => Err(ShellError::labeled_error( - "Could not calculate stddev", - "error occurred here", - name.span, - )), - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/math/sum.rs b/crates/nu-command/src/commands/math/sum.rs deleted file mode 100644 index 30ba8c22a5..0000000000 --- a/crates/nu-command/src/commands/math/sum.rs +++ /dev/null @@ -1,117 +0,0 @@ -use crate::commands::math::reducers::{reducer_for, Reduce}; -use crate::commands::math::utils::run_with_function; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; - -use nu_protocol::{Primitive, Signature, UntaggedValue, Value}; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "math sum" - } - - fn signature(&self) -> Signature { - Signature::build("math sum") - } - - fn usage(&self) -> &str { - "Finds the sum of a list of numbers or tables" - } - - fn run(&self, args: CommandArgs) -> Result { - run_with_function(args, summation) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Sum a list of numbers", - 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 | math sum", - result: None, - }, - ] - } -} - -fn to_byte(value: &Value) -> Option { - match &value.value { - UntaggedValue::Primitive(Primitive::Int(num)) => { - Some(UntaggedValue::Primitive(Primitive::Filesize(*num as u64)).into_untagged_value()) - } - _ => None, - } -} - -pub fn summation(values: &[Value], name: &Tag) -> Result { - let sum = reducer_for(Reduce::Summation); - - let first = values.get(0).ok_or_else(|| { - ShellError::labeled_error( - "Cannot perform aggregate math operation on empty data", - "expected input", - name.span, - ) - })?; - - match first { - v if v.is_filesize() => to_byte(&sum( - UntaggedValue::int(0).into_untagged_value(), - values - .to_vec() - .iter() - .map(|v| match v { - Value { - value: UntaggedValue::Primitive(Primitive::Filesize(num)), - .. - } => UntaggedValue::int(*num as i64).into_untagged_value(), - other => other.clone(), - }) - .collect::>(), - )?) - .ok_or_else(|| { - ShellError::labeled_error( - "could not convert to big decimal", - "could not convert to big decimal", - &name.span, - ) - }), - v if v.is_duration() => sum(UntaggedValue::int(0).into_untagged_value(), values.to_vec()), - // v is nothing primitive - v if v.is_none() => sum( - UntaggedValue::int(0).into_untagged_value(), - values - .to_vec() - .iter() - .map(|v| match v { - Value { - value: UntaggedValue::Primitive(Primitive::Nothing), - .. - } => UntaggedValue::int(0).into_untagged_value(), - other => other.clone(), - }) - .collect::>(), - ), - _ => sum(UntaggedValue::int(0).into_untagged_value(), values.to_vec()), - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/math/utils.rs b/crates/nu-command/src/commands/math/utils.rs deleted file mode 100644 index 19ee44b84e..0000000000 --- a/crates/nu-command/src/commands/math/utils.rs +++ /dev/null @@ -1,99 +0,0 @@ -use crate::prelude::*; -use nu_errors::ShellError; -use nu_protocol::{Dictionary, Primitive, UntaggedValue, Value}; - -use indexmap::map::IndexMap; - -pub type MathFunction = fn(values: &[Value], tag: &Tag) -> Result; - -pub fn run_with_function( - args: impl Into, - mf: MathFunction, -) -> Result { - let RunnableContext { - mut input, - call_info, - .. - } = args.into(); - let name = call_info.name_tag; - - let values: Vec = input.drain_vec(); - - let res = calculate(&values, &name, mf); - match res { - Ok(v) => { - if v.value.is_table() { - Ok(OutputStream::from( - v.table_entries().cloned().collect::>(), - )) - } else { - Ok(OutputStream::one(v)) - } - } - Err(e) => Err(e), - } -} - -pub type BigIntFunction = fn(val: BigInt) -> Value; - -pub type IntFunction = fn(val: i64) -> Value; - -pub type DecimalFunction = fn(val: BigDecimal) -> Value; - -pub type DefaultFunction = fn(val: UntaggedValue) -> Value; - -pub fn run_with_numerical_functions_on_stream( - input: InputStream, - int_function: IntFunction, - big_int_function: BigIntFunction, - decimal_function: DecimalFunction, - default_function: DefaultFunction, -) -> Result { - let mapped = input.map(move |val| match val.value { - UntaggedValue::Primitive(Primitive::Int(val)) => int_function(val), - UntaggedValue::Primitive(Primitive::BigInt(val)) => big_int_function(val), - UntaggedValue::Primitive(Primitive::Decimal(val)) => decimal_function(val), - other => default_function(other), - }); - Ok(mapped.into_output_stream()) -} - -pub fn calculate(values: &[Value], name: &Tag, mf: MathFunction) -> Result { - if values.iter().all(|v| v.is_primitive()) { - mf(values, name) - } else { - // If we are not dealing with Primitives, then perhaps we are dealing with a table - // Create a key for each column name - let mut column_values = IndexMap::new(); - for value in values { - if let UntaggedValue::Row(row_dict) = &value.value { - for (key, value) in &row_dict.entries { - column_values - .entry(key.clone()) - .and_modify(|v: &mut Vec| v.push(value.clone())) - .or_insert(vec![value.clone()]); - } - } - } - // The mathematical function operates over the columns of the table - let mut column_totals = IndexMap::new(); - for (col_name, col_vals) in column_values { - if let Ok(out) = mf(&col_vals, name) { - column_totals.insert(col_name, out); - } - } - - if column_totals.keys().len() == 0 { - return Err(ShellError::labeled_error( - "Attempted to compute values that can't be operated on", - "value appears here", - name.span, - )); - } - - Ok(UntaggedValue::Row(Dictionary { - entries: column_totals, - }) - .into_untagged_value()) - } -} diff --git a/crates/nu-command/src/commands/math/variance.rs b/crates/nu-command/src/commands/math/variance.rs deleted file mode 100644 index a73b4e60f7..0000000000 --- a/crates/nu-command/src/commands/math/variance.rs +++ /dev/null @@ -1,236 +0,0 @@ -use crate::prelude::*; -use bigdecimal::FromPrimitive; -use nu_data::value::compute_values; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{hir::Operator, Dictionary, Primitive, Signature, UntaggedValue, Value}; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "math variance" - } - - fn signature(&self) -> Signature { - Signature::build("math variance").switch("sample", "calculate sample variance", Some('s')) - } - - fn usage(&self) -> &str { - "Finds the variance of a list of numbers or tables" - } - - fn run(&self, mut args: CommandArgs) -> Result { - let sample: bool = args.has_flag("sample"); - let values: Vec = args.input.drain_vec(); - let name = args.call_info.name_tag; - - let n = if sample { - values.len() - 1 - } else { - values.len() - }; - - let res = if values.iter().all(|v| v.is_primitive()) { - compute_variance(&values, n, &name) - } else { - // If we are not dealing with Primitives, then perhaps we are dealing with a table - // Create a key for each column name - let mut column_values = IndexMap::new(); - for value in values { - if let UntaggedValue::Row(row_dict) = &value.value { - for (key, value) in &row_dict.entries { - column_values - .entry(key.clone()) - .and_modify(|v: &mut Vec| v.push(value.clone())) - .or_insert(vec![value.clone()]); - } - } - } - // The mathematical function operates over the columns of the table - let mut column_totals = IndexMap::new(); - for (col_name, col_vals) in column_values { - if let Ok(out) = compute_variance(&col_vals, n, &name) { - column_totals.insert(col_name, out); - } - } - - if column_totals.keys().len() == 0 { - return Err(ShellError::labeled_error( - "Attempted to compute values that can't be operated on", - "value appears here", - name.span, - )); - } - - Ok(UntaggedValue::Row(Dictionary { - entries: column_totals, - }) - .into_untagged_value()) - }?; - - if res.value.is_table() { - Ok(OutputStream::from( - res.table_entries().cloned().collect::>(), - )) - } else { - Ok(OutputStream::one(res)) - } - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Get the variance of a list of numbers", - example: "echo [1 2 3 4 5] | math variance", - result: Some(vec![UntaggedValue::decimal_from_float( - 2.0, - Span::unknown(), - ) - .into()]), - }, - Example { - description: "Get the sample variance of a list of numbers", - example: "echo [1 2 3 4 5] | math variance -s", - result: Some(vec![UntaggedValue::decimal_from_float( - 2.5, - Span::unknown(), - ) - .into()]), - }, - ] - } -} - -fn sum_of_squares(values: &[Value], name: &Tag) -> Result { - let n = 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 mut sum_x = UntaggedValue::int(0).into_untagged_value(); - let mut sum_x2 = UntaggedValue::int(0).into_untagged_value(); - for value in values { - let v = match value { - Value { - value: UntaggedValue::Primitive(Primitive::Filesize(num)), - .. - } => { - UntaggedValue::from(Primitive::Int(*num as i64)) - }, - Value { - value: UntaggedValue::Primitive(num), - .. - } => { - UntaggedValue::from(num.clone()) - }, - _ => { - return Err(ShellError::labeled_error( - "Attempted to compute the sum of squared values of a value that cannot be summed or squared.", - "value appears here", - value.tag.span, - )) - } - }; - let v_squared = compute_values(Operator::Multiply, &v, &v); - match v_squared { - // X^2 - Ok(x2) => { - sum_x2 = match compute_values(Operator::Plus, &sum_x2, &x2) { - Ok(v) => v.into_untagged_value(), - Err((left_type, right_type)) => { - return Err(ShellError::coerce_error( - left_type.spanned(name.span), - right_type.spanned(name.span), - )) - } - }; - } - Err((left_type, right_type)) => { - return Err(ShellError::coerce_error( - left_type.spanned(value.tag.span), - right_type.spanned(value.tag.span), - )) - } - }; - sum_x = match compute_values(Operator::Plus, &sum_x, &v) { - Ok(v) => v.into_untagged_value(), - Err((left_type, right_type)) => { - return Err(ShellError::coerce_error( - left_type.spanned(name.span), - right_type.spanned(name.span), - )) - } - }; - } - - let sum_x_squared = match compute_values(Operator::Multiply, &sum_x, &sum_x) { - Ok(v) => v.into_untagged_value(), - Err((left_type, right_type)) => { - return Err(ShellError::coerce_error( - left_type.spanned(name.span), - right_type.spanned(name.span), - )) - } - }; - let sum_x_squared_div_n = match compute_values(Operator::Divide, &sum_x_squared, &n.into()) { - Ok(v) => v.into_untagged_value(), - Err((left_type, right_type)) => { - return Err(ShellError::coerce_error( - left_type.spanned(name.span), - right_type.spanned(name.span), - )) - } - }; - let ss = match compute_values(Operator::Minus, &sum_x2, &sum_x_squared_div_n) { - Ok(v) => v.into_untagged_value(), - Err((left_type, right_type)) => { - return Err(ShellError::coerce_error( - left_type.spanned(name.span), - right_type.spanned(name.span), - )) - } - }; - - Ok(ss) -} - -#[cfg(test)] -pub fn variance(values: &[Value], name: &Tag) -> Result { - compute_variance(values, values.len(), name) -} - -pub fn compute_variance(values: &[Value], n: usize, name: &Tag) -> Result { - let ss = sum_of_squares(values, name)?; - let n = BigDecimal::from_usize(n).ok_or_else(|| { - ShellError::labeled_error( - "could not convert to big decimal", - "could not convert to big decimal", - &name.span, - ) - })?; - let variance = compute_values(Operator::Divide, &ss, &n.into()); - match variance { - Ok(value) => Ok(value.into_value(name)), - Err((_, _)) => Err(ShellError::labeled_error( - "could not calculate variance of non-integer or unrelated types", - "source", - name, - )), - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/mod.rs b/crates/nu-command/src/commands/mod.rs deleted file mode 100644 index b6f6cae59e..0000000000 --- a/crates/nu-command/src/commands/mod.rs +++ /dev/null @@ -1,141 +0,0 @@ -mod charting; -mod config; -mod conversions; -mod core_commands; -#[cfg(feature = "dataframe")] -mod dataframe; -mod env; -mod filesystem; -mod filters; -mod formats; -mod generators; -mod math; -mod network; -mod path; -mod pathvar; -mod platform; -mod random; -mod shells; -mod strings; -mod system; -mod viewers; - -pub use charting::*; -pub use config::*; -pub use conversions::*; -pub use core_commands::*; -#[cfg(feature = "dataframe")] -pub use dataframe::{ - DataFrame, DataFrameAggregate, DataFrameAllFalse, DataFrameAllTrue, DataFrameAppend, - DataFrameArgMax, DataFrameArgMin, DataFrameArgSort, DataFrameArgTrue, DataFrameArgUnique, - DataFrameColumn, DataFrameConcatenate, DataFrameContains, DataFrameCumulative, DataFrameDTypes, - DataFrameDescribe, DataFrameDrop, DataFrameDropDuplicates, DataFrameDropNulls, - DataFrameDummies, DataFrameFilter, DataFrameFirst, DataFrameGet, DataFrameGetDay, - DataFrameGetHour, DataFrameGetMinute, DataFrameGetMonth, DataFrameGetNanoSecond, - DataFrameGetOrdinal, DataFrameGetSecond, DataFrameGetWeek, DataFrameGetWeekDay, - DataFrameGetYear, DataFrameGroupBy, DataFrameIsDuplicated, DataFrameIsIn, DataFrameIsNotNull, - DataFrameIsNull, DataFrameIsUnique, DataFrameJoin, DataFrameLast, DataFrameList, DataFrameMelt, - DataFrameNNull, DataFrameNUnique, DataFrameNot, DataFrameOpen, DataFramePivot, DataFrameRename, - DataFrameReplace, DataFrameReplaceAll, DataFrameRolling, DataFrameSample, DataFrameSelect, - DataFrameSeriesRename, DataFrameSet, DataFrameSetWithIdx, DataFrameShape, DataFrameShift, - DataFrameShow, DataFrameSlice, DataFrameSort, DataFrameStrFTime, DataFrameStringLengths, - DataFrameStringSlice, DataFrameTake, DataFrameToCsv, DataFrameToDF, DataFrameToLowercase, - DataFrameToParquet, DataFrameToUppercase, DataFrameUnique, DataFrameValueCounts, - DataFrameWhere, DataFrameWithColumn, -}; -pub use env::*; -pub use filesystem::*; -pub use filters::*; -pub use formats::*; -pub use generators::*; -pub use math::*; -pub use network::*; -pub use path::*; -pub use pathvar::*; -pub use platform::*; -pub use random::*; -pub use shells::*; -pub use strings::*; -pub use system::*; -pub use viewers::*; - -#[cfg(test)] -mod tests { - use super::*; - use crate::examples::{test_anchors, test_examples}; - use nu_engine::{whole_stream_command, Command}; - use nu_errors::ShellError; - - fn full_tests() -> Vec { - vec![ - whole_stream_command(ErrorMake), - whole_stream_command(Drop), - whole_stream_command(DropNth), - whole_stream_command(DropColumn), - whole_stream_command(Append), - whole_stream_command(GroupBy), - whole_stream_command(Insert), - whole_stream_command(MoveColumn), - whole_stream_command(Update), - whole_stream_command(Empty), - whole_stream_command(Nth), - // whole_stream_command(Select), - // whole_stream_command(Get), - // Str Command Suite - whole_stream_command(Str), - whole_stream_command(StrToDecimal), - whole_stream_command(StrToInteger), - whole_stream_command(StrDowncase), - whole_stream_command(StrUpcase), - whole_stream_command(StrCapitalize), - whole_stream_command(StrFindReplace), - whole_stream_command(StrSubstring), - whole_stream_command(StrToDatetime), - whole_stream_command(StrContains), - whole_stream_command(StrIndexOf), - whole_stream_command(StrTrim), - whole_stream_command(StrStartsWith), - whole_stream_command(StrEndsWith), - //whole_stream_command(StrCollect), - whole_stream_command(StrLength), - whole_stream_command(StrLPad), - whole_stream_command(StrReverse), - whole_stream_command(StrRPad), - whole_stream_command(StrCamelCase), - whole_stream_command(StrPascalCase), - whole_stream_command(StrKebabCase), - whole_stream_command(StrSnakeCase), - whole_stream_command(StrScreamingSnakeCase), - whole_stream_command(ToMarkdown), - ] - } - - fn only_examples() -> Vec { - let mut commands = full_tests(); - commands.extend([ - whole_stream_command(UpdateCells), - whole_stream_command(Zip), - whole_stream_command(Flatten), - ]); - commands - } - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - for cmd in only_examples() { - println!("cmd: {}", cmd.name()); - test_examples(cmd)?; - } - - Ok(()) - } - - #[test] - fn tracks_metadata() -> Result<(), ShellError> { - for cmd in full_tests() { - test_anchors(cmd)?; - } - - Ok(()) - } -} diff --git a/crates/nu-command/src/commands/network/fetch.rs b/crates/nu-command/src/commands/network/fetch.rs deleted file mode 100644 index 82b5df9898..0000000000 --- a/crates/nu-command/src/commands/network/fetch.rs +++ /dev/null @@ -1,377 +0,0 @@ -use crate::prelude::*; -use base64::encode; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{CommandAction, ReturnSuccess, ReturnValue, Value}; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue}; -use nu_source::{AnchorLocation, Span, Tag}; -use std::path::PathBuf; -use std::str::FromStr; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "fetch" - } - - fn signature(&self) -> Signature { - Signature::build("fetch") - .desc("Load from a URL into a cell, convert to table if possible (avoid by appending '--raw').") - .required( - "URL", - SyntaxShape::String, - "the URL to fetch the contents from", - ) - .named( - "user", - SyntaxShape::Any, - "the username when authenticating", - Some('u'), - ) - .named( - "password", - SyntaxShape::Any, - "the password when authenticating", - Some('p'), - ) - .switch("raw", "fetch contents as text rather than a table", Some('r')) - .switch("insecure", "allow insecure server connections when using SSL", Some('k')) - .filter() - } - - fn usage(&self) -> &str { - "Fetch the contents from a URL (HTTP GET operation)." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - run_fetch(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Fetch content from url.com", - example: "fetch url.com", - result: None, - }, - Example { - description: "Fetch content from url.com, with username and password", - example: "fetch -u myuser -p mypass url.com", - result: None, - }, - ] - } -} - -fn run_fetch(args: CommandArgs) -> Result { - let mut fetch_helper = Fetch::new(); - - fetch_helper.setup(args)?; - - let runtime = tokio::runtime::Runtime::new()?; - Ok(vec![runtime.block_on(fetch( - &fetch_helper.path.clone().ok_or_else(|| { - ShellError::labeled_error( - "internal error: path not set", - "path not set", - &fetch_helper.tag, - ) - })?, - fetch_helper.has_raw, - fetch_helper.has_insecure, - fetch_helper.user.clone(), - fetch_helper.password, - ))] - .into_iter() - .into_action_stream()) - - //fetch.setup(callinfo)?; -} - -#[derive(Default)] -pub struct Fetch { - pub path: Option, - pub tag: Tag, - pub has_raw: bool, - pub has_insecure: bool, - pub user: Option, - pub password: Option, -} - -impl Fetch { - pub fn new() -> Fetch { - Fetch { - path: None, - tag: Tag::unknown(), - has_raw: false, - has_insecure: false, - user: None, - password: None, - } - } - - pub fn setup(&mut self, args: CommandArgs) -> Result<(), ShellError> { - self.path = Some({ - args.req(0).map_err(|_| { - ShellError::labeled_error( - "No file or directory specified", - "for command", - &args.name_tag(), - ) - })? - }); - self.tag = args.name_tag(); - - self.has_raw = args.has_flag("raw"); - - self.has_insecure = args.has_flag("insecure"); - - self.user = args.get_flag("user")?; - - self.password = args.get_flag("password")?; - - Ok(()) - } -} - -pub async fn fetch( - path: &Value, - has_raw: bool, - has_insecure: bool, - user: Option, - password: Option, -) -> ReturnValue { - let path_str = path.as_string()?; - let path_span = path.tag.span; - - let result = helper(&path_str, path_span, has_raw, has_insecure, user, password).await; - - if let Err(e) = result { - return Err(e); - } - let (file_extension, value) = result?; - - let file_extension = if has_raw { - None - } else { - // If the extension could not be determined via mimetype, try to use the path - // extension. Some file types do not declare their mimetypes (such as bson files). - file_extension.or_else(|| path_str.split('.').last().map(String::from)) - }; - - if let Some(extension) = file_extension { - Ok(ReturnSuccess::Action(CommandAction::AutoConvert( - value, extension, - ))) - } else { - ReturnSuccess::value(value) - } -} - -// Helper function that actually goes to retrieve the resource from the url given -// The Option return a possible file extension which can be used in AutoConvert commands -async fn helper( - location: &str, - span: Span, - has_raw: bool, - has_insecure: bool, - user: Option, - password: Option, -) -> std::result::Result<(Option, Value), ShellError> { - let url = match url::Url::parse(location) { - Ok(u) => u, - Err(e) => { - return Err(ShellError::labeled_error( - format!("Incomplete or incorrect url:\n{:?}", e), - "expected a full url", - span, - )); - } - }; - - let login = match (user, password) { - (Some(user), Some(password)) => Some(encode(&format!("{}:{}", user, password))), - (Some(user), _) => Some(encode(&format!("{}:", user))), - _ => None, - }; - - let client = http_client(has_insecure); - let mut request = client.get(url); - - if let Some(login) = login { - request = request.header("Authorization", format!("Basic {}", login)); - } - - let generate_error = |t: &str, e: reqwest::Error, span: &Span| { - ShellError::labeled_error( - format!("Could not load {} from remote url: {:?}", t, e), - "could not load", - span, - ) - }; - let tag = Tag { - span, - anchor: Some(AnchorLocation::Url(location.to_string())), - }; - - match request.send().await { - Ok(r) => match r.headers().get("content-type") { - Some(content_type) => { - let content_type = content_type.to_str().map_err(|e| { - ShellError::labeled_error(e.to_string(), "MIME type were invalid", &tag) - })?; - let content_type = mime::Mime::from_str(content_type).map_err(|_| { - ShellError::labeled_error( - format!("MIME type unknown: {}", content_type), - "given unknown MIME type", - span, - ) - })?; - match (content_type.type_(), content_type.subtype()) { - (mime::APPLICATION, mime::XML) => Ok(( - Some("xml".to_string()), - UntaggedValue::string( - r.text() - .await - .map_err(|e| generate_error("text", e, &span))?, - ) - .into_value(tag), - )), - (mime::APPLICATION, mime::JSON) => Ok(( - Some("json".to_string()), - UntaggedValue::string( - r.text() - .await - .map_err(|e| generate_error("text", e, &span))?, - ) - .into_value(tag), - )), - (mime::APPLICATION, mime::OCTET_STREAM) => { - let buf: Vec = r - .bytes() - .await - .map_err(|e| generate_error("binary", e, &span))? - .to_vec(); - Ok((None, UntaggedValue::binary(buf).into_value(tag))) - } - (mime::IMAGE, mime::SVG) => Ok(( - Some("svg".to_string()), - UntaggedValue::string( - r.text() - .await - .map_err(|e| generate_error("svg", e, &span))?, - ) - .into_value(tag), - )), - (mime::IMAGE, image_ty) => { - let buf: Vec = r - .bytes() - .await - .map_err(|e| generate_error("image", e, &span))? - .to_vec(); - Ok(( - Some(image_ty.to_string()), - UntaggedValue::binary(buf).into_value(tag), - )) - } - (mime::TEXT, mime::HTML) => Ok(( - Some("html".to_string()), - UntaggedValue::string( - r.text() - .await - .map_err(|e| generate_error("text", e, &span))?, - ) - .into_value(tag), - )), - (mime::TEXT, mime::CSV) => Ok(( - Some("csv".to_string()), - UntaggedValue::string( - r.text() - .await - .map_err(|e| generate_error("text", e, &span))?, - ) - .into_value(tag), - )), - (mime::TEXT, mime::PLAIN) => { - let path_extension = url::Url::parse(location) - .map_err(|_| { - ShellError::labeled_error( - format!("Cannot parse URL: {}", location), - "cannot parse", - span, - ) - })? - .path_segments() - .and_then(|segments| segments.last()) - .and_then(|name| if name.is_empty() { None } else { Some(name) }) - .and_then(|name| { - PathBuf::from(name) - .extension() - .map(|name| name.to_string_lossy().to_string()) - }); - - Ok(( - path_extension, - UntaggedValue::string( - r.text() - .await - .map_err(|e| generate_error("text", e, &span))?, - ) - .into_value(tag), - )) - } - (_ty, _sub_ty) if has_raw => { - let raw_bytes = r.bytes().await; - let raw_bytes = match raw_bytes { - Ok(r) => r, - Err(e) => { - return Err(ShellError::labeled_error( - "error with raw_bytes", - e.to_string(), - &span, - )); - } - }; - - // For unsupported MIME types, we do not know if the data is UTF-8, - // so we get the raw body bytes and try to convert to UTF-8 if possible. - match std::str::from_utf8(&raw_bytes) { - Ok(response_str) => { - Ok((None, UntaggedValue::string(response_str).into_value(tag))) - } - Err(_) => Ok(( - None, - UntaggedValue::binary(raw_bytes.to_vec()).into_value(tag), - )), - } - } - (ty, sub_ty) => Err(ShellError::unimplemented(format!( - "Not yet supported MIME type: {} {}", - ty, sub_ty - ))), - } - } - // TODO: Should this return "nothing" or Err? - None => Ok(( - None, - UntaggedValue::string("No content type found".to_owned()).into_value(tag), - )), - }, - Err(e) => Err(ShellError::labeled_error( - "url could not be opened", - e.to_string(), - span, - )), - } -} - -// Only panics if the user agent is invalid but we define it statically so either -// it always or never fails -fn http_client(allow_insecure: bool) -> reqwest::Client { - reqwest::Client::builder() - .user_agent("nushell") - .danger_accept_invalid_certs(allow_insecure) - .build() - .expect("Failed to build reqwest client") -} diff --git a/crates/nu-command/src/commands/network/mod.rs b/crates/nu-command/src/commands/network/mod.rs deleted file mode 100644 index d6e32be4bd..0000000000 --- a/crates/nu-command/src/commands/network/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -#[cfg(feature = "fetch")] -mod fetch; -#[cfg(feature = "fetch")] -pub use fetch::Command as Fetch; - -#[cfg(feature = "post")] -mod post; -#[cfg(feature = "post")] -pub use post::Command as Post; - -mod url_; -pub use url_::*; diff --git a/crates/nu-command/src/commands/network/post.rs b/crates/nu-command/src/commands/network/post.rs deleted file mode 100644 index be472d4baa..0000000000 --- a/crates/nu-command/src/commands/network/post.rs +++ /dev/null @@ -1,646 +0,0 @@ -use crate::prelude::*; -use base64::encode; -use mime::Mime; -use nu_engine::WholeStreamCommand; -use nu_errors::{CoerceInto, ShellError}; -use nu_protocol::{ - CommandAction, Primitive, ReturnSuccess, ReturnValue, UnspannedPathMember, UntaggedValue, Value, -}; -use nu_protocol::{Signature, SyntaxShape}; -use nu_source::{AnchorLocation, Tag, TaggedItem}; -use num_traits::cast::ToPrimitive; -use std::path::PathBuf; -use std::str::FromStr; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "post" - } - - fn signature(&self) -> Signature { - Signature::build("post") - .desc("Post content to a URL and retrieve data as a table if possible.") - .required("path", SyntaxShape::Any, "the URL to post to") - .required("body", SyntaxShape::Any, "the contents of the post body") - .named( - "user", - SyntaxShape::Any, - "the username when authenticating", - Some('u'), - ) - .named( - "password", - SyntaxShape::Any, - "the password when authenticating", - Some('p'), - ) - .named( - "content-type", - SyntaxShape::Any, - "the MIME type of content to post", - Some('t'), - ) - .named( - "content-length", - SyntaxShape::Any, - "the length of the content being posted", - Some('l'), - ) - .switch( - "raw", - "return values as a string instead of a table", - Some('r'), - ) - .switch( - "insecure", - "allow insecure server connections when using SSL", - Some('k'), - ) - .filter() - } - - fn usage(&self) -> &str { - "Post a body to a URL (HTTP POST operation)." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - run_post(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Post content to url.com", - example: "post url.com 'body'", - result: None, - }, - Example { - description: "Post content to url.com, with username and password", - example: "post -u myuser -p mypass url.com 'body'", - result: None, - }, - ] - } -} - -fn run_post(args: CommandArgs) -> Result { - let mut helper = Post::new(); - - helper.setup(args)?; - - let runtime = tokio::runtime::Runtime::new()?; - Ok(vec![runtime.block_on(post_helper( - &helper.path.clone().ok_or_else(|| { - ShellError::labeled_error("expected a 'path'", "expected a 'path'", &helper.tag) - })?, - helper.has_raw, - helper.has_insecure, - &helper.body.clone().ok_or_else(|| { - ShellError::labeled_error("expected a 'body'", "expected a 'body'", &helper.tag) - })?, - helper.user.clone(), - helper.password.clone(), - &helper.headers, - ))] - .into_iter() - .into_action_stream()) - - //fetch.setup(callinfo)?; -} - -#[derive(Clone)] -pub enum HeaderKind { - ContentType(String), - ContentLength(String), -} - -#[derive(Default)] -pub struct Post { - pub path: Option, - pub has_raw: bool, - pub has_insecure: bool, - pub body: Option, - pub user: Option, - pub password: Option, - pub headers: Vec, - pub tag: Tag, -} - -impl Post { - pub fn new() -> Post { - Post { - path: None, - has_raw: false, - has_insecure: false, - body: None, - user: None, - password: None, - headers: vec![], - tag: Tag::default(), - } - } - - pub fn setup(&mut self, args: CommandArgs) -> Result<(), ShellError> { - self.path = Some({ - args.req(0).map_err(|_| { - ShellError::labeled_error( - "No file or directory specified", - "for command", - &args.name_tag(), - ) - })? - }); - - self.body = { - let file = args.req(1).map_err(|_| { - ShellError::labeled_error("No body specified", "for command", &args.name_tag()) - })?; - Some(file) - }; - - self.tag = args.name_tag(); - - self.has_raw = args.has_flag("raw"); - - self.has_insecure = args.has_flag("insecure"); - - self.user = args.get_flag("user")?; - - self.password = args.get_flag("password")?; - - self.headers = get_headers(&args)?; - - Ok(()) - } -} - -pub async fn post_helper( - path: &Value, - has_raw: bool, - has_insecure: bool, - body: &Value, - user: Option, - password: Option, - headers: &[HeaderKind], -) -> ReturnValue { - let path_tag = path.tag.clone(); - let path_str = path.as_string()?; - - let (file_extension, contents, contents_tag) = post( - &path_str, - has_insecure, - body, - user, - password, - headers, - path_tag.clone(), - ) - .await?; - - let file_extension = if has_raw { - None - } else { - // If the extension could not be determined via mimetype, try to use the path - // extension. Some file types do not declare their mimetypes (such as bson files). - file_extension.or_else(|| path_str.split('.').last().map(String::from)) - }; - - let tagged_contents = contents.into_value(&contents_tag); - - if let Some(extension) = file_extension { - Ok(ReturnSuccess::Action(CommandAction::AutoConvert( - tagged_contents, - extension, - ))) - } else { - ReturnSuccess::value(tagged_contents) - } -} - -pub async fn post( - location: &str, - allow_insecure: bool, - body: &Value, - user: Option, - password: Option, - headers: &[HeaderKind], - tag: Tag, -) -> Result<(Option, UntaggedValue, Tag), ShellError> { - if location.starts_with("http:") || location.starts_with("https:") { - let login = match (user, password) { - (Some(user), Some(password)) => Some(encode(&format!("{}:{}", user, password))), - (Some(user), _) => Some(encode(&format!("{}:", user))), - _ => None, - }; - let response = match body { - Value { - value: UntaggedValue::Primitive(Primitive::String(body_str)), - .. - } => { - let mut s = http_client(allow_insecure) - .post(location) - .body(body_str.to_string()); - if let Some(login) = login { - s = s.header("Authorization", format!("Basic {}", login)); - } - - for h in headers { - s = match h { - HeaderKind::ContentType(ct) => s.header("Content-Type", ct), - HeaderKind::ContentLength(cl) => s.header("Content-Length", cl), - }; - } - - s.send().await - } - Value { - value: UntaggedValue::Primitive(Primitive::Binary(b)), - .. - } => { - let mut s = http_client(allow_insecure) - .post(location) - .body(Vec::from(&b[..])); - if let Some(login) = login { - s = s.header("Authorization", format!("Basic {}", login)); - } - s.send().await - } - Value { value, tag } => { - match value_to_json_value(&value.clone().into_untagged_value()) { - Ok(json_value) => match serde_json::to_string(&json_value) { - Ok(result_string) => { - let mut s = http_client(allow_insecure) - .post(location) - .body(result_string); - - if let Some(login) = login { - s = s.header("Authorization", format!("Basic {}", login)); - } - s.send().await - } - _ => { - return Err(ShellError::labeled_error( - "Could not automatically convert table", - "needs manual conversion", - tag, - )); - } - }, - _ => { - return Err(ShellError::labeled_error( - "Could not automatically convert table", - "needs manual conversion", - tag, - )); - } - } - } - }; - match response { - Ok(r) => match r.headers().get("content-type") { - Some(content_type) => { - let content_type = content_type.to_str().map_err(|e| { - ShellError::labeled_error(e.to_string(), "MIME type were invalid", &tag) - })?; - let content_type = Mime::from_str(content_type).map_err(|_| { - ShellError::labeled_error( - format!("Unknown MIME type: {}", content_type), - "unknown MIME type", - &tag, - ) - })?; - match (content_type.type_(), content_type.subtype()) { - (mime::APPLICATION, mime::XML) => Ok(( - Some("xml".to_string()), - UntaggedValue::string(r.text().await.map_err(|_| { - ShellError::labeled_error( - "Could not load text from remote url", - "could not load", - &tag, - ) - })?), - Tag { - anchor: Some(AnchorLocation::Url(location.to_string())), - span: tag.span, - }, - )), - (mime::APPLICATION, mime::JSON) => Ok(( - Some("json".to_string()), - UntaggedValue::string(r.text().await.map_err(|_| { - ShellError::labeled_error( - "Could not load text from remote url", - "could not load", - &tag, - ) - })?), - Tag { - anchor: Some(AnchorLocation::Url(location.to_string())), - span: tag.span, - }, - )), - (mime::APPLICATION, mime::OCTET_STREAM) => { - let buf: Vec = r - .bytes() - .await - .map_err(|_| { - ShellError::labeled_error( - "Could not load binary file", - "could not load", - &tag, - ) - })? - .to_vec(); - Ok(( - None, - UntaggedValue::binary(buf), - Tag { - anchor: Some(AnchorLocation::Url(location.to_string())), - span: tag.span, - }, - )) - } - (mime::IMAGE, image_ty) => { - let buf: Vec = r - .bytes() - .await - .map_err(|_| { - ShellError::labeled_error( - "Could not load image file", - "could not load", - &tag, - ) - })? - .to_vec(); - Ok(( - Some(image_ty.to_string()), - UntaggedValue::binary(buf), - Tag { - anchor: Some(AnchorLocation::Url(location.to_string())), - span: tag.span, - }, - )) - } - (mime::TEXT, mime::HTML) => Ok(( - Some("html".to_string()), - UntaggedValue::string(r.text().await.map_err(|_| { - ShellError::labeled_error( - "Could not load text from remote url", - "could not load", - &tag, - ) - })?), - Tag { - anchor: Some(AnchorLocation::Url(location.to_string())), - span: tag.span, - }, - )), - (mime::TEXT, mime::PLAIN) => { - let path_extension = url::Url::parse(location) - .map_err(|_| { - ShellError::labeled_error( - format!("could not parse URL: {}", location), - "could not parse URL", - &tag, - ) - })? - .path_segments() - .and_then(|segments| segments.last()) - .and_then(|name| if name.is_empty() { None } else { Some(name) }) - .and_then(|name| { - PathBuf::from(name) - .extension() - .map(|name| name.to_string_lossy().to_string()) - }); - - Ok(( - path_extension, - UntaggedValue::string(r.text().await.map_err(|_| { - ShellError::labeled_error( - "Could not load text from remote url", - "could not load", - &tag, - ) - })?), - Tag { - anchor: Some(AnchorLocation::Url(location.to_string())), - span: tag.span, - }, - )) - } - (ty, sub_ty) => Ok(( - None, - UntaggedValue::string(format!( - "Not yet supported MIME type: {} {}", - ty, sub_ty - )), - Tag { - anchor: Some(AnchorLocation::Url(location.to_string())), - span: tag.span, - }, - )), - } - } - None => Ok(( - None, - UntaggedValue::string("No content type found".to_owned()), - Tag { - anchor: Some(AnchorLocation::Url(location.to_string())), - span: tag.span, - }, - )), - }, - Err(_) => Err(ShellError::labeled_error( - "URL could not be opened", - "url not found", - tag, - )), - } - } else { - Err(ShellError::labeled_error( - "Expected a url", - "needs a url", - tag, - )) - } -} - -// FIXME FIXME FIXME -// Ultimately, we don't want to duplicate to-json here, but we need to because there isn't an easy way to call into it, yet -pub fn value_to_json_value(v: &Value) -> Result { - Ok(match &v.value { - UntaggedValue::Primitive(Primitive::Boolean(b)) => serde_json::Value::Bool(*b), - UntaggedValue::Primitive(Primitive::Filesize(b)) => serde_json::Value::Number( - serde_json::Number::from(b.to_u64().expect("What about really big numbers")), - ), - UntaggedValue::Primitive(Primitive::Duration(i)) => serde_json::Value::Number( - serde_json::Number::from_f64( - i.to_f64().expect("TODO: What about really big decimals?"), - ) - .ok_or_else(|| { - ShellError::labeled_error( - "Can not convert big decimal to f64", - "cannot convert big decimal to f64", - &v.tag, - ) - })?, - ), - UntaggedValue::Primitive(Primitive::Date(d)) => serde_json::Value::String(d.to_string()), - UntaggedValue::Primitive(Primitive::EndOfStream) => serde_json::Value::Null, - UntaggedValue::Primitive(Primitive::BeginningOfStream) => serde_json::Value::Null, - UntaggedValue::Primitive(Primitive::Decimal(f)) => serde_json::Value::Number( - serde_json::Number::from_f64( - f.to_f64().expect("TODO: What about really big decimals?"), - ) - .ok_or_else(|| { - ShellError::labeled_error( - "Can not convert big decimal to f64", - "cannot convert big decimal to f64", - &v.tag, - ) - })?, - ), - UntaggedValue::Primitive(Primitive::Int(i)) => { - serde_json::Value::Number(serde_json::Number::from(*i)) - } - UntaggedValue::Primitive(Primitive::BigInt(i)) => { - serde_json::Value::Number(serde_json::Number::from(CoerceInto::::coerce_into( - i.tagged(&v.tag), - "converting to JSON number", - )?)) - } - UntaggedValue::Primitive(Primitive::Nothing) => serde_json::Value::Null, - UntaggedValue::Primitive(Primitive::GlobPattern(s)) => serde_json::Value::String(s.clone()), - UntaggedValue::Primitive(Primitive::String(s)) => serde_json::Value::String(s.clone()), - UntaggedValue::Primitive(Primitive::ColumnPath(path)) => serde_json::Value::Array( - path.iter() - .map(|x| match &x.unspanned { - UnspannedPathMember::String(string) => { - Ok(serde_json::Value::String(string.clone())) - } - UnspannedPathMember::Int(int) => { - Ok(serde_json::Value::Number(serde_json::Number::from(*int))) - } - }) - .collect::, ShellError>>()?, - ), - UntaggedValue::Primitive(Primitive::FilePath(s)) => { - serde_json::Value::String(s.display().to_string()) - } - - UntaggedValue::Table(l) => serde_json::Value::Array(json_list(l)?), - #[cfg(feature = "dataframe")] - UntaggedValue::DataFrame(_) | UntaggedValue::FrameStruct(_) => { - return Err(ShellError::labeled_error( - "Cannot convert data struct", - "Cannot convert data struct", - &v.tag, - )) - } - UntaggedValue::Error(e) => return Err(e.clone()), - UntaggedValue::Block(_) | UntaggedValue::Primitive(Primitive::Range(_)) => { - serde_json::Value::Null - } - UntaggedValue::Primitive(Primitive::Binary(b)) => { - let mut output = vec![]; - - for item in b { - output.push(serde_json::Value::Number( - serde_json::Number::from_f64(*item as f64).ok_or_else(|| { - ShellError::labeled_error( - "Cannot create number from from f64", - "cannot created number from f64", - &v.tag, - ) - })?, - )); - } - serde_json::Value::Array(output) - } - UntaggedValue::Row(o) => { - let mut m = serde_json::Map::new(); - for (k, v) in &o.entries { - m.insert(k.clone(), value_to_json_value(v)?); - } - serde_json::Value::Object(m) - } - }) -} - -fn json_list(input: &[Value]) -> Result, ShellError> { - let mut out = vec![]; - - for value in input { - out.push(value_to_json_value(value)?); - } - - Ok(out) -} - -fn get_headers(args: &CommandArgs) -> Result, ShellError> { - let mut headers = vec![]; - - match extract_header_value(args, "content-type") { - Ok(h) => { - if let Some(ct) = h { - headers.push(HeaderKind::ContentType(ct)) - } - } - Err(e) => { - return Err(e); - } - }; - - match extract_header_value(args, "content-length") { - Ok(h) => { - if let Some(cl) = h { - headers.push(HeaderKind::ContentLength(cl)) - } - } - Err(e) => { - return Err(e); - } - }; - - Ok(headers) -} - -fn extract_header_value(args: &CommandArgs, key: &str) -> Result, ShellError> { - if args.has_flag(key) { - let tagged = args.get_flag(key)?; - let val = match tagged { - Some(Value { - value: UntaggedValue::Primitive(Primitive::String(s)), - .. - }) => s, - Some(Value { tag, .. }) => { - return Err(ShellError::labeled_error( - format!("{} not in expected format. Expected string.", key), - "post error", - tag, - )); - } - _ => { - return Err(ShellError::labeled_error( - format!("{} not in expected format. Expected string.", key), - "post error", - Tag::unknown(), - )); - } - }; - return Ok(Some(val)); - } - - Ok(None) -} - -// Only panics if the user agent is invalid but we define it statically so either -// it always or never fails -fn http_client(allow_insecure: bool) -> reqwest::Client { - reqwest::Client::builder() - .user_agent("nushell") - .danger_accept_invalid_certs(allow_insecure) - .build() - .expect("Failed to build reqwest client") -} diff --git a/crates/nu-command/src/commands/network/url_/command.rs b/crates/nu-command/src/commands/network/url_/command.rs deleted file mode 100644 index e541912cf9..0000000000 --- a/crates/nu-command/src/commands/network/url_/command.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, UntaggedValue}; - -pub struct Url; - -impl WholeStreamCommand for Url { - fn name(&self) -> &str { - "url" - } - - fn signature(&self) -> Signature { - Signature::build("url") - } - - fn usage(&self) -> &str { - "Apply url function." - } - - fn run(&self, args: CommandArgs) -> Result { - Ok(OutputStream::one( - UntaggedValue::string(get_full_help(&Url, args.scope())).into_value(Tag::unknown()), - )) - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::Url; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Url {}) - } -} diff --git a/crates/nu-command/src/commands/network/url_/host.rs b/crates/nu-command/src/commands/network/url_/host.rs deleted file mode 100644 index 0571f20ae6..0000000000 --- a/crates/nu-command/src/commands/network/url_/host.rs +++ /dev/null @@ -1,59 +0,0 @@ -use url::Url; - -use super::operate; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ColumnPath, Signature, SyntaxShape, Value}; - -pub struct UrlHost; - -impl WholeStreamCommand for UrlHost { - fn name(&self) -> &str { - "url host" - } - - fn signature(&self) -> Signature { - Signature::build("url host").rest( - "rest", - SyntaxShape::ColumnPath, - "optionally operate by column path", - ) - } - - fn usage(&self) -> &str { - "gets the host of a url" - } - - fn run(&self, args: CommandArgs) -> Result { - let rest: Vec = args.rest(0)?; - let input = args.input; - - Ok(operate(input, rest, &host)) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Get host of a url", - example: "echo 'http://www.example.com/foo/bar' | url host", - result: Some(vec![Value::from("www.example.com")]), - }] - } -} - -fn host(url: &Url) -> &str { - url.host_str().unwrap_or("") -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::UrlHost; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(UrlHost {}) - } -} diff --git a/crates/nu-command/src/commands/network/url_/mod.rs b/crates/nu-command/src/commands/network/url_/mod.rs deleted file mode 100644 index 082552dd1c..0000000000 --- a/crates/nu-command/src/commands/network/url_/mod.rs +++ /dev/null @@ -1,62 +0,0 @@ -mod command; -mod host; -mod path; -mod query; -mod scheme; - -use crate::prelude::*; -use nu_errors::ShellError; -use nu_protocol::{ColumnPath, Primitive, ShellTypeName, UntaggedValue, Value}; -use url::Url; - -pub use command::Url as UrlCommand; -pub use host::UrlHost; -pub use path::UrlPath; -pub use query::UrlQuery; -pub use scheme::UrlScheme; - -fn handle_value(action: &F, v: &Value) -> Result -where - F: Fn(&Url) -> &str + Send + 'static, -{ - let a = |url| UntaggedValue::string(action(url)); - let v = match &v.value { - UntaggedValue::Primitive(Primitive::String(s)) => match Url::parse(s) { - Ok(url) => a(&url).into_value(v.tag()), - Err(_) => UntaggedValue::string("").into_value(v.tag()), - }, - other => { - let got = format!("got {}", other.type_name()); - return Err(ShellError::labeled_error( - "value is not a string", - got, - v.tag().span, - )); - } - }; - Ok(v) -} - -fn operate(input: crate::InputStream, paths: Vec, action: &'static F) -> OutputStream -where - F: Fn(&Url) -> &str + Send + Sync + 'static, -{ - input - .map(move |v| { - if paths.is_empty() { - handle_value(&action, &v) - } else { - let mut ret = v; - - for path in &paths { - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| handle_value(&action, old)), - )?; - } - - Ok(ret) - } - }) - .into_input_stream() -} diff --git a/crates/nu-command/src/commands/network/url_/path.rs b/crates/nu-command/src/commands/network/url_/path.rs deleted file mode 100644 index 75525ad534..0000000000 --- a/crates/nu-command/src/commands/network/url_/path.rs +++ /dev/null @@ -1,62 +0,0 @@ -use url::Url; - -use super::operate; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ColumnPath, Signature, SyntaxShape, Value}; - -pub struct UrlPath; - -impl WholeStreamCommand for UrlPath { - fn name(&self) -> &str { - "url path" - } - - fn signature(&self) -> Signature { - Signature::build("url path").rest( - "rest", - SyntaxShape::ColumnPath, - "optionally operate by column path", - ) - } - - fn usage(&self) -> &str { - "gets the path of a url" - } - - fn run(&self, args: CommandArgs) -> Result { - let rest: Vec = args.rest(0)?; - let input = args.input; - - Ok(operate(input, rest, &Url::path)) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Get path of a url", - example: "echo 'http://www.example.com/foo/bar' | url path", - result: Some(vec![Value::from("/foo/bar")]), - }, - Example { - description: "A trailing slash will be reflected in the path", - example: "echo 'http://www.example.com' | url path", - result: Some(vec![Value::from("/")]), - }, - ] - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::UrlPath; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(UrlPath {}) - } -} diff --git a/crates/nu-command/src/commands/network/url_/query.rs b/crates/nu-command/src/commands/network/url_/query.rs deleted file mode 100644 index ce6a5cf47f..0000000000 --- a/crates/nu-command/src/commands/network/url_/query.rs +++ /dev/null @@ -1,65 +0,0 @@ -use url::Url; - -use super::operate; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ColumnPath, Signature, SyntaxShape, Value}; - -pub struct UrlQuery; - -impl WholeStreamCommand for UrlQuery { - fn name(&self) -> &str { - "url query" - } - - fn signature(&self) -> Signature { - Signature::build("url query").rest( - "rest", - SyntaxShape::ColumnPath, - "optionally operate by column path", - ) - } - - fn usage(&self) -> &str { - "gets the query of a url" - } - - fn run(&self, args: CommandArgs) -> Result { - let rest: Vec = args.rest(0)?; - let input = args.input; - Ok(operate(input, rest, &query)) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Get query of a url", - example: "echo 'http://www.example.com/?foo=bar&baz=quux' | url query", - result: Some(vec![Value::from("foo=bar&baz=quux")]), - }, - Example { - description: "No query gives the empty string", - example: "echo 'http://www.example.com/' | url query", - result: Some(vec![Value::from("")]), - }, - ] - } -} - -fn query(url: &Url) -> &str { - url.query().unwrap_or("") -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::UrlQuery; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(UrlQuery {}) - } -} diff --git a/crates/nu-command/src/commands/network/url_/scheme.rs b/crates/nu-command/src/commands/network/url_/scheme.rs deleted file mode 100644 index 643883bc59..0000000000 --- a/crates/nu-command/src/commands/network/url_/scheme.rs +++ /dev/null @@ -1,60 +0,0 @@ -use url::Url; - -use super::operate; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ColumnPath, Signature, SyntaxShape, Value}; - -pub struct UrlScheme; - -impl WholeStreamCommand for UrlScheme { - fn name(&self) -> &str { - "url scheme" - } - - fn signature(&self) -> Signature { - Signature::build("url scheme").rest( - "rest", - SyntaxShape::ColumnPath, - "optionally operate by path", - ) - } - - fn usage(&self) -> &str { - "gets the scheme (eg http, file) of a url" - } - - fn run(&self, args: CommandArgs) -> Result { - let rest: Vec = args.rest(0)?; - Ok(operate(args.input, rest, &Url::scheme)) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Get scheme of a url", - example: "echo 'http://www.example.com' | url scheme", - result: Some(vec![Value::from("http")]), - }, - Example { - description: "You get an empty string if there is no scheme", - example: "echo 'test' | url scheme", - result: Some(vec![Value::from("")]), - }, - ] - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::UrlScheme; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(UrlScheme {}) - } -} diff --git a/crates/nu-command/src/commands/path/basename.rs b/crates/nu-command/src/commands/path/basename.rs deleted file mode 100644 index 21a1901b8d..0000000000 --- a/crates/nu-command/src/commands/path/basename.rs +++ /dev/null @@ -1,127 +0,0 @@ -use super::{column_paths_from_args, operate, PathSubcommandArguments}; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ColumnPath, Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tagged; -use std::path::Path; - -pub struct PathBasename; - -struct PathBasenameArguments { - columns: Vec, - replace: Option>, -} - -impl PathSubcommandArguments for PathBasenameArguments { - fn get_column_paths(&self) -> &Vec { - &self.columns - } -} - -impl WholeStreamCommand for PathBasename { - fn name(&self) -> &str { - "path basename" - } - - fn signature(&self) -> Signature { - Signature::build("path basename") - .named( - "columns", - SyntaxShape::Table, - "Optionally operate by column path", - Some('c'), - ) - .named( - "replace", - SyntaxShape::String, - "Return original path with basename replaced by this string", - Some('r'), - ) - } - - fn usage(&self) -> &str { - "Get the final component of a path" - } - - fn run(&self, args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let cmd_args = Arc::new(PathBasenameArguments { - columns: column_paths_from_args(&args)?, - replace: args.get_flag("replace")?, - }); - - Ok(operate(args.input, &action, tag.span, cmd_args)) - } - - #[cfg(windows)] - fn examples(&self) -> Vec { - vec![ - Example { - description: "Get basename of a path", - example: "'C:\\Users\\joe\\test.txt' | path basename", - result: Some(vec![Value::from("test.txt")]), - }, - Example { - description: "Get basename of a path in a column", - example: "ls .. | path basename -c [ name ]", - result: None, - }, - Example { - description: "Replace basename of a path", - example: "'C:\\Users\\joe\\test.txt' | path basename -r 'spam.png'", - result: Some(vec![Value::from(UntaggedValue::filepath( - "C:\\Users\\joe\\spam.png", - ))]), - }, - ] - } - - #[cfg(not(windows))] - fn examples(&self) -> Vec { - vec![ - Example { - description: "Get basename of a path", - example: "'/home/joe/test.txt' | path basename", - result: Some(vec![Value::from("test.txt")]), - }, - Example { - description: "Get basename of a path in a column", - example: "ls .. | path basename -c [ name ]", - result: None, - }, - Example { - description: "Replace basename of a path", - example: "'/home/joe/test.txt' | path basename -r 'spam.png'", - result: Some(vec![Value::from(UntaggedValue::filepath( - "/home/joe/spam.png", - ))]), - }, - ] - } -} - -fn action(path: &Path, tag: Tag, args: &PathBasenameArguments) -> Value { - let untagged = match args.replace { - Some(ref basename) => UntaggedValue::filepath(path.with_file_name(&basename.item)), - None => UntaggedValue::string(match path.file_name() { - Some(filename) => filename.to_string_lossy(), - None => "".into(), - }), - }; - - untagged.into_value(tag) -} - -#[cfg(test)] -mod tests { - use super::PathBasename; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(PathBasename {}) - } -} diff --git a/crates/nu-command/src/commands/path/command.rs b/crates/nu-command/src/commands/path/command.rs deleted file mode 100644 index 22a0d30eb3..0000000000 --- a/crates/nu-command/src/commands/path/command.rs +++ /dev/null @@ -1,54 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, UntaggedValue}; - -pub struct Path; - -impl WholeStreamCommand for Path { - fn name(&self) -> &str { - "path" - } - - fn signature(&self) -> Signature { - Signature::build("path") - } - - fn usage(&self) -> &str { - "Explore and manipulate paths." - } - - fn extra_usage(&self) -> &str { - r#"There are three ways to represent a path: - -* As a path literal, e.g., '/home/viking/spam.txt' -* As a structured path: a table with 'parent', 'stem', and 'extension' (and -* 'prefix' on Windows) columns. This format is produced by the 'path parse' - subcommand. -* As an inner list of path parts, e.g., '[[ / home viking spam.txt ]]'. - Splitting into parts is done by the `path split` command. - -All subcommands accept all three variants as an input. Furthermore, the 'path -join' subcommand can be used to join the structured path or path parts back into -the path literal."# - } - - fn run(&self, args: CommandArgs) -> Result { - Ok(OutputStream::one( - UntaggedValue::string(get_full_help(&Path, args.scope())).into_value(Tag::unknown()), - )) - } -} - -#[cfg(test)] -mod tests { - use super::Path; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Path {}) - } -} diff --git a/crates/nu-command/src/commands/path/dirname.rs b/crates/nu-command/src/commands/path/dirname.rs deleted file mode 100644 index 7e000629de..0000000000 --- a/crates/nu-command/src/commands/path/dirname.rs +++ /dev/null @@ -1,166 +0,0 @@ -use super::{column_paths_from_args, operate, PathSubcommandArguments}; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ColumnPath, Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tagged; -use std::path::Path; - -pub struct PathDirname; - -struct PathDirnameArguments { - columns: Vec, - replace: Option>, - num_levels: Option>, -} - -impl PathSubcommandArguments for PathDirnameArguments { - fn get_column_paths(&self) -> &Vec { - &self.columns - } -} - -impl WholeStreamCommand for PathDirname { - fn name(&self) -> &str { - "path dirname" - } - - fn signature(&self) -> Signature { - Signature::build("path dirname") - .named( - "columns", - SyntaxShape::Table, - "Optionally operate by column path", - Some('c'), - ) - .named( - "replace", - SyntaxShape::String, - "Return original path with dirname replaced by this string", - Some('r'), - ) - .named( - "num-levels", - SyntaxShape::Int, - "Number of directories to walk up", - Some('n'), - ) - } - - fn usage(&self) -> &str { - "Get the parent directory of a path" - } - - fn run(&self, args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let cmd_args = Arc::new(PathDirnameArguments { - columns: column_paths_from_args(&args)?, - replace: args.get_flag("replace")?, - num_levels: args.get_flag("num-levels")?, - }); - - Ok(operate(args.input, &action, tag.span, cmd_args)) - } - - #[cfg(windows)] - fn examples(&self) -> Vec { - vec![ - Example { - description: "Get dirname of a path", - example: "'C:\\Users\\joe\\code\\test.txt' | path dirname", - result: Some(vec![Value::from(UntaggedValue::filepath( - "C:\\Users\\joe\\code", - ))]), - }, - Example { - description: "Get dirname of a path in a column", - example: "ls ('.' | path expand) | path dirname -c [ name ]", - result: None, - }, - Example { - description: "Walk up two levels", - example: "'C:\\Users\\joe\\code\\test.txt' | path dirname -n 2", - result: Some(vec![Value::from(UntaggedValue::filepath("C:\\Users\\joe"))]), - }, - Example { - description: "Replace the part that would be returned with a custom path", - example: - "'C:\\Users\\joe\\code\\test.txt' | path dirname -n 2 -r C:\\Users\\viking", - result: Some(vec![Value::from(UntaggedValue::filepath( - "C:\\Users\\viking\\code\\test.txt", - ))]), - }, - ] - } - - #[cfg(not(windows))] - fn examples(&self) -> Vec { - vec![ - Example { - description: "Get dirname of a path", - example: "'/home/joe/code/test.txt' | path dirname", - result: Some(vec![Value::from(UntaggedValue::filepath("/home/joe/code"))]), - }, - Example { - description: "Get dirname of a path in a column", - example: "ls ('.' | path expand) | path dirname -c [ name ]", - result: None, - }, - Example { - description: "Walk up two levels", - example: "'/home/joe/code/test.txt' | path dirname -n 2", - result: Some(vec![Value::from(UntaggedValue::filepath("/home/joe"))]), - }, - Example { - description: "Replace the part that would be returned with a custom path", - example: "'/home/joe/code/test.txt' | path dirname -n 2 -r /home/viking", - result: Some(vec![Value::from(UntaggedValue::filepath( - "/home/viking/code/test.txt", - ))]), - }, - ] - } -} - -fn action(path: &Path, tag: Tag, args: &PathDirnameArguments) -> Value { - let num_levels = args.num_levels.as_ref().map_or(1, |tagged| tagged.item); - - let mut dirname = path; - let mut reached_top = false; // end early if somebody passes -n 99999999 - for _ in 0..num_levels { - dirname = dirname.parent().unwrap_or_else(|| { - reached_top = true; - dirname - }); - if reached_top { - break; - } - } - - let untagged = match args.replace { - Some(ref newdir) => { - let remainder = path.strip_prefix(dirname).unwrap_or(dirname); - if !remainder.as_os_str().is_empty() { - UntaggedValue::filepath(Path::new(&newdir.item).join(remainder)) - } else { - UntaggedValue::filepath(Path::new(&newdir.item)) - } - } - None => UntaggedValue::filepath(dirname), - }; - - untagged.into_value(tag) -} - -#[cfg(test)] -mod tests { - use super::PathDirname; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(PathDirname {}) - } -} diff --git a/crates/nu-command/src/commands/path/exists.rs b/crates/nu-command/src/commands/path/exists.rs deleted file mode 100644 index e88b3b6437..0000000000 --- a/crates/nu-command/src/commands/path/exists.rs +++ /dev/null @@ -1,95 +0,0 @@ -use super::{column_paths_from_args, operate, PathSubcommandArguments}; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ColumnPath, Signature, SyntaxShape, UntaggedValue, Value}; -use std::path::Path; - -pub struct PathExists; - -struct PathExistsArguments { - columns: Vec, -} - -impl PathSubcommandArguments for PathExistsArguments { - fn get_column_paths(&self) -> &Vec { - &self.columns - } -} - -impl WholeStreamCommand for PathExists { - fn name(&self) -> &str { - "path exists" - } - - fn signature(&self) -> Signature { - Signature::build("path exists").named( - "columns", - SyntaxShape::Table, - "Optionally operate by column path", - Some('c'), - ) - } - - fn usage(&self) -> &str { - "Check whether a path exists" - } - - fn run(&self, args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let cmd_args = Arc::new(PathExistsArguments { - columns: column_paths_from_args(&args)?, - }); - - Ok(operate(args.input, &action, tag.span, cmd_args)) - } - - #[cfg(windows)] - fn examples(&self) -> Vec { - vec![ - Example { - description: "Check if a file exists", - example: "'C:\\Users\\joe\\todo.txt' | path exists", - result: Some(vec![Value::from(UntaggedValue::boolean(false))]), - }, - Example { - description: "Check if a file exists in a column", - example: "ls | path exists -c [ name ]", - result: None, - }, - ] - } - - #[cfg(not(windows))] - fn examples(&self) -> Vec { - vec![ - Example { - description: "Check if a file exists", - example: "'/home/joe/todo.txt' | path exists", - result: Some(vec![Value::from(UntaggedValue::boolean(false))]), - }, - Example { - description: "Check if a file exists in a column", - example: "ls | path exists -c [ name ]", - result: None, - }, - ] - } -} - -fn action(path: &Path, tag: Tag, _args: &PathExistsArguments) -> Value { - UntaggedValue::boolean(path.exists()).into_value(tag) -} - -#[cfg(test)] -mod tests { - use super::PathExists; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(PathExists {}) - } -} diff --git a/crates/nu-command/src/commands/path/expand.rs b/crates/nu-command/src/commands/path/expand.rs deleted file mode 100644 index e30504e9c7..0000000000 --- a/crates/nu-command/src/commands/path/expand.rs +++ /dev/null @@ -1,134 +0,0 @@ -use super::{column_paths_from_args, operate, PathSubcommandArguments}; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_path::{canonicalize, expand_path}; -use nu_protocol::{ColumnPath, Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Span; -use std::path::Path; - -pub struct PathExpand; - -struct PathExpandArguments { - strict: bool, - columns: Vec, -} - -impl PathSubcommandArguments for PathExpandArguments { - fn get_column_paths(&self) -> &Vec { - &self.columns - } -} - -impl WholeStreamCommand for PathExpand { - fn name(&self) -> &str { - "path expand" - } - - fn signature(&self) -> Signature { - Signature::build("path expand") - .switch( - "strict", - "Throw an error if the path could not be expanded", - Some('s'), - ) - .named( - "columns", - SyntaxShape::Table, - "Optionally operate by column path", - Some('c'), - ) - } - - fn usage(&self) -> &str { - "Try to expand a path to its absolute form" - } - - fn run(&self, args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let cmd_args = Arc::new(PathExpandArguments { - strict: args.has_flag("strict"), - columns: column_paths_from_args(&args)?, - }); - - Ok(operate(args.input, &action, tag.span, cmd_args)) - } - - #[cfg(windows)] - fn examples(&self) -> Vec { - vec![ - Example { - description: "Expand an absolute path", - example: r"'C:\Users\joe\foo\..\bar' | path expand", - result: Some(vec![ - UntaggedValue::filepath(r"C:\Users\joe\bar").into_value(Span::new(0, 25)) - ]), - }, - Example { - description: "Expand a path in a column", - example: "ls | path expand -c [ name ]", - result: None, - }, - Example { - description: "Expand a relative path", - example: r"'foo\..\bar' | path expand", - result: Some(vec![ - UntaggedValue::filepath("bar").into_value(Span::new(0, 12)) - ]), - }, - ] - } - - #[cfg(not(windows))] - fn examples(&self) -> Vec { - vec![ - Example { - description: "Expand an absolute path", - example: "'/home/joe/foo/../bar' | path expand", - result: Some(vec![ - UntaggedValue::filepath("/home/joe/bar").into_value(Span::new(0, 22)) - ]), - }, - Example { - description: "Expand a path in a column", - example: "ls | path expand -c [ name ]", - result: None, - }, - Example { - description: "Expand a relative path", - example: "'foo/../bar' | path expand", - result: Some(vec![ - UntaggedValue::filepath("bar").into_value(Span::new(0, 12)) - ]), - }, - ] - } -} - -fn action(path: &Path, tag: Tag, args: &PathExpandArguments) -> Value { - if let Ok(p) = canonicalize(path) { - UntaggedValue::filepath(p).into_value(tag) - } else if args.strict { - Value::error(ShellError::labeled_error( - "Could not expand path", - "could not be expanded (path might not exist, non-final \ - component is not a directory, or other cause)", - tag.span, - )) - } else { - UntaggedValue::filepath(expand_path(path)).into_value(tag) - } -} - -#[cfg(test)] -mod tests { - use super::PathExpand; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(PathExpand {}) - } -} diff --git a/crates/nu-command/src/commands/path/join.rs b/crates/nu-command/src/commands/path/join.rs deleted file mode 100644 index a81eaf300f..0000000000 --- a/crates/nu-command/src/commands/path/join.rs +++ /dev/null @@ -1,193 +0,0 @@ -use super::{ - column_paths_from_args, handle_value, join_path, operate_column_paths, PathSubcommandArguments, -}; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ColumnPath, Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tagged; -use std::path::{Path, PathBuf}; - -pub struct PathJoin; - -struct PathJoinArguments { - columns: Vec, - append: Option>, -} - -impl PathSubcommandArguments for PathJoinArguments { - fn get_column_paths(&self) -> &Vec { - &self.columns - } -} - -impl WholeStreamCommand for PathJoin { - fn name(&self) -> &str { - "path join" - } - - fn signature(&self) -> Signature { - Signature::build("path join") - .named( - "columns", - SyntaxShape::Table, - "Optionally operate by column path", - Some('c'), - ) - .optional( - "append", - SyntaxShape::FilePath, - "Path to append to the input", - ) - } - - fn usage(&self) -> &str { - "Join a structured path or a list of path parts." - } - - fn extra_usage(&self) -> &str { - r#"Optionally, append an additional path to the result. It is designed to accept -the output of 'path parse' and 'path split' subcommands."# - } - - fn run(&self, args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let cmd_args = Arc::new(PathJoinArguments { - columns: column_paths_from_args(&args)?, - append: args.opt(0)?, - }); - - Ok(operate_join(args.input, &action, tag, cmd_args)) - } - - #[cfg(windows)] - fn examples(&self) -> Vec { - vec![ - Example { - description: "Append a filename to a path", - example: r"'C:\Users\viking' | path join spam.txt", - result: Some(vec![Value::from(UntaggedValue::filepath( - r"C:\Users\viking\spam.txt", - ))]), - }, - Example { - description: "Append a filename to a path inside a column", - example: r"ls | path join spam.txt -c [ name ]", - result: None, - }, - Example { - description: "Join a list of parts into a path", - example: r"[ 'C:' '\' 'Users' 'viking' 'spam.txt' ] | path join", - result: Some(vec![Value::from(UntaggedValue::filepath( - r"C:\Users\viking\spam.txt", - ))]), - }, - Example { - description: "Join a structured path into a path", - example: r"[ [parent stem extension]; ['C:\Users\viking' 'spam' 'txt']] | path join", - result: Some(vec![Value::from(UntaggedValue::filepath( - r"C:\Users\viking\spam.txt", - ))]), - }, - ] - } - - #[cfg(not(windows))] - fn examples(&self) -> Vec { - vec![ - Example { - description: "Append a filename to a path", - example: r"'/home/viking' | path join spam.txt", - result: Some(vec![Value::from(UntaggedValue::filepath( - r"/home/viking/spam.txt", - ))]), - }, - Example { - description: "Append a filename to a path inside a column", - example: r"ls | path join spam.txt -c [ name ]", - result: None, - }, - Example { - description: "Join a list of parts into a path", - example: r"[ '/' 'home' 'viking' 'spam.txt' ] | path join", - result: Some(vec![Value::from(UntaggedValue::filepath( - r"/home/viking/spam.txt", - ))]), - }, - Example { - description: "Join a structured path into a path", - example: r"[[ parent stem extension ]; [ '/home/viking' 'spam' 'txt' ]] | path join", - result: Some(vec![Value::from(UntaggedValue::filepath( - r"/home/viking/spam.txt", - ))]), - }, - ] - } -} - -fn operate_join( - input: crate::InputStream, - action: &'static F, - tag: Tag, - args: Arc, -) -> OutputStream -where - T: PathSubcommandArguments + Send + Sync + 'static, - F: Fn(&Path, Tag, &T) -> Value + Send + Sync + 'static, -{ - let span = tag.span; - - if args.get_column_paths().is_empty() { - let mut parts = input.peekable(); - let has_rows = matches!( - parts.peek(), - Some(&Value { - value: UntaggedValue::Row(_), - .. - }) - ); - - if has_rows { - // operate one-by-one like the other path subcommands - parts - .into_iter() - .map( - move |v| match handle_value(&action, &v, span, Arc::clone(&args)) { - Ok(v) => v, - Err(e) => Value::error(e), - }, - ) - .into_output_stream() - } else { - // join the whole input stream - match join_path(&parts.collect_vec(), &span) { - Ok(path_buf) => OutputStream::one(action(&path_buf, tag, &args)), - Err(e) => OutputStream::one(Value::error(e)), - } - } - } else { - operate_column_paths(input, action, span, args) - } -} - -fn action(path: &Path, tag: Tag, args: &PathJoinArguments) -> Value { - if let Some(ref append) = args.append { - UntaggedValue::filepath(path.join(&append.item)).into_value(tag) - } else { - UntaggedValue::filepath(path).into_value(tag) - } -} - -#[cfg(test)] -mod tests { - use super::PathJoin; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(PathJoin {}) - } -} diff --git a/crates/nu-command/src/commands/path/mod.rs b/crates/nu-command/src/commands/path/mod.rs deleted file mode 100644 index ffd2fba52b..0000000000 --- a/crates/nu-command/src/commands/path/mod.rs +++ /dev/null @@ -1,251 +0,0 @@ -mod basename; -mod command; -mod dirname; -mod exists; -mod expand; -mod join; -mod parse; -mod relative_to; -mod split; -mod r#type; - -use crate::prelude::*; -use nu_errors::ShellError; -use nu_protocol::{ - ColumnPath, Dictionary, MaybeOwned, Primitive, ShellTypeName, UntaggedValue, Value, -}; -use nu_source::Span; -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -pub use basename::PathBasename; -pub use command::Path as PathCommand; -pub use dirname::PathDirname; -pub use exists::PathExists; -pub use expand::PathExpand; -pub use join::PathJoin; -pub use parse::PathParse; -pub use r#type::PathType; -pub use relative_to::PathRelativeTo; -pub use split::PathSplit; - -#[cfg(windows)] -const ALLOWED_COLUMNS: [&str; 4] = ["prefix", "parent", "stem", "extension"]; -#[cfg(not(windows))] -const ALLOWED_COLUMNS: [&str; 3] = ["parent", "stem", "extension"]; - -trait PathSubcommandArguments { - fn get_column_paths(&self) -> &Vec; -} - -fn encode_path( - entries: &Dictionary, - orig_span: Span, - new_span: Span, -) -> Result { - if entries.length() == 0 { - return Err(ShellError::labeled_error_with_secondary( - "Empty table cannot be encoded as a path", - "got empty table", - new_span, - "originates from here", - orig_span, - )); - } - - for col in entries.keys() { - if !ALLOWED_COLUMNS.contains(&col.as_str()) { - let msg = format!( - "Column '{}' is not valid for a structured path. Allowed columns are: {}", - col, - ALLOWED_COLUMNS.join(", ") - ); - return Err(ShellError::labeled_error_with_secondary( - "Expected structured path table", - msg, - new_span, - "originates from here", - orig_span, - )); - } - } - - // At this point, the row is known to have >0 columns, all of them allowed - let mut result = PathBuf::new(); - - #[cfg(windows)] - if let MaybeOwned::Borrowed(val) = entries.get_data("prefix") { - let s = val.as_string()?; - if !s.is_empty() { - result.push(&s); - } - }; - - if let MaybeOwned::Borrowed(val) = entries.get_data("parent") { - let p = val.as_string()?; - if !p.is_empty() { - result.push(p); - } - }; - - let mut basename = String::new(); - - if let MaybeOwned::Borrowed(val) = entries.get_data("stem") { - let s = val.as_string()?; - if !s.is_empty() { - basename.push_str(&s); - } - }; - - if let MaybeOwned::Borrowed(val) = entries.get_data("extension") { - let s = val.as_string()?; - if !s.is_empty() { - basename.push('.'); - basename.push_str(&s); - } - }; - - if !basename.is_empty() { - result.push(basename); - } - - Ok(result) -} - -fn join_path(parts: &[Value], new_span: &Span) -> Result { - parts - .iter() - .map(|part| match &part.value { - UntaggedValue::Primitive(Primitive::String(s)) => Ok(Path::new(s)), - UntaggedValue::Primitive(Primitive::FilePath(pb)) => Ok(pb.as_path()), - _ => { - let got = format!("got {}", part.type_name()); - Err(ShellError::labeled_error_with_secondary( - "Cannot join values that are not paths or strings.", - got, - new_span, - "originates from here", - part.tag.span, - )) - } - }) - .collect() -} - -fn handle_value(action: &F, v: &Value, span: Span, args: Arc) -> Result -where - T: PathSubcommandArguments, - F: Fn(&Path, Tag, &T) -> Value, -{ - match &v.value { - UntaggedValue::Primitive(Primitive::FilePath(buf)) => Ok(action(buf, v.tag(), &args)), - UntaggedValue::Primitive(Primitive::String(s)) => Ok(action(s.as_ref(), v.tag(), &args)), - UntaggedValue::Row(entries) => { - // implicit path join makes all subcommands understand the structured path - let path_buf = encode_path(entries, v.tag().span, span)?; - Ok(action(&path_buf, v.tag(), &args)) - } - UntaggedValue::Table(parts) => { - // implicit path join makes all subcommands understand path split into parts - let path_buf = join_path(parts, &span)?; - Ok(action(&path_buf, v.tag(), &args)) - } - other => { - let got = format!("got {}", other.type_name()); - Err(ShellError::labeled_error_with_secondary( - "Value is a not string, path, row, or table", - got, - span, - "originates from here", - v.tag().span, - )) - } - } -} - -fn operate_column_paths( - input: crate::InputStream, - action: &'static F, - span: Span, - args: Arc, -) -> OutputStream -where - T: PathSubcommandArguments + Send + Sync + 'static, - F: Fn(&Path, Tag, &T) -> Value + Send + Sync + 'static, -{ - input - .map(move |v| { - let mut ret = v; - - for path in args.get_column_paths() { - let cloned_args = Arc::clone(&args); - ret = match ret.swap_data_by_column_path( - path, - Box::new(move |old| handle_value(&action, old, span, cloned_args)), - ) { - Ok(v) => v, - Err(e) => Value::error(e), - }; - } - - ret - }) - .into_output_stream() -} - -fn operate( - input: crate::InputStream, - action: &'static F, - span: Span, - args: Arc, -) -> OutputStream -where - T: PathSubcommandArguments + Send + Sync + 'static, - F: Fn(&Path, Tag, &T) -> Value + Send + Sync + 'static, -{ - if args.get_column_paths().is_empty() { - input - .map( - move |v| match handle_value(&action, &v, span, Arc::clone(&args)) { - Ok(v) => v, - Err(e) => Value::error(e), - }, - ) - .into_output_stream() - } else { - operate_column_paths(input, action, span, args) - } -} - -fn column_paths_from_args(args: &CommandArgs) -> Result, ShellError> { - let column_paths: Option> = args.get_flag("columns")?; - let has_columns = column_paths.is_some(); - let column_paths = match column_paths { - Some(cols) => { - let mut c = Vec::new(); - for col in cols { - let colpath = ColumnPath::build(&col.convert_to_string().spanned_unknown()); - if !colpath.is_empty() { - c.push(colpath) - } - } - c - } - None => Vec::new(), - }; - - if has_columns && column_paths.is_empty() { - let colval: Option = args.get_flag("columns")?; - let colspan = match colval { - Some(v) => v.tag.span, - None => Span::unknown(), - }; - return Err(ShellError::labeled_error( - "Requires a list of columns", - "must be a list of columns", - colspan, - )); - } - - Ok(column_paths) -} diff --git a/crates/nu-command/src/commands/path/parse.rs b/crates/nu-command/src/commands/path/parse.rs deleted file mode 100644 index 81fd30ea44..0000000000 --- a/crates/nu-command/src/commands/path/parse.rs +++ /dev/null @@ -1,181 +0,0 @@ -use super::{column_paths_from_args, operate, PathSubcommandArguments}; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ColumnPath, Signature, SyntaxShape, TaggedDictBuilder, UntaggedValue, Value}; -use nu_source::Tagged; -#[cfg(windows)] -use std::path::Component; -use std::path::Path; - -pub struct PathParse; - -struct PathParseArguments { - columns: Vec, - extension: Option>, -} - -impl PathSubcommandArguments for PathParseArguments { - fn get_column_paths(&self) -> &Vec { - &self.columns - } -} - -impl WholeStreamCommand for PathParse { - fn name(&self) -> &str { - "path parse" - } - - fn signature(&self) -> Signature { - Signature::build("path parse") - .named( - "columns", - SyntaxShape::Table, - "Optionally operate by column path", - Some('c'), - ) - .named( - "extension", - SyntaxShape::String, - "Manually supply the extension (without the dot)", - Some('e'), - ) - } - - fn usage(&self) -> &str { - "Convert a path into structured data." - } - - fn extra_usage(&self) -> &str { - r#"Each path is split into a table with 'parent', 'stem' and 'extension' fields. -On Windows, an extra 'prefix' column is added."# - } - - fn run(&self, args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let cmd_args = Arc::new(PathParseArguments { - columns: column_paths_from_args(&args)?, - extension: args.get_flag("extension")?, - }); - - Ok(operate(args.input, &action, tag.span, cmd_args)) - } - - #[cfg(windows)] - fn examples(&self) -> Vec { - vec![ - Example { - description: "Parse a single path", - example: r"'C:\Users\viking\spam.txt' | path parse", - result: None, - }, - Example { - description: "Replace a complex extension", - example: r"'C:\Users\viking\spam.tar.gz' | path parse -e tar.gz | update extension { 'txt' }", - result: None, - }, - Example { - description: "Ignore the extension", - example: r"'C:\Users\viking.d' | path parse -e ''", - result: None, - }, - Example { - description: "Parse all paths under the 'name' column", - example: r"ls | path parse -c [ name ]", - result: None, - }, - ] - } - - #[cfg(not(windows))] - fn examples(&self) -> Vec { - vec![ - Example { - description: "Parse a path", - example: r"'/home/viking/spam.txt' | path parse", - result: None, - }, - Example { - description: "Replace a complex extension", - example: r"'/home/viking/spam.tar.gz' | path parse -e tar.gz | update extension { 'txt' }", - result: None, - }, - Example { - description: "Ignore the extension", - example: r"'/etc/conf.d' | path parse -e ''", - result: None, - }, - Example { - description: "Parse all paths under the 'name' column", - example: r"ls | path parse -c [ name ]", - result: None, - }, - ] - } -} - -fn action(path: &Path, tag: Tag, args: &PathParseArguments) -> Value { - let mut dict = TaggedDictBuilder::new(&tag); - - #[cfg(windows)] - { - // The prefix is only valid on Windows. On non-Windows, it's always empty. - let prefix = match path.components().next() { - Some(Component::Prefix(prefix_component)) => { - prefix_component.as_os_str().to_string_lossy() - } - _ => "".into(), - }; - dict.insert_untagged("prefix", UntaggedValue::string(prefix)); - } - - let parent = path.parent().unwrap_or_else(|| "".as_ref()); - dict.insert_untagged("parent", UntaggedValue::filepath(parent)); - - let basename = path - .file_name() - .unwrap_or_else(|| "".as_ref()) - .to_string_lossy(); - - match &args.extension { - Some(Tagged { item: ext, .. }) => { - let ext_with_dot = [".", ext].concat(); - if basename.ends_with(&ext_with_dot) && !ext.is_empty() { - let stem = basename.trim_end_matches(&ext_with_dot); - dict.insert_untagged("stem", UntaggedValue::string(stem)); - dict.insert_untagged("extension", UntaggedValue::string(ext)); - } else { - dict.insert_untagged("stem", UntaggedValue::string(basename)); - dict.insert_untagged("extension", UntaggedValue::string("")); - } - } - None => { - let stem = path - .file_stem() - .unwrap_or_else(|| "".as_ref()) - .to_string_lossy(); - let extension = path - .extension() - .unwrap_or_else(|| "".as_ref()) - .to_string_lossy(); - - dict.insert_untagged("stem", UntaggedValue::string(stem)); - dict.insert_untagged("extension", UntaggedValue::string(extension)); - } - } - - dict.into_value() -} - -#[cfg(test)] -mod tests { - use super::PathParse; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(PathParse {}) - } -} diff --git a/crates/nu-command/src/commands/path/relative_to.rs b/crates/nu-command/src/commands/path/relative_to.rs deleted file mode 100644 index d62b262db2..0000000000 --- a/crates/nu-command/src/commands/path/relative_to.rs +++ /dev/null @@ -1,133 +0,0 @@ -use super::{column_paths_from_args, operate, PathSubcommandArguments}; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ColumnPath, Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tagged; -use std::path::{Path, PathBuf}; - -pub struct PathRelativeTo; - -struct PathRelativeToArguments { - path: Tagged, - columns: Vec, -} - -impl PathSubcommandArguments for PathRelativeToArguments { - fn get_column_paths(&self) -> &Vec { - &self.columns - } -} - -impl WholeStreamCommand for PathRelativeTo { - fn name(&self) -> &str { - "path relative-to" - } - - fn signature(&self) -> Signature { - Signature::build("path relative-to") - .required( - "path", - SyntaxShape::FilePath, - "Parent shared with the input path", - ) - .named( - "columns", - SyntaxShape::Table, - "Optionally operate by column path", - Some('c'), - ) - } - - fn usage(&self) -> &str { - "Get a path as relative to another path." - } - - fn extra_usage(&self) -> &str { - r#"Can be used only when the input and the argument paths are either both -absolute or both relative. The argument path needs to be a parent of the input -path."# - } - - fn run(&self, args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let cmd_args = Arc::new(PathRelativeToArguments { - path: args.req(0)?, - columns: column_paths_from_args(&args)?, - }); - - Ok(operate(args.input, &action, tag.span, cmd_args)) - } - - #[cfg(windows)] - fn examples(&self) -> Vec { - vec![ - Example { - description: "Find a relative path from two absolute paths", - example: r"'C:\Users\viking' | path relative-to 'C:\Users'", - result: Some(vec![Value::from(UntaggedValue::filepath(r"viking"))]), - }, - Example { - description: "Find a relative path from two absolute paths in a column", - example: "ls ~ | path relative-to ~ -c [ name ]", - result: None, - }, - Example { - description: "Find a relative path from two relative paths", - example: r"'eggs\bacon\sausage\spam' | path relative-to 'eggs\bacon\sausage'", - result: Some(vec![Value::from(UntaggedValue::filepath(r"spam"))]), - }, - ] - } - - #[cfg(not(windows))] - fn examples(&self) -> Vec { - vec![ - Example { - description: "Find a relative path from two absolute paths", - example: r"'/home/viking' | path relative-to '/home'", - result: Some(vec![Value::from(UntaggedValue::filepath(r"viking"))]), - }, - Example { - description: "Find a relative path from two absolute paths in a column", - example: "ls ~ | path relative-to ~ -c [ name ]", - result: None, - }, - Example { - description: "Find a relative path from two relative paths", - example: r"'eggs/bacon/sausage/spam' | path relative-to 'eggs/bacon/sausage'", - result: Some(vec![Value::from(UntaggedValue::filepath(r"spam"))]), - }, - ] - } -} - -fn action(path: &Path, tag: Tag, args: &PathRelativeToArguments) -> Value { - match path.strip_prefix(&args.path.item) { - Ok(p) => UntaggedValue::filepath(p).into_value(tag), - Err(_) => Value::error(ShellError::labeled_error_with_secondary( - format!( - "'{}' is not a subpath of '{}'", - path.to_string_lossy(), - &args.path.item.to_string_lossy() - ), - "should be a parent of the input path", - args.path.tag.span, - "originates from here", - tag.span, - )), - } -} - -#[cfg(test)] -mod tests { - use super::PathRelativeTo; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(PathRelativeTo {}) - } -} diff --git a/crates/nu-command/src/commands/path/split.rs b/crates/nu-command/src/commands/path/split.rs deleted file mode 100644 index d51505b2da..0000000000 --- a/crates/nu-command/src/commands/path/split.rs +++ /dev/null @@ -1,149 +0,0 @@ -use super::{column_paths_from_args, handle_value, operate_column_paths, PathSubcommandArguments}; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ColumnPath, Signature, SyntaxShape, UntaggedValue, Value}; -use std::path::Path; - -pub struct PathSplit; - -struct PathSplitArguments { - columns: Vec, -} - -impl PathSubcommandArguments for PathSplitArguments { - fn get_column_paths(&self) -> &Vec { - &self.columns - } -} - -impl WholeStreamCommand for PathSplit { - fn name(&self) -> &str { - "path split" - } - - fn signature(&self) -> Signature { - Signature::build("path split").named( - "columns", - SyntaxShape::Table, - "Optionally operate by column path", - Some('c'), - ) - } - - fn usage(&self) -> &str { - "Split a path into parts by a separator." - } - - fn run(&self, args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let cmd_args = Arc::new(PathSplitArguments { - columns: column_paths_from_args(&args)?, - }); - - Ok(operate_split(args.input, &action, tag.span, cmd_args)) - } - - #[cfg(windows)] - fn examples(&self) -> Vec { - vec![ - Example { - description: "Split a path into parts", - example: r"'C:\Users\viking\spam.txt' | path split", - result: Some(vec![ - Value::from(UntaggedValue::string("C:")), - Value::from(UntaggedValue::string(r"\")), - Value::from(UntaggedValue::string("Users")), - Value::from(UntaggedValue::string("viking")), - Value::from(UntaggedValue::string("spam.txt")), - ]), - }, - Example { - description: "Split all paths under the 'name' column", - example: r"ls ('.' | path expand) | path split -c [ name ]", - result: None, - }, - ] - } - - #[cfg(not(windows))] - fn examples(&self) -> Vec { - vec![ - Example { - description: "Split a path into parts", - example: r"'/home/viking/spam.txt' | path split", - result: Some(vec![ - Value::from(UntaggedValue::string("/")), - Value::from(UntaggedValue::string("home")), - Value::from(UntaggedValue::string("viking")), - Value::from(UntaggedValue::string("spam.txt")), - ]), - }, - Example { - description: "Split all paths under the 'name' column", - example: r"ls ('.' | path expand) | path split -c [ name ]", - result: None, - }, - ] - } -} - -fn operate_split( - input: crate::InputStream, - action: &'static F, - span: Span, - args: Arc, -) -> OutputStream -where - T: PathSubcommandArguments + Send + Sync + 'static, - F: Fn(&Path, Tag, &T) -> Value + Send + Sync + 'static, -{ - if args.get_column_paths().is_empty() { - // Do not wrap result into a table - input - .flat_map(move |v| { - let split_result = handle_value(&action, &v, span, Arc::clone(&args)); - - match split_result { - Ok(Value { - value: UntaggedValue::Table(parts), - .. - }) => parts.into_iter().into_output_stream(), - Err(e) => OutputStream::one(Value::error(e)), - _ => OutputStream::one(Value::error(ShellError::labeled_error( - "Internal Error", - "unexpected result from the split function", - span, - ))), - } - }) - .into_output_stream() - } else { - operate_column_paths(input, action, span, args) - } -} - -fn action(path: &Path, tag: Tag, _args: &PathSplitArguments) -> Value { - let parts: Vec = path - .components() - .map(|comp| { - let s = comp.as_os_str().to_string_lossy(); - UntaggedValue::string(s).into_value(&tag) - }) - .collect(); - - UntaggedValue::table(&parts).into_value(tag) -} - -#[cfg(test)] -mod tests { - use super::PathSplit; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(PathSplit {}) - } -} diff --git a/crates/nu-command/src/commands/path/type.rs b/crates/nu-command/src/commands/path/type.rs deleted file mode 100644 index 041f9fbf0b..0000000000 --- a/crates/nu-command/src/commands/path/type.rs +++ /dev/null @@ -1,85 +0,0 @@ -use super::{column_paths_from_args, operate, PathSubcommandArguments}; -use crate::prelude::*; -use nu_engine::filesystem::filesystem_shell::get_file_type; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ColumnPath, Signature, SyntaxShape, UntaggedValue, Value}; -use std::path::Path; - -pub struct PathType; - -struct PathTypeArguments { - columns: Vec, -} - -impl PathSubcommandArguments for PathTypeArguments { - fn get_column_paths(&self) -> &Vec { - &self.columns - } -} - -impl WholeStreamCommand for PathType { - fn name(&self) -> &str { - "path type" - } - - fn signature(&self) -> Signature { - Signature::build("path type").named( - "columns", - SyntaxShape::Table, - "Optionally operate by column path", - Some('c'), - ) - } - - fn usage(&self) -> &str { - "Get the type of the object a path refers to (e.g., file, dir, symlink)" - } - - fn run(&self, args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let cmd_args = Arc::new(PathTypeArguments { - columns: column_paths_from_args(&args)?, - }); - - Ok(operate(args.input, &action, tag.span, cmd_args)) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Show type of a filepath", - example: "'.' | path type", - result: Some(vec![Value::from("Dir")]), - }, - Example { - description: "Show type of a filepath in a column", - example: "ls | path type -c [ name ]", - result: None, - }, - ] - } -} - -fn action(path: &Path, tag: Tag, _args: &PathTypeArguments) -> Value { - let meta = std::fs::symlink_metadata(path); - let untagged = UntaggedValue::string(match &meta { - Ok(md) => get_file_type(md), - Err(_) => "", - }); - - untagged.into_value(tag) -} - -#[cfg(test)] -mod tests { - use super::PathType; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(PathType {}) - } -} diff --git a/crates/nu-command/src/commands/pathvar/add.rs b/crates/nu-command/src/commands/pathvar/add.rs deleted file mode 100644 index 4605a626f3..0000000000 --- a/crates/nu-command/src/commands/pathvar/add.rs +++ /dev/null @@ -1,71 +0,0 @@ -use super::get_var; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape}; -use nu_source::Tagged; -use nu_test_support::NATIVE_PATH_ENV_SEPARATOR; -use std::path::PathBuf; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "pathvar add" - } - - fn signature(&self) -> Signature { - Signature::build("pathvar add") - .required("path", SyntaxShape::FilePath, "path to add") - .named( - "var", - SyntaxShape::String, - "Use a different variable than PATH", - Some('v'), - ) - } - - fn usage(&self) -> &str { - "Add a filepath to the start of the pathvar" - } - - fn run(&self, args: CommandArgs) -> Result { - add(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Add /usr/local/bin to the pathvar", - example: "pathvar add /usr/local/bin", - result: None, - }] - } -} - -pub fn add(args: CommandArgs) -> Result { - let ctx = &args.context; - - let var = get_var(&args)?; - let path_to_add: Tagged = args.req(0)?; - let path = path_to_add.item.into_os_string().into_string(); - - if let Ok(mut path) = path { - path.push(NATIVE_PATH_ENV_SEPARATOR); - if let Some(old_pathvar) = ctx.scope.get_env(&var) { - path.push_str(&old_pathvar); - ctx.scope.add_env_var(&var.item, path); - Ok(OutputStream::empty()) - } else { - Err(ShellError::unexpected(&format!( - "Variable {} not set", - &var.item - ))) - } - } else { - Err(ShellError::labeled_error( - "Invalid path.", - "cannot convert to string", - path_to_add.tag, - )) - } -} diff --git a/crates/nu-command/src/commands/pathvar/append.rs b/crates/nu-command/src/commands/pathvar/append.rs deleted file mode 100644 index 855cb179ca..0000000000 --- a/crates/nu-command/src/commands/pathvar/append.rs +++ /dev/null @@ -1,71 +0,0 @@ -use super::get_var; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape}; -use nu_source::Tagged; -use nu_test_support::NATIVE_PATH_ENV_SEPARATOR; -use std::path::PathBuf; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "pathvar append" - } - - fn signature(&self) -> Signature { - Signature::build("pathvar append") - .required("path", SyntaxShape::FilePath, "path to append") - .named( - "var", - SyntaxShape::String, - "Use a different variable than PATH", - Some('v'), - ) - } - - fn usage(&self) -> &str { - "Add a path to the end of the pathvar" - } - - fn run(&self, args: CommandArgs) -> Result { - add(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Append /bin to the pathvar", - example: "pathvar append /bin", - result: None, - }] - } -} - -pub fn add(args: CommandArgs) -> Result { - let ctx = &args.context; - - let var = get_var(&args)?; - let path_to_append_arg: Tagged = args.req(0)?; - let path_to_append = path_to_append_arg.item.into_os_string().into_string(); - - if let Ok(path) = path_to_append { - if let Some(mut pathvar) = ctx.scope.get_env(&var) { - pathvar.push(NATIVE_PATH_ENV_SEPARATOR); - pathvar.push_str(&path); - ctx.scope.add_env_var(&var.item, pathvar); - Ok(OutputStream::empty()) - } else { - Err(ShellError::unexpected(&format!( - "Variable {} not set", - &var.item - ))) - } - } else { - Err(ShellError::labeled_error( - "Invalid path.", - "cannot convert to string", - path_to_append_arg.tag, - )) - } -} diff --git a/crates/nu-command/src/commands/pathvar/command.rs b/crates/nu-command/src/commands/pathvar/command.rs deleted file mode 100644 index 74d84eff48..0000000000 --- a/crates/nu-command/src/commands/pathvar/command.rs +++ /dev/null @@ -1,75 +0,0 @@ -use super::get_var; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, Value}; -use nu_test_support::NATIVE_PATH_ENV_SEPARATOR; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "pathvar" - } - - fn signature(&self) -> Signature { - Signature::build("pathvar").named( - "var", - SyntaxShape::String, - "Use a different variable than PATH", - Some('v'), - ) - } - - fn usage(&self) -> &str { - r#"Manipulate the PATH variable (pathvar) or a different variable following the -same rules."# - } - - fn run(&self, args: CommandArgs) -> Result { - get_pathvar(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Display the current session's pathvar", - example: "pathvar", - result: None, - }, - Example { - description: "Display the current session's LD_LIBRARY_PATH", - example: "pathvar -v LD_LIBRARY_PATH", - result: None, - }, - Example { - description: "Add /usr/bin to the pathvar", - example: "pathvar add /usr/bin", - result: None, - }, - Example { - description: "Remove the 3rd path in the pathvar", - example: "pathvar remove 2", - result: None, - }, - ] - } -} - -pub fn get_pathvar(args: CommandArgs) -> Result { - let var = get_var(&args)?; - - if let Some(pathvar) = args.context.scope.get_env(&var) { - let pathvar: Vec = pathvar - .split(NATIVE_PATH_ENV_SEPARATOR) - .map(Value::from) - .collect(); - - Ok(OutputStream::from(pathvar)) - } else { - Err(ShellError::unexpected(&format!( - "Variable {} not set", - &var.item - ))) - } -} diff --git a/crates/nu-command/src/commands/pathvar/mod.rs b/crates/nu-command/src/commands/pathvar/mod.rs deleted file mode 100644 index 9825ccefdb..0000000000 --- a/crates/nu-command/src/commands/pathvar/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -pub mod add; -pub mod append; -pub mod command; -pub mod remove; -pub mod reset; -pub mod save; - -pub use add::SubCommand as PathvarAdd; -pub use append::SubCommand as PathvarAppend; -pub use command::Command as Pathvar; -pub use remove::SubCommand as PathvarRemove; -pub use reset::SubCommand as PathvarReset; -pub use save::SubCommand as PathvarSave; - -use nu_engine::CommandArgs; -use nu_errors::ShellError; -use nu_source::{Tagged, TaggedItem}; -use nu_test_support::NATIVE_PATH_ENV_VAR; - -fn get_var(args: &CommandArgs) -> Result, ShellError> { - Ok(args - .get_flag("var")? - .unwrap_or_else(|| String::from(NATIVE_PATH_ENV_VAR)) - .tagged_unknown()) -} diff --git a/crates/nu-command/src/commands/pathvar/remove.rs b/crates/nu-command/src/commands/pathvar/remove.rs deleted file mode 100644 index 035004843b..0000000000 --- a/crates/nu-command/src/commands/pathvar/remove.rs +++ /dev/null @@ -1,79 +0,0 @@ -use super::get_var; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape}; -use nu_source::Tagged; -use nu_test_support::NATIVE_PATH_ENV_SEPARATOR; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "pathvar remove" - } - - fn signature(&self) -> Signature { - Signature::build("pathvar remove") - .required( - "index", - SyntaxShape::Int, - "index of the path to remove (starting at 0)", - ) - .named( - "var", - SyntaxShape::String, - "Use a different variable than PATH", - Some('v'), - ) - } - - fn usage(&self) -> &str { - "Remove a path from the pathvar" - } - - fn run(&self, args: CommandArgs) -> Result { - remove(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Remove the second path from the pathvar", - example: "pathvar remove 1", - result: None, - }] - } -} - -pub fn remove(args: CommandArgs) -> Result { - let ctx = &args.context; - - let var = get_var(&args)?; - let index_to_remove_arg: Tagged = args.req(0)?; - let index_to_remove = index_to_remove_arg.item as usize; - - if let Some(old_pathvar) = ctx.scope.get_env(&var) { - let mut paths: Vec<&str> = old_pathvar.split(NATIVE_PATH_ENV_SEPARATOR).collect(); - - if index_to_remove >= paths.len() { - return Err(ShellError::labeled_error( - "Index out of bounds", - format!("the index must be between 0 and {}", paths.len() - 1), - index_to_remove_arg.tag, - )); - } - - paths.remove(index_to_remove); - ctx.scope.add_env_var( - &var.item, - paths.join(&NATIVE_PATH_ENV_SEPARATOR.to_string()), - ); - - Ok(OutputStream::empty()) - } else { - Err(ShellError::unexpected(&format!( - "Variable {} not set", - &var.item - ))) - } -} diff --git a/crates/nu-command/src/commands/pathvar/reset.rs b/crates/nu-command/src/commands/pathvar/reset.rs deleted file mode 100644 index e8038e13ff..0000000000 --- a/crates/nu-command/src/commands/pathvar/reset.rs +++ /dev/null @@ -1,63 +0,0 @@ -use super::get_var; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue}; -use nu_test_support::NATIVE_PATH_ENV_SEPARATOR; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "pathvar reset" - } - - fn signature(&self) -> Signature { - Signature::build("pathvar reset").named( - "var", - SyntaxShape::String, - "Use a different variable than PATH", - Some('v'), - ) - } - - fn usage(&self) -> &str { - "Reset the pathvar to the one specified in the config" - } - - fn run(&self, args: CommandArgs) -> Result { - reset(args) - } -} - -pub fn reset(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - let ctx = &args.context; - - let var = get_var(&args)?; - let var_lower = var.clone().map(|s| s.to_lowercase()); - - if let Some(global_cfg) = &mut ctx.configs().lock().global_config { - let default_pathvar = global_cfg.vars.get(&var_lower.item); - if let Some(pathvar) = default_pathvar { - if let UntaggedValue::Table(paths) = &pathvar.value { - let pathvar_str = paths - .iter() - .map(|x| x.as_string().expect("Error converting path to string")) - .join(&NATIVE_PATH_ENV_SEPARATOR.to_string()); - ctx.scope.add_env_var(&var.item, pathvar_str); - } - } else { - return Err(ShellError::untagged_runtime_error(&format!( - "Default {} is not set in config file.", - &var_lower.item - ))); - } - Ok(OutputStream::empty()) - } else { - let value = UntaggedValue::Error(crate::commands::config::err_no_global_cfg_present()) - .into_value(name); - - Ok(OutputStream::one(value)) - } -} diff --git a/crates/nu-command/src/commands/pathvar/save.rs b/crates/nu-command/src/commands/pathvar/save.rs deleted file mode 100644 index 5684019a2e..0000000000 --- a/crates/nu-command/src/commands/pathvar/save.rs +++ /dev/null @@ -1,69 +0,0 @@ -use super::get_var; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value}; -use nu_test_support::NATIVE_PATH_ENV_SEPARATOR; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "pathvar save" - } - - fn signature(&self) -> Signature { - Signature::build("pathvar save").named( - "var", - SyntaxShape::String, - "Use a different variable than PATH", - Some('v'), - ) - } - - fn usage(&self) -> &str { - "Save the current pathvar to the config file" - } - - fn run(&self, args: CommandArgs) -> Result { - save(args) - } -} -pub fn save(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - let ctx = &args.context; - - let var = get_var(&args)?; - let var_lower = var.clone().map(|s| s.to_lowercase()); - - if let Some(global_cfg) = &mut ctx.configs().lock().global_config { - if let Some(pathvar) = ctx.scope.get_env(&var) { - let paths: Vec = pathvar - .split(NATIVE_PATH_ENV_SEPARATOR) - .map(Value::from) - .collect(); - - let span_range = 0..paths.len(); - let row = Value::new( - UntaggedValue::Table(paths), - Tag::from(Span::from(&span_range)), - ); - - global_cfg.vars.insert(var_lower.item, row); - global_cfg.write()?; - ctx.reload_config(global_cfg)?; - - Ok(OutputStream::empty()) - } else { - Err(ShellError::unexpected(&format!( - "Variable {} not set", - &var.item - ))) - } - } else { - let value = UntaggedValue::Error(crate::commands::config::err_no_global_cfg_present()) - .into_value(name); - - Ok(OutputStream::one(value)) - } -} diff --git a/crates/nu-command/src/commands/platform/ansi/command.rs b/crates/nu-command/src/commands/platform/ansi/command.rs deleted file mode 100644 index 3c1e2ceef8..0000000000 --- a/crates/nu-command/src/commands/platform/ansi/command.rs +++ /dev/null @@ -1,362 +0,0 @@ -use crate::prelude::*; -use nu_ansi_term::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tagged; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "ansi" - } - - fn signature(&self) -> Signature { - Signature::build("ansi") - .optional( - "code", - SyntaxShape::Any, - "the name of the code to use like 'green' or 'reset' to reset the color", - ) - .named( - "escape", // \x1b[ - SyntaxShape::Any, - "escape sequence without the escape character(s)", - Some('e'), - ) - .named( - "osc", // \x1b] - SyntaxShape::Any, - "operating system command (ocs) escape sequence without the escape character(s)", - Some('o'), - ) - } - - fn usage(&self) -> &str { - "Output ANSI codes to change color." - } - - fn extra_usage(&self) -> &str { - r#"For escape sequences: -Escape: '\x1b[' is not required for --escape parameter -Format: #(;#)m -Example: 1;31m for bold red or 2;37;41m for dimmed white fg with red bg -There can be multiple text formatting sequence numbers -separated by a ; and ending with an m where the # is of the -following values: - attributes - 0 reset / normal display - 1 bold or increased intensity - 2 faint or decreased intensity - 3 italic on (non-mono font) - 4 underline on - 5 slow blink on - 6 fast blink on - 7 reverse video on - 8 nondisplayed (invisible) on - 9 strike-through on - - foreground/bright colors background/bright colors - 30/90 black 40/100 black - 31/91 red 41/101 red - 32/92 green 42/102 green - 33/93 yellow 43/103 yellow - 34/94 blue 44/104 blue - 35/95 magenta 45/105 magenta - 36/96 cyan 46/106 cyan - 37/97 white 47/107 white - https://en.wikipedia.org/wiki/ANSI_escape_code - -OSC: '\x1b]' is not required for --osc parameter -Example: echo [(ansi -o '0') 'some title' (char bel)] | str collect -Format: # - 0 Set window title and icon name - 1 Set icon name - 2 Set window title - 4 Set/read color palette - 9 iTerm2 Grown notifications - 10 Set foreground color (x11 color spec) - 11 Set background color (x11 color spec) - ... others"# - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Change color to green", - example: r#"ansi green"#, - result: Some(vec![Value::from("\u{1b}[32m")]), - }, - Example { - description: "Reset the color", - example: r#"ansi reset"#, - result: Some(vec![Value::from("\u{1b}[0m")]), - }, - Example { - description: - "Use ansi to color text (rb = red bold, gb = green bold, pb = purple bold)", - example: r#"echo [(ansi rb) Hello " " (ansi gb) Nu " " (ansi pb) World] | str collect"#, - result: Some(vec![Value::from( - "\u{1b}[1;31mHello \u{1b}[1;32mNu \u{1b}[1;35mWorld", - )]), - }, - Example { - description: - "Use ansi to color text (rb = red bold, gb = green bold, pb = purple bold)", - example: r#"echo [(ansi -e '3;93;41m') Hello (ansi reset) " " (ansi gb) Nu " " (ansi pb) World] | str collect"#, - result: Some(vec![Value::from( - "\u{1b}[3;93;41mHello\u{1b}[0m \u{1b}[1;32mNu \u{1b}[1;35mWorld", - )]), - }, - ] - } - - fn run(&self, args: CommandArgs) -> Result { - let code: Option> = args.opt(0)?; - let escape: Option> = args.get_flag("escape")?; - let osc: Option> = args.get_flag("osc")?; - - if let Some(e) = escape { - let esc_vec: Vec = e.item.chars().collect(); - if esc_vec[0] == '\\' { - return Err(ShellError::labeled_error( - "no need for escape characters", - "no need for escape characters", - e.tag(), - )); - } - let output = format!("\x1b[{}", e.item); - return Ok(OutputStream::one( - UntaggedValue::string(output).into_value(e.tag()), - )); - } - - if let Some(o) = osc { - let osc_vec: Vec = o.item.chars().collect(); - if osc_vec[0] == '\\' { - return Err(ShellError::labeled_error( - "no need for escape characters", - "no need for escape characters", - o.tag(), - )); - } - - //Operating system command aka osc ESC ] <- note the right brace, not left brace for osc - // OCS's need to end with a bell '\x07' char - let output = format!("\x1b]{};", o.item); - return Ok(OutputStream::one( - UntaggedValue::string(output).into_value(o.tag()), - )); - } - - if let Some(code) = code { - let ansi_code = str_to_ansi(&code.item); - - if let Some(output) = ansi_code { - Ok(OutputStream::one( - UntaggedValue::string(output).into_value(code.tag()), - )) - } else { - Err(ShellError::labeled_error( - "Unknown ansi code", - "unknown ansi code", - code.tag(), - )) - } - } else { - Err(ShellError::labeled_error( - "Expected ansi code", - "expect ansi code", - &args.call_info.name_tag, - )) - } - } -} - -pub fn str_to_ansi(s: &str) -> Option { - match s { - "g" | "green" => Some(Color::Green.prefix().to_string()), - "gb" | "green_bold" => Some(Color::Green.bold().prefix().to_string()), - "gu" | "green_underline" => Some(Color::Green.underline().prefix().to_string()), - "gi" | "green_italic" => Some(Color::Green.italic().prefix().to_string()), - "gd" | "green_dimmed" => Some(Color::Green.dimmed().prefix().to_string()), - "gr" | "green_reverse" => Some(Color::Green.reverse().prefix().to_string()), - - "lg" | "light_green" => Some(Color::LightGreen.prefix().to_string()), - "lgb" | "light_green_bold" => Some(Color::LightGreen.bold().prefix().to_string()), - "lgu" | "light_green_underline" => Some(Color::LightGreen.underline().prefix().to_string()), - "lgi" | "light_green_italic" => Some(Color::LightGreen.italic().prefix().to_string()), - "lgd" | "light_green_dimmed" => Some(Color::LightGreen.dimmed().prefix().to_string()), - "lgr" | "light_green_reverse" => Some(Color::LightGreen.reverse().prefix().to_string()), - - "r" | "red" => Some(Color::Red.prefix().to_string()), - "rb" | "red_bold" => Some(Color::Red.bold().prefix().to_string()), - "ru" | "red_underline" => Some(Color::Red.underline().prefix().to_string()), - "ri" | "red_italic" => Some(Color::Red.italic().prefix().to_string()), - "rd" | "red_dimmed" => Some(Color::Red.dimmed().prefix().to_string()), - "rr" | "red_reverse" => Some(Color::Red.reverse().prefix().to_string()), - - "lr" | "light_red" => Some(Color::LightRed.prefix().to_string()), - "lrb" | "light_red_bold" => Some(Color::LightRed.bold().prefix().to_string()), - "lru" | "light_red_underline" => Some(Color::LightRed.underline().prefix().to_string()), - "lri" | "light_red_italic" => Some(Color::LightRed.italic().prefix().to_string()), - "lrd" | "light_red_dimmed" => Some(Color::LightRed.dimmed().prefix().to_string()), - "lrr" | "light_red_reverse" => Some(Color::LightRed.reverse().prefix().to_string()), - - "u" | "blue" => Some(Color::Blue.prefix().to_string()), - "ub" | "blue_bold" => Some(Color::Blue.bold().prefix().to_string()), - "uu" | "blue_underline" => Some(Color::Blue.underline().prefix().to_string()), - "ui" | "blue_italic" => Some(Color::Blue.italic().prefix().to_string()), - "ud" | "blue_dimmed" => Some(Color::Blue.dimmed().prefix().to_string()), - "ur" | "blue_reverse" => Some(Color::Blue.reverse().prefix().to_string()), - - "lu" | "light_blue" => Some(Color::LightBlue.prefix().to_string()), - "lub" | "light_blue_bold" => Some(Color::LightBlue.bold().prefix().to_string()), - "luu" | "light_blue_underline" => Some(Color::LightBlue.underline().prefix().to_string()), - "lui" | "light_blue_italic" => Some(Color::LightBlue.italic().prefix().to_string()), - "lud" | "light_blue_dimmed" => Some(Color::LightBlue.dimmed().prefix().to_string()), - "lur" | "light_blue_reverse" => Some(Color::LightBlue.reverse().prefix().to_string()), - - "b" | "black" => Some(Color::Black.prefix().to_string()), - "bb" | "black_bold" => Some(Color::Black.bold().prefix().to_string()), - "bu" | "black_underline" => Some(Color::Black.underline().prefix().to_string()), - "bi" | "black_italic" => Some(Color::Black.italic().prefix().to_string()), - "bd" | "black_dimmed" => Some(Color::Black.dimmed().prefix().to_string()), - "br" | "black_reverse" => Some(Color::Black.reverse().prefix().to_string()), - - "ligr" | "light_gray" => Some(Color::LightGray.prefix().to_string()), - "ligrb" | "light_gray_bold" => Some(Color::LightGray.bold().prefix().to_string()), - "ligru" | "light_gray_underline" => Some(Color::LightGray.underline().prefix().to_string()), - "ligri" | "light_gray_italic" => Some(Color::LightGray.italic().prefix().to_string()), - "ligrd" | "light_gray_dimmed" => Some(Color::LightGray.dimmed().prefix().to_string()), - "ligrr" | "light_gray_reverse" => Some(Color::LightGray.reverse().prefix().to_string()), - - "y" | "yellow" => Some(Color::Yellow.prefix().to_string()), - "yb" | "yellow_bold" => Some(Color::Yellow.bold().prefix().to_string()), - "yu" | "yellow_underline" => Some(Color::Yellow.underline().prefix().to_string()), - "yi" | "yellow_italic" => Some(Color::Yellow.italic().prefix().to_string()), - "yd" | "yellow_dimmed" => Some(Color::Yellow.dimmed().prefix().to_string()), - "yr" | "yellow_reverse" => Some(Color::Yellow.reverse().prefix().to_string()), - - "ly" | "light_yellow" => Some(Color::LightYellow.prefix().to_string()), - "lyb" | "light_yellow_bold" => Some(Color::LightYellow.bold().prefix().to_string()), - "lyu" | "light_yellow_underline" => { - Some(Color::LightYellow.underline().prefix().to_string()) - } - "lyi" | "light_yellow_italic" => Some(Color::LightYellow.italic().prefix().to_string()), - "lyd" | "light_yellow_dimmed" => Some(Color::LightYellow.dimmed().prefix().to_string()), - "lyr" | "light_yellow_reverse" => Some(Color::LightYellow.reverse().prefix().to_string()), - - "p" | "purple" => Some(Color::Purple.prefix().to_string()), - "pb" | "purple_bold" => Some(Color::Purple.bold().prefix().to_string()), - "pu" | "purple_underline" => Some(Color::Purple.underline().prefix().to_string()), - "pi" | "purple_italic" => Some(Color::Purple.italic().prefix().to_string()), - "pd" | "purple_dimmed" => Some(Color::Purple.dimmed().prefix().to_string()), - "pr" | "purple_reverse" => Some(Color::Purple.reverse().prefix().to_string()), - - "lp" | "light_purple" => Some(Color::LightPurple.prefix().to_string()), - "lpb" | "light_purple_bold" => Some(Color::LightPurple.bold().prefix().to_string()), - "lpu" | "light_purple_underline" => { - Some(Color::LightPurple.underline().prefix().to_string()) - } - "lpi" | "light_purple_italic" => Some(Color::LightPurple.italic().prefix().to_string()), - "lpd" | "light_purple_dimmed" => Some(Color::LightPurple.dimmed().prefix().to_string()), - "lpr" | "light_purple_reverse" => Some(Color::LightPurple.reverse().prefix().to_string()), - - "c" | "cyan" => Some(Color::Cyan.prefix().to_string()), - "cb" | "cyan_bold" => Some(Color::Cyan.bold().prefix().to_string()), - "cu" | "cyan_underline" => Some(Color::Cyan.underline().prefix().to_string()), - "ci" | "cyan_italic" => Some(Color::Cyan.italic().prefix().to_string()), - "cd" | "cyan_dimmed" => Some(Color::Cyan.dimmed().prefix().to_string()), - "cr" | "cyan_reverse" => Some(Color::Cyan.reverse().prefix().to_string()), - - "lc" | "light_cyan" => Some(Color::LightCyan.prefix().to_string()), - "lcb" | "light_cyan_bold" => Some(Color::LightCyan.bold().prefix().to_string()), - "lcu" | "light_cyan_underline" => Some(Color::LightCyan.underline().prefix().to_string()), - "lci" | "light_cyan_italic" => Some(Color::LightCyan.italic().prefix().to_string()), - "lcd" | "light_cyan_dimmed" => Some(Color::LightCyan.dimmed().prefix().to_string()), - "lcr" | "light_cyan_reverse" => Some(Color::LightCyan.reverse().prefix().to_string()), - - "w" | "white" => Some(Color::White.prefix().to_string()), - "wb" | "white_bold" => Some(Color::White.bold().prefix().to_string()), - "wu" | "white_underline" => Some(Color::White.underline().prefix().to_string()), - "wi" | "white_italic" => Some(Color::White.italic().prefix().to_string()), - "wd" | "white_dimmed" => Some(Color::White.dimmed().prefix().to_string()), - "wr" | "white_reverse" => Some(Color::White.reverse().prefix().to_string()), - - "dgr" | "dark_gray" => Some(Color::DarkGray.prefix().to_string()), - "dgrb" | "dark_gray_bold" => Some(Color::DarkGray.bold().prefix().to_string()), - "dgru" | "dark_gray_underline" => Some(Color::DarkGray.underline().prefix().to_string()), - "dgri" | "dark_gray_italic" => Some(Color::DarkGray.italic().prefix().to_string()), - "dgrd" | "dark_gray_dimmed" => Some(Color::DarkGray.dimmed().prefix().to_string()), - "dgrr" | "dark_gray_reverse" => Some(Color::DarkGray.reverse().prefix().to_string()), - - "reset" => Some("\x1b[0m".to_owned()), - - // Reference for ansi codes https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797 - // Another good reference http://ascii-table.com/ansi-escape-sequences.php - - // For setting title like `echo [(char title) (pwd) (char bel)] | str collect` - "title" => Some("\x1b]2;".to_string()), // ESC]2; xterm sets window title using OSC syntax escapes - - // Ansi Erase Sequences - "clear_screen" => Some("\x1b[J".to_string()), // clears the screen - "clear_screen_from_cursor_to_end" => Some("\x1b[0J".to_string()), // clears from cursor until end of screen - "clear_screen_from_cursor_to_beginning" => Some("\x1b[1J".to_string()), // clears from cursor to beginning of screen - "cls" | "clear_entire_screen" => Some("\x1b[2J".to_string()), // clears the entire screen - "erase_line" => Some("\x1b[K".to_string()), // clears the current line - "erase_line_from_cursor_to_end" => Some("\x1b[0K".to_string()), // clears from cursor to end of line - "erase_line_from_cursor_to_beginning" => Some("\x1b[1K".to_string()), // clears from cursor to start of line - "erase_entire_line" => Some("\x1b[2K".to_string()), // clears entire line - - // Turn on/off cursor - "cursor_off" => Some("\x1b[?25l".to_string()), - "cursor_on" => Some("\x1b[?25h".to_string()), - - // Turn on/off blinking - "cursor_blink_off" => Some("\x1b[?12l".to_string()), - "cursor_blink_on" => Some("\x1b[?12h".to_string()), - - // Cursor position in ESC [ ;R where r = row and c = column - "cursor_position" => Some("\x1b[6n".to_string()), - - // Report Terminal Identity - "identity" => Some("\x1b[0c".to_string()), - - // Ansi escape only - CSI command - "csi" | "escape" | "escape_left" => Some("\x1b[".to_string()), - // OSC escape (Operating system command) - "osc" | "escape_right" => Some("\x1b]".to_string()), - // OSC string terminator - "string_terminator" | "st" | "str_term" => Some("\x1b\\".to_string()), - - // Ansi Rgb - Needs to be 32;2;r;g;b or 48;2;r;g;b - // assuming the rgb will be passed via command and no here - "rgb_fg" => Some("\x1b[38;2;".to_string()), - "rgb_bg" => Some("\x1b[48;2;".to_string()), - - // Ansi color index - Needs 38;5;idx or 48;5;idx where idx = 0 to 255 - "idx_fg" | "color_idx_fg" => Some("\x1b[38;5;".to_string()), - "idx_bg" | "color_idx_bg" => Some("\x1b[48;5;".to_string()), - - // Returns terminal size like "[;R" where r is rows and c is columns - // This should work assuming your terminal is not greater than 999x999 - "size" => Some("\x1b[s\x1b[999;999H\x1b[6n\x1b[u".to_string()), - - _ => None, - } -} - -#[cfg(test)] -mod tests { - use super::Command; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Command {}) - } -} diff --git a/crates/nu-command/src/commands/platform/ansi/gradient.rs b/crates/nu-command/src/commands/platform/ansi/gradient.rs deleted file mode 100644 index d8433ca8f0..0000000000 --- a/crates/nu-command/src/commands/platform/ansi/gradient.rs +++ /dev/null @@ -1,326 +0,0 @@ -use crate::prelude::*; -use nu_ansi_term::{build_all_gradient_text, gradient::TargetGround, Gradient, Rgb}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::ShellTypeName; -use nu_protocol::{Primitive, Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tag; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "ansi gradient" - } - - fn signature(&self) -> Signature { - Signature::build("ansi gradient") - .named( - "fgstart", - SyntaxShape::String, - "foreground gradient start color in hex (0x123456)", - Some('a'), - ) - .named( - "fgend", - SyntaxShape::String, - "foreground gradient end color in hex", - Some('b'), - ) - .named( - "bgstart", - SyntaxShape::String, - "background gradient start color in hex", - Some('c'), - ) - .named( - "bgend", - SyntaxShape::String, - "background gradient end color in hex", - Some('d'), - ) - .rest( - "rest", - SyntaxShape::ColumnPath, - "optionally, draw gradients using text from column paths", - ) - } - - fn usage(&self) -> &str { - "draw text with a provided start and end code making a gradient" - } - - fn run(&self, args: CommandArgs) -> Result { - operate(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "draw text in a gradient with foreground start and end colors", - example: - "echo 'Hello, Nushell! This is a gradient.' | ansi gradient --fgstart 0x40c9ff --fgend 0xe81cff", - result: None, - }, - Example { - description: "draw text in a gradient with foreground start and end colors and background start and end colors", - example: - "echo 'Hello, Nushell! This is a gradient.' | ansi gradient --fgstart 0x40c9ff --fgend 0xe81cff --bgstart 0xe81cff --bgend 0x40c9ff", - result: None, - }, - Example { - description: "draw text in a gradient by specifying foreground start color - end color is assumed to be black", - example: - "echo 'Hello, Nushell! This is a gradient.' | ansi gradient --fgstart 0x40c9ff", - result: None, - }, - Example { - description: "draw text in a gradient by specifying foreground end color - start color is assumed to be black", - example: - "echo 'Hello, Nushell! This is a gradient.' | ansi gradient --fgend 0xe81cff", - result: None, - }, - ] - } -} - -fn operate(args: CommandArgs) -> Result { - let fgstart: Option = args.get_flag("fgstart")?; - let fgend: Option = args.get_flag("fgend")?; - let bgstart: Option = args.get_flag("bgstart")?; - let bgend: Option = args.get_flag("bgend")?; - let column_paths: Vec<_> = args.rest(0)?; - - let fgs_hex = fgstart.map(|color| Rgb::from_hex_string(color.convert_to_string())); - let fge_hex = fgend.map(|color| Rgb::from_hex_string(color.convert_to_string())); - let bgs_hex = bgstart.map(|color| Rgb::from_hex_string(color.convert_to_string())); - let bge_hex = bgend.map(|color| Rgb::from_hex_string(color.convert_to_string())); - - let result: Vec = args - .input - .map(move |v| { - if column_paths.is_empty() { - action(&v, v.tag(), fgs_hex, fge_hex, bgs_hex, bge_hex) - } else { - let mut ret = v; - - for path in &column_paths { - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| { - action(old, old.tag(), fgs_hex, fge_hex, bgs_hex, bge_hex) - }), - )?; - } - - Ok(ret) - } - }) - .collect::, _>>()?; - - Ok(OutputStream::from_stream(result.into_iter())) -} - -fn action( - input: &Value, - tag: impl Into, - fg_start: Option, - fg_end: Option, - bg_start: Option, - bg_end: Option, -) -> Result { - let tag = tag.into(); - - match &input.value { - UntaggedValue::Primitive(Primitive::String(astring)) => { - match (fg_start, fg_end, bg_start, bg_end) { - (None, None, None, None) => { - // Error - no colors - Err(ShellError::labeled_error( - "please supply color parameters", - "please supply foreground and/or background color parameters", - Tag::unknown(), - )) - } - (None, None, None, Some(bg_end)) => { - // Error - missing bg_start, so assume black - let bg_start = Rgb::new(0, 0, 0); - let gradient = Gradient::new(bg_start, bg_end); - let gradient_string = gradient.build(astring, TargetGround::Background); - Ok(UntaggedValue::string(gradient_string).into_value(tag)) - } - (None, None, Some(bg_start), None) => { - // Error - missing bg_end, so assume black - let bg_end = Rgb::new(0, 0, 0); - let gradient = Gradient::new(bg_start, bg_end); - let gradient_string = gradient.build(astring, TargetGround::Background); - Ok(UntaggedValue::string(gradient_string).into_value(tag)) - } - (None, None, Some(bg_start), Some(bg_end)) => { - // Background Only - let gradient = Gradient::new(bg_start, bg_end); - let gradient_string = gradient.build(astring, TargetGround::Background); - Ok(UntaggedValue::string(gradient_string).into_value(tag)) - } - (None, Some(fg_end), None, None) => { - // Error - missing fg_start, so assume black - let fg_start = Rgb::new(0, 0, 0); - let gradient = Gradient::new(fg_start, fg_end); - let gradient_string = gradient.build(astring, TargetGround::Foreground); - Ok(UntaggedValue::string(gradient_string).into_value(tag)) - } - (None, Some(fg_end), None, Some(bg_end)) => { - // missing fg_start and bg_start, so assume black - let fg_start = Rgb::new(0, 0, 0); - let bg_start = Rgb::new(0, 0, 0); - let fg_gradient = Gradient::new(fg_start, fg_end); - let bg_gradient = Gradient::new(bg_start, bg_end); - let gradient_string = - build_all_gradient_text(astring, fg_gradient, bg_gradient); - Ok(UntaggedValue::string(gradient_string).into_value(tag)) - } - (None, Some(fg_end), Some(bg_start), None) => { - // Error - missing fg_start and bg_end - let fg_start = Rgb::new(0, 0, 0); - let bg_end = Rgb::new(0, 0, 0); - let fg_gradient = Gradient::new(fg_start, fg_end); - let bg_gradient = Gradient::new(bg_start, bg_end); - let gradient_string = - build_all_gradient_text(astring, fg_gradient, bg_gradient); - Ok(UntaggedValue::string(gradient_string).into_value(tag)) - } - (None, Some(fg_end), Some(bg_start), Some(bg_end)) => { - // Error - missing fg_start, so assume black - let fg_start = Rgb::new(0, 0, 0); - let fg_gradient = Gradient::new(fg_start, fg_end); - let bg_gradient = Gradient::new(bg_start, bg_end); - let gradient_string = - build_all_gradient_text(astring, fg_gradient, bg_gradient); - Ok(UntaggedValue::string(gradient_string).into_value(tag)) - } - (Some(fg_start), None, None, None) => { - // Error - missing fg_end, so assume black - let fg_end = Rgb::new(0, 0, 0); - let gradient = Gradient::new(fg_start, fg_end); - let gradient_string = gradient.build(astring, TargetGround::Foreground); - Ok(UntaggedValue::string(gradient_string).into_value(tag)) - } - (Some(fg_start), None, None, Some(bg_end)) => { - // Error - missing fg_end, bg_start, so assume black - let fg_end = Rgb::new(0, 0, 0); - let bg_start = Rgb::new(0, 0, 0); - let fg_gradient = Gradient::new(fg_start, fg_end); - let bg_gradient = Gradient::new(bg_start, bg_end); - let gradient_string = - build_all_gradient_text(astring, fg_gradient, bg_gradient); - Ok(UntaggedValue::string(gradient_string).into_value(tag)) - } - (Some(fg_start), None, Some(bg_start), None) => { - // Error - missing fg_end, bg_end, so assume black - let fg_end = Rgb::new(0, 0, 0); - let bg_end = Rgb::new(0, 0, 0); - let fg_gradient = Gradient::new(fg_start, fg_end); - let bg_gradient = Gradient::new(bg_start, bg_end); - let gradient_string = - build_all_gradient_text(astring, fg_gradient, bg_gradient); - Ok(UntaggedValue::string(gradient_string).into_value(tag)) - } - (Some(fg_start), None, Some(bg_start), Some(bg_end)) => { - // Error - missing fg_end, so assume black - let fg_end = Rgb::new(0, 0, 0); - let fg_gradient = Gradient::new(fg_start, fg_end); - let bg_gradient = Gradient::new(bg_start, bg_end); - let gradient_string = - build_all_gradient_text(astring, fg_gradient, bg_gradient); - Ok(UntaggedValue::string(gradient_string).into_value(tag)) - } - (Some(fg_start), Some(fg_end), None, None) => { - // Foreground Only - let gradient = Gradient::new(fg_start, fg_end); - let gradient_string = gradient.build(astring, TargetGround::Foreground); - Ok(UntaggedValue::string(gradient_string).into_value(tag)) - } - (Some(fg_start), Some(fg_end), None, Some(bg_end)) => { - // Error - missing bg_start, so assume black - let bg_start = Rgb::new(0, 0, 0); - let fg_gradient = Gradient::new(fg_start, fg_end); - let bg_gradient = Gradient::new(bg_start, bg_end); - let gradient_string = - build_all_gradient_text(astring, fg_gradient, bg_gradient); - Ok(UntaggedValue::string(gradient_string).into_value(tag)) - } - (Some(fg_start), Some(fg_end), Some(bg_start), None) => { - // Error - missing bg_end, so assume black - let bg_end = Rgb::new(0, 0, 0); - let fg_gradient = Gradient::new(fg_start, fg_end); - let bg_gradient = Gradient::new(bg_start, bg_end); - let gradient_string = - build_all_gradient_text(astring, fg_gradient, bg_gradient); - Ok(UntaggedValue::string(gradient_string).into_value(tag)) - } - (Some(fg_start), Some(fg_end), Some(bg_start), Some(bg_end)) => { - // Foreground and Background Gradient - let fg_gradient = Gradient::new(fg_start, fg_end); - let bg_gradient = Gradient::new(bg_start, bg_end); - let gradient_string = - build_all_gradient_text(astring, fg_gradient, bg_gradient); - Ok(UntaggedValue::string(gradient_string).into_value(tag)) - } - } - } - other => { - let got = format!("got {}", other.type_name()); - - Err(ShellError::labeled_error( - "value is not string", - got, - tag.span, - )) - } - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::{action, SubCommand}; - use nu_ansi_term::Rgb; - use nu_protocol::UntaggedValue; - use nu_source::Tag; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } - - // #[test] - // fn test_stripping() { - // let input_string = - // UntaggedValue::string("\u{1b}[3;93;41mHello\u{1b}[0m \u{1b}[1;32mNu \u{1b}[1;35mWorld") - // .into_untagged_value(); - // let expected = UntaggedValue::string("Hello Nu World").into_untagged_value(); - - // let actual = action(&input_string, Tag::unknown()).unwrap(); - // assert_eq!(actual, expected); - // } - - #[test] - fn test_fg_gradient() { - let input_string = UntaggedValue::string("Hello, World!").into_untagged_value(); - let expected = UntaggedValue::string("\u{1b}[38;2;64;201;255mH\u{1b}[38;2;76;187;254me\u{1b}[38;2;89;174;254ml\u{1b}[38;2;102;160;254ml\u{1b}[38;2;115;147;254mo\u{1b}[38;2;128;133;254m,\u{1b}[38;2;141;120;254m \u{1b}[38;2;153;107;254mW\u{1b}[38;2;166;94;254mo\u{1b}[38;2;179;80;254mr\u{1b}[38;2;192;67;254ml\u{1b}[38;2;205;53;254md\u{1b}[38;2;218;40;254m!\u{1b}[0m").into_untagged_value(); - let fg_start = Rgb::from_hex_string("0x40c9ff".to_string()); - let fg_end = Rgb::from_hex_string("0xe81cff".to_string()); - let actual = action( - &input_string, - Tag::unknown(), - Some(fg_start), - Some(fg_end), - None, - None, - ) - .unwrap(); - assert_eq!(actual, expected); - } -} diff --git a/crates/nu-command/src/commands/platform/ansi/mod.rs b/crates/nu-command/src/commands/platform/ansi/mod.rs deleted file mode 100644 index e7bbad5b79..0000000000 --- a/crates/nu-command/src/commands/platform/ansi/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod command; -mod gradient; -mod strip; - -pub use command::Command as Ansi; -pub use gradient::SubCommand as AnsiGradient; -pub use strip::SubCommand as AnsiStrip; diff --git a/crates/nu-command/src/commands/platform/ansi/strip.rs b/crates/nu-command/src/commands/platform/ansi/strip.rs deleted file mode 100644 index 6e59c824ee..0000000000 --- a/crates/nu-command/src/commands/platform/ansi/strip.rs +++ /dev/null @@ -1,116 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::ShellTypeName; -use nu_protocol::{Primitive, Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tag; -use strip_ansi_escapes::strip; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "ansi strip" - } - - fn signature(&self) -> Signature { - Signature::build("ansi strip").rest( - "rest", - SyntaxShape::ColumnPath, - "optionally, remove ansi sequences by column paths", - ) - } - - fn usage(&self) -> &str { - "strip ansi escape sequences from string" - } - - fn run(&self, args: CommandArgs) -> Result { - operate(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "strip ansi escape sequences from string", - example: "echo [(ansi gb) 'hello' (ansi reset)] | str collect | ansi strip", - result: None, - }] - } -} - -fn operate(args: CommandArgs) -> Result { - let column_paths: Vec<_> = args.rest(0)?; - - let result: Vec = args - .input - .map(move |v| { - if column_paths.is_empty() { - action(&v, v.tag()) - } else { - let mut ret = v; - - for path in &column_paths { - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| action(old, old.tag())), - )?; - } - - Ok(ret) - } - }) - .collect::, _>>()?; - - Ok(OutputStream::from_stream(result.into_iter())) -} - -fn action(input: &Value, tag: impl Into) -> Result { - match &input.value { - UntaggedValue::Primitive(Primitive::String(astring)) => { - let stripped_string = { - if let Ok(bytes) = strip(&astring) { - String::from_utf8_lossy(&bytes).to_string() - } else { - astring.to_string() - } - }; - - Ok(UntaggedValue::string(stripped_string).into_value(tag)) - } - other => { - let got = format!("got {}", other.type_name()); - - Err(ShellError::labeled_error( - "value is not string", - got, - tag.into().span, - )) - } - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::{action, SubCommand}; - use nu_protocol::UntaggedValue; - use nu_source::Tag; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } - - #[test] - fn test_stripping() { - let input_string = - UntaggedValue::string("\u{1b}[3;93;41mHello\u{1b}[0m \u{1b}[1;32mNu \u{1b}[1;35mWorld") - .into_untagged_value(); - let expected = UntaggedValue::string("Hello Nu World").into_untagged_value(); - - let actual = action(&input_string, Tag::unknown()).unwrap(); - assert_eq!(actual, expected); - } -} diff --git a/crates/nu-command/src/commands/platform/benchmark.rs b/crates/nu-command/src/commands/platform/benchmark.rs deleted file mode 100644 index a275863721..0000000000 --- a/crates/nu-command/src/commands/platform/benchmark.rs +++ /dev/null @@ -1,225 +0,0 @@ -use crate::prelude::*; -// #[cfg(feature = "rich-benchmark")] -// use heim::cpu::time; -use nu_engine::run_block; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - hir::{ - Block, CapturedBlock, ClassifiedCommand, ExternalRedirection, Group, InternalCommand, - Pipeline, - }, - Dictionary, Signature, SyntaxShape, UntaggedValue, Value, -}; -use rand::{ - distributions::{Alphanumeric, Distribution}, - thread_rng, Rng, -}; -use std::time::Instant; - -pub struct Benchmark; - -#[derive(Debug)] -struct BenchmarkArgs { - block: CapturedBlock, - passthrough: Option, -} - -impl WholeStreamCommand for Benchmark { - fn name(&self) -> &str { - "benchmark" - } - - fn signature(&self) -> Signature { - Signature::build("benchmark") - .required( - "block", - SyntaxShape::Block, - "the block to run and benchmark", - ) - .named( - "passthrough", - SyntaxShape::Block, - "Display the benchmark results and pass through the block's output", - Some('p'), - ) - } - - fn usage(&self) -> &str { - "Runs a block and returns the time it took to execute it." - } - - fn run(&self, args: CommandArgs) -> Result { - benchmark(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Benchmarks a command within a block", - example: "benchmark { sleep 500ms }", - result: None, - }, - Example { - description: "Benchmarks a command within a block and passes its output through", - example: "echo 45 | benchmark { sleep 500ms } --passthrough {}", - result: Some(vec![UntaggedValue::int(45).into()]), - }, - ] - } -} - -fn benchmark(args: CommandArgs) -> Result { - let tag = args.call_info.args.span; - let mut context = args.context.clone(); - let scope = args.scope().clone(); - - let cmd_args = BenchmarkArgs { - block: args.req(0)?, - passthrough: args.get_flag("passthrough")?, - }; - - let env = scope.get_env_vars(); - let name = generate_free_name(&env); - - scope.add_env_var(name, generate_random_env_value()); - - let start_time = Instant::now(); - - // #[cfg(feature = "rich-benchmark")] - // let start = time(); - - context.scope.enter_scope(); - let result = run_block( - &cmd_args.block.block, - &context, - args.input, - ExternalRedirection::StdoutAndStderr, - ); - - context.scope.exit_scope(); - let output = result?.into_vec(); - - // #[cfg(feature = "rich-benchmark")] - // let end = time(); - - let end_time = Instant::now(); - context.clear_errors(); - - // return basic runtime - //#[cfg(not(feature = "rich-benchmark"))] - { - let mut indexmap = IndexMap::with_capacity(1); - - let real_time = (end_time - start_time).as_nanos() as i64; - indexmap.insert("real time".to_string(), real_time); - benchmark_output(indexmap, output, cmd_args.passthrough, &tag, &mut context) - } - // return advanced stats - // #[cfg(feature = "rich-benchmark")] - // if let (Ok(start), Ok(end)) = (start, end) { - // let mut indexmap = IndexMap::with_capacity(4); - - // let real_time = into_big_int(end_time - start_time); - // indexmap.insert("real time".to_string(), real_time); - - // let user_time = into_big_int(end.user() - start.user()); - // indexmap.insert("user time".to_string(), user_time); - - // let system_time = into_big_int(end.system() - start.system()); - // indexmap.insert("system time".to_string(), system_time); - - // let idle_time = into_big_int(end.idle() - start.idle()); - // indexmap.insert("idle time".to_string(), idle_time); - - // benchmark_output(indexmap, output, passthrough, &tag, &mut context) - // } else { - // Err(ShellError::untagged_runtime_error( - // "Could not retrieve CPU time", - // )) - // } -} - -fn benchmark_output( - indexmap: IndexMap, - block_output: Output, - passthrough: Option, - tag: T, - context: &mut EvaluationContext, -) -> Result -where - T: Into + Copy, - Output: Into, -{ - let value = UntaggedValue::Row(Dictionary::from( - indexmap - .into_iter() - .map(|(k, v)| (k, UntaggedValue::duration(v).into_value(tag))) - .collect::>(), - )) - .into_value(tag); - - if let Some(time_block) = passthrough { - let benchmark_output = InputStream::one(value); - - // add autoview for an empty block - let time_block = add_implicit_autoview(time_block.block); - - context.scope.enter_scope(); - let result = run_block( - &time_block, - context, - benchmark_output, - ExternalRedirection::StdoutAndStderr, - ); - context.scope.exit_scope(); - result?; - context.clear_errors(); - - Ok(block_output.into()) - } else { - let benchmark_output = OutputStream::one(value); - Ok(benchmark_output) - } -} - -fn add_implicit_autoview(mut block: Arc) -> Arc { - if let Some(block) = std::sync::Arc::::get_mut(&mut block) { - if block.block.is_empty() { - let group = Group::new( - vec![{ - let mut commands = Pipeline::new(block.span); - commands.push(ClassifiedCommand::Internal(InternalCommand::new( - "autoview".to_string(), - block.span, - block.span, - ))); - commands - }], - block.span, - ); - block.push(group); - } - } - block -} - -fn generate_random_env_value() -> String { - let mut thread_rng = thread_rng(); - let len = thread_rng.gen_range(1..16 * 1024); - Alphanumeric - .sample_iter(&mut thread_rng) - .take(len) - .map(char::from) - .collect() -} - -fn generate_free_name(env: &indexmap::IndexMap) -> String { - let mut thread_rng = thread_rng(); - loop { - let candidate_name = format!("NU_RANDOM_VALUE_{}", thread_rng.gen::()); - if !env.contains_key(&candidate_name) { - return candidate_name; - } - } -} diff --git a/crates/nu-command/src/commands/platform/clear.rs b/crates/nu-command/src/commands/platform/clear.rs deleted file mode 100644 index e5dde549d0..0000000000 --- a/crates/nu-command/src/commands/platform/clear.rs +++ /dev/null @@ -1,44 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::Signature; -use std::process::Command; - -pub struct Clear; - -impl WholeStreamCommand for Clear { - fn name(&self) -> &str { - "clear" - } - - fn signature(&self) -> Signature { - Signature::build("clear") - } - - fn usage(&self) -> &str { - "Clears the terminal." - } - - fn run(&self, _: CommandArgs) -> Result { - if cfg!(windows) { - Command::new("cmd") - .args(["/C", "cls"]) - .status() - .expect("failed to execute process"); - } else if cfg!(unix) { - Command::new("/bin/sh") - .args(["-c", "clear"]) - .status() - .expect("failed to execute process"); - } - Ok(InputStream::empty()) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Clear the screen", - example: "clear", - result: None, - }] - } -} diff --git a/crates/nu-command/src/commands/platform/du.rs b/crates/nu-command/src/commands/platform/du.rs deleted file mode 100644 index efc508bc60..0000000000 --- a/crates/nu-command/src/commands/platform/du.rs +++ /dev/null @@ -1,179 +0,0 @@ -use crate::prelude::*; -use glob::*; -use nu_engine::WholeStreamCommand; -use nu_engine::{DirBuilder, DirInfo, FileInfo}; -use nu_errors::ShellError; -use nu_protocol::{ReturnSuccess, Signature, SyntaxShape}; -use nu_source::Tagged; -use std::path::PathBuf; - -const NAME: &str = "du"; -const GLOB_PARAMS: MatchOptions = MatchOptions { - case_sensitive: true, - require_literal_separator: true, - require_literal_leading_dot: false, -}; - -pub struct Du; - -#[derive(Deserialize, Clone)] -pub struct DuArgs { - path: Option>, - all: bool, - deref: bool, - exclude: Option>, - #[serde(rename = "max-depth")] - max_depth: Option>, - #[serde(rename = "min-size")] - min_size: Option>, -} - -impl WholeStreamCommand for Du { - fn name(&self) -> &str { - NAME - } - - fn signature(&self) -> Signature { - Signature::build(NAME) - .optional("path", SyntaxShape::GlobPattern, "starting directory") - .switch( - "all", - "Output file sizes as well as directory sizes", - Some('a'), - ) - .switch( - "deref", - "Dereference symlinks to their targets for size", - Some('r'), - ) - .named( - "exclude", - SyntaxShape::GlobPattern, - "Exclude these file names", - Some('x'), - ) - .named( - "max-depth", - SyntaxShape::Int, - "Directory recursion limit", - Some('d'), - ) - .named( - "min-size", - SyntaxShape::Int, - "Exclude files below this size", - Some('m'), - ) - } - - fn usage(&self) -> &str { - "Find disk usage sizes of specified items." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - du(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Disk usage of the current directory", - example: "du", - result: None, - }] - } -} - -fn du(args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let ctrl_c = args.ctrl_c(); - let ctrl_c_copy = ctrl_c.clone(); - - let args = DuArgs { - path: args.opt(0)?, - all: args.has_flag("all"), - deref: args.has_flag("deref"), - exclude: args.get_flag("exclude")?, - max_depth: args.get_flag("max-depth")?, - min_size: args.get_flag("min_size")?, - }; - - let exclude = args.exclude.map_or(Ok(None), move |x| { - Pattern::new(&x.item) - .map(Some) - .map_err(|e| ShellError::labeled_error(e.msg, "glob error", x.tag.clone())) - })?; - - let include_files = args.all; - let paths = match args.path { - Some(p) => { - let p = p.item.to_str().expect("Why isn't this encoded properly?"); - glob::glob_with(p, GLOB_PARAMS) - } - None => glob::glob_with("*", GLOB_PARAMS), - } - .map_err(|e| ShellError::labeled_error(e.msg, "glob error", tag.clone()))? - .filter(move |p| { - if include_files { - true - } else { - match p { - Ok(f) if f.is_dir() => true, - Err(e) if e.path().is_dir() => true, - _ => false, - } - } - }) - .map(|v| v.map_err(glob_err_into)); - - let all = args.all; - let deref = args.deref; - let max_depth = args.max_depth.map(|f| f.item); - let min_size = args.min_size.map(|f| f.item); - - let params = DirBuilder { - tag: tag.clone(), - min: min_size, - deref, - exclude, - all, - }; - - let inp = paths; - - Ok(inp - .flat_map(move |path| match path { - Ok(p) => { - let mut output = vec![]; - if p.is_dir() { - output.push(Ok(ReturnSuccess::Value( - DirInfo::new(p, ¶ms, max_depth, ctrl_c.clone()).into(), - ))); - } else if let Ok(v) = FileInfo::new(p, deref, tag.clone()) { - output.push(Ok(ReturnSuccess::Value(v.into()))); - } - - output - } - Err(e) => vec![Err(e)], - }) - .interruptible(ctrl_c_copy) - .into_action_stream()) -} - -fn glob_err_into(e: GlobError) -> ShellError { - let e = e.into_error(); - ShellError::from(e) -} - -#[cfg(test)] -mod tests { - use super::Du; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Du {}) - } -} diff --git a/crates/nu-command/src/commands/platform/exec.rs b/crates/nu-command/src/commands/platform/exec.rs deleted file mode 100644 index 429b5498cb..0000000000 --- a/crates/nu-command/src/commands/platform/exec.rs +++ /dev/null @@ -1,95 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape}; - -#[cfg(unix)] -use nu_source::Tagged; -#[cfg(unix)] -use std::path::PathBuf; - -pub struct Exec; - -#[cfg(unix)] -pub struct ExecArgs { - pub command: Tagged, - pub rest: Vec>, -} - -impl WholeStreamCommand for Exec { - fn name(&self) -> &str { - "exec" - } - - fn signature(&self) -> Signature { - Signature::build("exec") - .required("command", SyntaxShape::FilePath, "the command to execute") - .rest( - "rest", - SyntaxShape::GlobPattern, - "any additional arguments for the command", - ) - } - - fn usage(&self) -> &str { - "Execute a command, replacing the current process." - } - - fn extra_usage(&self) -> &str { - "Currently supported only on Unix-based systems." - } - - fn run(&self, args: CommandArgs) -> Result { - exec(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Execute external 'ps aux' tool", - example: "exec ps aux", - result: None, - }, - Example { - description: "Execute 'nautilus'", - example: "exec nautilus", - result: None, - }, - ] - } -} - -#[cfg(unix)] -fn exec(args: CommandArgs) -> Result { - use std::os::unix::process::CommandExt; - use std::process::Command; - - let name = args.call_info.name_tag.clone(); - - let args = ExecArgs { - command: args.req(0)?, - rest: args.rest(1)?, - }; - - let mut command = Command::new(args.command.item); - for tagged_arg in args.rest { - command.arg(tagged_arg.item); - } - - let err = command.exec(); // this replaces our process, should not return - - Err(ShellError::labeled_error( - "Error on exec", - err.to_string(), - &name, - )) -} - -#[cfg(not(unix))] -fn exec(args: CommandArgs) -> Result { - Err(ShellError::labeled_error( - "Error on exec", - "exec is not supported on your platform", - &args.call_info.name_tag, - )) -} diff --git a/crates/nu-command/src/commands/platform/kill.rs b/crates/nu-command/src/commands/platform/kill.rs deleted file mode 100644 index 2f508a1a21..0000000000 --- a/crates/nu-command/src/commands/platform/kill.rs +++ /dev/null @@ -1,140 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape}; -use nu_source::Tagged; -use std::process::{Command, Stdio}; - -pub struct Kill; - -impl WholeStreamCommand for Kill { - fn name(&self) -> &str { - "kill" - } - - fn signature(&self) -> Signature { - let signature = Signature::build("kill") - .required( - "pid", - SyntaxShape::Int, - "process id of process that is to be killed", - ) - .rest("rest", SyntaxShape::Int, "rest of processes to kill") - .switch("force", "forcefully kill the process", Some('f')) - .switch("quiet", "won't print anything to the console", Some('q')); - - if cfg!(windows) { - return signature; - } - - signature.named( - "signal", - SyntaxShape::Int, - "signal decimal number to be sent instead of the default 15 (unsupported on Windows)", - Some('s'), - ) - } - - fn usage(&self) -> &str { - "Kill a process using the process id." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - kill(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Kill the pid using the most memory", - example: "ps | sort-by mem | last | kill $it.pid", - result: None, - }, - Example { - description: "Force kill a given pid", - example: "kill --force 12345", - result: None, - }, - Example { - description: "Send INT signal", - example: "kill -s 2 12345", - result: None, - }, - ] - } -} - -fn kill(args: CommandArgs) -> Result { - let pid: Tagged = args.req(0)?; - let rest: Vec> = args.rest(1)?; - let force: Option> = args.get_flag("force")?; - let quiet: bool = args.has_flag("quiet"); - let signal: Option> = args.get_flag("signal")?; - - let mut cmd = if cfg!(windows) { - let mut cmd = Command::new("taskkill"); - - if matches!(force, Some(Tagged { item: true, .. })) { - cmd.arg("/F"); - } - - cmd.arg("/PID"); - cmd.arg(pid.item().to_string()); - - // each pid must written as `/PID 0` otherwise - // taskkill will act as `killall` unix command - for id in &rest { - cmd.arg("/PID"); - cmd.arg(id.item().to_string()); - } - - cmd - } else { - let mut cmd = Command::new("kill"); - - if matches!(force, Some(Tagged { item: true, .. })) { - if let Some(signal_value) = signal { - return Err(ShellError::labeled_error_with_secondary( - "mixing force and signal options is not supported", - "signal option", - signal_value.tag(), - "force option", - force.expect("internal error: expected value").tag(), - )); - } - cmd.arg("-9"); - } else if let Some(signal_value) = signal { - cmd.arg(format!("-{}", signal_value.item())); - } - - cmd.arg(pid.item().to_string()); - - cmd.args(rest.iter().map(move |id| id.item().to_string())); - - cmd - }; - - // pipe everything to null - if quiet { - cmd.stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()); - } - - cmd.status().expect("failed to execute shell command"); - - Ok(ActionStream::empty()) -} - -#[cfg(test)] -mod tests { - use super::Kill; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Kill {}) - } -} diff --git a/crates/nu-command/src/commands/platform/mod.rs b/crates/nu-command/src/commands/platform/mod.rs deleted file mode 100644 index 03734919bb..0000000000 --- a/crates/nu-command/src/commands/platform/mod.rs +++ /dev/null @@ -1,23 +0,0 @@ -mod ansi; -mod benchmark; -mod clear; -mod du; -mod exec; -mod kill; -mod pwd; -mod run_external; -mod sleep; -mod termsize; -mod which_; - -pub use ansi::*; -pub use benchmark::Benchmark; -pub use clear::Clear; -pub use du::Du; -pub use exec::Exec; -pub use kill::Kill; -pub use pwd::Pwd; -pub use run_external::RunExternalCommand; -pub use sleep::Sleep; -pub use termsize::TermSize; -pub use which_::Which; diff --git a/crates/nu-command/src/commands/platform/pwd.rs b/crates/nu-command/src/commands/platform/pwd.rs deleted file mode 100644 index a9965ec938..0000000000 --- a/crates/nu-command/src/commands/platform/pwd.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::Signature; - -pub struct Pwd; - -impl WholeStreamCommand for Pwd { - fn name(&self) -> &str { - "pwd" - } - - fn signature(&self) -> Signature { - Signature::build("pwd") - } - - fn usage(&self) -> &str { - "Output the current working directory." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - pwd(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Print the current working directory", - example: "pwd", - result: None, - }] - } -} - -pub fn pwd(args: CommandArgs) -> Result { - let shell_manager = args.shell_manager(); - - shell_manager.pwd(args) -} - -#[cfg(test)] -mod tests { - use super::Pwd; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Pwd {}) - } -} diff --git a/crates/nu-command/src/commands/platform/run_external.rs b/crates/nu-command/src/commands/platform/run_external.rs deleted file mode 100644 index ce61f999bd..0000000000 --- a/crates/nu-command/src/commands/platform/run_external.rs +++ /dev/null @@ -1,181 +0,0 @@ -use crate::classified::external; -use crate::prelude::*; - -use derive_new::new; -use std::path::PathBuf; - -use nu_engine::WholeStreamCommand; -use nu_engine::{evaluate_baseline_expr, shell::CdArgs}; -use nu_errors::ShellError; -use nu_path::{canonicalize, trim_trailing_slash}; -use nu_protocol::{ - hir::{ExternalArgs, ExternalCommand, SpannedExpression}, - Primitive, UntaggedValue, -}; -use nu_protocol::{Signature, SyntaxShape}; -use nu_source::Tagged; - -#[derive(new)] -pub struct RunExternalCommand { - /// Whether or not nushell is being used in an interactive context - pub(crate) interactive: bool, -} - -fn spanned_expression_to_string( - expr: SpannedExpression, - ctx: &EvaluationContext, -) -> Result { - let value = evaluate_baseline_expr(&expr, ctx)?; - - if let UntaggedValue::Primitive(Primitive::String(s)) = value.value { - Ok(s) - } else { - Err(ShellError::labeled_error( - "Expected string for command name", - "expected string", - expr.span, - )) - } -} - -impl WholeStreamCommand for RunExternalCommand { - fn name(&self) -> &str { - "run_external" - } - - fn signature(&self) -> Signature { - Signature::build(self.name()).rest("rest", SyntaxShape::Any, "external command arguments") - } - - fn usage(&self) -> &str { - "Runs external command (not a nushell builtin)" - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Run the external echo command", - example: "run_external echo 'nushell'", - result: None, - }] - } - - fn is_private(&self) -> bool { - true - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - let positionals = args.call_info.args.positional.clone().ok_or_else(|| { - ShellError::untagged_runtime_error("positional arguments unexpectedly empty") - })?; - - let mut positionals = positionals.into_iter(); - - let external_redirection = args.call_info.args.external_redirection; - - let expr = positionals.next().ok_or_else(|| { - ShellError::untagged_runtime_error("run_external called with no arguments") - })?; - - let name = spanned_expression_to_string(expr, &args.context)?; - - let mut external_context = args.context.clone(); - - let is_interactive = self.interactive; - - let command = ExternalCommand { - name, - name_tag: args.call_info.name_tag.clone(), - args: ExternalArgs { - list: positionals.collect(), - span: args.call_info.args.span, - }, - }; - - // If we're in interactive mode, we will "auto cd". That is, instead of interpreting - // this as an external command, we will see it as a path and `cd` into it. - if is_interactive { - if let Some(path) = maybe_autocd_dir(&command, &mut external_context) { - let cd_args = CdArgs { - path: Some(Tagged { - item: PathBuf::from(path), - tag: args.call_info.name_tag.clone(), - }), - }; - - let result = external_context - .shell_manager() - .cd(cd_args, args.call_info.name_tag); - - return Ok(result?.into_action_stream()); - } - } - - let input = args.input; - let result = external::run_external_command( - command, - &mut external_context, - input, - external_redirection, - ); - - // When externals return, don't let them mess up the ansi escapes - #[cfg(windows)] - { - let _ = nu_ansi_term::enable_ansi_support(); - } - - Ok(result?.into_action_stream()) - } -} - -#[allow(unused_variables)] -fn maybe_autocd_dir(cmd: &ExternalCommand, ctx: &mut EvaluationContext) -> Option { - // We will "auto cd" if - // - the command name ends in a path separator, or - // - it's not a command on the path and no arguments were given. - let name = &cmd.name; - ctx.sync_path_to_env(); - let path_name = if name.ends_with(std::path::is_separator) - || (cmd.args.is_empty() - && PathBuf::from(name).is_dir() - && canonicalize(name).is_ok() - && !ctx.host().lock().is_external_cmd(name)) - { - Some(trim_trailing_slash(name)) - } else { - None - }; - - path_name.map(|name| { - #[cfg(windows)] - { - if name.ends_with(':') { - // This looks like a drive shortcut. We need to a) switch drives and b) go back to the previous directory we were viewing on that drive - // But first, we need to save where we are now - let current_path = ctx.shell_manager().path(); - - let split_path: Vec<_> = current_path.split(':').collect(); - if split_path.len() > 1 { - ctx.windows_drives_previous_cwd() - .lock() - .insert(split_path[0].to_string(), current_path); - } - - let name = name.to_uppercase(); - let new_drive: Vec<_> = name.split(':').collect(); - - if let Some(val) = ctx.windows_drives_previous_cwd().lock().get(new_drive[0]) { - val.to_string() - } else { - name - } - } else { - name.to_string() - } - } - #[cfg(not(windows))] - { - name.to_string() - } - }) -} diff --git a/crates/nu-command/src/commands/platform/sleep.rs b/crates/nu-command/src/commands/platform/sleep.rs deleted file mode 100644 index 9e86d1d72f..0000000000 --- a/crates/nu-command/src/commands/platform/sleep.rs +++ /dev/null @@ -1,132 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tagged; -use std::{ - sync::atomic::Ordering, - thread, - time::{Duration, Instant}, -}; - -const CTRL_C_CHECK_INTERVAL: Duration = Duration::from_millis(100); - -pub struct Sleep; - -impl WholeStreamCommand for Sleep { - fn name(&self) -> &str { - "sleep" - } - - fn signature(&self) -> Signature { - Signature::build("sleep") - .required("duration", SyntaxShape::Duration, "time to sleep") - .rest("rest", SyntaxShape::Duration, "additional time") - } - - fn usage(&self) -> &str { - "Delay for a specified amount of time." - } - - fn run(&self, args: CommandArgs) -> Result { - let ctrl_c = args.ctrl_c(); - - let duration: Tagged = args.req(0)?; - let rest: Vec> = args.rest(1)?; - - let total_dur = Duration::from_nanos(if duration.item > 0 { - duration.item as u64 - } else { - 0 - }) + rest - .iter() - .map(|val| Duration::from_nanos(if val.item > 0 { val.item as u64 } else { 0 })) - .sum::(); - - //SleepHandler::new(total_dur, ctrl_c); - // this is necessary because the following 2 commands gave different results: - // `echo | sleep 1sec` - nothing - // `sleep 1sec` - table with 0 elements - - Ok(SleepIterator::new(total_dur, ctrl_c).into_output_stream()) - - // if input.is_empty() { - // Ok(OutputStream::empty()) - // } else { - // Ok(input.into()) - // } - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Sleep for 1sec", - example: "sleep 1sec", - result: None, - }, - Example { - description: "Sleep for 3sec", - example: "sleep 1sec 1sec 1sec", - result: None, - }, - Example { - description: "Send output after 1sec", - example: "sleep 1sec; echo done", - result: Some(vec![UntaggedValue::string("done").into()]), - }, - ] - } -} - -struct SleepIterator { - total_dur: Duration, - ctrl_c: Arc, -} - -impl SleepIterator { - pub fn new(total_dur: Duration, ctrl_c: Arc) -> Self { - Self { total_dur, ctrl_c } - } -} - -impl Iterator for SleepIterator { - type Item = Value; - - fn next(&mut self) -> Option { - let start = Instant::now(); - loop { - thread::sleep(CTRL_C_CHECK_INTERVAL); - if start.elapsed() >= self.total_dur { - break; - } - - if self.ctrl_c.load(Ordering::SeqCst) { - break; - } - } - - None - } -} - -#[cfg(test)] -mod tests { - use super::Sleep; - use nu_errors::ShellError; - use std::time::Instant; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - let start = Instant::now(); - let results = test_examples(Sleep {}); - let elapsed = start.elapsed(); - println!("{:?}", elapsed); - // only examples with actual output are run - assert!(elapsed >= std::time::Duration::from_secs(1)); - assert!(elapsed < std::time::Duration::from_secs(2)); - - results - } -} diff --git a/crates/nu-command/src/commands/platform/termsize.rs b/crates/nu-command/src/commands/platform/termsize.rs deleted file mode 100644 index 482857bf3e..0000000000 --- a/crates/nu-command/src/commands/platform/termsize.rs +++ /dev/null @@ -1,80 +0,0 @@ -use crate::prelude::*; -use indexmap::IndexMap; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Dictionary, Signature, UntaggedValue}; - -pub struct TermSize; - -impl WholeStreamCommand for TermSize { - fn name(&self) -> &str { - "term size" - } - - fn signature(&self) -> Signature { - Signature::build("term size") - .switch("wide", "Report only the width of the terminal", Some('w')) - .switch("tall", "Report only the height of the terminal", Some('t')) - } - - fn usage(&self) -> &str { - "Returns the terminal size as W H" - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let wide = args.has_flag("wide"); - let tall = args.has_flag("tall"); - - let size = term_size::dimensions(); - match size { - Some((w, h)) => { - if wide && !tall { - Ok(ActionStream::one( - UntaggedValue::int(w as i64).into_value(tag), - )) - } else if !wide && tall { - Ok(ActionStream::one( - UntaggedValue::int(h as i64).into_value(tag), - )) - } else { - let mut indexmap = IndexMap::with_capacity(2); - indexmap.insert( - "width".to_string(), - UntaggedValue::int(w as i64).into_value(&tag), - ); - indexmap.insert( - "height".to_string(), - UntaggedValue::int(h as i64).into_value(&tag), - ); - let value = UntaggedValue::Row(Dictionary::from(indexmap)).into_value(&tag); - Ok(ActionStream::one(value)) - } - } - _ => Ok(ActionStream::one( - UntaggedValue::string("0 0".to_string()).into_value(tag), - )), - } - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Return the width height of the terminal", - example: "term size", - result: None, - }, - Example { - description: "Return the width of the terminal", - example: "term size -w", - result: None, - }, - Example { - description: "Return the height (t for tall) of the terminal", - example: "term size -t", - result: None, - }, - ] - } -} diff --git a/crates/nu-command/src/commands/platform/which_.rs b/crates/nu-command/src/commands/platform/which_.rs deleted file mode 100644 index 45a0a3f9ef..0000000000 --- a/crates/nu-command/src/commands/platform/which_.rs +++ /dev/null @@ -1,239 +0,0 @@ -use crate::prelude::*; -use indexmap::map::IndexMap; -use log::trace; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Primitive, Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tagged; - -pub struct Which; - -impl WholeStreamCommand for Which { - fn name(&self) -> &str { - "which" - } - - fn signature(&self) -> Signature { - Signature::build("which") - .required("application", SyntaxShape::String, "application") - .rest("rest", SyntaxShape::String, "additional applications") - .switch("all", "list all executables", Some('a')) - } - - fn usage(&self) -> &str { - "Finds a program file, alias or custom command." - } - - fn run(&self, args: CommandArgs) -> Result { - which(args) - } -} - -/// Shortcuts for creating an entry to the output table -fn entry(arg: impl Into, path: Value, builtin: bool, tag: Tag) -> Value { - let mut map = IndexMap::new(); - map.insert( - "arg".to_string(), - UntaggedValue::Primitive(Primitive::String(arg.into())).into_value(tag.clone()), - ); - map.insert("path".to_string(), path); - map.insert( - "builtin".to_string(), - UntaggedValue::boolean(builtin).into_value(tag.clone()), - ); - - UntaggedValue::row(map).into_value(tag) -} - -macro_rules! create_entry { - ($arg:expr, $path:expr, $tag:expr, $is_builtin:expr) => { - entry( - $arg.clone(), - UntaggedValue::Primitive(Primitive::String($path.to_string())).into_value($tag.clone()), - $is_builtin, - $tag, - ) - }; -} - -fn get_entries_in_aliases(scope: &Scope, name: &str, tag: Tag) -> Vec { - let aliases = scope - .get_aliases_with_name(name) - .unwrap_or_default() - .into_iter() - .map(|spans| { - spans - .into_iter() - .map(|span| span.item) - .collect::>() - .join(" ") - }) - .map(|alias| { - create_entry!( - name, - format!("Nushell alias: {}", alias), - tag.clone(), - false - ) - }) - .collect::>(); - trace!("Found {} aliases", aliases.len()); - aliases -} - -fn get_entries_in_custom_command(scope: &Scope, name: &str, tag: Tag) -> Vec { - scope - .get_custom_commands_with_name(name) - .unwrap_or_default() - .into_iter() - .map(|_| create_entry!(name, "Nushell custom command", tag.clone(), false)) - .collect() -} - -fn get_entry_in_commands(scope: &Scope, name: &str, tag: Tag) -> Option { - if scope.has_command(name) { - Some(create_entry!(name, "Nushell built-in command", tag, true)) - } else { - None - } -} - -fn get_entries_in_nu( - scope: &Scope, - name: &str, - tag: Tag, - skip_after_first_found: bool, -) -> Vec { - let mut all_entries = vec![]; - - all_entries.extend(get_entries_in_aliases(scope, name, tag.clone())); - if !all_entries.is_empty() && skip_after_first_found { - return all_entries; - } - - all_entries.extend(get_entries_in_custom_command(scope, name, tag.clone())); - if !all_entries.is_empty() && skip_after_first_found { - return all_entries; - } - - if let Some(entry) = get_entry_in_commands(scope, name, tag) { - all_entries.push(entry); - } - - all_entries -} - -#[allow(unused)] -macro_rules! entry_path { - ($arg:expr, $path:expr, $tag:expr) => { - entry( - $arg.clone(), - UntaggedValue::Primitive(Primitive::FilePath($path)).into_value($tag.clone()), - false, - $tag, - ) - }; -} - -#[cfg(feature = "which")] -fn get_first_entry_in_path(item: &str, tag: Tag) -> Option { - which::which(item) - .map(|path| entry_path!(item, path, tag)) - .ok() -} - -#[cfg(not(feature = "which"))] -fn get_first_entry_in_path(_: &str, _: Tag) -> Option { - None -} - -#[cfg(feature = "which")] -fn get_all_entries_in_path(item: &str, tag: Tag) -> Vec { - which::which_all(&item) - .map(|iter| { - iter.map(|path| entry_path!(item, path, tag.clone())) - .collect() - }) - .unwrap_or_default() -} -#[cfg(not(feature = "which"))] -fn get_all_entries_in_path(_: &str, _: Tag) -> Vec { - vec![] -} - -#[derive(Debug)] -struct WhichArgs { - applications: Vec>, - all: bool, -} - -fn which_single(application: Tagged, all: bool, scope: &Scope) -> Vec { - let (external, prog_name) = if application.starts_with('^') { - (true, application.item[1..].to_string()) - } else { - (false, application.item.clone()) - }; - - //If prog_name is an external command, don't search for nu-specific programs - //If all is false, we can save some time by only searching for the first matching - //program - //This match handles all different cases - match (all, external) { - (true, true) => get_all_entries_in_path(&prog_name, application.tag), - (true, false) => { - let mut output: Vec = vec![]; - output.extend(get_entries_in_nu( - scope, - &prog_name, - application.tag.clone(), - false, - )); - output.extend(get_all_entries_in_path(&prog_name, application.tag)); - output - } - (false, true) => { - if let Some(entry) = get_first_entry_in_path(&prog_name, application.tag) { - return vec![entry]; - } - vec![] - } - (false, false) => { - let nu_entries = get_entries_in_nu(scope, &prog_name, application.tag.clone(), true); - if !nu_entries.is_empty() { - return vec![nu_entries[0].clone()]; - } else if let Some(entry) = get_first_entry_in_path(&prog_name, application.tag) { - return vec![entry]; - } - vec![] - } - } -} - -fn which(args: CommandArgs) -> Result { - let which_args = WhichArgs { - applications: args.rest_with_minimum(0, 1)?, - all: args.has_flag("all"), - }; - - let mut output = vec![]; - - for app in which_args.applications { - let values = which_single(app, which_args.all, args.scope()); - output.extend(values); - } - - Ok(output.into_iter().into_output_stream()) -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::Which; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Which {}) - } -} diff --git a/crates/nu-command/src/commands/random/bool.rs b/crates/nu-command/src/commands/random/bool.rs deleted file mode 100644 index a7c8c3ed5e..0000000000 --- a/crates/nu-command/src/commands/random/bool.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue}; -use nu_source::Tagged; -use rand::prelude::{thread_rng, Rng}; - -pub struct SubCommand; - -pub struct BoolArgs { - bias: Option>, -} - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "random bool" - } - - fn signature(&self) -> Signature { - Signature::build("random bool").named( - "bias", - SyntaxShape::Number, - "Adjusts the probability of a \"true\" outcome", - Some('b'), - ) - } - - fn usage(&self) -> &str { - "Generate a random boolean value" - } - - fn run(&self, args: CommandArgs) -> Result { - bool_command(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Generate a random boolean value", - example: "random bool", - result: None, - }, - Example { - description: "Generate a random boolean value with a 75% chance of \"true\"", - example: "random bool --bias 0.75", - result: None, - }, - ] - } -} - -pub fn bool_command(args: CommandArgs) -> Result { - let cmd_args = BoolArgs { - bias: args.get_flag("bias")?, - }; - - let mut probability = 0.5; - - if let Some(prob) = cmd_args.bias { - probability = *prob as f64; - - let probability_is_valid = (0.0..=1.0).contains(&probability); - - if !probability_is_valid { - return Err(ShellError::labeled_error( - "The probability is invalid", - "invalid probability", - prob.span(), - )); - } - } - - let mut rng = thread_rng(); - let bool_result: bool = rng.gen_bool(probability); - - Ok(OutputStream::one(UntaggedValue::boolean(bool_result))) -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/random/chars.rs b/crates/nu-command/src/commands/random/chars.rs deleted file mode 100644 index 986c8256b1..0000000000 --- a/crates/nu-command/src/commands/random/chars.rs +++ /dev/null @@ -1,83 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue}; -use nu_source::Tagged; -use rand::distributions::{Alphanumeric, Distribution}; -use rand::thread_rng; - -pub struct SubCommand; - -pub struct CharsArgs { - length: Option>, -} - -const DEFAULT_CHARS_LENGTH: u32 = 25; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "random chars" - } - - fn signature(&self) -> Signature { - Signature::build("random chars").named( - "length", - SyntaxShape::Int, - "Number of chars", - Some('l'), - ) - } - - fn usage(&self) -> &str { - "Generate random chars" - } - - fn run(&self, args: CommandArgs) -> Result { - chars(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Generate random chars", - example: "random chars", - result: None, - }, - Example { - description: "Generate random chars with specified length", - example: "random chars -l 20", - result: None, - }, - ] - } -} - -pub fn chars(args: CommandArgs) -> Result { - let cmd_args = CharsArgs { - length: args.get_flag("length")?, - }; - - let chars_length = cmd_args.length.map_or(DEFAULT_CHARS_LENGTH, |l| l.item); - let mut rng = thread_rng(); - - let random_string: String = Alphanumeric - .sample_iter(&mut rng) - .take(chars_length as usize) - .map(char::from) - .collect(); - - Ok(OutputStream::one(UntaggedValue::string(random_string))) -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/random/command.rs b/crates/nu-command/src/commands/random/command.rs deleted file mode 100644 index af4f709222..0000000000 --- a/crates/nu-command/src/commands/random/command.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, UntaggedValue}; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "random" - } - - fn signature(&self) -> Signature { - Signature::build("random") - } - - fn usage(&self) -> &str { - "Generate random values." - } - - fn run(&self, args: CommandArgs) -> Result { - Ok(OutputStream::one(UntaggedValue::string(get_full_help( - &Command, - args.scope(), - )))) - } -} diff --git a/crates/nu-command/src/commands/random/decimal.rs b/crates/nu-command/src/commands/random/decimal.rs deleted file mode 100644 index fbef27800c..0000000000 --- a/crates/nu-command/src/commands/random/decimal.rs +++ /dev/null @@ -1,105 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Range, Signature, SyntaxShape, UntaggedValue}; -use nu_source::Tagged; -use rand::prelude::{thread_rng, Rng}; -use std::cmp::Ordering; - -pub struct SubCommand; - -pub struct DecimalArgs { - range: Option>, -} - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "random decimal" - } - - fn signature(&self) -> Signature { - Signature::build("random decimal").optional("range", SyntaxShape::Range, "Range of values") - } - - fn usage(&self) -> &str { - "Generate a random decimal within a range [min..max]" - } - - fn run(&self, args: CommandArgs) -> Result { - decimal(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Generate a default decimal value between 0 and 1", - example: "random decimal", - result: None, - }, - Example { - description: "Generate a random decimal less than or equal to 500", - example: "random decimal ..500", - result: None, - }, - Example { - description: "Generate a random decimal greater than or equal to 100000", - example: "random decimal 100000..", - result: None, - }, - Example { - description: "Generate a random decimal between 1.0 and 1.1", - example: "random decimal 1.0..1.1", - result: None, - }, - ] - } -} - -pub fn decimal(args: CommandArgs) -> Result { - let cmd_args = DecimalArgs { - range: args.opt(0)?, - }; - - let (min, max) = if let Some(range) = &cmd_args.range { - (range.item.min_f64()?, range.item.max_f64()?) - } else { - (0.0, 1.0) - }; - - match min.partial_cmp(&max) { - Some(Ordering::Greater) => Err(ShellError::labeled_error( - format!("Invalid range {}..{}", min, max), - "expected a valid range", - cmd_args - .range - .expect("Unexpected ordering error in random decimal") - .span(), - )), - Some(Ordering::Equal) => Ok(OutputStream::one(UntaggedValue::decimal_from_float( - min, - Span::new(64, 64), - ))), - _ => { - let mut thread_rng = thread_rng(); - let result: f64 = thread_rng.gen_range(min..max); - - Ok(OutputStream::one(UntaggedValue::decimal_from_float( - result, - Span::new(64, 64), - ))) - } - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/random/dice.rs b/crates/nu-command/src/commands/random/dice.rs deleted file mode 100644 index 1b13d2f29b..0000000000 --- a/crates/nu-command/src/commands/random/dice.rs +++ /dev/null @@ -1,99 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue}; -use nu_source::Tagged; -use rand::prelude::{thread_rng, Rng}; - -pub struct SubCommand; - -pub struct DiceArgs { - dice: Option>, - sides: Option>, -} - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "random dice" - } - - fn signature(&self) -> Signature { - Signature::build("random dice") - .named( - "dice", - SyntaxShape::Int, - "The amount of dice being rolled", - Some('d'), - ) - .named( - "sides", - SyntaxShape::Int, - "The amount of sides a die has", - Some('s'), - ) - } - - fn usage(&self) -> &str { - "Generate a random dice roll" - } - - fn run(&self, args: CommandArgs) -> Result { - dice(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Roll 1 dice with 6 sides each", - example: "random dice", - result: None, - }, - Example { - description: "Roll 10 dice with 12 sides each", - example: "random dice -d 10 -s 12", - result: None, - }, - ] - } -} - -pub fn dice(args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let cmd_args = DiceArgs { - dice: args.get_flag("dice")?, - sides: args.get_flag("sides")?, - }; - - let dice = if let Some(dice_tagged) = cmd_args.dice { - *dice_tagged - } else { - 1 - }; - - let sides = if let Some(sides_tagged) = cmd_args.sides { - *sides_tagged - } else { - 6 - }; - - let iter = (0..dice).map(move |_| { - let mut thread_rng = thread_rng(); - UntaggedValue::int(thread_rng.gen_range(1..sides + 1)).into_value(tag.clone()) - }); - - Ok(iter.into_output_stream()) -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/random/integer.rs b/crates/nu-command/src/commands/random/integer.rs deleted file mode 100644 index 39569472b5..0000000000 --- a/crates/nu-command/src/commands/random/integer.rs +++ /dev/null @@ -1,105 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Range, Signature, SyntaxShape, UntaggedValue}; -use nu_source::Tagged; -use rand::prelude::{thread_rng, Rng}; -use std::cmp::Ordering; - -pub struct SubCommand; - -pub struct IntegerArgs { - range: Option>, -} - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "random integer" - } - - fn signature(&self) -> Signature { - Signature::build("random integer").optional("range", SyntaxShape::Range, "Range of values") - } - - fn usage(&self) -> &str { - "Generate a random integer [min..max]" - } - - fn run(&self, args: CommandArgs) -> Result { - integer(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Generate an unconstrained random integer", - example: "random integer", - result: None, - }, - Example { - description: "Generate a random integer less than or equal to 500", - example: "random integer ..500", - result: None, - }, - Example { - description: "Generate a random integer greater than or equal to 100000", - example: "random integer 100000..", - result: None, - }, - Example { - description: "Generate a random integer between 1 and 10", - example: "random integer 1..10", - result: None, - }, - ] - } -} - -pub fn integer(args: CommandArgs) -> Result { - let cmd_args = IntegerArgs { - range: args.opt(0)?, - }; - - let (min, max) = if let Some(range) = &cmd_args.range { - (range.min_i64()?, range.max_i64()?) - } else { - (0, i64::MAX) - }; - - match min.cmp(&max) { - Ordering::Greater => Err(ShellError::labeled_error( - format!("Invalid range {}..{}", min, max), - "expected a valid range", - cmd_args - .range - .expect("Unexpected ordering error in random integer") - .span(), - )), - Ordering::Equal => Ok(OutputStream::one( - UntaggedValue::int(min).into_value(Tag::unknown()), - )), - _ => { - let mut thread_rng = thread_rng(); - // add 1 to max, because gen_range is right-exclusive - let max = max.saturating_add(1); - let result: i64 = thread_rng.gen_range(min..max); - - Ok(OutputStream::one( - UntaggedValue::int(result).into_value(Tag::unknown()), - )) - } - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/random/mod.rs b/crates/nu-command/src/commands/random/mod.rs deleted file mode 100644 index 5decd94757..0000000000 --- a/crates/nu-command/src/commands/random/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -pub mod command; - -pub mod bool; -pub mod chars; -pub mod decimal; -pub mod dice; -pub mod integer; -#[cfg(feature = "uuid_crate")] -pub mod uuid; - -pub use command::Command as Random; - -pub use self::bool::SubCommand as RandomBool; -pub use chars::SubCommand as RandomChars; -pub use decimal::SubCommand as RandomDecimal; -pub use dice::SubCommand as RandomDice; -pub use integer::SubCommand as RandomInteger; -#[cfg(feature = "uuid_crate")] -pub use uuid::SubCommand as RandomUUID; diff --git a/crates/nu-command/src/commands/random/uuid.rs b/crates/nu-command/src/commands/random/uuid.rs deleted file mode 100644 index 47d01a3ed1..0000000000 --- a/crates/nu-command/src/commands/random/uuid.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::Signature; -use uuid_crate::Uuid; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "random uuid" - } - - fn signature(&self) -> Signature { - Signature::build("random uuid") - } - - fn usage(&self) -> &str { - "Generate a random uuid4 string" - } - - fn run(&self, args: CommandArgs) -> Result { - uuid(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Generate a random uuid4 string", - example: "random uuid", - result: None, - }] - } -} - -pub fn uuid(_args: CommandArgs) -> Result { - let uuid_4 = Uuid::new_v4().to_hyphenated().to_string(); - - Ok(OutputStream::one(uuid_4)) -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/shells/command.rs b/crates/nu-command/src/commands/shells/command.rs deleted file mode 100644 index 8f44416f05..0000000000 --- a/crates/nu-command/src/commands/shells/command.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, TaggedDictBuilder}; -use std::sync::atomic::Ordering; - -pub struct Shells; - -impl WholeStreamCommand for Shells { - fn name(&self) -> &str { - "shells" - } - - fn signature(&self) -> Signature { - Signature::build("shells") - } - - fn usage(&self) -> &str { - "Display the list of current shells." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - Ok(shells(args)) - } -} - -fn shells(args: CommandArgs) -> ActionStream { - let mut shells_out = VecDeque::new(); - let shell_manager = args.shell_manager(); - let tag = args.call_info.name_tag; - let active_index = shell_manager.current_shell.load(Ordering::SeqCst); - - for (index, shell) in shell_manager.shells.lock().iter().enumerate() { - let mut dict = TaggedDictBuilder::new(&tag); - - if index == active_index { - dict.insert_untagged("active", true); - } else { - dict.insert_untagged("active", false); - } - dict.insert_untagged("name", shell.name()); - dict.insert_untagged("path", shell.path()); - - shells_out.push_back(dict.into_value()); - } - - shells_out.into() -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::Shells; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Shells {}) - } -} diff --git a/crates/nu-command/src/commands/shells/enter.rs b/crates/nu-command/src/commands/shells/enter.rs deleted file mode 100644 index 9736a666cc..0000000000 --- a/crates/nu-command/src/commands/shells/enter.rs +++ /dev/null @@ -1,163 +0,0 @@ -use crate::prelude::*; -use nu_engine::UnevaluatedCallInfo; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::hir::ExternalRedirection; -use nu_protocol::{ - CommandAction, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value, -}; -use nu_source::Tagged; -use std::path::PathBuf; - -pub struct Enter; - -impl WholeStreamCommand for Enter { - fn name(&self) -> &str { - "enter" - } - - fn signature(&self) -> Signature { - Signature::build("enter") - .required( - "location", - SyntaxShape::FilePath, - "the location to create a new shell from", - ) - .named( - "encoding", - SyntaxShape::String, - "encoding to use to open file", - Some('e'), - ) - } - - fn usage(&self) -> &str { - "Create a new shell and begin at this path." - } - - fn extra_usage(&self) -> &str { - r#"Multiple encodings are supported for reading text files by using -the '--encoding ' parameter. Here is an example of a few: -big5, euc-jp, euc-kr, gbk, iso-8859-1, utf-16, cp1252, latin5 - -For a more complete list of encodings please refer to the encoding_rs -documentation link at https://docs.rs/encoding_rs/0.8.28/encoding_rs/#statics"# - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - enter(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Enter a path as a new shell", - example: "enter ../projectB", - result: None, - }, - Example { - description: "Enter a file as a new shell", - example: "enter package.json", - result: None, - }, - Example { - description: "Enters file with iso-8859-1 encoding", - example: "enter file.csv --encoding iso-8859-1", - result: None, - }, - ] - } -} - -fn enter(args: CommandArgs) -> Result { - let head = args.call_info.args.head.clone(); - let context = args.context.clone(); - let scope = args.scope().clone(); - let path = args.context.shell_manager().path(); - - let location: Tagged = args.req(0)?; - let encoding: Option> = args.get_flag("encoding")?; - let location_string = location.display().to_string(); - - if location.is_dir() { - Ok(ActionStream::one(ReturnSuccess::action( - CommandAction::EnterShell(location_string), - ))) - } else { - // If it's a file, attempt to open the file as a value and enter it - let cwd = path; - - let full_path = std::path::PathBuf::from(cwd); - let span = location.span(); - - let (file_extension, tagged_contents) = crate::commands::filesystem::open::fetch( - &full_path, - &PathBuf::from(location_string), - span, - encoding, - )?; - - match tagged_contents.value { - UntaggedValue::Primitive(Primitive::String(_)) => { - if let Some(extension) = file_extension { - let command_name = format!("from {}", extension); - if let Some(converter) = scope.get_command(&command_name) { - let tag = tagged_contents.tag.clone(); - let new_args = CommandArgs { - context, - call_info: UnevaluatedCallInfo { - args: nu_protocol::hir::Call { - head, - positional: None, - named: None, - span: Span::unknown(), - external_redirection: ExternalRedirection::Stdout, - }, - name_tag: tag.clone(), - }, - input: InputStream::one(tagged_contents), - }; - let mut result = converter.run(new_args)?; - let result_vec: Vec = result.drain_vec(); - Ok(result_vec - .into_iter() - .map(move |res| { - let Value { value, .. } = res; - Ok(ReturnSuccess::Action(CommandAction::EnterValueShell( - Value { - value, - tag: tag.clone(), - }, - ))) - }) - .into_action_stream()) - } else { - Ok(ActionStream::one(ReturnSuccess::action( - CommandAction::EnterValueShell(tagged_contents), - ))) - } - } else { - Ok(ActionStream::one(ReturnSuccess::action( - CommandAction::EnterValueShell(tagged_contents), - ))) - } - } - _ => Ok(ActionStream::one(ReturnSuccess::action( - CommandAction::EnterValueShell(tagged_contents), - ))), - } - } -} - -#[cfg(test)] -mod tests { - use super::Enter; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Enter {}) - } -} diff --git a/crates/nu-command/src/commands/shells/exit.rs b/crates/nu-command/src/commands/shells/exit.rs deleted file mode 100644 index cc6754d156..0000000000 --- a/crates/nu-command/src/commands/shells/exit.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::prelude::*; -use nu_errors::ShellError; -use nu_protocol::{CommandAction, ReturnSuccess, Signature, SyntaxShape}; - -pub struct Exit; - -impl WholeStreamCommand for Exit { - fn name(&self) -> &str { - "exit" - } - - fn signature(&self) -> Signature { - Signature::build("exit") - .optional( - "code", - SyntaxShape::Number, - "Status code to return if this was the last shell or --now was specified", - ) - .switch("now", "Exit out of the shell immediately", Some('n')) - } - - fn usage(&self) -> &str { - "Exit the current shell (or all shells)." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - exit(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Exit the current shell", - example: "exit", - result: None, - }, - Example { - description: "Exit all shells (exiting Nu)", - example: "exit --now", - result: None, - }, - ] - } -} - -pub fn exit(args: CommandArgs) -> Result { - let code = if let Some(value) = args.opt::(0)? { - value as i32 - } else { - 0 - }; - - let command_action = if args.has_flag("now") { - CommandAction::Exit(code) - } else { - CommandAction::LeaveShell(code) - }; - - Ok(ActionStream::one(ReturnSuccess::action(command_action))) -} - -#[cfg(test)] -mod tests { - use super::Exit; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Exit {}) - } -} diff --git a/crates/nu-command/src/commands/shells/goto.rs b/crates/nu-command/src/commands/shells/goto.rs deleted file mode 100644 index d1396c2dc6..0000000000 --- a/crates/nu-command/src/commands/shells/goto.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{CommandAction, ReturnSuccess, Signature, SyntaxShape}; - -pub struct Goto; - -impl WholeStreamCommand for Goto { - fn name(&self) -> &str { - "g" - } - - fn signature(&self) -> Signature { - Signature::build("g").required("index", SyntaxShape::Int, "the shell's index to go to") - } - - fn usage(&self) -> &str { - "Go to specified shell." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - goto(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Enter the first shell", - example: "g 0", - result: None, - }] - } -} - -fn goto(args: CommandArgs) -> Result { - Ok(ActionStream::one(ReturnSuccess::action( - CommandAction::GotoShell(args.req(0)?), - ))) -} - -#[cfg(test)] -mod tests { - use super::Goto; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Goto {}) - } -} diff --git a/crates/nu-command/src/commands/shells/mod.rs b/crates/nu-command/src/commands/shells/mod.rs deleted file mode 100644 index 7c4897b1f0..0000000000 --- a/crates/nu-command/src/commands/shells/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -mod command; -mod enter; -mod exit; -mod goto; -mod next; -mod prev; - -pub use command::Shells; -pub use enter::Enter; -pub use exit::Exit; -pub use goto::Goto; -pub use next::Next; -pub use prev::Previous; diff --git a/crates/nu-command/src/commands/shells/next.rs b/crates/nu-command/src/commands/shells/next.rs deleted file mode 100644 index 73df6aaa24..0000000000 --- a/crates/nu-command/src/commands/shells/next.rs +++ /dev/null @@ -1,41 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{CommandAction, ReturnSuccess, Signature}; - -pub struct Next; - -impl WholeStreamCommand for Next { - fn name(&self) -> &str { - "n" - } - - fn signature(&self) -> Signature { - Signature::build("n") - } - - fn usage(&self) -> &str { - "Go to next shell." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - Ok(next(args)) - } -} - -fn next(_args: CommandArgs) -> ActionStream { - vec![Ok(ReturnSuccess::Action(CommandAction::NextShell))].into() -} - -#[cfg(test)] -mod tests { - use super::Next; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Next {}) - } -} diff --git a/crates/nu-command/src/commands/shells/prev.rs b/crates/nu-command/src/commands/shells/prev.rs deleted file mode 100644 index d32a904228..0000000000 --- a/crates/nu-command/src/commands/shells/prev.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::prelude::*; -use nu_errors::ShellError; -use nu_protocol::{CommandAction, ReturnSuccess, Signature}; - -use nu_engine::WholeStreamCommand; - -pub struct Previous; - -impl WholeStreamCommand for Previous { - fn name(&self) -> &str { - "p" - } - - fn signature(&self) -> Signature { - Signature::build("p") - } - - fn usage(&self) -> &str { - "Go to previous shell." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - Ok(previous(args)) - } -} - -fn previous(_args: CommandArgs) -> ActionStream { - vec![Ok(ReturnSuccess::Action(CommandAction::PreviousShell))].into() -} - -#[cfg(test)] -mod tests { - use super::Previous; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Previous {}) - } -} diff --git a/crates/nu-command/src/commands/strings/build_string.rs b/crates/nu-command/src/commands/strings/build_string.rs deleted file mode 100644 index 774d3e6ada..0000000000 --- a/crates/nu-command/src/commands/strings/build_string.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::prelude::*; -use nu_errors::ShellError; - -use nu_data::value::format_leaf; -use nu_engine::WholeStreamCommand; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value}; - -pub struct BuildString; - -impl WholeStreamCommand for BuildString { - fn name(&self) -> &str { - "build-string" - } - - fn signature(&self) -> Signature { - Signature::build("build-string").rest( - "rest", - SyntaxShape::Any, - "all values to form into the string", - ) - } - - fn usage(&self) -> &str { - "Builds a string from the arguments." - } - - fn run(&self, args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let rest: Vec = args.rest(0)?; - - let mut output_string = String::new(); - - for r in rest { - output_string.push_str(&format_leaf(&r).plain_string(100_000)) - } - - Ok(OutputStream::one( - UntaggedValue::string(output_string).into_value(tag), - )) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Builds a string from a string and a number, without spaces between them", - example: "build-string 'foo' 3", - result: None, - }] - } -} diff --git a/crates/nu-command/src/commands/strings/char_.rs b/crates/nu-command/src/commands/strings/char_.rs deleted file mode 100644 index 75f7bd7f35..0000000000 --- a/crates/nu-command/src/commands/strings/char_.rs +++ /dev/null @@ -1,325 +0,0 @@ -use crate::prelude::*; -use indexmap::indexmap; -use indexmap::map::IndexMap; -use lazy_static::lazy_static; -use nu_engine::{FromValue, WholeStreamCommand}; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, TaggedDictBuilder, UntaggedValue, Value}; -use nu_source::Tagged; - -// Character used to separate directories in a Path Environment variable on windows is ";" -#[cfg(target_family = "windows")] -const ENV_PATH_SEPARATOR_CHAR: char = ';'; -// Character used to separate directories in a Path Environment variable on linux/mac/unix is ":" -#[cfg(not(target_family = "windows"))] -const ENV_PATH_SEPARATOR_CHAR: char = ':'; - -pub struct Char; - -struct CharArgs { - name: Option>, - rest: Vec, - list: bool, - unicode: bool, -} - -lazy_static! { - static ref CHAR_MAP: IndexMap<&'static str, String> = indexmap! { - // These are some regular characters that either can't be used or - // it's just easier to use them like this. - - // This are the "normal" characters section - "newline" => '\n'.to_string(), - "enter" => '\n'.to_string(), - "nl" => '\n'.to_string(), - "tab" => '\t'.to_string(), - "sp" => ' '.to_string(), - "space" => ' '.to_string(), - "pipe" => '|'.to_string(), - "left_brace" => '{'.to_string(), - "lbrace" => '{'.to_string(), - "right_brace" => '}'.to_string(), - "rbrace" => '}'.to_string(), - "left_paren" => '('.to_string(), - "lp" => '('.to_string(), - "lparen" => '('.to_string(), - "right_paren" => ')'.to_string(), - "rp" => ')'.to_string(), - "rparen" => ')'.to_string(), - "left_bracket" => '['.to_string(), - "lbracket" => '['.to_string(), - "right_bracket" => ']'.to_string(), - "rbracket" => ']'.to_string(), - "single_quote" => '\''.to_string(), - "squote" => '\''.to_string(), - "sq" => '\''.to_string(), - "double_quote" => '\"'.to_string(), - "dquote" => '\"'.to_string(), - "dq" => '\"'.to_string(), - "path_sep" => std::path::MAIN_SEPARATOR.to_string(), - "psep" => std::path::MAIN_SEPARATOR.to_string(), - "separator" => std::path::MAIN_SEPARATOR.to_string(), - "esep" => ENV_PATH_SEPARATOR_CHAR.to_string(), - "env_sep" => ENV_PATH_SEPARATOR_CHAR.to_string(), - "tilde" => '~'.to_string(), // ~ - "twiddle" => '~'.to_string(), // ~ - "squiggly" => '~'.to_string(), // ~ - "home" => '~'.to_string(), // ~ - "hash" => '#'.to_string(), // # - "hashtag" => '#'.to_string(), // # - "pound_sign" => '#'.to_string(), // # - "sharp" => '#'.to_string(), // # - "root" => '#'.to_string(), // # - "comma" => ','.to_string(), // , comma - "semicolon" => ';'.to_string(), // ; semicolon - "dollar" => '$'.to_string(), // $ dollar - "at" => '@'.to_string(), // @ at - "minus" => '-'.to_string(), // - minus - "subtract" => '-'.to_string(), - "dash" => '-'.to_string(), - "plus" => '+'.to_string(), // + plus - "add" => '+'.to_string(), - "divide" => '/'.to_string(), // / divide, slash - "slash" => '/'.to_string(), - "backslash" => '\\'.to_string(),// \ backslash - "percent" => '%'.to_string(), // % percent - "multiply" => '*'.to_string(), // * multiply - "greater_than" => '>'.to_string(), // > greater_than - "gt" => '>'.to_string(), - "less_than" => '<'.to_string(), // < less_than - "lt" => '<'.to_string(), - "and" => '&'.to_string(), // & and, ampersand - "ampersand" => '&'.to_string(), - "equal" => '='.to_string(), // = equal - "eq" => '='.to_string(), - "double_equal" => "==".to_string(), // == double_equal - "fat_arrow" => "=>".to_string(), // => fat_arrow - "right_arrow" => "->".to_string(), // -> right_arrow - "left_arrow" => "<-".to_string(), // <- left_arrow - "question" => '?'.to_string(), // ? question, q - "q" => '?'.to_string(), - "colon" => ':'.to_string(), // : colon - "underscore" => '_'.to_string(), - "u" => '_'.to_string(), - - // This is the unicode section - // Unicode names came from https://www.compart.com/en/unicode - // Private Use Area (U+E000-U+F8FF) - // Unicode can't be mixed with Ansi or it will break width calculation - "branch" => '\u{e0a0}'.to_string(), //  - "segment" => '\u{e0b0}'.to_string(), //  - - "identical_to" => '\u{2261}'.to_string(), // ≡ - "hamburger" => '\u{2261}'.to_string(), // ≡ - "not_identical_to" => '\u{2262}'.to_string(), // ≢ - "branch_untracked" => '\u{2262}'.to_string(), // ≢ - "strictly_equivalent_to" => '\u{2263}'.to_string(), // ≣ - "branch_identical" => '\u{2263}'.to_string(), // ≣ - - "upwards_arrow" => '\u{2191}'.to_string(), // ↑ - "branch_ahead" => '\u{2191}'.to_string(), // ↑ - "downwards_arrow" => '\u{2193}'.to_string(), // ↓ - "branch_behind" => '\u{2193}'.to_string(), // ↓ - "up_down_arrow" => '\u{2195}'.to_string(), // ↕ - "branch_ahead_behind" => '\u{2195}'.to_string(), // ↕ - - "black_right_pointing_triangle" => '\u{25b6}'.to_string(), // ▶ - "prompt" => '\u{25b6}'.to_string(), // ▶ - "vector_or_cross_product" => '\u{2a2f}'.to_string(), // ⨯ - "failed" => '\u{2a2f}'.to_string(), // ⨯ - "high_voltage_sign" => '\u{26a1}'.to_string(), // ⚡ - "elevated" => '\u{26a1}'.to_string(), // ⚡ - - // This is the emoji section - // Weather symbols - "sun" => "☀️".to_string(), - "sunny" => "☀️".to_string(), - "sunrise" => "☀️".to_string(), - "moon" => "🌛".to_string(), - "cloudy" => "☁️".to_string(), - "cloud" => "☁️".to_string(), - "clouds" => "☁️".to_string(), - "rainy" => "🌦️".to_string(), - "rain" => "🌦️".to_string(), - "foggy" => "🌫️".to_string(), - "fog" => "🌫️".to_string(), - "mist" => '\u{2591}'.to_string(), - "haze" => '\u{2591}'.to_string(), - "snowy" => "❄️".to_string(), - "snow" => "❄️".to_string(), - "thunderstorm" => "🌩️".to_string(), - "thunder" => "🌩️".to_string(), - - // This is the "other" section - "bel" => '\x07'.to_string(), // Terminal Bell - "backspace" => '\x08'.to_string(), // Backspace - }; -} - -impl WholeStreamCommand for Char { - fn name(&self) -> &str { - "char" - } - - fn signature(&self) -> Signature { - Signature::build("char") - .optional( - "character", - SyntaxShape::Any, - "the name of the character to output", - ) - .rest("rest", SyntaxShape::String, "multiple Unicode bytes") - .switch("list", "List all supported character names", Some('l')) - .switch("unicode", "Unicode string i.e. 1f378", Some('u')) - } - - fn usage(&self) -> &str { - "Output special characters (e.g., 'newline')." - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Output newline", - example: r#"char newline"#, - result: Some(vec![Value::from("\n")]), - }, - Example { - description: "Output prompt character, newline and a hamburger character", - example: r#"echo (char prompt) (char newline) (char hamburger)"#, - result: Some(vec![ - UntaggedValue::string("\u{25b6}").into(), - UntaggedValue::string("\n").into(), - UntaggedValue::string("\u{2261}").into(), - ]), - }, - Example { - description: "Output Unicode character", - example: r#"char -u 1f378"#, - result: Some(vec![Value::from("\u{1f378}")]), - }, - Example { - description: "Output multi-byte Unicode character", - example: r#"char -u 1F468 200D 1F466 200D 1F466"#, - result: Some(vec![Value::from( - "\u{1F468}\u{200D}\u{1F466}\u{200D}\u{1F466}", - )]), - }, - ] - } - - fn run(&self, args: CommandArgs) -> Result { - let args_tag = args.call_info.name_tag.clone(); - let args = CharArgs { - name: args.opt(0)?, - rest: args.rest(1)?, - list: args.has_flag("list"), - unicode: args.has_flag("unicode"), - }; - - if args.list { - Ok(CHAR_MAP - .iter() - .map(move |(name, s)| { - let mut dict = TaggedDictBuilder::with_capacity(&args_tag, 2); - dict.insert_untagged("name", UntaggedValue::string(*name)); - dict.insert_untagged("character", UntaggedValue::string(s)); - let unicode_parts: Vec = - s.chars().map(|c| format!("{:x}", c as u32)).collect(); - dict.insert_untagged("unicode", UntaggedValue::string(unicode_parts.join(" "))); - dict.into_value() - }) - .into_output_stream()) - } else if let Some(name) = args.name { - if args.unicode { - if !args.rest.is_empty() { - // Setup a new buffer to put all the Unicode bytes in - let mut multi_byte = String::new(); - // Get the first byte - let decoded_char = string_to_unicode_char(&name.item, &name.tag); - match decoded_char { - Ok(ch) => multi_byte.push(ch), - Err(e) => return Err(e), - } - // Get the rest of the bytes - for byte_part in args.rest { - let byte_part: Tagged = FromValue::from_value(&byte_part)?; - let decoded_char = string_to_unicode_char(&byte_part, &byte_part.tag); - match decoded_char { - Ok(ch) => multi_byte.push(ch), - Err(e) => return Err(e), - } - } - Ok(OutputStream::one( - UntaggedValue::string(multi_byte).into_value(name.tag), - )) - } else { - let decoded_char = string_to_unicode_char(&name.item, &name.tag); - if let Ok(ch) = decoded_char { - Ok(OutputStream::one( - UntaggedValue::string(ch).into_value(name.tag()), - )) - } else { - Err(ShellError::labeled_error( - "error decoding Unicode character", - "error decoding Unicode character", - name.tag(), - )) - } - } - } else { - let special_character = str_to_character(&name.item); - if let Some(output) = special_character { - Ok(OutputStream::one( - UntaggedValue::string(output).into_value(name.tag()), - )) - } else { - Err(ShellError::labeled_error( - "error finding named character", - "error finding named character", - name.tag(), - )) - } - } - } else { - Err(ShellError::labeled_error( - "char requires the name of the character", - "missing name of the character", - &args_tag, - )) - } - } -} - -fn string_to_unicode_char(s: &str, t: &Tag) -> Result { - let decoded_char = u32::from_str_radix(s, 16) - .ok() - .and_then(std::char::from_u32); - - if let Some(ch) = decoded_char { - Ok(ch) - } else { - Err(ShellError::labeled_error( - "error decoding Unicode character", - "error decoding Unicode character", - t, - )) - } -} - -fn str_to_character(s: &str) -> Option { - CHAR_MAP.get(s).map(|s| s.into()) -} - -#[cfg(test)] -mod tests { - use super::Char; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Char {}) - } -} diff --git a/crates/nu-command/src/commands/strings/detect/columns.rs b/crates/nu-command/src/commands/strings/detect/columns.rs deleted file mode 100644 index b2a1a61eb2..0000000000 --- a/crates/nu-command/src/commands/strings/detect/columns.rs +++ /dev/null @@ -1,283 +0,0 @@ -use std::{iter::Peekable, str::CharIndices}; - -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, TaggedDictBuilder, UntaggedValue}; -use nu_source::Spanned; - -type Input<'t> = Peekable>; - -pub struct DetectColumns; - -impl WholeStreamCommand for DetectColumns { - fn name(&self) -> &str { - "detect columns" - } - - fn signature(&self) -> Signature { - Signature::build("detect columns") - .named( - "skip", - SyntaxShape::Int, - "number of rows to skip before detecting", - Some('s'), - ) - .switch("no_headers", "don't detect headers", Some('n')) - } - - fn usage(&self) -> &str { - "splits contents across multiple columns via the separator." - } - - fn run(&self, args: CommandArgs) -> Result { - detect_columns(args) - } -} - -fn detect_columns(args: CommandArgs) -> Result { - let name_tag = args.name_tag(); - let num_rows_to_skip: Option = args.get_flag("skip")?; - let noheader = args.has_flag("no_headers"); - let input = args.input.collect_string(name_tag.clone())?; - - let input: Vec<_> = input - .lines() - .skip(num_rows_to_skip.unwrap_or_default()) - .map(|x| x.to_string()) - .collect(); - - let mut input = input.into_iter(); - let headers = input.next(); - - if let Some(orig_headers) = headers { - let headers = find_columns(&orig_headers); - - Ok((if noheader { - vec![orig_headers].into_iter().chain(input) - } else { - vec![].into_iter().chain(input) - }) - .map(move |x| { - let row = find_columns(&x); - - let mut dict = TaggedDictBuilder::new(name_tag.clone()); - - if headers.len() == row.len() && !noheader { - for (header, val) in headers.iter().zip(row.iter()) { - dict.insert_untagged(&header.item, UntaggedValue::string(&val.item)); - } - } else { - let mut pre_output = vec![]; - - // column counts don't line up, so see if we can figure out why - for cell in row { - for header in &headers { - if cell.span.start() <= header.span.end() - && cell.span.end() > header.span.start() - { - pre_output - .push((header.item.to_string(), UntaggedValue::string(&cell.item))); - } - } - } - - for header in &headers { - let mut found = false; - for pre_o in &pre_output { - if pre_o.0 == header.item { - found = true; - break; - } - } - - if !found { - pre_output.push((header.item.to_string(), UntaggedValue::nothing())); - } - } - - if noheader { - for header in headers.iter().enumerate() { - for pre_o in &pre_output { - if pre_o.0 == header.1.item { - dict.insert_untagged(format!("Column{}", header.0), pre_o.1.clone()) - } - } - } - } else { - for header in &headers { - for pre_o in &pre_output { - if pre_o.0 == header.item { - dict.insert_untagged(&header.item, pre_o.1.clone()) - } - } - } - } - } - - dict.into_value() - }) - .into_output_stream()) - } else { - Ok(OutputStream::empty()) - } -} - -pub fn find_columns(input: &str) -> Vec> { - let mut chars = input.char_indices().peekable(); - let mut output = vec![]; - - while let Some((_, c)) = chars.peek() { - if c.is_whitespace() { - // If the next character is non-newline whitespace, skip it. - - let _ = chars.next(); - } else { - // Otherwise, try to consume an unclassified token. - - let result = baseline(&mut chars); - - output.push(result); - } - } - - output -} - -#[derive(Clone, Copy)] -enum BlockKind { - Paren, - CurlyBracket, - SquareBracket, -} - -fn baseline(src: &mut Input) -> Spanned { - let mut token_contents = String::new(); - - let start_offset = if let Some((pos, _)) = src.peek() { - *pos - } else { - 0 - }; - - // This variable tracks the starting character of a string literal, so that - // we remain inside the string literal lexer mode until we encounter the - // closing quote. - let mut quote_start: Option = None; - - // This Vec tracks paired delimiters - let mut block_level: Vec = vec![]; - - // A baseline token is terminated if it's not nested inside of a paired - // delimiter and the next character is one of: `|`, `;`, `#` or any - // whitespace. - fn is_termination(block_level: &[BlockKind], c: char) -> bool { - block_level.is_empty() && (c.is_whitespace()) - } - - // The process of slurping up a baseline token repeats: - // - // - String literal, which begins with `'`, `"` or `\``, and continues until - // the same character is encountered again. - // - Delimiter pair, which begins with `[`, `(`, or `{`, and continues until - // the matching closing delimiter is found, skipping comments and string - // literals. - // - When not nested inside of a delimiter pair, when a terminating - // character (whitespace, `|`, `;` or `#`) is encountered, the baseline - // token is done. - // - Otherwise, accumulate the character into the current baseline token. - while let Some((_, c)) = src.peek() { - let c = *c; - - if quote_start.is_some() { - // If we encountered the closing quote character for the current - // string, we're done with the current string. - if Some(c) == quote_start { - quote_start = None; - } - } else if c == '\n' { - if is_termination(&block_level, c) { - break; - } - } else if c == '\'' || c == '"' || c == '`' { - // We encountered the opening quote of a string literal. - quote_start = Some(c); - } else if c == '[' { - // We encountered an opening `[` delimiter. - block_level.push(BlockKind::SquareBracket); - } else if c == ']' { - // We encountered a closing `]` delimiter. Pop off the opening `[` - // delimiter. - if let Some(BlockKind::SquareBracket) = block_level.last() { - let _ = block_level.pop(); - } - } else if c == '{' { - // We encountered an opening `{` delimiter. - block_level.push(BlockKind::CurlyBracket); - } else if c == '}' { - // We encountered a closing `}` delimiter. Pop off the opening `{`. - if let Some(BlockKind::CurlyBracket) = block_level.last() { - let _ = block_level.pop(); - } - } else if c == '(' { - // We enceountered an opening `(` delimiter. - block_level.push(BlockKind::Paren); - } else if c == ')' { - // We encountered a closing `)` delimiter. Pop off the opening `(`. - if let Some(BlockKind::Paren) = block_level.last() { - let _ = block_level.pop(); - } - } else if is_termination(&block_level, c) { - break; - } - - // Otherwise, accumulate the character into the current token. - token_contents.push(c); - - // Consume the character. - let _ = src.next(); - } - - let span = Span::new(start_offset, start_offset + token_contents.len()); - - // If there is still unclosed opening delimiters, close them and add - // synthetic closing characters to the accumulated token. - if block_level.last().is_some() { - // let delim: char = (*block).closing(); - // let cause = ParseError::unexpected_eof(delim.to_string(), span); - - // while let Some(bk) = block_level.pop() { - // token_contents.push(bk.closing()); - // } - - return token_contents.spanned(span); - } - - if quote_start.is_some() { - // The non-lite parse trims quotes on both sides, so we add the expected quote so that - // anyone wanting to consume this partial parse (e.g., completions) will be able to get - // correct information from the non-lite parse. - // token_contents.push(delimiter); - - // return ( - // token_contents.spanned(span), - // Some(ParseError::unexpected_eof(delimiter.to_string(), span)), - // ); - return token_contents.spanned(span); - } - - token_contents.spanned(span) -} - -#[cfg(test)] -mod tests { - use super::DetectColumns; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(DetectColumns {}) - } -} diff --git a/crates/nu-command/src/commands/strings/detect/mod.rs b/crates/nu-command/src/commands/strings/detect/mod.rs deleted file mode 100644 index 5c56257ba1..0000000000 --- a/crates/nu-command/src/commands/strings/detect/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod columns; - -pub use columns::DetectColumns; diff --git a/crates/nu-command/src/commands/strings/format/command.rs b/crates/nu-command/src/commands/strings/format/command.rs deleted file mode 100644 index eb402e3cd5..0000000000 --- a/crates/nu-command/src/commands/strings/format/command.rs +++ /dev/null @@ -1,142 +0,0 @@ -use crate::prelude::*; -use nu_engine::evaluate_baseline_expr; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue}; -use nu_source::Tagged; -use std::borrow::Borrow; - -pub struct Format; - -impl WholeStreamCommand for Format { - fn name(&self) -> &str { - "format" - } - - fn signature(&self) -> Signature { - Signature::build("format").required( - "pattern", - SyntaxShape::String, - "the pattern to output. Eg) \"{foo}: {bar}\"", - ) - } - - fn usage(&self) -> &str { - "Format columns into a string using a simple pattern." - } - - fn run(&self, args: CommandArgs) -> Result { - format_command(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Print filenames with their sizes", - example: "ls | format '{name}: {size}'", - result: None, - }] - } -} - -fn format_command(args: CommandArgs) -> Result { - let ctx = Arc::new(args.context.clone()); - let pattern: Tagged = args.req(0)?; - - let format_pattern = format(&pattern); - let commands = Arc::new(format_pattern); - - Ok(args - .input - .map(move |value| { - let mut output = String::new(); - let commands = commands.clone(); - let ctx = ctx.clone(); - - for command in &*commands { - match command { - FormatCommand::Text(s) => { - output.push_str(s); - } - FormatCommand::Column(c) => { - // FIXME: use the correct spans - let full_column_path = nu_parser::parse_full_column_path( - &(c.to_string()).spanned(Span::unknown()), - &ctx.scope, - ); - - ctx.scope.enter_scope(); - ctx.scope.add_var("$it", value.clone()); - let result = evaluate_baseline_expr(&full_column_path.0, &ctx); - ctx.scope.exit_scope(); - - if let Ok(c) = result { - output.push_str(&value::format_leaf(c.borrow()).plain_string(100_000)) - } else { - // That column doesn't match, so don't emit anything - } - } - } - } - - Ok(UntaggedValue::string(output).into_untagged_value()) - }) - .into_input_stream()) -} - -#[derive(Debug)] -enum FormatCommand { - Text(String), - Column(String), -} - -fn format(input: &str) -> Vec { - let mut output = vec![]; - - let mut loop_input = input.chars(); - loop { - let mut before = String::new(); - - for c in &mut loop_input { - if c == '{' { - break; - } - before.push(c); - } - - if !before.is_empty() { - output.push(FormatCommand::Text(before.to_string())); - } - // Look for column as we're now at one - let mut column = String::new(); - - for c in &mut loop_input { - if c == '}' { - break; - } - column.push(c); - } - - if !column.is_empty() { - output.push(FormatCommand::Column(column.to_string())); - } - - if before.is_empty() && column.is_empty() { - break; - } - } - - output -} - -#[cfg(test)] -mod tests { - use super::Format; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Format {}) - } -} diff --git a/crates/nu-command/src/commands/strings/format/format_filesize.rs b/crates/nu-command/src/commands/strings/format/format_filesize.rs deleted file mode 100644 index 23d56ffacb..0000000000 --- a/crates/nu-command/src/commands/strings/format/format_filesize.rs +++ /dev/null @@ -1,117 +0,0 @@ -use crate::prelude::*; -use nu_data::base::shape::InlineShape; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ColumnPath, Primitive::Filesize, Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tagged; -use nu_value_ext::get_data_by_column_path; - -pub struct FileSize; - -impl WholeStreamCommand for FileSize { - fn name(&self) -> &str { - "format filesize" - } - - fn signature(&self) -> Signature { - Signature::build("format filesize") - .required( - "field", - SyntaxShape::ColumnPath, - "the name of the column to update", - ) - .required( - "format value", - SyntaxShape::String, - "the format into which convert the filesizes", - ) - } - - fn usage(&self) -> &str { - "Converts a column of filesizes to some specified format" - } - - fn run(&self, args: CommandArgs) -> Result { - filesize(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Convert the size row to KB", - example: "ls | format filesize size KB", - result: None, - }, - Example { - description: "Convert the apparent row to B", - example: "du | format filesize apparent B", - result: None, - }, - ] - } -} - -fn process_row( - input: Value, - format: Tagged, - field: Arc, -) -> Result { - let replace_for = get_data_by_column_path(&input, &field, move |_, _, error| error); - match replace_for { - Ok(s) => { - if let Value { - value: UntaggedValue::Primitive(Filesize(fs)), - .. - } = s - { - let byte_format = InlineShape::format_bytes(fs, Some(&format.item)); - let byte_value = Value::from(byte_format.1); - Ok(input - .replace_data_at_column_path(&field, byte_value) - .expect( - "Given that the existence check was already done, this shouldn't trigger never", - )) - } else { - Err(ShellError::labeled_error( - "the data in this row is not of the type filesize", - "invalid datatype in row", - input.tag(), - )) - } - } - Err(e) => Err(e), - } -} - -fn filesize(args: CommandArgs) -> Result { - let field: ColumnPath = args.req(0)?; - let format: Tagged = args.req(1)?; - let field = Arc::new(field); - - Ok(args - .input - .flat_map(move |input| { - let format = format.clone(); - let field = field.clone(); - - match process_row(input, format, field) { - Ok(s) => Ok(s), - Err(e) => Err(e), - } - }) - .map(Ok) - .into_input_stream()) -} - -#[cfg(test)] -mod tests { - use super::FileSize; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(FileSize {}) - } -} diff --git a/crates/nu-command/src/commands/strings/format/mod.rs b/crates/nu-command/src/commands/strings/format/mod.rs deleted file mode 100644 index cd8267069c..0000000000 --- a/crates/nu-command/src/commands/strings/format/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod command; -pub mod format_filesize; - -pub use command::Format; -pub use format_filesize::FileSize; diff --git a/crates/nu-command/src/commands/strings/lines.rs b/crates/nu-command/src/commands/strings/lines.rs deleted file mode 100644 index 5d8a588dca..0000000000 --- a/crates/nu-command/src/commands/strings/lines.rs +++ /dev/null @@ -1,129 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Primitive, ReturnSuccess, Signature, UntaggedValue, Value}; -use parking_lot::Mutex; - -pub struct Lines; - -impl WholeStreamCommand for Lines { - fn name(&self) -> &str { - "lines" - } - - fn signature(&self) -> Signature { - Signature::build("lines") - } - - fn usage(&self) -> &str { - "Split single string into rows, one per line." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - lines(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Split multi-line string into lines", - example: r#"^echo "two\nlines" | lines"#, - result: None, - }] - } -} - -fn ends_with_line_ending(st: &str) -> bool { - let mut temp = st.to_string(); - let last = temp.pop(); - if let Some(c) = last { - c == '\n' - } else { - false - } -} - -fn lines(args: CommandArgs) -> Result { - let leftover_string = Arc::new(Mutex::new(String::new())); - let tag = args.name_tag(); - let name_span = tag.span; - - let eos = vec![UntaggedValue::Primitive(Primitive::EndOfStream).into_untagged_value()]; - - Ok(args - .input - .chain(eos) - .filter_map(move |item| { - let leftover_string = leftover_string.clone(); - match item { - Value { - value: UntaggedValue::Primitive(Primitive::String(st)), - .. - } => { - let mut leftover_string = leftover_string.lock(); - - let mut buffer = leftover_string.clone(); - buffer.push_str(&st); - - let mut lines: Vec = buffer.lines().map(|x| x.to_string()).collect(); - - leftover_string.clear(); - - if !ends_with_line_ending(&st) { - if let Some(last) = lines.pop() { - leftover_string.push_str(&last); - } - } - - if !lines.is_empty() { - let success_lines: Vec<_> = lines - .iter() - .map(|x| { - ReturnSuccess::value(UntaggedValue::string(x).into_untagged_value()) - }) - .collect(); - - Some(success_lines) - } else { - None - } - } - Value { - value: UntaggedValue::Primitive(Primitive::EndOfStream), - .. - } => { - let st = leftover_string.lock().clone(); - if !st.is_empty() { - Some(vec![ReturnSuccess::value( - UntaggedValue::string(st).into_untagged_value(), - )]) - } else { - None - } - } - Value { - tag: value_span, .. - } => Some(vec![Err(ShellError::labeled_error_with_secondary( - "Expected a string from pipeline", - "requires string input", - name_span, - "value originates from here", - value_span, - ))]), - } - }) - .flatten() - .into_action_stream()) -} - -#[cfg(test)] -mod tests { - use super::Lines; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Lines {}) - } -} diff --git a/crates/nu-command/src/commands/strings/mod.rs b/crates/nu-command/src/commands/strings/mod.rs deleted file mode 100644 index b92f5e2541..0000000000 --- a/crates/nu-command/src/commands/strings/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -mod build_string; -mod char_; -mod detect; -mod format; -mod lines; -mod parse; -mod size; -mod split; -mod split_by; -mod str_; - -pub use build_string::BuildString; -pub use char_::Char; -pub use detect::DetectColumns; -pub use format::*; -pub use lines::Lines; -pub use parse::*; -pub use size::Size; -pub use split::*; -pub use split_by::SplitBy; -pub use str_::*; diff --git a/crates/nu-command/src/commands/strings/parse/command.rs b/crates/nu-command/src/commands/strings/parse/command.rs deleted file mode 100644 index 656555d691..0000000000 --- a/crates/nu-command/src/commands/strings/parse/command.rs +++ /dev/null @@ -1,211 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, TaggedDictBuilder, UntaggedValue, Value}; -use nu_source::Tagged; - -use regex::{self, Regex}; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "parse" - } - - fn signature(&self) -> Signature { - Signature::build("parse") - .required( - "pattern", - SyntaxShape::String, - "the pattern to match. Eg) \"{foo}: {bar}\"", - ) - .switch("regex", "use full regex syntax for patterns", Some('r')) - } - - fn usage(&self) -> &str { - "Parse columns from string data using a simple pattern." - } - - fn run(&self, args: CommandArgs) -> Result { - operate(args) - } - - fn examples(&self) -> Vec { - let mut row = IndexMap::new(); - row.insert("foo".to_string(), Value::from("hi")); - row.insert("bar".to_string(), Value::from("there")); - vec![ - Example { - description: "Parse a string into two named columns", - example: "echo \"hi there\" | parse \"{foo} {bar}\"", - result: Some(vec![UntaggedValue::row(row.clone()).into()]), - }, - Example { - description: "Parse a string using regex pattern", - example: "echo \"hi there\" | parse -r \"(?P\\w+) (?P\\w+)\"", - result: Some(vec![UntaggedValue::row(row).into()]), - }, - ] - } -} - -pub fn operate(args: CommandArgs) -> Result { - let name_tag = args.call_info.name_tag.clone(); - - let pattern: Tagged = args.req(0)?; - let regex: bool = args.has_flag("regex"); - - let item_to_parse = if regex { - pattern.item.clone() - } else { - build_regex(&pattern.item, pattern.tag.clone())? - }; - - let regex_pattern = - Regex::new(&item_to_parse).map_err(|e| parse_regex_error(e, pattern.span()))?; - - let columns = column_names(®ex_pattern); - let mut parsed: VecDeque = VecDeque::new(); - - for v in args.input { - match v.as_string() { - Ok(s) => { - let results = regex_pattern.captures_iter(&s); - - for c in results { - let mut dict = TaggedDictBuilder::new(&v.tag); - - for (column_name, cap) in columns.iter().zip(c.iter().skip(1)) { - let cap_string = cap.map(|v| v.as_str()).unwrap_or("").to_string(); - dict.insert_untagged(column_name, UntaggedValue::string(cap_string)); - } - - parsed.push_back(dict.into_value()); - } - } - Err(_) => { - return Err(ShellError::labeled_error_with_secondary( - "Expected string input", - "expected string input", - &name_tag, - "value originated here", - v.tag, - )) - } - } - } - - Ok(parsed.into_iter().into_output_stream()) -} - -fn build_regex(input: &str, tag: Tag) -> Result { - let mut output = "(?s)\\A".to_string(); - - //let mut loop_input = input; - let mut loop_input = input.chars().peekable(); - loop { - let mut before = String::new(); - while let Some(c) = loop_input.next() { - if c == '{' { - // If '{{', still creating a plaintext parse command, but just for a single '{' char - if loop_input.peek() == Some(&'{') { - let _ = loop_input.next(); - } else { - break; - } - } - before.push(c); - } - - if !before.is_empty() { - output.push_str(®ex::escape(&before)); - } - - // Look for column as we're now at one - let mut column = String::new(); - while let Some(c) = loop_input.next() { - if c == '}' { - break; - } - column.push(c); - - if loop_input.peek().is_none() { - return Err(ShellError::labeled_error( - "Found opening `{` without an associated closing `}`", - "invalid parse pattern", - tag, - )); - } - } - - if !column.is_empty() { - output.push_str("(?P<"); - output.push_str(&column); - output.push_str(">.*?)"); - } - - if before.is_empty() && column.is_empty() { - break; - } - } - - output.push_str("\\z"); - Ok(output) -} - -fn column_names(regex: &Regex) -> Vec { - regex - .capture_names() - .enumerate() - .skip(1) - .map(|(i, name)| { - name.map(String::from) - .unwrap_or_else(|| format!("Capture{}", i)) - }) - .collect() -} - -fn parse_regex_error(e: regex::Error, base_span: Span) -> ShellError { - match e { - regex::Error::Syntax(msg) => { - let mut lines = msg.lines(); - - let main_msg = lines - .next() - .map(|l| l.replace(':', "")) - .expect("invalid regex pattern"); - - let span = lines.nth(1).and_then(|l| l.find('^')).map(|space| { - let start = base_span.start() + space - 3; - Span::for_char(start) - }); - - let msg = lines - .next() - .and_then(|l| l.split(':').nth(1)) - .map(|s| s.trim().to_string()); - - match (msg, span) { - (Some(msg), Some(span)) => { - ShellError::labeled_error_with_secondary(&msg, &msg, span, main_msg, span) - } - _ => ShellError::labeled_error("Invalid regex", "invalid regex", base_span), - } - } - _ => ShellError::labeled_error("Invalid regex", "invalid regex", base_span), - } -} - -#[cfg(test)] -mod tests { - use super::Command; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Command {}) - } -} diff --git a/crates/nu-command/src/commands/strings/parse/mod.rs b/crates/nu-command/src/commands/strings/parse/mod.rs deleted file mode 100644 index e7c608da74..0000000000 --- a/crates/nu-command/src/commands/strings/parse/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod command; - -pub use command::Command as Parse; diff --git a/crates/nu-command/src/commands/strings/size.rs b/crates/nu-command/src/commands/strings/size.rs deleted file mode 100644 index cd48623bd9..0000000000 --- a/crates/nu-command/src/commands/strings/size.rs +++ /dev/null @@ -1,126 +0,0 @@ -extern crate unicode_segmentation; - -use crate::prelude::*; -use indexmap::indexmap; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, TaggedDictBuilder, UntaggedValue, Value}; -use unicode_segmentation::UnicodeSegmentation; - -pub struct Size; - -impl WholeStreamCommand for Size { - fn name(&self) -> &str { - "size" - } - - fn signature(&self) -> Signature { - Signature::build("size") - } - - fn usage(&self) -> &str { - "Gather word count statistics on the text." - } - - fn run(&self, args: CommandArgs) -> Result { - size(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Count the number of words in a string", - example: r#"echo "There are seven words in this sentence" | size"#, - result: Some(vec![UntaggedValue::row(indexmap! { - "lines".to_string() => UntaggedValue::int(0).into(), - "words".to_string() => UntaggedValue::int(7).into(), - "chars".to_string() => UntaggedValue::int(38).into(), - "bytes".to_string() => UntaggedValue::int(38).into(), - }) - .into()]), - }, - Example { - description: "Counts Unicode characters correctly in a string", - example: r#"echo "Amélie Amelie" | size"#, - result: Some(vec![UntaggedValue::row(indexmap! { - "lines".to_string() => UntaggedValue::int(0).into(), - "words".to_string() => UntaggedValue::int(2).into(), - "chars".to_string() => UntaggedValue::int(13).into(), - "bytes".to_string() => UntaggedValue::int(15).into(), - }) - .into()]), - }, - ] - } -} - -fn size(args: CommandArgs) -> Result { - let input = args.input; - let tag = args.call_info.name_tag; - let name_span = tag.span; - - Ok(input - .map(move |v| { - if let Ok(s) = v.as_string() { - Ok(count(&s, &v.tag)) - } else { - Err(ShellError::labeled_error_with_secondary( - "Expected a string from pipeline", - "requires string input", - name_span, - "value originates from here", - v.tag.span, - )) - } - }) - .into_input_stream()) -} - -fn count(contents: &str, tag: impl Into) -> Value { - let mut lines: i64 = 0; - let mut words: i64 = 0; - let mut chars: i64 = 0; - let bytes = contents.len() as i64; - let mut end_of_word = true; - - for c in UnicodeSegmentation::graphemes(contents, true) { - chars += 1; - - match c { - "\n" => { - lines += 1; - end_of_word = true; - } - " " => end_of_word = true, - _ => { - if end_of_word { - words += 1; - } - end_of_word = false; - } - } - } - - let mut dict = TaggedDictBuilder::new(tag); - //TODO: add back in name when we have it in the tag - //dict.insert("name", value::string(name)); - dict.insert_untagged("lines", UntaggedValue::int(lines)); - dict.insert_untagged("words", UntaggedValue::int(words)); - dict.insert_untagged("chars", UntaggedValue::int(chars)); - dict.insert_untagged("bytes", UntaggedValue::int(bytes)); - - dict.into_value() -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::Size; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Size {}) - } -} diff --git a/crates/nu-command/src/commands/strings/split/chars.rs b/crates/nu-command/src/commands/strings/split/chars.rs deleted file mode 100644 index dda836bce5..0000000000 --- a/crates/nu-command/src/commands/strings/split/chars.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ReturnSuccess, Signature, Value}; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "split chars" - } - - fn signature(&self) -> Signature { - Signature::build("split chars") - } - - fn usage(&self) -> &str { - "splits a string's characters into separate rows" - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - Ok(split_chars(args)) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Split the string's characters into separate rows", - example: "echo 'hello' | split chars", - result: Some(vec![ - Value::from("h"), - Value::from("e"), - Value::from("l"), - Value::from("l"), - Value::from("o"), - ]), - }] - } -} - -fn split_chars(args: CommandArgs) -> ActionStream { - let name = args.call_info.name_tag.clone(); - let input = args.input; - input - .flat_map(move |v| { - if let Ok(s) = v.as_string() { - s.chars() - .collect::>() - .into_iter() - .map(move |x| ReturnSuccess::value(Value::from(x.to_string()))) - .into_action_stream() - } else { - ActionStream::one(Err(ShellError::labeled_error_with_secondary( - "Expected a string from pipeline", - "requires string input", - name.span, - "value originates from here", - v.tag.span, - ))) - } - }) - .into_action_stream() -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/strings/split/column.rs b/crates/nu-command/src/commands/strings/split/column.rs deleted file mode 100644 index d8c7a4665f..0000000000 --- a/crates/nu-command/src/commands/strings/split/column.rs +++ /dev/null @@ -1,111 +0,0 @@ -use crate::prelude::*; -use log::trace; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - Primitive, ReturnSuccess, Signature, SyntaxShape, TaggedDictBuilder, UntaggedValue, -}; -use nu_source::Tagged; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "split column" - } - - fn signature(&self) -> Signature { - Signature::build("split column") - .required( - "separator", - SyntaxShape::String, - "the character that denotes what separates columns", - ) - .switch("collapse-empty", "remove empty columns", Some('c')) - .rest( - "rest", - SyntaxShape::String, - "column names to give the new columns", - ) - } - - fn usage(&self) -> &str { - "splits contents across multiple columns via the separator." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - split_column(args) - } -} - -fn split_column(args: CommandArgs) -> Result { - let name_span = args.call_info.name_tag.span; - let separator: Tagged = args.req(0)?; - let rest: Vec> = args.rest(1)?; - let collapse_empty = args.has_flag("collapse-empty"); - let input = args.input; - - Ok(input - .map(move |v| { - if let Ok(s) = v.as_string() { - let splitter = separator.replace("\\n", "\n"); - trace!("splitting with {:?}", splitter); - - let split_result: Vec<_> = if collapse_empty { - s.split(&splitter).filter(|s| !s.is_empty()).collect() - } else { - s.split(&splitter).collect() - }; - - trace!("split result = {:?}", split_result); - - let positional: Vec<_> = rest.iter().map(|f| f.item.clone()).collect(); - - // If they didn't provide column names, make up our own - if positional.is_empty() { - let mut gen_columns = vec![]; - for i in 0..split_result.len() { - gen_columns.push(format!("Column{}", i + 1)); - } - - let mut dict = TaggedDictBuilder::new(&v.tag); - for (&k, v) in split_result.iter().zip(&gen_columns) { - dict.insert_untagged(v.clone(), Primitive::String(k.into())); - } - - ReturnSuccess::value(dict.into_value()) - } else { - let mut dict = TaggedDictBuilder::new(&v.tag); - for (&k, v) in split_result.iter().zip(&positional) { - dict.insert_untagged( - v, - UntaggedValue::Primitive(Primitive::String(k.into())), - ); - } - ReturnSuccess::value(dict.into_value()) - } - } else { - Err(ShellError::labeled_error_with_secondary( - "Expected a string from pipeline", - "requires string input", - name_span, - "value originates from here", - v.tag.span, - )) - } - }) - .into_action_stream()) -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/strings/split/command.rs b/crates/nu-command/src/commands/strings/split/command.rs deleted file mode 100644 index 2e9955326a..0000000000 --- a/crates/nu-command/src/commands/strings/split/command.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ReturnSuccess, Signature, UntaggedValue}; - -#[derive(Clone)] -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "split" - } - - fn signature(&self) -> Signature { - Signature::build("split") - } - - fn usage(&self) -> &str { - "Split contents across desired subcommand (like row, column) via the separator." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - Ok(ActionStream::one(Ok(ReturnSuccess::Value( - UntaggedValue::string(get_full_help(&Command, args.scope())).into_value(Tag::unknown()), - )))) - } -} - -#[cfg(test)] -mod tests { - use super::Command; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Command {}) - } -} diff --git a/crates/nu-command/src/commands/strings/split/mod.rs b/crates/nu-command/src/commands/strings/split/mod.rs deleted file mode 100644 index 18bc0717ac..0000000000 --- a/crates/nu-command/src/commands/strings/split/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub mod chars; -pub mod column; -pub mod command; -pub mod row; - -pub use chars::SubCommand as SplitChars; -pub use column::SubCommand as SplitColumn; -pub use command::Command as Split; -pub use row::SubCommand as SplitRow; diff --git a/crates/nu-command/src/commands/strings/split/row.rs b/crates/nu-command/src/commands/strings/split/row.rs deleted file mode 100644 index a86716703c..0000000000 --- a/crates/nu-command/src/commands/strings/split/row.rs +++ /dev/null @@ -1,86 +0,0 @@ -use crate::prelude::*; -use log::trace; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue}; -use nu_source::Tagged; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "split row" - } - - fn signature(&self) -> Signature { - Signature::build("split row").required( - "separator", - SyntaxShape::String, - "the character that denotes what separates rows", - ) - } - - fn usage(&self) -> &str { - "splits contents over multiple rows via the separator." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - split_row(args) - } -} - -fn split_row(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - - let separator: Tagged = args.req(0)?; - let input = args.input; - - Ok(input - .flat_map(move |v| { - if let Ok(s) = v.as_string() { - let splitter = separator.item.replace("\\n", "\n"); - trace!("splitting with {:?}", splitter); - let split_result: Vec = s - .split(&splitter) - .filter_map(|s| { - if s.trim() != "" { - Some(s.to_string()) - } else { - None - } - }) - .collect(); - - trace!("split result = {:?}", split_result); - - (split_result.into_iter().map(move |s| { - ReturnSuccess::value( - UntaggedValue::Primitive(Primitive::String(s)).into_value(&v.tag), - ) - })) - .into_action_stream() - } else { - ActionStream::one(Err(ShellError::labeled_error_with_secondary( - "Expected a string from pipeline", - "requires string input", - name.span, - "value originates from here", - v.tag.span, - ))) - } - }) - .into_action_stream()) -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/strings/split_by.rs b/crates/nu-command/src/commands/strings/split_by.rs deleted file mode 100644 index 069ef2ac83..0000000000 --- a/crates/nu-command/src/commands/strings/split_by.rs +++ /dev/null @@ -1,166 +0,0 @@ -use crate::prelude::*; -use crate::utils::suggestions::suggestions; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ReturnSuccess, Signature, SyntaxShape, Value}; -use nu_source::Tagged; -use nu_value_ext::as_string; - -pub struct SplitBy; - -impl WholeStreamCommand for SplitBy { - fn name(&self) -> &str { - "split-by" - } - - fn signature(&self) -> Signature { - Signature::build("split-by").optional( - "column_name", - SyntaxShape::String, - "the name of the column within the nested table to split by", - ) - } - - fn usage(&self) -> &str { - "Creates a new table with the data from the inner tables split by the column given." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - split_by(args) - } -} - -pub fn split_by(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - let column_name: Option> = args.opt(0)?; - - let values: Vec = args.input.collect(); - - if values.len() > 1 || values.is_empty() { - return Err(ShellError::labeled_error( - "Expected table from pipeline", - "requires a table input", - name, - )); - } - - let split = split(&column_name, &values[0], &name)?; - Ok(ActionStream::one(ReturnSuccess::value(split))) -} - -enum Grouper { - ByColumn(Option>), -} - -pub fn split( - column_name: &Option>, - values: &Value, - tag: impl Into, -) -> Result { - 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)), - } - }); - - nu_data::utils::split(values, &Some(block), &name) - } - Grouper::ByColumn(None) => { - let block = Box::new(move |_, row: &Value| as_string(row)); - - nu_data::utils::split(values, &Some(block), &name) - } - } -} - -#[cfg(test)] -mod tests { - use super::split; - use super::ShellError; - use nu_data::utils::helpers::committers_grouped_by_date; - use nu_protocol::UntaggedValue; - use nu_source::*; - use nu_test_support::value::{date, int, row, string, table}; - - #[test] - fn splits_inner_tables_by_key() { - let for_key = Some(String::from("country").tagged_unknown()); - - assert_eq!( - split(&for_key, &committers_grouped_by_date(), Tag::unknown()).unwrap(), - UntaggedValue::row(indexmap! { - "EC".into() => row(indexmap! { - "2019-07-23".into() => table(&[ - row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => date("2019-07-23"), "chickens".into() => int(10)}) - ]), - "2019-09-24".into() => table(&[ - row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => date("2019-09-24"), "chickens".into() => int(20)}) - ]), - "2019-10-10".into() => table(&[ - row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => date("2019-10-10"), "chickens".into() => int(30)}) - ]) - }), - "NZ".into() => row(indexmap! { - "2019-07-23".into() => table(&[ - row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => date("2019-07-23"), "chickens".into() => int(5)}) - ]), - "2019-09-24".into() => table(&[ - row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => date("2019-09-24"), "chickens".into() => int(10)}) - ]), - "2019-10-10".into() => table(&[ - row(indexmap!{"name".into() => string("JT"), "country".into() => string("NZ"), "date".into() => date("2019-10-10"), "chickens".into() => int(15)}) - ]) - }), - "US".into() => row(indexmap! { - "2019-07-23".into() => table(&[ - row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => date("2019-07-23"), "chickens".into() => int(2)}) - ]), - "2019-09-24".into() => table(&[ - row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => date("2019-09-24"), "chickens".into() => int(4)}) - ]), - "2019-10-10".into() => table(&[ - row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => date("2019-10-10"), "chickens".into() => int(6)}) - ]) - }) - }).into_untagged_value() - ); - } - - #[test] - fn errors_if_key_within_some_inner_table_is_missing() { - let for_key = Some(String::from("country").tagged_unknown()); - - let nu_releases = row(indexmap! { - "2019-07-23".into() => table(&[ - row(indexmap!{"name".into() => string("AR"), "country".into() => string("EC"), "date".into() => string("2019-07-23")}) - ]), - "2019-09-24".into() => table(&[ - row(indexmap!{"name".into() => UntaggedValue::string("JT").into_value(Tag::from(Span::new(5,10))), "date".into() => string("2019-09-24")}) - ]), - "October 10-2019".into() => table(&[ - row(indexmap!{"name".into() => string("YK"), "country".into() => string("US"), "date".into() => string("October 10-2019")}) - ]) - }); - - assert!(split(&for_key, &nu_releases, Tag::from(Span::new(5, 10))).is_err()); - } - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use super::SplitBy; - use crate::examples::test as test_examples; - - test_examples(SplitBy {}) - } -} diff --git a/crates/nu-command/src/commands/strings/str_/capitalize.rs b/crates/nu-command/src/commands/strings/str_/capitalize.rs deleted file mode 100644 index 176aa164a3..0000000000 --- a/crates/nu-command/src/commands/strings/str_/capitalize.rs +++ /dev/null @@ -1,123 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::ShellTypeName; -use nu_protocol::{ColumnPath, Primitive, Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tag; -use nu_value_ext::ValueExt; - -struct Arguments { - column_paths: Vec, -} - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "str capitalize" - } - - fn signature(&self) -> Signature { - Signature::build("str capitalize").rest( - "rest", - SyntaxShape::ColumnPath, - "optionally capitalize text by column paths", - ) - } - - fn usage(&self) -> &str { - "capitalizes text" - } - - fn run(&self, args: CommandArgs) -> Result { - operate(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Capitalize contents", - example: "echo 'good day' | str capitalize", - result: Some(vec![Value::from("Good day")]), - }] - } -} - -fn operate(args: CommandArgs) -> Result { - let (options, input) = ( - Arguments { - column_paths: args.rest(0)?, - }, - args.input, - ); - - Ok(input - .map(move |v| { - if options.column_paths.is_empty() { - action(&v, v.tag()) - } else { - let mut ret = v; - - for path in &options.column_paths { - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| action(old, old.tag())), - )?; - } - - Ok(ret) - } - }) - .into_input_stream()) -} - -fn action(input: &Value, tag: impl Into) -> Result { - match &input.value { - UntaggedValue::Primitive(Primitive::String(s)) => { - let mut capitalized = String::new(); - - for (idx, character) in s.chars().enumerate() { - let out = if idx == 0 { - character.to_uppercase().to_string() - } else { - character.to_lowercase().to_string() - }; - - capitalized.push_str(&out); - } - - Ok(UntaggedValue::string(capitalized).into_value(tag)) - } - other => { - let got = format!("got {}", other.type_name()); - Err(ShellError::labeled_error( - "value is not string", - got, - tag.into().span, - )) - } - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::{action, SubCommand}; - use nu_source::Tag; - use nu_test_support::value::string; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } - - #[test] - fn capitalizes() { - let word = string("andres"); - let expected = string("Andres"); - - let actual = action(&word, Tag::unknown()).unwrap(); - assert_eq!(actual, expected); - } -} diff --git a/crates/nu-command/src/commands/strings/str_/case/camel_case.rs b/crates/nu-command/src/commands/strings/str_/case/camel_case.rs deleted file mode 100644 index db1b9ac62f..0000000000 --- a/crates/nu-command/src/commands/strings/str_/case/camel_case.rs +++ /dev/null @@ -1,70 +0,0 @@ -use super::{operate, to_lower_camel_case}; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, Value}; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "str camel-case" - } - - fn signature(&self) -> Signature { - Signature::build("str camel-case").rest( - "rest", - SyntaxShape::ColumnPath, - "optionally convert text to camelCase by column paths", - ) - } - - fn usage(&self) -> &str { - "converts a string to camelCase" - } - - fn run(&self, args: CommandArgs) -> Result { - operate(args, &to_lower_camel_case) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "convert a string to camelCase", - example: "echo 'NuShell' | str camel-case", - result: Some(vec![Value::from("nuShell")]), - }] - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::{to_lower_camel_case, SubCommand}; - use crate::commands::strings::str_::case::action; - use nu_source::Tag; - use nu_test_support::value::string; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } - - #[test] - fn camel_case_from_kebab() { - let word = string("this-is-the-first-case"); - let expected = string("thisIsTheFirstCase"); - - let actual = action(&word, Tag::unknown(), &to_lower_camel_case).unwrap(); - assert_eq!(actual, expected); - } - #[test] - fn camel_case_from_snake() { - let word = string("this_is_the_second_case"); - let expected = string("thisIsTheSecondCase"); - - let actual = action(&word, Tag::unknown(), &to_lower_camel_case).unwrap(); - assert_eq!(actual, expected); - } -} diff --git a/crates/nu-command/src/commands/strings/str_/case/kebab_case.rs b/crates/nu-command/src/commands/strings/str_/case/kebab_case.rs deleted file mode 100644 index 9fff038658..0000000000 --- a/crates/nu-command/src/commands/strings/str_/case/kebab_case.rs +++ /dev/null @@ -1,70 +0,0 @@ -use super::{operate, to_kebab_case}; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, Value}; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "str kebab-case" - } - - fn signature(&self) -> Signature { - Signature::build("str kebab-case").rest( - "rest", - SyntaxShape::ColumnPath, - "optionally convert text to kebab-case by column paths", - ) - } - - fn usage(&self) -> &str { - "converts a string to kebab-case" - } - - fn run(&self, args: CommandArgs) -> Result { - operate(args, &to_kebab_case) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "convert a string to kebab-case", - example: "echo 'NuShell' | str kebab-case", - result: Some(vec![Value::from("nu-shell")]), - }] - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::{to_kebab_case, SubCommand}; - use crate::commands::strings::str_::case::action; - use nu_source::Tag; - use nu_test_support::value::string; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } - - #[test] - fn kebab_case_from_camel() { - let word = string("thisIsTheFirstCase"); - let expected = string("this-is-the-first-case"); - - let actual = action(&word, Tag::unknown(), &to_kebab_case).unwrap(); - assert_eq!(actual, expected); - } - #[test] - fn kebab_case_from_screaming_snake() { - let word = string("THIS_IS_THE_SECOND_CASE"); - let expected = string("this-is-the-second-case"); - - let actual = action(&word, Tag::unknown(), &to_kebab_case).unwrap(); - assert_eq!(actual, expected); - } -} diff --git a/crates/nu-command/src/commands/strings/str_/case/mod.rs b/crates/nu-command/src/commands/strings/str_/case/mod.rs deleted file mode 100644 index 86831443fd..0000000000 --- a/crates/nu-command/src/commands/strings/str_/case/mod.rs +++ /dev/null @@ -1,93 +0,0 @@ -pub mod camel_case; -pub mod kebab_case; -pub mod pascal_case; -pub mod screaming_snake_case; -pub mod snake_case; - -use crate::prelude::*; -use nu_errors::ShellError; -use nu_protocol::ShellTypeName; -use nu_protocol::{ColumnPath, Primitive, UntaggedValue, Value}; -use nu_source::Tag; -use nu_value_ext::ValueExt; - -pub use camel_case::SubCommand as CamelCase; -pub use pascal_case::SubCommand as PascalCase; -pub use screaming_snake_case::SubCommand as ScreamingSnakeCase; -pub use snake_case::SubCommand as SnakeCase; - -use heck::ToKebabCase; -use heck::ToLowerCamelCase; -use heck::ToShoutySnakeCase; -use heck::ToSnakeCase; -use heck::ToUpperCamelCase; -macro_rules! create_heck_function { - ($func_name:ident) => { - pub fn $func_name(a_slice: &str) -> String { - a_slice.$func_name() - } - }; -} -create_heck_function!(to_upper_camel_case); -create_heck_function!(to_lower_camel_case); -create_heck_function!(to_kebab_case); -create_heck_function!(to_shouty_snake_case); -create_heck_function!(to_snake_case); - -struct Arguments { - column_paths: Vec, -} - -pub fn operate(args: CommandArgs, case_operation: &'static F) -> Result -where - F: Fn(&str) -> String + Send + Sync + 'static, -{ - let (options, input) = ( - Arguments { - column_paths: args.rest(0)?, - }, - args.input, - ); - - Ok(input - .map(move |v| { - if options.column_paths.is_empty() { - action(&v, v.tag(), &case_operation) - } else { - let mut ret = v; - - for path in &options.column_paths { - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| action(old, old.tag(), &case_operation)), - )?; - } - - Ok(ret) - } - }) - .into_input_stream()) -} - -pub fn action( - input: &Value, - tag: impl Into, - case_operation: &F, -) -> Result -where - F: Fn(&str) -> String + Send + Sync + 'static, -{ - match &input.value { - UntaggedValue::Primitive(Primitive::String(s)) => { - Ok(UntaggedValue::string(case_operation(s)).into_value(tag)) - } - other => { - let got = format!("got {}", other.type_name()); - Err(ShellError::labeled_error( - "value is not string", - got, - tag.into().span, - )) - } - } -} diff --git a/crates/nu-command/src/commands/strings/str_/case/pascal_case.rs b/crates/nu-command/src/commands/strings/str_/case/pascal_case.rs deleted file mode 100644 index 2a32980836..0000000000 --- a/crates/nu-command/src/commands/strings/str_/case/pascal_case.rs +++ /dev/null @@ -1,70 +0,0 @@ -use super::{operate, to_upper_camel_case}; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, Value}; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "str pascal-case" - } - - fn signature(&self) -> Signature { - Signature::build("str pascal-case").rest( - "rest", - SyntaxShape::ColumnPath, - "optionally convert text to PascalCase by column paths", - ) - } - - fn usage(&self) -> &str { - "converts a string to PascalCase" - } - - fn run(&self, args: CommandArgs) -> Result { - operate(args, &to_upper_camel_case) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "convert a string to PascalCase", - example: "echo 'nu-shell' | str pascal-case", - result: Some(vec![Value::from("NuShell")]), - }] - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::{to_upper_camel_case, SubCommand}; - use crate::commands::strings::str_::case::action; - use nu_source::Tag; - use nu_test_support::value::string; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } - - #[test] - fn pascal_case_from_kebab() { - let word = string("this-is-the-first-case"); - let expected = string("ThisIsTheFirstCase"); - - let actual = action(&word, Tag::unknown(), &to_upper_camel_case).unwrap(); - assert_eq!(actual, expected); - } - #[test] - fn pascal_case_from_snake() { - let word = string("this_is_the_second_case"); - let expected = string("ThisIsTheSecondCase"); - - let actual = action(&word, Tag::unknown(), &to_upper_camel_case).unwrap(); - assert_eq!(actual, expected); - } -} diff --git a/crates/nu-command/src/commands/strings/str_/case/screaming_snake_case.rs b/crates/nu-command/src/commands/strings/str_/case/screaming_snake_case.rs deleted file mode 100644 index 7fc4c3dcf3..0000000000 --- a/crates/nu-command/src/commands/strings/str_/case/screaming_snake_case.rs +++ /dev/null @@ -1,70 +0,0 @@ -use super::{operate, to_shouty_snake_case}; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, Value}; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "str screaming-snake-case" - } - - fn signature(&self) -> Signature { - Signature::build("str screaming-snake-case").rest( - "rest", - SyntaxShape::ColumnPath, - "optionally convert text to SCREAMING_SNAKE_CASE by column paths", - ) - } - - fn usage(&self) -> &str { - "converts a string to SCREAMING_SNAKE_CASE" - } - - fn run(&self, args: CommandArgs) -> Result { - operate(args, &to_shouty_snake_case) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "convert a string to SCREAMING_SNAKE_CASE", - example: "echo 'NuShell' | str screaming-snake-case", - result: Some(vec![Value::from("NU_SHELL")]), - }] - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::{to_shouty_snake_case, SubCommand}; - use crate::commands::strings::str_::case::action; - use nu_source::Tag; - use nu_test_support::value::string; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } - - #[test] - fn snake_case_from_kebab() { - let word = string("this-is-the-first-case"); - let expected = string("THIS_IS_THE_FIRST_CASE"); - - let actual = action(&word, Tag::unknown(), &to_shouty_snake_case).unwrap(); - assert_eq!(actual, expected); - } - #[test] - fn snake_case_from_snake() { - let word = string("this_is_the_second_case"); - let expected = string("THIS_IS_THE_SECOND_CASE"); - - let actual = action(&word, Tag::unknown(), &to_shouty_snake_case).unwrap(); - assert_eq!(actual, expected); - } -} diff --git a/crates/nu-command/src/commands/strings/str_/case/snake_case.rs b/crates/nu-command/src/commands/strings/str_/case/snake_case.rs deleted file mode 100644 index b2d8655202..0000000000 --- a/crates/nu-command/src/commands/strings/str_/case/snake_case.rs +++ /dev/null @@ -1,70 +0,0 @@ -use super::{operate, to_snake_case}; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, Value}; - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "str snake-case" - } - - fn signature(&self) -> Signature { - Signature::build("str snake-case").rest( - "rest", - SyntaxShape::ColumnPath, - "optionally convert text to snake_case by column paths", - ) - } - - fn usage(&self) -> &str { - "converts a string to snake_case" - } - - fn run(&self, args: CommandArgs) -> Result { - operate(args, &to_snake_case) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "convert a string to snake_case", - example: "echo 'NuShell' | str snake-case", - result: Some(vec![Value::from("nu_shell")]), - }] - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::{to_snake_case, SubCommand}; - use crate::commands::strings::str_::case::action; - use nu_source::Tag; - use nu_test_support::value::string; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } - - #[test] - fn snake_case_from_kebab() { - let word = string("this-is-the-first-case"); - let expected = string("this_is_the_first_case"); - - let actual = action(&word, Tag::unknown(), &to_snake_case).unwrap(); - assert_eq!(actual, expected); - } - #[test] - fn snake_case_from_camel() { - let word = string("thisIsTheSecondCase"); - let expected = string("this_is_the_second_case"); - - let actual = action(&word, Tag::unknown(), &to_snake_case).unwrap(); - assert_eq!(actual, expected); - } -} diff --git a/crates/nu-command/src/commands/strings/str_/collect.rs b/crates/nu-command/src/commands/strings/str_/collect.rs deleted file mode 100644 index d1d7324656..0000000000 --- a/crates/nu-command/src/commands/strings/str_/collect.rs +++ /dev/null @@ -1,108 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tagged; - -pub struct SubCommand; - -struct Arguments { - separator: Option>, -} - -impl Arguments { - pub fn separator(&self) -> &str { - self.separator.as_ref().map_or("", |sep| &sep.item) - } -} - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "str collect" - } - - fn signature(&self) -> Signature { - Signature::build("str collect").desc(self.usage()).optional( - "separator", - SyntaxShape::String, - "the separator to put between the different values", - ) - } - - fn usage(&self) -> &str { - "collects a list of strings into a string" - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - collect(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Collect a list of string", - example: "echo ['a' 'b' 'c'] | str collect", - result: Some(vec![Value::from("abc")]), - }] - } -} - -pub fn collect(args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - - let (options, input) = ( - Arguments { - separator: args.opt(0)?, - }, - args.input, - ); - - let separator = options.separator(); - - let strings: Vec> = input.map(|value| value.as_string()).collect(); - let strings: Result, _> = strings.into_iter().collect::>(); - - match strings { - Ok(strings) => { - let output = strings.join(separator); - - Ok(ActionStream::one(ReturnSuccess::value( - UntaggedValue::string(output).into_value(tag), - ))) - } - Err(err) => match err.error { - nu_errors::ProximateShellError::TypeError { actual, .. } => { - if let Some(item) = actual.item { - Err(ShellError::labeled_error_with_secondary( - "could not convert to string", - format!("tried to convert '{}' in input to a string", item), - tag.span, - format!("'{}' value originated here", item), - actual.span, - )) - } else { - Err(ShellError::labeled_error_with_secondary( - "could not convert to string", - "failed to convert input to strings", - tag.span, - "non-string found here", - actual.span, - )) - } - } - _ => Err(err), - }, - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/strings/str_/command.rs b/crates/nu-command/src/commands/strings/str_/command.rs deleted file mode 100644 index f2063ec0bf..0000000000 --- a/crates/nu-command/src/commands/strings/str_/command.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ReturnSuccess, Signature, SyntaxShape, UntaggedValue}; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "str" - } - - fn signature(&self) -> Signature { - Signature::build("str").rest( - "rest", - SyntaxShape::ColumnPath, - "optionally convert by column paths", - ) - } - - fn usage(&self) -> &str { - "Apply string function." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - Ok(ActionStream::one(ReturnSuccess::value( - UntaggedValue::string(get_full_help(&Command, args.scope())).into_value(Tag::unknown()), - ))) - } -} - -#[cfg(test)] -mod tests { - use super::Command; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Command {}) - } -} diff --git a/crates/nu-command/src/commands/strings/str_/contains.rs b/crates/nu-command/src/commands/strings/str_/contains.rs deleted file mode 100644 index 9660419e68..0000000000 --- a/crates/nu-command/src/commands/strings/str_/contains.rs +++ /dev/null @@ -1,199 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::ShellTypeName; -use nu_protocol::{ - ColumnPath, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value, -}; -use nu_source::{Tag, Tagged}; -use nu_value_ext::ValueExt; - -struct Arguments { - pattern: Tagged, - insensitive: bool, - column_paths: Vec, -} - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "str contains" - } - - fn signature(&self) -> Signature { - Signature::build("str contains") - .required("pattern", SyntaxShape::String, "the pattern to find") - .rest( - "rest", - SyntaxShape::ColumnPath, - "optionally check if string contains pattern by column paths", - ) - .switch("insensitive", "search is case insensitive", Some('i')) - } - - fn usage(&self) -> &str { - "Checks if string contains pattern" - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - operate(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Check if string contains pattern", - example: "echo 'my_library.rb' | str contains '.rb'", - result: Some(vec![UntaggedValue::boolean(true).into_untagged_value()]), - }, - Example { - description: "Check if string contains pattern case insensitive", - example: "echo 'my_library.rb' | str contains -i '.RB'", - result: Some(vec![UntaggedValue::boolean(true).into_untagged_value()]), - }, - ] - } -} - -fn operate(args: CommandArgs) -> Result { - let (options, input) = ( - Arc::new(Arguments { - pattern: args.req(0)?, - insensitive: args.has_flag("insensitive"), - column_paths: args.rest(1)?, - }), - args.input, - ); - - Ok(input - .map(move |v| { - if options.column_paths.is_empty() { - ReturnSuccess::value(action(&v, &options, v.tag())?) - } else { - let mut ret = v; - - for path in &options.column_paths { - let options = options.clone(); - - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| action(old, &options, old.tag())), - )?; - } - - ReturnSuccess::value(ret) - } - }) - .into_action_stream()) -} - -fn action( - input: &Value, - Arguments { - ref pattern, - insensitive, - .. - }: &Arguments, - tag: impl Into, -) -> Result { - match &input.value { - UntaggedValue::Primitive(Primitive::String(s)) => { - let contains = if *insensitive { - s.to_lowercase().contains(&pattern.to_lowercase()) - } else { - s.contains(&pattern.item) - }; - - Ok(UntaggedValue::boolean(contains).into_value(tag)) - } - other => { - let got = format!("got {}", other.type_name()); - Err(ShellError::labeled_error( - "value is not string", - got, - tag.into().span, - )) - } - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::{action, Arguments, SubCommand}; - use nu_protocol::UntaggedValue; - use nu_source::{Tag, TaggedItem}; - use nu_test_support::value::string; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } - - #[test] - fn string_contains_other_string_case_sensitive() { - let word = string("Cargo.tomL"); - - let options = Arguments { - pattern: String::from(".tomL").tagged_unknown(), - insensitive: false, - column_paths: vec![], - }; - - let expected = UntaggedValue::boolean(true).into_untagged_value(); - let actual = action(&word, &options, Tag::unknown()).unwrap(); - - assert_eq!(actual, expected); - } - - #[test] - fn string_does_not_contain_other_string_case_sensitive() { - let word = string("Cargo.tomL"); - - let options = Arguments { - pattern: String::from("Lomt.").tagged_unknown(), - insensitive: false, - column_paths: vec![], - }; - - let expected = UntaggedValue::boolean(false).into_untagged_value(); - let actual = action(&word, &options, Tag::unknown()).unwrap(); - - assert_eq!(actual, expected); - } - - #[test] - fn string_contains_other_string_case_insensitive() { - let word = string("Cargo.ToMl"); - - let options = Arguments { - pattern: String::from(".TOML").tagged_unknown(), - insensitive: true, - column_paths: vec![], - }; - - let expected = UntaggedValue::boolean(true).into_untagged_value(); - let actual = action(&word, &options, Tag::unknown()).unwrap(); - - assert_eq!(actual, expected); - } - - #[test] - fn string_does_not_contain_other_string_case_insensitive() { - let word = string("Cargo.tOml"); - - let options = Arguments { - pattern: String::from("lomt.").tagged_unknown(), - insensitive: true, - column_paths: vec![], - }; - - let expected = UntaggedValue::boolean(false).into_untagged_value(); - let actual = action(&word, &options, Tag::unknown()).unwrap(); - - assert_eq!(actual, expected); - } -} diff --git a/crates/nu-command/src/commands/strings/str_/downcase.rs b/crates/nu-command/src/commands/strings/str_/downcase.rs deleted file mode 100644 index 5d1bfe4cf3..0000000000 --- a/crates/nu-command/src/commands/strings/str_/downcase.rs +++ /dev/null @@ -1,112 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::ShellTypeName; -use nu_protocol::{ - ColumnPath, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value, -}; -use nu_source::Tag; -use nu_value_ext::ValueExt; - -struct Arguments { - column_paths: Vec, -} - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "str downcase" - } - - fn signature(&self) -> Signature { - Signature::build("str downcase").rest( - "rest", - SyntaxShape::ColumnPath, - "optionally downcase text by column paths", - ) - } - - fn usage(&self) -> &str { - "downcases text" - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - operate(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Downcase contents", - example: "echo 'NU' | str downcase", - result: Some(vec![Value::from("nu")]), - }] - } -} - -fn operate(args: CommandArgs) -> Result { - let (options, input) = ( - Arguments { - column_paths: args.rest(0)?, - }, - args.input, - ); - - Ok(input - .map(move |v| { - if options.column_paths.is_empty() { - ReturnSuccess::value(action(&v, v.tag())?) - } else { - let mut ret = v; - - for path in &options.column_paths { - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| action(old, old.tag())), - )?; - } - - ReturnSuccess::value(ret) - } - }) - .into_action_stream()) -} - -fn action(input: &Value, tag: impl Into) -> Result { - match &input.value { - UntaggedValue::Primitive(Primitive::String(s)) => { - Ok(UntaggedValue::string(s.to_ascii_lowercase()).into_value(tag)) - } - other => { - let got = format!("got {}", other.type_name()); - Err(ShellError::labeled_error( - "value is not string", - got, - tag.into().span, - )) - } - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::{action, SubCommand}; - use nu_source::Tag; - use nu_test_support::value::string; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } - - #[test] - fn downcases() { - let word = string("ANDRES"); - - let actual = action(&word, Tag::unknown()).unwrap(); - assert_eq!(actual, string("andres")); - } -} diff --git a/crates/nu-command/src/commands/strings/str_/ends_with.rs b/crates/nu-command/src/commands/strings/str_/ends_with.rs deleted file mode 100644 index 648c8f6f4a..0000000000 --- a/crates/nu-command/src/commands/strings/str_/ends_with.rs +++ /dev/null @@ -1,132 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::ShellTypeName; -use nu_protocol::{ - ColumnPath, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value, -}; -use nu_source::{Tag, Tagged}; -use nu_value_ext::ValueExt; - -pub struct SubCommand; - -struct Arguments { - pattern: Tagged, - column_paths: Vec, -} - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "str ends-with" - } - - fn signature(&self) -> Signature { - Signature::build("str ends-with") - .required("pattern", SyntaxShape::String, "the pattern to match") - .rest( - "rest", - SyntaxShape::ColumnPath, - "optionally matches suffix of text by column paths", - ) - } - - fn usage(&self) -> &str { - "checks if string ends with pattern" - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - operate(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Checks if string ends with '.rb' pattern", - example: "echo 'my_library.rb' | str ends-with '.rb'", - result: Some(vec![UntaggedValue::boolean(true).into_untagged_value()]), - }] - } -} - -fn operate(args: CommandArgs) -> Result { - let (options, input) = ( - Arc::new(Arguments { - pattern: args.req(0)?, - column_paths: args.rest(1)?, - }), - args.input, - ); - - Ok(input - .map(move |v| { - if options.column_paths.is_empty() { - ReturnSuccess::value(action(&v, &options.pattern, v.tag())?) - } else { - let mut ret = v; - - for path in &options.column_paths { - let options = options.clone(); - - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| action(old, &options.pattern, old.tag())), - )?; - } - - ReturnSuccess::value(ret) - } - }) - .into_action_stream()) -} - -fn action(input: &Value, pattern: &str, tag: impl Into) -> Result { - match &input.value { - UntaggedValue::Primitive(Primitive::String(s)) => { - let ends_with = s.ends_with(pattern); - Ok(UntaggedValue::boolean(ends_with).into_value(tag)) - } - other => { - let got = format!("got {}", other.type_name()); - Err(ShellError::labeled_error( - "value is not string", - got, - tag.into().span, - )) - } - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::{action, SubCommand}; - use nu_protocol::UntaggedValue; - use nu_source::Tag; - use nu_test_support::value::string; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } - - #[test] - fn str_ends_with_pattern() { - let word = string("Cargo.toml"); - let pattern = ".toml"; - let expected = UntaggedValue::boolean(true).into_untagged_value(); - - let actual = action(&word, pattern, Tag::unknown()).unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn str_does_not_end_with_pattern() { - let word = string("Cargo.toml"); - let pattern = "Car"; - let expected = UntaggedValue::boolean(false).into_untagged_value(); - - let actual = action(&word, pattern, Tag::unknown()).unwrap(); - assert_eq!(actual, expected); - } -} diff --git a/crates/nu-command/src/commands/strings/str_/find_replace.rs b/crates/nu-command/src/commands/strings/str_/find_replace.rs deleted file mode 100644 index d0de70a327..0000000000 --- a/crates/nu-command/src/commands/strings/str_/find_replace.rs +++ /dev/null @@ -1,160 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::ShellTypeName; -use nu_protocol::{ - ColumnPath, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value, -}; -use nu_source::{Tag, Tagged}; -use nu_value_ext::ValueExt; -use regex::Regex; - -struct Arguments { - all: bool, - find: Tagged, - replace: Tagged, - column_paths: Vec, -} - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "str find-replace" - } - - fn signature(&self) -> Signature { - Signature::build("str find-replace") - .required("find", SyntaxShape::String, "the pattern to find") - .required("replace", SyntaxShape::String, "the replacement pattern") - .rest( - "rest", - SyntaxShape::ColumnPath, - "optionally find and replace text by column paths", - ) - .switch("all", "replace all occurrences of find string", Some('a')) - } - - fn usage(&self) -> &str { - "finds and replaces text" - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - operate(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Find and replace contents with capture group", - example: "echo 'my_library.rb' | str find-replace '(.+).rb' '$1.nu'", - result: Some(vec![Value::from("my_library.nu")]), - }, - Example { - description: "Find and replace all occurrences of find string", - example: "echo 'abc abc abc' | str find-replace -a 'b' 'z'", - result: Some(vec![Value::from("azc azc azc")]), - }, - ] - } -} - -struct FindReplace<'a>(&'a str, &'a str); - -fn operate(args: CommandArgs) -> Result { - let (options, input) = ( - Arc::new(Arguments { - all: args.has_flag("all"), - find: args.req(0)?, - replace: args.req(1)?, - column_paths: args.rest(2)?, - }), - args.input, - ); - - Ok(input - .map(move |v| { - if options.column_paths.is_empty() { - ReturnSuccess::value(action(&v, &options, v.tag())?) - } else { - let mut ret = v; - - for path in &options.column_paths { - let options = options.clone(); - - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| action(old, &options, old.tag())), - )?; - } - - ReturnSuccess::value(ret) - } - }) - .into_action_stream()) -} - -fn action( - input: &Value, - Arguments { - find, replace, all, .. - }: &Arguments, - tag: impl Into, -) -> Result { - match &input.value { - UntaggedValue::Primitive(Primitive::String(s)) => { - let FindReplace(find, replacement) = FindReplace(find, replace); - let regex = Regex::new(find); - - Ok(match regex { - Ok(re) => { - if *all { - UntaggedValue::string(re.replace_all(s, replacement).to_owned()) - } else { - UntaggedValue::string(re.replace(s, replacement).to_owned()) - } - } - Err(_) => UntaggedValue::string(s), - } - .into_value(tag)) - } - other => { - let got = format!("got {}", other.type_name()); - Err(ShellError::labeled_error( - "value is not string", - got, - tag.into().span, - )) - } - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::{action, Arguments, SubCommand}; - use nu_source::{Tag, TaggedItem}; - use nu_test_support::value::string; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } - - #[test] - fn can_have_capture_groups() { - let word = string("Cargo.toml"); - - let options = Arguments { - find: String::from("Cargo.(.+)").tagged_unknown(), - replace: String::from("Carga.$1").tagged_unknown(), - column_paths: vec![], - all: false, - }; - - let actual = action(&word, &options, Tag::unknown()).unwrap(); - assert_eq!(actual, string("Carga.toml")); - } -} diff --git a/crates/nu-command/src/commands/strings/str_/index_of.rs b/crates/nu-command/src/commands/strings/str_/index_of.rs deleted file mode 100644 index 16d0dcef2e..0000000000 --- a/crates/nu-command/src/commands/strings/str_/index_of.rs +++ /dev/null @@ -1,342 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::ShellTypeName; -use nu_protocol::{ - ColumnPath, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value, -}; -use nu_source::{Tag, Tagged}; -use nu_value_ext::{as_string, ValueExt}; - -struct Arguments { - end: bool, - pattern: Tagged, - range: Option, - column_paths: Vec, -} - -pub struct SubCommand; - -#[derive(Clone)] -pub struct IndexOfOptionalBounds(i32, i32); - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "str index-of" - } - - fn signature(&self) -> Signature { - Signature::build("str index-of") - .required( - "pattern", - SyntaxShape::String, - "the pattern to find index of", - ) - .rest( - "rest", - SyntaxShape::ColumnPath, - "optionally returns index of pattern in string by column paths", - ) - .named( - "range", - SyntaxShape::Any, - "optional start and/or end index", - Some('r'), - ) - .switch("end", "search from the end of the string", Some('e')) - } - - fn usage(&self) -> &str { - "Returns starting index of given pattern in string counting from 0. Returns -1 when there are no results." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - operate(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Returns index of pattern in string", - example: "echo 'my_library.rb' | str index-of '.rb'", - result: Some(vec![UntaggedValue::int(10).into_untagged_value()]), - }, - Example { - description: "Returns index of pattern in string with start index", - example: "echo '.rb.rb' | str index-of '.rb' -r '1,'", - result: Some(vec![UntaggedValue::int(3).into_untagged_value()]), - }, - Example { - description: "Returns index of pattern in string with end index", - example: "echo '123456' | str index-of '6' -r ',4'", - result: Some(vec![UntaggedValue::int(-1).into_untagged_value()]), - }, - Example { - description: "Returns index of pattern in string with start and end index", - example: "echo '123456' | str index-of '3' -r '1,4'", - result: Some(vec![UntaggedValue::int(2).into_untagged_value()]), - }, - Example { - description: "Alternatively you can use this form", - example: "echo '123456' | str index-of '3' -r [1 4]", - result: Some(vec![UntaggedValue::int(2).into_untagged_value()]), - }, - Example { - description: "Returns index of pattern in string", - example: "echo '/this/is/some/path/file.txt' | str index-of '/' -e", - result: Some(vec![UntaggedValue::int(18).into_untagged_value()]), - }, - ] - } -} - -fn operate(args: CommandArgs) -> Result { - let (options, input) = ( - Arc::new(Arguments { - pattern: args.req(0)?, - range: args.get_flag("range")?, - end: args.has_flag("end"), - column_paths: args.rest(1)?, - }), - args.input, - ); - - Ok(input - .map(move |v| { - if options.column_paths.is_empty() { - ReturnSuccess::value(action(&v, &options, v.tag())?) - } else { - let mut ret = v; - - for path in &options.column_paths { - let options = options.clone(); - - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| action(old, &options, old.tag())), - )?; - } - - ReturnSuccess::value(ret) - } - }) - .into_action_stream()) -} - -fn action( - input: &Value, - Arguments { - ref pattern, - range, - end, - .. - }: &Arguments, - tag: impl Into, -) -> Result { - let tag = tag.into(); - - let range = match range { - Some(range) => range.clone(), - None => UntaggedValue::string("").into_value(&tag), - }; - - let r = process_range(input, &range)?; - - match &input.value { - UntaggedValue::Primitive(Primitive::String(s)) => { - let start_index = r.0 as usize; - let end_index = r.1 as usize; - - if *end { - if let Some(result) = s[start_index..end_index].rfind(&**pattern) { - Ok(UntaggedValue::int(result as i64 + start_index as i64).into_value(tag)) - } else { - Ok(UntaggedValue::int(-1).into_value(tag)) - } - } else if let Some(result) = s[start_index..end_index].find(&**pattern) { - Ok(UntaggedValue::int(result as i64 + start_index as i64).into_value(tag)) - } else { - Ok(UntaggedValue::int(-1).into_value(tag)) - } - } - other => { - let got = format!("got {}", other.type_name()); - Err(ShellError::labeled_error( - "value is not string", - got, - tag.span, - )) - } - } -} - -fn process_range(input: &Value, range: &Value) -> Result { - let input_len = match &input.value { - UntaggedValue::Primitive(Primitive::String(s)) => s.len(), - _ => 0, - }; - let min_index_str = String::from("0"); - let max_index_str = input_len.to_string(); - let r = match &range.value { - UntaggedValue::Primitive(Primitive::String(s)) => { - let indexes: Vec<&str> = s.split(',').collect(); - - let start_index = indexes.get(0).unwrap_or(&&min_index_str[..]).to_string(); - - let end_index = indexes.get(1).unwrap_or(&&max_index_str[..]).to_string(); - - Ok((start_index, end_index)) - } - UntaggedValue::Table(indexes) => { - if indexes.len() > 2 { - Err(ShellError::labeled_error( - "there shouldn't be more than two indexes", - "too many indexes", - range.tag(), - )) - } else { - let idx: Vec = indexes - .iter() - .map(|v| as_string(v).unwrap_or_else(|_| String::from(""))) - .collect(); - - let start_index = idx.get(0).unwrap_or(&min_index_str).to_string(); - let end_index = idx.get(1).unwrap_or(&max_index_str).to_string(); - - Ok((start_index, end_index)) - } - } - other => { - let got = format!("got {}", other.type_name()); - Err(ShellError::labeled_error( - "value is not string", - got, - range.tag(), - )) - } - }?; - - let start_index = r.0.parse::().unwrap_or(0); - let end_index = r.1.parse::().unwrap_or(input_len as i32); - - if start_index < 0 || start_index > end_index { - return Err(ShellError::labeled_error( - "start index can't be negative or greater than end index", - "Invalid start index", - range.tag(), - )); - } - - if end_index < 0 || end_index < start_index || end_index > input_len as i32 { - return Err(ShellError::labeled_error( - "end index can't be negative, smaller than start index or greater than input length", - "Invalid end index", - range.tag(), - )); - } - Ok(IndexOfOptionalBounds(start_index, end_index)) -} -#[cfg(test)] -mod tests { - use super::ShellError; - use super::{action, Arguments, SubCommand}; - use nu_source::{Tag, TaggedItem}; - use nu_test_support::value::{int, string}; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } - - #[test] - fn returns_index_of_substring() { - let word = string("Cargo.tomL"); - - let options = Arguments { - pattern: String::from(".tomL").tagged_unknown(), - range: Some(string("")), - column_paths: vec![], - end: false, - }; - - let actual = action(&word, &options, Tag::unknown()).unwrap(); - - assert_eq!(actual, int(5)); - } - #[test] - fn index_of_does_not_exist_in_string() { - let word = string("Cargo.tomL"); - - let options = Arguments { - pattern: String::from("Lm").tagged_unknown(), - range: Some(string("")), - column_paths: vec![], - end: false, - }; - - let actual = action(&word, &options, Tag::unknown()).unwrap(); - assert_eq!(actual, int(-1)); - } - - #[test] - fn returns_index_of_next_substring() { - let word = string("Cargo.Cargo"); - - let options = Arguments { - pattern: String::from("Cargo").tagged_unknown(), - range: Some(string("1,")), - column_paths: vec![], - end: false, - }; - - let actual = action(&word, &options, Tag::unknown()).unwrap(); - assert_eq!(actual, int(6)); - } - - #[test] - fn index_does_not_exist_due_to_end_index() { - let word = string("Cargo.Banana"); - - let options = Arguments { - pattern: String::from("Banana").tagged_unknown(), - range: Some(string(",5")), - column_paths: vec![], - end: false, - }; - - let actual = action(&word, &options, Tag::unknown()).unwrap(); - assert_eq!(actual, int(-1)); - } - - #[test] - fn returns_index_of_nums_in_middle_due_to_index_limit_from_both_ends() { - let word = string("123123123"); - - let options = Arguments { - pattern: String::from("123").tagged_unknown(), - range: Some(string("2,6")), - column_paths: vec![], - end: false, - }; - - let actual = action(&word, &options, Tag::unknown()).unwrap(); - assert_eq!(actual, int(3)); - } - - #[test] - fn index_does_not_exists_due_to_strict_bounds() { - let word = string("123456"); - - let options = Arguments { - pattern: String::from("1").tagged_unknown(), - range: Some(string("2,4")), - column_paths: vec![], - end: false, - }; - - let actual = action(&word, &options, Tag::unknown()).unwrap(); - assert_eq!(actual, int(-1)); - } -} diff --git a/crates/nu-command/src/commands/strings/str_/length.rs b/crates/nu-command/src/commands/strings/str_/length.rs deleted file mode 100644 index 3c6329492f..0000000000 --- a/crates/nu-command/src/commands/strings/str_/length.rs +++ /dev/null @@ -1,111 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::ShellTypeName; -use nu_protocol::{ - ColumnPath, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value, -}; - -pub struct SubCommand; - -struct Arguments { - column_paths: Vec, -} - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "str length" - } - - fn signature(&self) -> Signature { - Signature::build("str length").rest( - "rest", - SyntaxShape::ColumnPath, - "optionally find length of text by column paths", - ) - } - - fn usage(&self) -> &str { - "outputs the lengths of the strings in the pipeline" - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - operate(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Return the lengths of multiple strings", - example: "echo 'hello' | str length", - result: Some(vec![UntaggedValue::int(5).into_untagged_value()]), - }, - Example { - description: "Return the lengths of multiple strings", - example: "echo 'hi' 'there' | str length", - result: Some(vec![ - UntaggedValue::int(2).into_untagged_value(), - UntaggedValue::int(5).into_untagged_value(), - ]), - }, - ] - } -} - -fn operate(args: CommandArgs) -> Result { - let (options, input) = ( - Arguments { - column_paths: args.rest(0)?, - }, - args.input, - ); - - Ok(input - .map(move |v| { - if options.column_paths.is_empty() { - ReturnSuccess::value(action(&v, v.tag())?) - } else { - let mut ret = v; - - for path in &options.column_paths { - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| action(old, old.tag())), - )?; - } - - ReturnSuccess::value(ret) - } - }) - .into_action_stream()) -} - -fn action(input: &Value, tag: impl Into) -> Result { - match &input.value { - UntaggedValue::Primitive(Primitive::String(s)) => { - Ok(UntaggedValue::int(s.len() as i64).into_value(tag)) - } - other => { - let got = format!("got {}", other.type_name()); - - Err(ShellError::labeled_error( - "value is not string", - got, - tag.into().span, - )) - } - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/strings/str_/lpad.rs b/crates/nu-command/src/commands/strings/str_/lpad.rs deleted file mode 100644 index 7ec42130e9..0000000000 --- a/crates/nu-command/src/commands/strings/str_/lpad.rs +++ /dev/null @@ -1,189 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::ShellTypeName; -use nu_protocol::{ - ColumnPath, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value, -}; -use nu_source::{Tag, Tagged}; -use nu_value_ext::ValueExt; - -struct Arguments { - length: Tagged, - character: Tagged, - column_paths: Vec, -} - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "str lpad" - } - - fn signature(&self) -> Signature { - Signature::build("str lpad") - .required_named("length", SyntaxShape::Int, "length to pad to", Some('l')) - .required_named( - "character", - SyntaxShape::String, - "character to pad with", - Some('c'), - ) - .rest( - "rest", - SyntaxShape::ColumnPath, - "optionally check if string contains pattern by column paths", - ) - } - - fn usage(&self) -> &str { - "pad a string with a character a certain length" - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - operate(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Left pad a string with a character a number of places", - example: "echo 'nushell' | str lpad -l 10 -c '*'", - result: Some(vec![ - UntaggedValue::string("***nushell").into_untagged_value() - ]), - }, - Example { - description: "Left pad a string with a character a number of places", - example: "echo '123' | str lpad -l 10 -c '0'", - result: Some(vec![ - UntaggedValue::string("0000000123").into_untagged_value() - ]), - }, - Example { - description: "Use lpad to truncate a string", - example: "echo '123456789' | str lpad -l 3 -c '0'", - result: Some(vec![UntaggedValue::string("123").into_untagged_value()]), - }, - Example { - description: "Use lpad to pad Unicode", - example: "echo '▉' | str lpad -l 10 -c '▉'", - result: Some(vec![ - UntaggedValue::string("▉▉▉▉▉▉▉▉▉▉").into_untagged_value() - ]), - }, - ] - } -} - -fn operate(args: CommandArgs) -> Result { - let (options, input) = ( - Arc::new(Arguments { - length: args.req_named("length")?, - character: args.req_named("character")?, - column_paths: args.rest(0)?, - }), - args.input, - ); - - Ok(input - .map(move |v| { - if options.column_paths.is_empty() { - ReturnSuccess::value(action(&v, &options, v.tag())?) - } else { - let mut ret = v; - - for path in &options.column_paths { - let options = options.clone(); - - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| action(old, &options, old.tag())), - )?; - } - - ReturnSuccess::value(ret) - } - }) - .into_action_stream()) -} - -fn action( - input: &Value, - Arguments { - character, length, .. - }: &Arguments, - tag: impl Into, -) -> Result { - match &input.value { - UntaggedValue::Primitive(Primitive::String(s)) => { - if **length < s.len() { - Ok( - UntaggedValue::string(s.chars().take(**length).collect::()) - .into_value(tag), - ) - } else { - let mut res = character.repeat(**length - s.chars().count()); - res += s; - Ok(UntaggedValue::string(res).into_value(tag)) - } - } - other => { - let got = format!("got {}", other.type_name()); - Err(ShellError::labeled_error( - "value is not string", - got, - tag.into().span, - )) - } - } -} - -#[cfg(test)] -mod tests { - use super::{action, Arguments, SubCommand}; - use nu_errors::ShellError; - use nu_protocol::UntaggedValue; - use nu_source::{Tag, TaggedItem}; - use nu_test_support::value::string; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } - - #[test] - fn left_pad_with_zeros() { - let word = string("123"); - - let options = Arguments { - character: String::from("0").tagged_unknown(), - length: 10_usize.tagged_unknown(), - column_paths: vec![], - }; - - let expected = UntaggedValue::string("0000000123").into_untagged_value(); - let actual = action(&word, &options, Tag::unknown()).unwrap(); - - assert_eq!(actual, expected); - } - - #[test] - fn left_pad_but_truncate() { - let word = string("123456789"); - - let options = Arguments { - character: String::from("0").tagged_unknown(), - length: 3_usize.tagged_unknown(), - column_paths: vec![], - }; - - let expected = UntaggedValue::string("123").into_untagged_value(); - let actual = action(&word, &options, Tag::unknown()).unwrap(); - - assert_eq!(actual, expected); - } -} diff --git a/crates/nu-command/src/commands/strings/str_/mod.rs b/crates/nu-command/src/commands/strings/str_/mod.rs deleted file mode 100644 index 5fcc4da113..0000000000 --- a/crates/nu-command/src/commands/strings/str_/mod.rs +++ /dev/null @@ -1,45 +0,0 @@ -mod capitalize; -mod case; -mod collect; -mod command; -mod contains; -mod downcase; -mod ends_with; -mod find_replace; -mod index_of; -mod length; -mod lpad; -mod reverse; -mod rpad; -mod starts_with; -mod substring; -mod to_datetime; -mod to_decimal; -mod to_integer; -mod trim; -mod upcase; - -pub use capitalize::SubCommand as StrCapitalize; -pub use case::camel_case::SubCommand as StrCamelCase; -pub use case::kebab_case::SubCommand as StrKebabCase; -pub use case::pascal_case::SubCommand as StrPascalCase; -pub use case::screaming_snake_case::SubCommand as StrScreamingSnakeCase; -pub use case::snake_case::SubCommand as StrSnakeCase; -pub use collect::SubCommand as StrCollect; -pub use command::Command as Str; -pub use contains::SubCommand as StrContains; -pub use downcase::SubCommand as StrDowncase; -pub use ends_with::SubCommand as StrEndsWith; -pub use find_replace::SubCommand as StrFindReplace; -pub use index_of::SubCommand as StrIndexOf; -pub use length::SubCommand as StrLength; -pub use lpad::SubCommand as StrLPad; -pub use reverse::SubCommand as StrReverse; -pub use rpad::SubCommand as StrRPad; -pub use starts_with::SubCommand as StrStartsWith; -pub use substring::SubCommand as StrSubstring; -pub use to_datetime::SubCommand as StrToDatetime; -pub use to_decimal::SubCommand as StrToDecimal; -pub use to_integer::SubCommand as StrToInteger; -pub use trim::Trim as StrTrim; -pub use upcase::SubCommand as StrUpcase; diff --git a/crates/nu-command/src/commands/strings/str_/reverse.rs b/crates/nu-command/src/commands/strings/str_/reverse.rs deleted file mode 100644 index 7bc78b794b..0000000000 --- a/crates/nu-command/src/commands/strings/str_/reverse.rs +++ /dev/null @@ -1,100 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::ShellTypeName; -use nu_protocol::{ - ColumnPath, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value, -}; - -pub struct SubCommand; - -struct Arguments { - column_paths: Vec, -} - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "str reverse" - } - - fn signature(&self) -> Signature { - Signature::build("str reverse").rest( - "rest", - SyntaxShape::ColumnPath, - "optionally reverse text by column paths", - ) - } - - fn usage(&self) -> &str { - "outputs the reversals of the strings in the pipeline" - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - operate(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Return the reversals of multiple strings", - example: "echo 'Nushell' | str reverse", - result: Some(vec![UntaggedValue::string("llehsuN").into_untagged_value()]), - }] - } -} - -fn operate(args: CommandArgs) -> Result { - let (options, input) = ( - Arguments { - column_paths: args.rest(0)?, - }, - args.input, - ); - - Ok(input - .map(move |v| { - if options.column_paths.is_empty() { - ReturnSuccess::value(action(&v, v.tag())?) - } else { - let mut ret = v; - - for path in &options.column_paths { - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| action(old, old.tag())), - )?; - } - - ReturnSuccess::value(ret) - } - }) - .into_action_stream()) -} - -fn action(input: &Value, tag: impl Into) -> Result { - match &input.value { - UntaggedValue::Primitive(Primitive::String(s)) => { - Ok(UntaggedValue::string(s.chars().rev().collect::()).into_value(tag)) - } - other => { - let got = format!("got {}", other.type_name()); - Err(ShellError::labeled_error( - "value is not string", - got, - tag.into().span, - )) - } - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/commands/strings/str_/rpad.rs b/crates/nu-command/src/commands/strings/str_/rpad.rs deleted file mode 100644 index 35b6391008..0000000000 --- a/crates/nu-command/src/commands/strings/str_/rpad.rs +++ /dev/null @@ -1,189 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::ShellTypeName; -use nu_protocol::{ - ColumnPath, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value, -}; -use nu_source::{Tag, Tagged}; -use nu_value_ext::ValueExt; - -struct Arguments { - length: Tagged, - character: Tagged, - column_paths: Vec, -} - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "str rpad" - } - - fn signature(&self) -> Signature { - Signature::build("str rpad") - .required_named("length", SyntaxShape::Int, "length to pad to", Some('l')) - .required_named( - "character", - SyntaxShape::String, - "character to pad with", - Some('c'), - ) - .rest( - "rest", - SyntaxShape::ColumnPath, - "optionally check if string contains pattern by column paths", - ) - } - - fn usage(&self) -> &str { - "pad a string with a character a certain length" - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - operate(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Right pad a string with a character a number of places", - example: "echo 'nushell' | str rpad -l 10 -c '*'", - result: Some(vec![ - UntaggedValue::string("nushell***").into_untagged_value() - ]), - }, - Example { - description: "Right pad a string with a character a number of places", - example: "echo '123' | str rpad -l 10 -c '0'", - result: Some(vec![ - UntaggedValue::string("1230000000").into_untagged_value() - ]), - }, - Example { - description: "Use rpad to truncate a string", - example: "echo '123456789' | str rpad -l 3 -c '0'", - result: Some(vec![UntaggedValue::string("123").into_untagged_value()]), - }, - Example { - description: "Use rpad to pad Unicode", - example: "echo '▉' | str rpad -l 10 -c '▉'", - result: Some(vec![ - UntaggedValue::string("▉▉▉▉▉▉▉▉▉▉").into_untagged_value() - ]), - }, - ] - } -} - -fn operate(args: CommandArgs) -> Result { - let (options, input) = ( - Arc::new(Arguments { - length: args.req_named("length")?, - character: args.req_named("character")?, - column_paths: args.rest(0)?, - }), - args.input, - ); - - Ok(input - .map(move |v| { - if options.column_paths.is_empty() { - ReturnSuccess::value(action(&v, &options, v.tag())?) - } else { - let mut ret = v; - - for path in &options.column_paths { - let options = options.clone(); - - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| action(old, &options, old.tag())), - )?; - } - - ReturnSuccess::value(ret) - } - }) - .into_action_stream()) -} - -fn action( - input: &Value, - Arguments { - length, character, .. - }: &Arguments, - tag: impl Into, -) -> Result { - match &input.value { - UntaggedValue::Primitive(Primitive::String(s)) => { - if **length < s.len() { - Ok( - UntaggedValue::string(s.chars().take(**length).collect::()) - .into_value(tag), - ) - } else { - let mut res = s.to_string(); - res += &character.repeat(**length - s.chars().count()); - Ok(UntaggedValue::string(res).into_value(tag)) - } - } - other => { - let got = format!("got {}", other.type_name()); - Err(ShellError::labeled_error( - "value is not string", - got, - tag.into().span, - )) - } - } -} - -#[cfg(test)] -mod tests { - use super::{action, Arguments, SubCommand}; - use nu_errors::ShellError; - use nu_protocol::UntaggedValue; - use nu_source::{Tag, TaggedItem}; - use nu_test_support::value::string; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } - - #[test] - fn right_pad_with_zeros() { - let word = string("123"); - - let options = Arguments { - character: String::from("0").tagged_unknown(), - length: 10_usize.tagged_unknown(), - column_paths: vec![], - }; - - let expected = UntaggedValue::string("1230000000").into_untagged_value(); - let actual = action(&word, &options, Tag::unknown()).unwrap(); - - assert_eq!(actual, expected); - } - - #[test] - fn right_pad_but_truncate() { - let word = string("123456789"); - - let options = Arguments { - character: String::from("0").tagged_unknown(), - length: 3_usize.tagged_unknown(), - column_paths: vec![], - }; - - let expected = UntaggedValue::string("123").into_untagged_value(); - let actual = action(&word, &options, Tag::unknown()).unwrap(); - - assert_eq!(actual, expected); - } -} diff --git a/crates/nu-command/src/commands/strings/str_/starts_with.rs b/crates/nu-command/src/commands/strings/str_/starts_with.rs deleted file mode 100644 index 2a3311b81a..0000000000 --- a/crates/nu-command/src/commands/strings/str_/starts_with.rs +++ /dev/null @@ -1,132 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::ShellTypeName; -use nu_protocol::{ - ColumnPath, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value, -}; -use nu_source::{Tag, Tagged}; -use nu_value_ext::ValueExt; - -pub struct SubCommand; - -struct Arguments { - pattern: Tagged, - column_paths: Vec, -} - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "str starts-with" - } - - fn signature(&self) -> Signature { - Signature::build("str starts-with") - .required("pattern", SyntaxShape::String, "the pattern to match") - .rest( - "rest", - SyntaxShape::ColumnPath, - "optionally matches prefix of text by column paths", - ) - } - - fn usage(&self) -> &str { - "checks if string starts with pattern" - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - operate(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Checks if string starts with 'my' pattern", - example: "echo 'my_library.rb' | str starts-with 'my'", - result: Some(vec![UntaggedValue::boolean(true).into_untagged_value()]), - }] - } -} - -fn operate(args: CommandArgs) -> Result { - let (options, input) = ( - Arc::new(Arguments { - pattern: args.req(0)?, - column_paths: args.rest(1)?, - }), - args.input, - ); - - Ok(input - .map(move |v| { - if options.column_paths.is_empty() { - ReturnSuccess::value(action(&v, &options.pattern, v.tag())?) - } else { - let mut ret = v; - - for path in &options.column_paths { - let options = options.clone(); - - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| action(old, &options.pattern, old.tag())), - )?; - } - - ReturnSuccess::value(ret) - } - }) - .into_action_stream()) -} - -fn action(input: &Value, pattern: &str, tag: impl Into) -> Result { - match &input.value { - UntaggedValue::Primitive(Primitive::String(s)) => { - let starts_with = s.starts_with(pattern); - Ok(UntaggedValue::boolean(starts_with).into_value(tag)) - } - other => { - let got = format!("got {}", other.type_name()); - Err(ShellError::labeled_error( - "value is not string", - got, - tag.into().span, - )) - } - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::{action, SubCommand}; - use nu_protocol::UntaggedValue; - use nu_source::Tag; - use nu_test_support::value::string; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } - - #[test] - fn str_starts_with_pattern() { - let word = string("Cargo.toml"); - let pattern = "Car"; - let expected = UntaggedValue::boolean(true).into_untagged_value(); - - let actual = action(&word, pattern, Tag::unknown()).unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn str_does_not_start_with_pattern() { - let word = string("Cargo.toml"); - let pattern = ".toml"; - let expected = UntaggedValue::boolean(false).into_untagged_value(); - - let actual = action(&word, pattern, Tag::unknown()).unwrap(); - assert_eq!(actual, expected); - } -} diff --git a/crates/nu-command/src/commands/strings/str_/substring.rs b/crates/nu-command/src/commands/strings/str_/substring.rs deleted file mode 100644 index 5946540066..0000000000 --- a/crates/nu-command/src/commands/strings/str_/substring.rs +++ /dev/null @@ -1,353 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::ShellTypeName; -use nu_protocol::{ - ColumnPath, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value, -}; -use nu_source::Tag; -use nu_value_ext::{as_string, ValueExt}; - -use std::cmp::Ordering; -use std::convert::TryInto; - -struct Arguments { - range: Value, - column_paths: Vec, -} - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "str substring" - } - - fn signature(&self) -> Signature { - Signature::build("str substring") - .required( - "range", - SyntaxShape::Any, - "the indexes to substring [start end]", - ) - .rest( - "rest", - SyntaxShape::ColumnPath, - "optionally substring text by column paths", - ) - } - - fn usage(&self) -> &str { - "substrings text" - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - operate(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Get a substring from the text", - example: "echo 'good nushell' | str substring [5 12]", - result: Some(vec![Value::from("nushell")]), - }, - Example { - description: "Alternatively, you can use the form", - example: "echo 'good nushell' | str substring '5,12'", - result: Some(vec![Value::from("nushell")]), - }, - Example { - description: "Drop the last `n` characters from the string", - example: "echo 'good nushell' | str substring ',-5'", - result: Some(vec![Value::from("good nu")]), - }, - Example { - description: "Get the remaining characters from a starting index", - example: "echo 'good nushell' | str substring '5,'", - result: Some(vec![Value::from("nushell")]), - }, - Example { - description: "Get the characters from the beginning until ending index", - example: "echo 'good nushell' | str substring ',7'", - result: Some(vec![Value::from("good nu")]), - }, - ] - } -} - -#[derive(Clone)] -struct Substring(isize, isize); - -impl From<(isize, isize)> for Substring { - fn from(input: (isize, isize)) -> Substring { - Substring(input.0, input.1) - } -} - -struct SubstringText(String, String); - -fn operate(args: CommandArgs) -> Result { - let name = args.call_info.name_tag.clone(); - - let (options, input) = ( - Arguments { - range: args.req(0)?, - column_paths: args.rest(1)?, - }, - args.input, - ); - - let indexes = Arc::new(process_arguments(&options, name)?.into()); - - Ok(input - .map(move |v| { - if options.column_paths.is_empty() { - ReturnSuccess::value(action(&v, &indexes, v.tag())?) - } else { - let mut ret = v; - - for path in &options.column_paths { - let indexes = indexes.clone(); - - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| action(old, &indexes, old.tag())), - )?; - } - - ReturnSuccess::value(ret) - } - }) - .into_action_stream()) -} - -fn action(input: &Value, options: &Substring, tag: impl Into) -> Result { - let tag = tag.into(); - - match &input.value { - UntaggedValue::Primitive(Primitive::String(s)) => { - let len: isize = s.len().try_into().map_err(|_| { - ShellError::labeled_error( - "could not perform substring", - "could not perform substring", - tag.span, - ) - })?; - - let start: isize = if options.0 < 0 { - options.0 + len - } else { - options.0 - }; - let end: isize = if options.1 < 0 { - std::cmp::max(len + options.1, 0) - } else { - options.1 - }; - - if start < len && end >= 0 { - match start.cmp(&end) { - Ordering::Equal => Ok(UntaggedValue::string("").into_value(tag)), - Ordering::Greater => Err(ShellError::labeled_error( - "End must be greater than or equal to Start", - "End must be greater than or equal to Start", - tag.span, - )), - Ordering::Less => Ok(UntaggedValue::string(if end == isize::max_value() { - s.chars().skip(start as usize).collect::() - } else { - s.chars() - .skip(start as usize) - .take((end - start) as usize) - .collect::() - }) - .into_value(tag)), - } - } else { - Ok(UntaggedValue::string("").into_value(tag)) - } - } - other => { - let got = format!("got {}", other.type_name()); - Err(ShellError::labeled_error( - "value is not string", - got, - tag.span, - )) - } - } -} - -fn process_arguments( - options: &Arguments, - name: impl Into, -) -> Result<(isize, isize), ShellError> { - let name = name.into(); - - let search = match &options.range.value { - UntaggedValue::Table(indexes) => { - if indexes.len() > 2 { - Err(ShellError::labeled_error( - "could not perform substring", - "could not perform substring", - name.span, - )) - } else { - let idx: Vec = indexes - .iter() - .map(|v| as_string(v).unwrap_or_else(|_| String::from(""))) - .collect(); - - let start = idx - .get(0) - .ok_or_else(|| { - ShellError::labeled_error( - "could not perform substring", - "could not perform substring", - name.span, - ) - })? - .to_string(); - let end = idx - .get(1) - .ok_or_else(|| { - ShellError::labeled_error( - "could not perform substring", - "could not perform substring", - name.span, - ) - })? - .to_string(); - - Ok(SubstringText(start, end)) - } - } - UntaggedValue::Primitive(Primitive::String(indexes)) => { - let idx: Vec<&str> = indexes.split(',').collect(); - - let start = idx - .get(0) - .ok_or_else(|| { - ShellError::labeled_error( - "could not perform substring", - "could not perform substring", - name.span, - ) - })? - .to_string(); - let end = idx - .get(1) - .ok_or_else(|| { - ShellError::labeled_error( - "could not perform substring", - "could not perform substring", - name.span, - ) - })? - .to_string(); - - Ok(SubstringText(start, end)) - } - _ => Err(ShellError::labeled_error( - "could not perform substring", - "could not perform substring", - name.span, - )), - }?; - - let start = match &search { - SubstringText(start, _) if start.is_empty() || start == "_" => 0, - SubstringText(start, _) => start.trim().parse().map_err(|_| { - ShellError::labeled_error( - "could not perform substring", - "could not perform substring", - name.span, - ) - })?, - }; - - let end = match &search { - SubstringText(_, end) if end.is_empty() || end == "_" => isize::max_value(), - SubstringText(_, end) => end.trim().parse().map_err(|_| { - ShellError::labeled_error( - "could not perform substring", - "could not perform substring", - name.span, - ) - })?, - }; - - Ok((start, end)) -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::{action, SubCommand, Substring}; - use nu_source::Tag; - use nu_test_support::value::string; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } - - struct Expectation<'a> { - options: (isize, isize), - expected: &'a str, - } - - impl Expectation<'_> { - fn options(&self) -> Substring { - Substring(self.options.0, self.options.1) - } - } - - fn expectation(word: &str, indexes: (isize, isize)) -> Expectation { - Expectation { - options: indexes, - expected: word, - } - } - - #[test] - fn substrings_indexes() { - let word = string("andres"); - - let cases = vec![ - expectation("a", (0, 1)), - expectation("an", (0, 2)), - expectation("and", (0, 3)), - expectation("andr", (0, 4)), - expectation("andre", (0, 5)), - expectation("andres", (0, 6)), - expectation("", (0, -6)), - expectation("a", (0, -5)), - expectation("an", (0, -4)), - expectation("and", (0, -3)), - expectation("andr", (0, -2)), - expectation("andre", (0, -1)), - // str substring [ -4 , _ ] - // str substring -4 , - expectation("dres", (-4, isize::max_value())), - expectation("", (0, -110)), - expectation("", (6, 0)), - expectation("", (6, -1)), - expectation("", (6, -2)), - expectation("", (6, -3)), - expectation("", (6, -4)), - expectation("", (6, -5)), - expectation("", (6, -6)), - ]; - - for expectation in &cases { - let expected = expectation.expected; - let actual = action(&word, &expectation.options(), Tag::unknown()).unwrap(); - - assert_eq!(actual, string(expected)); - } - } -} diff --git a/crates/nu-command/src/commands/strings/str_/to_datetime.rs b/crates/nu-command/src/commands/strings/str_/to_datetime.rs deleted file mode 100644 index d88b1337a7..0000000000 --- a/crates/nu-command/src/commands/strings/str_/to_datetime.rs +++ /dev/null @@ -1,373 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{ - ColumnPath, Primitive, ReturnSuccess, ShellTypeName, Signature, SyntaxShape, UntaggedValue, - Value, -}; -use nu_source::{Tag, Tagged}; -use nu_value_ext::ValueExt; - -use chrono::{DateTime, FixedOffset, Local, LocalResult, Offset, TimeZone, Utc}; - -struct Arguments { - timezone: Option>, - offset: Option>, - format: Option>, - column_paths: Vec, -} - -// In case it may be confused with chrono::TimeZone -#[derive(Clone)] -enum Zone { - Utc, - Local, - East(u8), - West(u8), - Error, // we want the nullshell to cast it instead of rust -} - -impl Zone { - fn new(i: i16) -> Self { - if i.abs() <= 12 { - // guanranteed here - if i >= 0 { - Self::East(i as u8) // won't go out of range - } else { - Self::West(-i as u8) // same here - } - } else { - Self::Error // Out of range - } - } - fn from_string(s: String) -> Self { - match s.to_lowercase().as_str() { - "utc" | "u" => Self::Utc, - "local" | "l" => Self::Local, - _ => Self::Error, - } - } -} - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "str to-datetime" - } - - fn signature(&self) -> Signature { - Signature::build("str to-datetime") - .named( - "timezone", - SyntaxShape::String, - "Specify timezone if the input is timestamp, like 'UTC/u' or 'LOCAL/l'", - Some('z'), - ) - .named( - "offset", - SyntaxShape::Int, - "Specify timezone by offset if the input is timestamp, like '+8', '-4', prior than timezone", - Some('o'), - ) - .named( - "format", - SyntaxShape::String, - "Specify date and time formatting", - Some('f'), - ) - .rest( -"rest", - SyntaxShape::Any, - "optionally convert text into datetime by column paths", - ) - } - - fn usage(&self) -> &str { - "converts text into datetime" - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - operate(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Convert to datetime", - example: "echo '16.11.1984 8:00 am +0000' | str to-datetime", - result: None, - }, - Example { - description: "Convert to datetime", - example: "echo '2020-08-04T16:39:18+00:00' | str to-datetime", - result: None, - }, - Example { - description: "Convert to datetime using a custom format", - example: "echo '20200904_163918+0000' | str to-datetime -f '%Y%m%d_%H%M%S%z'", - result: None, - }, - Example { - description: "Convert timestamp (no larger than 8e+12) to datetime using a specified timezone", - example: "echo '1614434140' | str to-datetime -z 'UTC'", - result: None, - }, - Example { - description: - "Convert timestamp (no larger than 8e+12) to datetime using a specified timezone offset (between -12 and 12)", - example: "echo '1614434140' | str to-datetime -o '+9'", - result: None, - }, - ] - } -} - -#[derive(Clone)] -struct DatetimeFormat(String); - -fn operate(args: CommandArgs) -> Result { - let options = Arguments { - timezone: args.get_flag("timezone")?, - offset: args.get_flag("offset")?, - format: args.get_flag("format")?, - column_paths: args.rest(0)?, - }; - let input = args.input; - - // if zone-offset is specified, then zone will be neglected - let zone_options = if let Some(Tagged { - item: zone_offset, - tag, - }) = &options.offset - { - Some(Tagged { - item: Zone::new(*zone_offset), - tag: tag.into(), - }) - } else if let Some(Tagged { item: zone, tag }) = &options.timezone { - Some(Tagged { - item: Zone::from_string(zone.clone()), - tag: tag.into(), - }) - } else { - None - }; - - let format_options = if let Some(Tagged { item: fmt, .. }) = &options.format { - Some(DatetimeFormat(fmt.to_string())) - } else { - None - }; - - Ok(input - .map(move |v| { - if options.column_paths.is_empty() { - ReturnSuccess::value(action(&v, &zone_options, &format_options, v.tag())?) - } else { - let mut ret = v; - - for path in &options.column_paths { - let zone_options = zone_options.clone(); - let format_options = format_options.clone(); - - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| action(old, &zone_options, &format_options, old.tag())), - )?; - } - - ReturnSuccess::value(ret) - } - }) - .into_action_stream()) -} - -fn action( - input: &Value, - timezone: &Option>, - dateformat: &Option, - tag: impl Into, -) -> Result { - match &input.value { - UntaggedValue::Primitive(Primitive::String(s)) => { - let ts = s.parse::(); - // if timezone if specified, first check if the input is a timestamp. - if let Some(tz) = timezone { - const TIMESTAMP_BOUND: i64 = 8.2e+12 as i64; - // Since the timestamp method of chrono itself don't throw an error (it just panicked) - // We have to manually guard it. - if let Ok(t) = ts { - if t.abs() > TIMESTAMP_BOUND { - return Err(ShellError::labeled_error( - "could not parse input as a valid timestamp", - "given timestamp is out of range, it should between -8e+12 and 8e+12", - tag.into().span, - )); - } - const HOUR: i32 = 3600; - let stampout = match tz.item { - Zone::Utc => UntaggedValue::date(Utc.timestamp(t, 0)), - Zone::Local => UntaggedValue::date(Local.timestamp(t, 0)), - Zone::East(i) => { - let eastoffset = FixedOffset::east((i as i32) * HOUR); - UntaggedValue::date(eastoffset.timestamp(t, 0)) - } - Zone::West(i) => { - let westoffset = FixedOffset::west((i as i32) * HOUR); - UntaggedValue::date(westoffset.timestamp(t, 0)) - } - Zone::Error => { - return Err(ShellError::labeled_error( - "could not continue to convert timestamp", - "given timezone or offset is invalid", - tz.tag().span, - )); - } - }; - return Ok(stampout.into_value(tag)); - } - }; - // if it's not, continue and negelect the timezone option. - let out = match dateformat { - Some(dt) => match DateTime::parse_from_str(s, &dt.0) { - Ok(d) => UntaggedValue::date(d), - Err(reason) => { - return Err(ShellError::labeled_error( - format!("could not parse as datetime using format '{}'", dt.0), - reason.to_string(), - tag.into().span, - )) - } - }, - None => match dtparse::parse(s) { - Ok((native_dt, fixed_offset)) => { - let offset = match fixed_offset { - Some(fo) => fo, - None => FixedOffset::east(0).fix(), - }; - match offset.from_local_datetime(&native_dt) { - LocalResult::Single(d) => UntaggedValue::date(d), - LocalResult::Ambiguous(d, _) => UntaggedValue::date(d), - LocalResult::None => { - return Err(ShellError::labeled_error( - "could not convert to a timezone-aware datetime", - "local time representation is invalid", - tag.into().span, - )) - } - } - } - Err(reason) => { - return Err(ShellError::labeled_error( - "could not parse as datetime", - reason.to_string(), - tag.into().span, - )) - } - }, - }; - - Ok(out.into_value(tag)) - } - other => { - let got = format!("got {}", other.type_name()); - Err(ShellError::labeled_error( - "value is not string", - got, - tag.into().span, - )) - } - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::{action, DatetimeFormat, SubCommand, Zone}; - use nu_protocol::{Primitive, UntaggedValue}; - use nu_source::{Tag, Tagged}; - use nu_test_support::value::string; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } - - #[test] - fn takes_a_date_format() { - let date_str = string("16.11.1984 8:00 am +0000"); - - let fmt_options = Some(DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string())); - - let actual = action(&date_str, &None, &fmt_options, Tag::unknown()).unwrap(); - - match actual.value { - UntaggedValue::Primitive(Primitive::Date(_)) => {} - _ => panic!("Didn't convert to date"), - } - } - - #[test] - fn takes_iso8601_date_format() { - let date_str = string("2020-08-04T16:39:18+00:00"); - let actual = action(&date_str, &None, &None, Tag::unknown()).unwrap(); - match actual.value { - UntaggedValue::Primitive(Primitive::Date(_)) => {} - _ => panic!("Didn't convert to date"), - } - } - - #[test] - fn takes_timestamp_offset() { - let date_str = string("1614434140"); - let timezone_option = Some(Tagged { - item: Zone::East(8), - tag: Tag::unknown(), - }); - let actual = action(&date_str, &timezone_option, &None, Tag::unknown()).unwrap(); - match actual.value { - UntaggedValue::Primitive(Primitive::Date(_)) => {} - _ => panic!("Didn't convert to date"), - } - } - - #[test] - fn takes_timestamp() { - let date_str = string("1614434140"); - let timezone_option = Some(Tagged { - item: Zone::Local, - tag: Tag::unknown(), - }); - let actual = action(&date_str, &timezone_option, &None, Tag::unknown()).unwrap(); - match actual.value { - UntaggedValue::Primitive(Primitive::Date(_)) => {} - _ => panic!("Didn't convert to date"), - } - } - - #[test] - fn takes_invalid_timestamp() { - let date_str = string("10440970000000"); - let timezone_option = Some(Tagged { - item: Zone::Utc, - tag: Tag::unknown(), - }); - let actual = action(&date_str, &timezone_option, &None, Tag::unknown()); - - assert!(actual.is_err()); - } - - #[test] - fn communicates_parsing_error_given_an_invalid_datetimelike_string() { - let date_str = string("16.11.1984 8:00 am Oops0000"); - - let fmt_options = Some(DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string())); - - let actual = action(&date_str, &None, &fmt_options, Tag::unknown()); - - assert!(actual.is_err()); - } -} diff --git a/crates/nu-command/src/commands/strings/str_/to_decimal.rs b/crates/nu-command/src/commands/strings/str_/to_decimal.rs deleted file mode 100644 index a61779e59d..0000000000 --- a/crates/nu-command/src/commands/strings/str_/to_decimal.rs +++ /dev/null @@ -1,137 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::ShellTypeName; -use nu_protocol::{ - ColumnPath, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value, -}; -use nu_source::Tag; -use nu_value_ext::ValueExt; - -use bigdecimal::BigDecimal; -use std::str::FromStr; - -struct Arguments { - column_paths: Vec, -} - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "str to-decimal" - } - - fn signature(&self) -> Signature { - Signature::build("str to-decimal").rest( - "rest", - SyntaxShape::ColumnPath, - "optionally convert text into decimal by column paths", - ) - } - - fn usage(&self) -> &str { - "converts text into decimal" - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - operate(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Convert to decimal", - example: "echo '3.1415' | str to-decimal", - result: None, - }] - } -} - -fn operate(args: CommandArgs) -> Result { - let (options, input) = ( - Arguments { - column_paths: args.rest(0)?, - }, - args.input, - ); - - Ok(input - .map(move |v| { - if options.column_paths.is_empty() { - ReturnSuccess::value(action(&v, v.tag())?) - } else { - let mut ret = v; - - for path in &options.column_paths { - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| action(old, old.tag())), - )?; - } - - ReturnSuccess::value(ret) - } - }) - .into_action_stream()) -} - -fn action(input: &Value, tag: impl Into) -> Result { - match &input.value { - UntaggedValue::Primitive(Primitive::String(s)) => { - let other = s.trim(); - let out = match BigDecimal::from_str(other) { - Ok(v) => UntaggedValue::decimal(v), - Err(reason) => { - return Err(ShellError::labeled_error( - "could not parse as decimal", - reason.to_string(), - tag.into().span, - )) - } - }; - Ok(out.into_value(tag)) - } - other => { - let got = format!("got {}", other.type_name()); - Err(ShellError::labeled_error( - "value is not string", - got, - tag.into().span, - )) - } - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::{action, SubCommand}; - use nu_source::Tag; - use nu_test_support::value::{decimal_from_float, string}; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } - - #[test] - #[allow(clippy::approx_constant)] - fn turns_to_integer() { - let word = string("3.1415"); - let expected = decimal_from_float(3.1415); - - let actual = action(&word, Tag::unknown()).unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn communicates_parsing_error_given_an_invalid_decimallike_string() { - let decimal_str = string("11.6anra"); - - let actual = action(&decimal_str, Tag::unknown()); - - assert!(actual.is_err()); - } -} diff --git a/crates/nu-command/src/commands/strings/str_/to_integer.rs b/crates/nu-command/src/commands/strings/str_/to_integer.rs deleted file mode 100644 index 5d976dac6a..0000000000 --- a/crates/nu-command/src/commands/strings/str_/to_integer.rs +++ /dev/null @@ -1,201 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::ShellTypeName; -use nu_protocol::{ - ColumnPath, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value, -}; -use nu_source::{Tag, Tagged}; -use nu_value_ext::ValueExt; - -struct Arguments { - radix: Option>, - column_paths: Vec, -} - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "str to-int" - } - - fn signature(&self) -> Signature { - Signature::build("str to-int") - .named("radix", SyntaxShape::Number, "radix of integer", Some('r')) - .rest( - "rest", - SyntaxShape::ColumnPath, - "optionally convert text into integer by column paths", - ) - } - - fn usage(&self) -> &str { - "converts text into integer" - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - operate(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Convert to an integer", - example: "echo '255' | str to-int", - result: Some(vec![UntaggedValue::int(255).into()]), - }, - Example { - description: "Convert str column to an integer", - example: "echo [['count']; ['255']] | str to-int count | get count", - result: Some(vec![UntaggedValue::int(255).into()]), - }, - Example { - description: "Convert to integer from binary", - example: "echo '1101' | str to-int -r 2", - result: Some(vec![UntaggedValue::int(13).into()]), - }, - Example { - description: "Convert to integer from hex", - example: "echo 'FF' | str to-int -r 16", - result: Some(vec![UntaggedValue::int(255).into()]), - }, - ] - } -} - -fn operate(args: CommandArgs) -> Result { - let options = Arguments { - radix: args.get_flag("radix")?, - column_paths: args.rest(0)?, - }; - let input = args.input; - - let radix = options.radix.as_ref().map(|r| r.item).unwrap_or(10); - - Ok(input - .map(move |v| { - if options.column_paths.is_empty() { - ReturnSuccess::value(action(&v, v.tag(), radix)?) - } else { - let mut ret = v; - - for path in &options.column_paths { - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| action(old, old.tag(), radix)), - )?; - } - - ReturnSuccess::value(ret) - } - }) - .into_action_stream()) -} - -fn action(input: &Value, tag: impl Into, radix: u32) -> Result { - match &input.value { - UntaggedValue::Primitive(Primitive::String(s)) => { - let trimmed = s.trim(); - - let out = match trimmed { - b if b.starts_with("0b") => { - let num = match i64::from_str_radix(b.trim_start_matches("0b"), 2) { - Ok(n) => n, - Err(reason) => { - return Err(ShellError::labeled_error( - "could not parse as integer", - reason.to_string(), - tag.into().span, - )) - } - }; - UntaggedValue::int(num) - } - h if h.starts_with("0x") => { - let num = match i64::from_str_radix(h.trim_start_matches("0x"), 16) { - Ok(n) => n, - Err(reason) => { - return Err(ShellError::labeled_error( - "could not parse as int", - reason.to_string(), - tag.into().span, - )) - } - }; - UntaggedValue::int(num) - } - _ => { - let num = match i64::from_str_radix(trimmed, radix) { - Ok(n) => n, - Err(reason) => { - return Err(ShellError::labeled_error( - "could not parse as int", - reason.to_string(), - tag.into().span, - )) - } - }; - UntaggedValue::int(num) - } - }; - - Ok(out.into_value(tag)) - } - other => { - let got = format!("got {}", other.type_name()); - Err(ShellError::labeled_error( - "value is not string", - got, - tag.into().span, - )) - } - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::{action, SubCommand}; - use nu_source::Tag; - use nu_test_support::value::{int, string}; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } - - #[test] - fn turns_to_integer() { - let word = string("10"); - let expected = int(10); - - let actual = action(&word, Tag::unknown(), 10).unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn turns_binary_to_integer() { - let s = string("0b101"); - let actual = action(&s, Tag::unknown(), 10).unwrap(); - assert_eq!(actual, int(5)); - } - - #[test] - fn turns_hex_to_integer() { - let s = string("0xFF"); - let actual = action(&s, Tag::unknown(), 16).unwrap(); - assert_eq!(actual, int(255)); - } - - #[test] - fn communicates_parsing_error_given_an_invalid_integerlike_string() { - let integer_str = string("36anra"); - - let actual = action(&integer_str, Tag::unknown(), 10); - - assert!(actual.is_err()); - } -} diff --git a/crates/nu-command/src/commands/strings/str_/trim/command.rs b/crates/nu-command/src/commands/strings/str_/trim/command.rs deleted file mode 100644 index a0898acb9e..0000000000 --- a/crates/nu-command/src/commands/strings/str_/trim/command.rs +++ /dev/null @@ -1,851 +0,0 @@ -use super::operate; -use crate::commands::strings::str_::trim::ClosureFlags; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, Value}; -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "str trim" - } - - fn signature(&self) -> Signature { - Signature::build("str trim") - .rest( -"rest", - SyntaxShape::ColumnPath, - "optionally trim text by column paths", - ) - .named( - "char", - SyntaxShape::String, - "character to trim (default: whitespace)", - Some('c'), - ) - .switch( - "left", - "trims characters only from the beginning of the string (default: whitespace)", - Some('l'), - ) - .switch( - "right", - "trims characters only from the end of the string (default: whitespace)", - Some('r'), - ) - .switch( - "all", - "trims all characters from both sides of the string *and* in the middle (default: whitespace)", - Some('a'), - ) - .switch("both", "trims all characters from left and right side of the string (default: whitespace)", Some('b')) - .switch("format", "trims spaces replacing multiple characters with singles in the middle (default: whitespace)", Some('f')) - } - fn usage(&self) -> &str { - "trims text" - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - operate(args, &trim) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Trim whitespace", - example: "echo 'Nu shell ' | str trim", - result: Some(vec![Value::from("Nu shell")]), - }, - Example { - description: "Trim a specific character", - example: "echo '=== Nu shell ===' | str trim -c '=' | str trim", - result: Some(vec![Value::from("Nu shell")]), - }, - Example { - description: "Trim all characters", - example: "echo ' Nu shell ' | str trim -a", - result: Some(vec![Value::from("Nushell")]), - }, - Example { - description: "Trim whitespace from the beginning of string", - example: "echo ' Nu shell ' | str trim -l", - result: Some(vec![Value::from("Nu shell ")]), - }, - Example { - description: "Trim a specific character", - example: "echo '=== Nu shell ===' | str trim -c '='", - result: Some(vec![Value::from(" Nu shell ")]), - }, - Example { - description: "Trim whitespace from the end of string", - example: "echo ' Nu shell ' | str trim -r", - result: Some(vec![Value::from(" Nu shell")]), - }, - Example { - description: "Trim a specific character", - example: "echo '=== Nu shell ===' | str trim -r -c '='", - result: Some(vec![Value::from("=== Nu shell ")]), - }, - ] - } -} - -fn trim(s: &str, char_: Option, closure_flags: &ClosureFlags) -> String { - let ClosureFlags { - left_trim, - right_trim, - all_flag, - both_flag, - format_flag, - } = closure_flags; - let delimiters = match char_ { - Some(c) => vec![c], - // Trying to make this trim work like rust default trim() - // which uses is_whitespace() as a default - None => vec![ - ' ', // space - '\x09', // horizontal tab - '\x0A', // new line, line feed - '\x0B', // vertical tab - '\x0C', // form feed, new page - '\x0D', // carriage return - ], //whitespace - }; - - if *left_trim { - s.trim_start_matches(&delimiters[..]).to_string() - } else if *right_trim { - s.trim_end_matches(&delimiters[..]).to_string() - } else if *all_flag { - s.split(&delimiters[..]) - .filter(|s| !s.is_empty()) - .collect::() - } else if *both_flag { - s.trim_matches(&delimiters[..]).to_string() - } else if *format_flag { - // The idea here is to use regex to go through these delimiters and - // where there are multiple, replace them with singles - - // create our return string which is a copy of the original string - let mut return_string = String::from(s); - // Iterate through the delimiters replacing them with regex friendly names - for r in &delimiters { - let reg = match r { - ' ' => r"\s".to_string(), - '\x09' => r"\t".to_string(), - '\x0A' => r"\n".to_string(), - '\x0B' => r"\v".to_string(), - '\x0C' => r"\f".to_string(), - '\x0D' => r"\r".to_string(), - _ => format!(r"\{}", r), - }; - // create a regex string that looks for 2 or more of each of these characters - let re_str = format!("{}{{2,}}", reg); - // create the regex - let re = regex::Regex::new(&re_str).expect("Error creating regular expression"); - // replace all multiple occurrences with single occurrences represented by r - let new_str = re.replace_all(&return_string, r.to_string()); - // update the return string so the next loop has the latest changes - return_string = new_str.to_string(); - } - // for good measure, trim_matches, which gets the start and end - // theoretically we shouldn't have to do this but from my testing, we do. - return_string.trim_matches(&delimiters[..]).to_string() - } else { - s.trim().to_string() - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::SubCommand; - use crate::commands::strings::str_::trim::command::trim; - use crate::commands::strings::str_::trim::{action, ActionMode, ClosureFlags}; - use nu_protocol::row; - use nu_source::Tag; - use nu_test_support::value::{int, string, table}; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - test_examples(SubCommand {}) - } - - #[test] - fn trims() { - let word = string("andres "); - let expected = string("andres"); - let closure_flags = ClosureFlags { - both_flag: true, - ..Default::default() - }; - - let actual = action( - &word, - Tag::unknown(), - None, - &closure_flags, - &trim, - ActionMode::Local, - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn trims_global() { - let word = string(" global "); - let expected = string("global"); - let closure_flags = ClosureFlags { - both_flag: true, - ..Default::default() - }; - - let actual = action( - &word, - Tag::unknown(), - None, - &closure_flags, - &trim, - ActionMode::Global, - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn global_trim_ignores_numbers() { - let number = int(2020); - let expected = int(2020); - let closure_flags = ClosureFlags { - both_flag: true, - ..Default::default() - }; - - let actual = action( - &number, - Tag::unknown(), - None, - &closure_flags, - &trim, - ActionMode::Global, - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn global_trim_row() { - let row = row!["a".to_string() => string(" c "), " b ".to_string() => string(" d ")]; - let expected = row!["a".to_string() => string("c"), " b ".to_string() => string("d")]; - let closure_flags = ClosureFlags { - both_flag: true, - ..Default::default() - }; - - let actual = action( - &row, - Tag::unknown(), - None, - &closure_flags, - &trim, - ActionMode::Global, - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn global_trim_table() { - let row = table(&[string(" a "), int(65), string(" d")]); - let expected = table(&[string("a"), int(65), string("d")]); - let closure_flags = ClosureFlags { - both_flag: true, - ..Default::default() - }; - - let actual = action( - &row, - Tag::unknown(), - None, - &closure_flags, - &trim, - ActionMode::Global, - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn trims_custom_character_both_ends() { - let word = string("!#andres#!"); - let expected = string("#andres#"); - let closure_flags = ClosureFlags { - both_flag: true, - ..Default::default() - }; - - let actual = action( - &word, - Tag::unknown(), - Some('!'), - &closure_flags, - &trim, - ActionMode::Local, - ) - .unwrap(); - assert_eq!(actual, expected); - } - #[test] - fn trims_all_white_space() { - let word = string(" Value1 a lot of spaces "); - let expected = string("Value1alotofspaces"); - let closure_flags = ClosureFlags { - all_flag: true, - ..Default::default() - }; - - let actual = action( - &word, - Tag::unknown(), - Some(' '), - &closure_flags, - &trim, - ActionMode::Local, - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn global_trims_row_all_white_space() { - let row = row!["a".to_string() => string(" nu shell "), " b ".to_string() => string(" b c d e ")]; - let expected = - row!["a".to_string() => string("nushell"), " b ".to_string() => string("bcde")]; - let closure_flags = ClosureFlags { - all_flag: true, - ..Default::default() - }; - - let actual = action( - &row, - Tag::unknown(), - None, - &closure_flags, - &trim, - ActionMode::Global, - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn global_trims_table_all_white_space() { - let row = table(&[string(" nu shell "), int(65), string(" d")]); - let expected = table(&[string("nushell"), int(65), string("d")]); - let closure_flags = ClosureFlags { - all_flag: true, - ..Default::default() - }; - - let actual = action( - &row, - Tag::unknown(), - None, - &closure_flags, - &trim, - ActionMode::Global, - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn trims_all_custom_character() { - let word = string(".Value1.a.lot..of...dots."); - let expected = string("Value1alotofdots"); - let closure_flags = ClosureFlags { - all_flag: true, - ..Default::default() - }; - - let actual = action( - &word, - Tag::unknown(), - Some('.'), - &closure_flags, - &trim, - ActionMode::Local, - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn global_trims_row_all_custom_character() { - let row = row!["a".to_string() => string("!!!!nu!!shell!!!"), " b ".to_string() => string("!!b!c!!d!e!!")]; - let expected = - row!["a".to_string() => string("nushell"), " b ".to_string() => string("bcde")]; - let closure_flags = ClosureFlags { - all_flag: true, - ..Default::default() - }; - - let actual = action( - &row, - Tag::unknown(), - Some('!'), - &closure_flags, - &trim, - ActionMode::Global, - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn global_trims_table_all_custom_character() { - let row = table(&[string("##nu####shell##"), int(65), string("#d")]); - let expected = table(&[string("nushell"), int(65), string("d")]); - let closure_flags = ClosureFlags { - all_flag: true, - ..Default::default() - }; - - let actual = action( - &row, - Tag::unknown(), - Some('#'), - &closure_flags, - &trim, - ActionMode::Global, - ) - .unwrap(); - assert_eq!(actual, expected); - } - #[test] - fn trims_whitespace_from_left() { - let word = string(" andres "); - let expected = string("andres "); - let closure_flags = ClosureFlags { - left_trim: true, - ..Default::default() - }; - - let actual = action( - &word, - Tag::unknown(), - None, - &closure_flags, - &trim, - ActionMode::Local, - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn global_trim_left_ignores_numbers() { - let number = int(2020); - let expected = int(2020); - let closure_flags = ClosureFlags { - left_trim: true, - ..Default::default() - }; - - let actual = action( - &number, - Tag::unknown(), - None, - &closure_flags, - &trim, - ActionMode::Global, - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn trims_left_global() { - let word = string(" global "); - let expected = string("global "); - let closure_flags = ClosureFlags { - left_trim: true, - ..Default::default() - }; - - let actual = action( - &word, - Tag::unknown(), - None, - &closure_flags, - &trim, - ActionMode::Global, - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn global_trim_left_row() { - let row = row!["a".to_string() => string(" c "), " b ".to_string() => string(" d ")]; - let expected = row!["a".to_string() => string("c "), " b ".to_string() => string("d ")]; - let closure_flags = ClosureFlags { - left_trim: true, - ..Default::default() - }; - - let actual = action( - &row, - Tag::unknown(), - None, - &closure_flags, - &trim, - ActionMode::Global, - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn global_trim_left_table() { - let row = table(&[string(" a "), int(65), string(" d")]); - let expected = table(&[string("a "), int(65), string("d")]); - let closure_flags = ClosureFlags { - left_trim: true, - ..Default::default() - }; - - let actual = action( - &row, - Tag::unknown(), - None, - &closure_flags, - &trim, - ActionMode::Global, - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn trims_custom_chars_from_left() { - let word = string("!!! andres !!!"); - let expected = string(" andres !!!"); - let closure_flags = ClosureFlags { - left_trim: true, - ..Default::default() - }; - - let actual = action( - &word, - Tag::unknown(), - Some('!'), - &closure_flags, - &trim, - ActionMode::Local, - ) - .unwrap(); - assert_eq!(actual, expected); - } - #[test] - fn trims_whitespace_from_right() { - let word = string(" andres "); - let expected = string(" andres"); - let closure_flags = ClosureFlags { - right_trim: true, - ..Default::default() - }; - - let actual = action( - &word, - Tag::unknown(), - None, - &closure_flags, - &trim, - ActionMode::Local, - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn trims_right_global() { - let word = string(" global "); - let expected = string(" global"); - let closure_flags = ClosureFlags { - right_trim: true, - ..Default::default() - }; - - let actual = action( - &word, - Tag::unknown(), - None, - &closure_flags, - &trim, - ActionMode::Global, - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn global_trim_right_ignores_numbers() { - let number = int(2020); - let expected = int(2020); - let closure_flags = ClosureFlags { - right_trim: true, - ..Default::default() - }; - - let actual = action( - &number, - Tag::unknown(), - None, - &closure_flags, - &trim, - ActionMode::Global, - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn global_trim_right_row() { - let row = row!["a".to_string() => string(" c "), " b ".to_string() => string(" d ")]; - let expected = row!["a".to_string() => string(" c"), " b ".to_string() => string(" d")]; - let closure_flags = ClosureFlags { - right_trim: true, - ..Default::default() - }; - - let actual = action( - &row, - Tag::unknown(), - None, - &closure_flags, - &trim, - ActionMode::Global, - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn global_trim_right_table() { - let row = table(&[string(" a "), int(65), string(" d")]); - let expected = table(&[string(" a"), int(65), string(" d")]); - let closure_flags = ClosureFlags { - right_trim: true, - ..Default::default() - }; - - let actual = action( - &row, - Tag::unknown(), - None, - &closure_flags, - &trim, - ActionMode::Global, - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn trims_custom_chars_from_right() { - let word = string("#@! andres !@#"); - let expected = string("#@! andres !@"); - let closure_flags = ClosureFlags { - right_trim: true, - ..Default::default() - }; - - let actual = action( - &word, - Tag::unknown(), - Some('#'), - &closure_flags, - &trim, - ActionMode::Local, - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn trims_whitespace_format_flag() { - let word = string(" nushell is great "); - let expected = string("nushell is great"); - let closure_flags = ClosureFlags { - format_flag: true, - ..Default::default() - }; - - let actual = action( - &word, - Tag::unknown(), - Some(' '), - &closure_flags, - &trim, - ActionMode::Local, - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn trims_format_flag_global() { - let word = string("global "); - let expected = string("global"); - let closure_flags = ClosureFlags { - format_flag: true, - ..Default::default() - }; - - let actual = action( - &word, - Tag::unknown(), - Some(' '), - &closure_flags, - &trim, - ActionMode::Local, - ) - .unwrap(); - assert_eq!(actual, expected); - } - #[test] - fn global_trim_format_flag_ignores_numbers() { - let number = int(2020); - let expected = int(2020); - let closure_flags = ClosureFlags { - format_flag: true, - ..Default::default() - }; - - let actual = action( - &number, - Tag::unknown(), - None, - &closure_flags, - &trim, - ActionMode::Global, - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn global_trim_format_flag_row() { - let row = row!["a".to_string() => string(" c "), " b ".to_string() => string(" b c d e ")]; - let expected = row!["a".to_string() => string("c"), " b ".to_string() => string("b c d e")]; - let closure_flags = ClosureFlags { - format_flag: true, - ..Default::default() - }; - - let actual = action( - &row, - Tag::unknown(), - None, - &closure_flags, - &trim, - ActionMode::Global, - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn global_trim_format_flag_table() { - let row = table(&[ - string(" a b c d "), - int(65), - string(" b c d e f"), - ]); - let expected = table(&[string("a b c d"), int(65), string("b c d e f")]); - let closure_flags = ClosureFlags { - format_flag: true, - ..Default::default() - }; - - let actual = action( - &row, - Tag::unknown(), - None, - &closure_flags, - &trim, - ActionMode::Global, - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn trims_custom_chars_format_flag() { - let word = string(".Value1.a..lot...of....dots."); - let expected = string("Value1.a.lot.of.dots"); - let closure_flags = ClosureFlags { - format_flag: true, - ..Default::default() - }; - - let actual = action( - &word, - Tag::unknown(), - Some('.'), - &closure_flags, - &trim, - ActionMode::Local, - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn trims_all_format_flag_whitespace() { - let word = string(" nushell is great "); - let expected = string("nushellisgreat"); - let closure_flags = ClosureFlags { - format_flag: true, - all_flag: true, - ..Default::default() - }; - - let actual = action( - &word, - Tag::unknown(), - Some(' '), - &closure_flags, - &trim, - ActionMode::Local, - ) - .unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn trims_all_format_flag_global() { - let word = string(" nushell is great "); - let expected = string("nushellisgreat"); - let closure_flags = ClosureFlags { - format_flag: true, - all_flag: true, - ..Default::default() - }; - - let actual = action( - &word, - Tag::unknown(), - Some(' '), - &closure_flags, - &trim, - ActionMode::Global, - ) - .unwrap(); - assert_eq!(actual, expected); - } -} diff --git a/crates/nu-command/src/commands/strings/str_/trim/mod.rs b/crates/nu-command/src/commands/strings/str_/trim/mod.rs deleted file mode 100644 index 115b3572d5..0000000000 --- a/crates/nu-command/src/commands/strings/str_/trim/mod.rs +++ /dev/null @@ -1,144 +0,0 @@ -mod command; -use crate::prelude::*; -use nu_errors::ShellError; -use nu_protocol::ShellTypeName; -use nu_protocol::{ColumnPath, Primitive, ReturnSuccess, UntaggedValue, Value}; -use nu_source::{Tag, Tagged}; -use nu_value_ext::ValueExt; -use std::iter::FromIterator; - -pub use command::SubCommand as Trim; - -struct Arguments { - character: Option>, - column_paths: Vec, -} - -#[derive(Default, Debug, Copy, Clone)] -pub struct ClosureFlags { - all_flag: bool, - left_trim: bool, - right_trim: bool, - format_flag: bool, - both_flag: bool, -} - -pub fn operate(args: CommandArgs, trim_operation: &'static F) -> Result -where - F: Fn(&str, Option, &ClosureFlags) -> String + Send + Sync + 'static, -{ - let (options, closure_flags, input) = ( - Arc::new(Arguments { - character: args.get_flag("char")?, - column_paths: args.rest(0)?, - }), - ClosureFlags { - all_flag: args.has_flag("all"), - left_trim: args.has_flag("left"), - right_trim: args.has_flag("right"), - format_flag: args.has_flag("format"), - both_flag: args.has_flag("both") - || (!args.has_flag("all") - && !args.has_flag("left") - && !args.has_flag("right") - && !args.has_flag("format")), // this is the case if no flags are provided - }, - args.input, - ); - let to_trim = options.character.as_ref().map(|tagged| tagged.item); - Ok(input - .map(move |v| { - if options.column_paths.is_empty() { - ReturnSuccess::value(action( - &v, - v.tag(), - to_trim, - &closure_flags, - &trim_operation, - ActionMode::Global, - )?) - } else { - let mut ret = v; - - for path in &options.column_paths { - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| { - action( - old, - old.tag(), - to_trim, - &closure_flags, - &trim_operation, - ActionMode::Local, - ) - }), - )?; - } - - ReturnSuccess::value(ret) - } - }) - .into_action_stream()) -} - -#[derive(Debug, Copy, Clone)] -pub enum ActionMode { - Local, - Global, -} - -pub fn action( - input: &Value, - tag: impl Into, - char_: Option, - closure_flags: &ClosureFlags, - trim_operation: &F, - mode: ActionMode, -) -> Result -where - F: Fn(&str, Option, &ClosureFlags) -> String + Send + Sync + 'static, -{ - let tag = tag.into(); - match &input.value { - UntaggedValue::Primitive(Primitive::String(s)) => { - Ok(UntaggedValue::string(trim_operation(s, char_, closure_flags)).into_value(tag)) - } - other => match mode { - ActionMode::Global => match other { - UntaggedValue::Row(dictionary) => { - let results: Result, ShellError> = dictionary - .entries() - .iter() - .map(|(k, v)| -> Result<_, ShellError> { - Ok(( - k.clone(), - action(v, tag.clone(), char_, closure_flags, trim_operation, mode)?, - )) - }) - .collect(); - let indexmap = IndexMap::from_iter(results?); - Ok(UntaggedValue::Row(indexmap.into()).into_value(tag)) - } - UntaggedValue::Table(values) => { - let values: Result, ShellError> = values - .iter() - .map(|v| -> Result<_, ShellError> { - action(v, tag.clone(), char_, closure_flags, trim_operation, mode) - }) - .collect(); - Ok(UntaggedValue::Table(values?).into_value(tag)) - } - _ => Ok(input.clone()), - }, - ActionMode::Local => { - let got = format!("got {}", other.type_name()); - Err(ShellError::labeled_error( - "value is not string", - got, - tag.span, - )) - } - }, - } -} diff --git a/crates/nu-command/src/commands/strings/str_/upcase.rs b/crates/nu-command/src/commands/strings/str_/upcase.rs deleted file mode 100644 index 285e55ee3f..0000000000 --- a/crates/nu-command/src/commands/strings/str_/upcase.rs +++ /dev/null @@ -1,112 +0,0 @@ -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::ShellTypeName; -use nu_protocol::{ - ColumnPath, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value, -}; -use nu_source::Tag; -use nu_value_ext::ValueExt; - -struct Arguments { - column_paths: Vec, -} - -pub struct SubCommand; - -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "str upcase" - } - - fn signature(&self) -> Signature { - Signature::build("str upcase").rest( - "rest", - SyntaxShape::ColumnPath, - "optionally upcase text by column paths", - ) - } - - fn usage(&self) -> &str { - "upcases text" - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - operate(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Upcase contents", - example: "echo 'nu' | str upcase", - result: Some(vec![Value::from("NU")]), - }] - } -} - -fn operate(args: CommandArgs) -> Result { - let (options, input) = ( - Arguments { - column_paths: args.rest(0)?, - }, - args.input, - ); - - Ok(input - .map(move |v| { - if options.column_paths.is_empty() { - ReturnSuccess::value(action(&v, v.tag())?) - } else { - let mut ret = v; - - for path in &options.column_paths { - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| action(old, old.tag())), - )?; - } - - ReturnSuccess::value(ret) - } - }) - .into_action_stream()) -} - -fn action(input: &Value, tag: impl Into) -> Result { - match &input.value { - UntaggedValue::Primitive(Primitive::String(s)) => { - Ok(UntaggedValue::string(s.to_ascii_uppercase()).into_value(tag)) - } - other => { - let got = format!("got {}", other.type_name()); - Err(ShellError::labeled_error( - "value is not string", - got, - tag.into().span, - )) - } - } -} - -#[cfg(test)] -mod tests { - use super::ShellError; - use super::{action, SubCommand}; - use nu_source::Tag; - use nu_test_support::value::string; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } - - #[test] - fn upcases() { - let word = string("andres"); - - let actual = action(&word, Tag::unknown()).unwrap(); - assert_eq!(actual, string("ANDRES")); - } -} diff --git a/crates/nu-command/src/commands/system/mod.rs b/crates/nu-command/src/commands/system/mod.rs deleted file mode 100644 index 6fd20a9361..0000000000 --- a/crates/nu-command/src/commands/system/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -#[cfg(feature = "ps")] -mod ps; -#[cfg(feature = "ps")] -pub use ps::Command as Ps; - -#[cfg(feature = "sys")] -mod sys; -#[cfg(feature = "sys")] -pub use sys::Command as Sys; diff --git a/crates/nu-command/src/commands/system/ps.rs b/crates/nu-command/src/commands/system/ps.rs deleted file mode 100644 index aca2c62c5a..0000000000 --- a/crates/nu-command/src/commands/system/ps.rs +++ /dev/null @@ -1,84 +0,0 @@ -use crate::prelude::*; -use nu_errors::ShellError; -use nu_protocol::{Signature, TaggedDictBuilder, UntaggedValue}; -use sysinfo::{PidExt, ProcessExt, System, SystemExt}; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "ps" - } - - fn signature(&self) -> Signature { - Signature::build("ps") - .desc("View information about system processes.") - .switch( - "long", - "list all available columns for each entry", - Some('l'), - ) - .filter() - } - - fn usage(&self) -> &str { - "View information about system processes." - } - - fn run(&self, args: CommandArgs) -> Result { - run_ps(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "List the system processes", - example: "ps", - result: None, - }] - } -} - -fn run_ps(args: CommandArgs) -> Result { - let long = args.has_flag("long"); - let mut sys = System::new_all(); - sys.refresh_all(); - - let mut output = vec![]; - - let result: Vec<_> = sys.processes().iter().map(|x| *x.0).collect(); - - for pid in result { - if let Some(result) = sys.process(pid) { - let mut dict = TaggedDictBuilder::new(args.name_tag()); - dict.insert_untagged("pid", UntaggedValue::int(pid.as_u32() as i64)); - dict.insert_untagged("name", UntaggedValue::string(result.name())); - dict.insert_untagged( - "status", - UntaggedValue::string(format!("{:?}", result.status())), - ); - dict.insert_untagged( - "cpu", - UntaggedValue::decimal_from_float(result.cpu_usage() as f64, args.name_tag().span), - ); - dict.insert_untagged("mem", UntaggedValue::filesize(result.memory() * 1000)); - dict.insert_untagged( - "virtual", - UntaggedValue::filesize(result.virtual_memory() * 1000), - ); - - if long { - if let Some(parent) = result.parent() { - dict.insert_untagged("parent", UntaggedValue::int(parent.as_u32() as i64)); - } else { - dict.insert_untagged("parent", UntaggedValue::nothing()); - } - dict.insert_untagged("exe", UntaggedValue::filepath(result.exe())); - dict.insert_untagged("command", UntaggedValue::string(result.cmd().join(" "))); - } - - output.push(dict.into_value()); - } - } - - Ok(output.into_iter().into_output_stream()) -} diff --git a/crates/nu-command/src/commands/system/sys.rs b/crates/nu-command/src/commands/system/sys.rs deleted file mode 100644 index 9ca64f515f..0000000000 --- a/crates/nu-command/src/commands/system/sys.rs +++ /dev/null @@ -1,248 +0,0 @@ -use crate::prelude::*; -use nu_errors::ShellError; -use nu_protocol::{Signature, TaggedDictBuilder, UntaggedValue}; -use sysinfo::{ComponentExt, DiskExt, NetworkExt, ProcessorExt, System, SystemExt, UserExt}; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "sys" - } - - fn signature(&self) -> Signature { - Signature::build("sys") - .desc("View information about the current system.") - .filter() - } - - fn usage(&self) -> &str { - "View information about the system." - } - - fn run(&self, args: CommandArgs) -> Result { - run_sys(args) - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Show info about the system", - example: "sys", - result: None, - }] - } -} - -fn run_sys(args: CommandArgs) -> Result { - let tag = args.name_tag(); - let mut sys = System::new(); - - let mut sysinfo = TaggedDictBuilder::with_capacity(&tag, 6); - - if let Some(host) = host(&mut sys, tag.clone()) { - sysinfo.insert_value("host", host); - } - if let Some(cpus) = cpu(&mut sys, tag.clone()) { - sysinfo.insert_value("cpu", cpus); - } - if let Some(disks) = disks(&mut sys, tag.clone()) { - sysinfo.insert_value("disks", disks); - } - if let Some(mem) = mem(&mut sys, tag.clone()) { - sysinfo.insert_value("mem", mem); - } - if let Some(temp) = temp(&mut sys, tag.clone()) { - sysinfo.insert_value("temp", temp); - } - if let Some(net) = net(&mut sys, tag) { - sysinfo.insert_value("net", net); - } - - Ok(vec![sysinfo.into_value()].into_iter().into_output_stream()) -} - -pub fn trim_cstyle_null(s: String) -> String { - s.trim_matches(char::from(0)).to_string() -} - -pub fn disks(sys: &mut System, tag: Tag) -> Option { - sys.refresh_disks(); - sys.refresh_disks_list(); - - let mut output = vec![]; - for disk in sys.disks() { - let mut dict = TaggedDictBuilder::new(&tag); - dict.insert_untagged( - "device", - UntaggedValue::string(trim_cstyle_null(disk.name().to_string_lossy().to_string())), - ); - dict.insert_untagged( - "type", - UntaggedValue::string(trim_cstyle_null( - String::from_utf8_lossy(disk.file_system()).to_string(), - )), - ); - dict.insert_untagged("mount", UntaggedValue::filepath(disk.mount_point())); - dict.insert_untagged("total", UntaggedValue::filesize(disk.total_space())); - dict.insert_untagged("free", UntaggedValue::filesize(disk.available_space())); - output.push(dict.into_value()); - } - if !output.is_empty() { - Some(UntaggedValue::Table(output)) - } else { - None - } -} - -pub fn net(sys: &mut System, tag: Tag) -> Option { - sys.refresh_networks(); - sys.refresh_networks_list(); - - let mut output = vec![]; - for (iface, data) in sys.networks() { - let mut dict = TaggedDictBuilder::new(&tag); - dict.insert_untagged( - "name", - UntaggedValue::string(trim_cstyle_null(iface.to_string())), - ); - dict.insert_untagged("sent", UntaggedValue::filesize(data.total_transmitted())); - dict.insert_untagged("recv", UntaggedValue::filesize(data.total_received())); - - output.push(dict.into_value()); - } - if !output.is_empty() { - Some(UntaggedValue::Table(output)) - } else { - None - } -} - -pub fn cpu(sys: &mut System, tag: Tag) -> Option { - sys.refresh_cpu(); - - let mut output = vec![]; - for cpu in sys.processors() { - let mut dict = TaggedDictBuilder::new(&tag); - dict.insert_untagged( - "name", - UntaggedValue::string(trim_cstyle_null(cpu.name().to_string())), - ); - dict.insert_untagged( - "brand", - UntaggedValue::string(trim_cstyle_null(cpu.brand().to_string())), - ); - dict.insert_untagged("freq", UntaggedValue::int(cpu.frequency() as i64)); - - output.push(dict.into_value()); - } - if !output.is_empty() { - Some(UntaggedValue::Table(output)) - } else { - None - } -} - -pub fn mem(sys: &mut System, tag: Tag) -> Option { - sys.refresh_memory(); - - let mut dict = TaggedDictBuilder::new(tag); - let total_mem = sys.total_memory(); - let free_mem = sys.free_memory(); - let total_swap = sys.total_swap(); - let free_swap = sys.free_swap(); - - dict.insert_untagged("total", UntaggedValue::filesize(total_mem * 1000)); - dict.insert_untagged("free", UntaggedValue::filesize(free_mem * 1000)); - dict.insert_untagged("swap total", UntaggedValue::filesize(total_swap * 1000)); - dict.insert_untagged("swap free", UntaggedValue::filesize(free_swap * 1000)); - - Some(dict.into_untagged_value()) -} - -pub fn host(sys: &mut System, tag: Tag) -> Option { - sys.refresh_users_list(); - - let mut dict = TaggedDictBuilder::new(&tag); - if let Some(name) = sys.name() { - dict.insert_untagged("name", UntaggedValue::string(trim_cstyle_null(name))); - } - if let Some(version) = sys.os_version() { - dict.insert_untagged( - "os version", - UntaggedValue::string(trim_cstyle_null(version)), - ); - } - if let Some(version) = sys.kernel_version() { - dict.insert_untagged( - "kernel version", - UntaggedValue::string(trim_cstyle_null(version)), - ); - } - if let Some(hostname) = sys.host_name() { - dict.insert_untagged( - "hostname", - UntaggedValue::string(trim_cstyle_null(hostname)), - ); - } - dict.insert_untagged( - "uptime", - UntaggedValue::duration(1000000000 * sys.uptime() as i64), - ); - - let mut users = vec![]; - for user in sys.users() { - let mut user_dict = TaggedDictBuilder::new(&tag); - user_dict.insert_untagged( - "name", - UntaggedValue::string(trim_cstyle_null(user.name().to_string())), - ); - - let mut groups = vec![]; - for group in user.groups() { - groups - .push(UntaggedValue::string(trim_cstyle_null(group.to_string())).into_value(&tag)); - } - user_dict.insert_untagged("groups", UntaggedValue::Table(groups)); - - users.push(user_dict.into_value()); - } - if !users.is_empty() { - dict.insert_untagged("sessions", UntaggedValue::Table(users)); - } - - Some(dict.into_untagged_value()) -} - -pub fn temp(sys: &mut System, tag: Tag) -> Option { - sys.refresh_components(); - sys.refresh_components_list(); - - let mut output = vec![]; - - for component in sys.components() { - let mut dict = TaggedDictBuilder::new(&tag); - - dict.insert_untagged("unit", UntaggedValue::string(component.label())); - dict.insert_untagged( - "temp", - UntaggedValue::decimal_from_float(component.temperature() as f64, tag.span), - ); - dict.insert_untagged( - "high", - UntaggedValue::decimal_from_float(component.max() as f64, tag.span), - ); - - if let Some(critical) = component.critical() { - dict.insert_untagged( - "critical", - UntaggedValue::decimal_from_float(critical as f64, tag.span), - ); - } - output.push(dict.into_value()); - } - if !output.is_empty() { - Some(UntaggedValue::Table(output)) - } else { - None - } -} diff --git a/crates/nu-command/src/commands/viewers/autoview/command.rs b/crates/nu-command/src/commands/viewers/autoview/command.rs deleted file mode 100644 index 9485332774..0000000000 --- a/crates/nu-command/src/commands/viewers/autoview/command.rs +++ /dev/null @@ -1,330 +0,0 @@ -use crate::commands::viewers::autoview::options::ConfigExtensions; -use crate::prelude::*; -use crate::primitive::get_color_config; -use nu_data::value::format_leaf; -use nu_engine::{UnevaluatedCallInfo, WholeStreamCommand}; -use nu_errors::ShellError; -use nu_protocol::hir::{self, Expression, ExternalRedirection, Literal, SpannedExpression}; -use nu_protocol::{Primitive, Signature, UntaggedValue, Value}; -use nu_table::TextStyle; - -#[cfg(feature = "dataframe")] -use nu_protocol::dataframe::FrameStruct; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "autoview" - } - - fn signature(&self) -> Signature { - Signature::build("autoview") - } - - fn usage(&self) -> &str { - "View the contents of the pipeline as a table or list." - } - - fn run(&self, args: CommandArgs) -> Result { - autoview(args) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Automatically view the results", - example: "ls | autoview", - result: None, - }, - Example { - description: "Autoview is also implied. The above can be written as", - example: "ls", - result: None, - }, - ] - } -} - -pub fn autoview(args: CommandArgs) -> Result { - let configuration = args.configs().lock().global_config(); - let tag = args.call_info.name_tag.clone(); - - let binary = args.scope().get_command("binaryview"); - let text = args.scope().get_command("textview"); - let table = args.scope().get_command("table"); - let context = args.context; - let mut input_stream = args.input; - - if let Some(x) = input_stream.next() { - match input_stream.next() { - Some(y) => { - let ctrl_c = context.ctrl_c().clone(); - let xy = vec![x, y]; - let xy_stream = xy.into_iter().chain(input_stream).interruptible(ctrl_c); - - let stream = InputStream::from_stream(xy_stream); - - if let Some(table) = table { - let command_args = create_default_command_args(&context, stream, tag); - let result = table.run(command_args)?; - let _ = result.collect::>(); - } - } - _ => { - match x { - Value { - value: UntaggedValue::Primitive(Primitive::String(ref s)), - tag: Tag { anchor, span }, - } if anchor.is_some() => { - if let Some(text) = text { - let command_args = create_default_command_args( - &context, - InputStream::one( - UntaggedValue::string(s).into_value(Tag { anchor, span }), - ), - tag, - ); - let result = text.run_with_actions(command_args)?; - let _ = result.collect::>(); - } else { - out!("{}", s); - } - } - Value { - value: UntaggedValue::Primitive(Primitive::String(s)), - .. - } => { - out!("{}", s); - } - Value { - value: UntaggedValue::Primitive(Primitive::FilePath(s)), - .. - } => { - out!("{}", s.display()); - } - Value { - value: UntaggedValue::Primitive(Primitive::Int(n)), - .. - } => { - out!("{}", n); - } - Value { - value: UntaggedValue::Primitive(Primitive::BigInt(n)), - .. - } => { - out!("{}", n); - } - Value { - value: UntaggedValue::Primitive(Primitive::Decimal(n)), - .. - } => { - // TODO: normalize decimal to remove trailing zeros. - // normalization will be available in next release of bigdecimal crate - let mut output = n.to_string(); - if output.contains('.') { - output = output.trim_end_matches('0').to_owned(); - } - if output.ends_with('.') { - output.push('0'); - } - out!("{}", output); - } - Value { - value: UntaggedValue::Primitive(Primitive::Boolean(b)), - .. - } => { - out!("{}", b); - } - Value { - value: UntaggedValue::Primitive(Primitive::Duration(_)), - .. - } => { - let output = format_leaf(&x).plain_string(100_000); - out!("{}", output); - } - Value { - value: UntaggedValue::Primitive(Primitive::Filesize(_)), - .. - } => { - let output = format_leaf(&x).plain_string(100_000); - out!("{}", output); - } - Value { - value: UntaggedValue::Primitive(Primitive::Date(d)), - .. - } => { - out!("{}", d); - } - Value { - value: UntaggedValue::Primitive(Primitive::Range(_)), - .. - } => { - let output = format_leaf(&x).plain_string(100_000); - out!("{}", output); - } - - Value { - value: UntaggedValue::Primitive(Primitive::Binary(ref b)), - .. - } => { - if let Some(binary) = binary { - let command_args = - create_default_command_args(&context, InputStream::one(x), tag); - let result = binary.run_with_actions(command_args)?; - let _ = result.collect::>(); - } else { - use nu_pretty_hex::*; - out!("{:?}", b.hex_dump()); - } - } - - Value { - value: UntaggedValue::Error(e), - .. - } => { - return Err(e); - } - - Value { - value: UntaggedValue::Row(ref row), - .. - } => { - let pivot_mode = configuration.pivot_mode(); - - let term_width = context.host().lock().width(); - if pivot_mode.is_always() - || (pivot_mode.is_auto() - && (row - .entries - .iter() - .map(|(_, v)| v.convert_to_string()) - .collect::>() - .iter() - .fold(0usize, |acc, len| acc + len.len()) - + row.entries.iter().count() * 2) - > term_width) - { - let mut entries = vec![]; - for (key, value) in &row.entries { - entries.push(vec![ - nu_table::StyledString::new( - key.to_string(), - TextStyle::new() - .alignment(nu_table::Alignment::Left) - .fg(nu_ansi_term::Color::Green) - .bold(Some(true)), - ), - nu_table::StyledString::new( - format_leaf(value).plain_string(100_000), - nu_table::TextStyle::basic_left(), - ), - ]); - } - - let color_hm = get_color_config(&configuration); - - let table = - nu_table::Table::new(vec![], entries, nu_table::Theme::compact()); - - println!("{}", nu_table::draw_table(&table, term_width, &color_hm)); - } else if let Some(table) = table { - let command_args = - create_default_command_args(&context, InputStream::one(x), tag); - let result = table.run(command_args)?; - let _ = result.collect::>(); - } else { - out!("{:?}", row); - } - } - #[cfg(feature = "dataframe")] - Value { - value: UntaggedValue::DataFrame(df), - tag, - } => { - if let Some(table) = table { - // TODO. Configure the parameter rows from file. It can be - // adjusted to see a certain amount of values in the head - let command_args = - create_default_command_args(&context, df.print()?.into(), tag); - let result = table.run(command_args)?; - let _ = result.collect::>(); - } - } - #[cfg(feature = "dataframe")] - Value { - value: UntaggedValue::FrameStruct(FrameStruct::GroupBy(groupby)), - tag, - } => { - if let Some(table) = table { - // TODO. Configure the parameter rows from file. It can be - // adjusted to see a certain amount of values in the head - let command_args = - create_default_command_args(&context, groupby.print()?.into(), tag); - let result = table.run(command_args)?; - let _ = result.collect::>(); - } - } - Value { - value: UntaggedValue::Primitive(Primitive::Nothing), - .. - } => { - // Do nothing - } - Value { - value: ref item, .. - } => { - if let Some(table) = table { - let command_args = - create_default_command_args(&context, InputStream::one(x), tag); - let result = table.run(command_args)?; - let _ = result.collect::>(); - } else { - out!("{:?}", item); - } - } - } - } - } - } - - Ok(InputStream::empty()) -} - -fn create_default_command_args( - context: &EvaluationContext, - input: InputStream, - tag: Tag, -) -> CommandArgs { - let span = tag.span; - CommandArgs { - context: context.clone(), - call_info: UnevaluatedCallInfo { - args: hir::Call { - head: Box::new(SpannedExpression::new( - Expression::Literal(Literal::String(String::new())), - span, - )), - positional: None, - named: None, - span, - external_redirection: ExternalRedirection::Stdout, - }, - name_tag: tag, - }, - input, - } -} - -#[cfg(test)] -mod tests { - use super::Command; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Command {}) - } -} diff --git a/crates/nu-command/src/commands/viewers/autoview/mod.rs b/crates/nu-command/src/commands/viewers/autoview/mod.rs deleted file mode 100644 index 5cb35ceb18..0000000000 --- a/crates/nu-command/src/commands/viewers/autoview/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod command; -mod options; - -pub use command::Command as Autoview; diff --git a/crates/nu-command/src/commands/viewers/autoview/options.rs b/crates/nu-command/src/commands/viewers/autoview/options.rs deleted file mode 100644 index ad6aa5fca6..0000000000 --- a/crates/nu-command/src/commands/viewers/autoview/options.rs +++ /dev/null @@ -1,51 +0,0 @@ -pub use nu_data::config::NuConfig; -use std::fmt::Debug; - -#[derive(PartialEq, Debug)] -pub enum AutoPivotMode { - Auto, - Always, - Never, -} - -impl AutoPivotMode { - pub fn is_auto(&self) -> bool { - matches!(self, AutoPivotMode::Auto) - } - - pub fn is_always(&self) -> bool { - matches!(self, AutoPivotMode::Always) - } - - #[allow(unused)] - pub fn is_never(&self) -> bool { - matches!(self, AutoPivotMode::Never) - } -} - -pub trait ConfigExtensions: Debug + Send { - fn pivot_mode(&self) -> AutoPivotMode; -} - -pub fn pivot_mode(config: &NuConfig) -> AutoPivotMode { - let vars = &config.vars; - - if let Some(mode) = vars.get("pivot_mode") { - let mode = match mode.as_string() { - Ok(m) if m.to_lowercase() == "auto" => AutoPivotMode::Auto, - Ok(m) if m.to_lowercase() == "always" => AutoPivotMode::Always, - Ok(m) if m.to_lowercase() == "never" => AutoPivotMode::Never, - _ => AutoPivotMode::Never, - }; - - return mode; - } - - AutoPivotMode::Never -} - -impl ConfigExtensions for NuConfig { - fn pivot_mode(&self) -> AutoPivotMode { - pivot_mode(self) - } -} diff --git a/crates/nu-command/src/commands/viewers/bat_constants.rs b/crates/nu-command/src/commands/viewers/bat_constants.rs deleted file mode 100644 index 0b7ca91a5b..0000000000 --- a/crates/nu-command/src/commands/viewers/bat_constants.rs +++ /dev/null @@ -1,358 +0,0 @@ -pub const BAT_LANGUAGES: &[&str] = &[ - "as", - "csv", - "tsv", - "applescript", - "script editor", - "s", - "S", - "adoc", - "asciidoc", - "asc", - "asa", - "yasm", - "nasm", - "asm", - "inc", - "mac", - "awk", - "bat", - "cmd", - "bib", - "sh", - "bash", - "zsh", - ".bash_aliases", - ".bash_completions", - ".bash_functions", - ".bash_login", - ".bash_logout", - ".bash_profile", - ".bash_variables", - ".bashrc", - ".profile", - ".textmate_init", - ".zshrc", - "PKGBUILD", - ".ebuild", - ".eclass", - "c", - "h", - "cs", - "csx", - "cpp", - "cc", - "cp", - "cxx", - "c++", - "C", - "h", - "hh", - "hpp", - "hxx", - "h++", - "inl", - "ipp", - "cabal", - "clj", - "cljc", - "cljs", - "edn", - "CMakeLists.txt", - "cmake", - "h.in", - "hh.in", - "hpp.in", - "hxx.in", - "h++.in", - "CMakeCache.txt", - "cr", - "css", - "css.erb", - "css.liquid", - "d", - "di", - "dart", - "diff", - "patch", - "Dockerfile", - "dockerfile", - "ex", - "exs", - "elm", - "erl", - "hrl", - "Emakefile", - "emakefile", - "fs", - "fsi", - "fsx", - "fs", - "fsi", - "fsx", - "fish", - "attributes", - "gitattributes", - ".gitattributes", - "COMMIT_EDITMSG", - "MERGE_MSG", - "TAG_EDITMSG", - "gitconfig", - ".gitconfig", - ".gitmodules", - "exclude", - "gitignore", - ".gitignore", - ".git", - "gitlog", - "git-rebase-todo", - "go", - "dot", - "DOT", - "gv", - "groovy", - "gvy", - "gradle", - "Jenkinsfile", - "hs", - "hs", - "hsc", - "show-nonprintable", - "html", - "htm", - "shtml", - "xhtml", - "asp", - "html.eex", - "yaws", - "rails", - "rhtml", - "erb", - "html.erb", - "adp", - "twig", - "html.twig", - "ini", - "INI", - "INF", - "reg", - "REG", - "lng", - "cfg", - "CFG", - "desktop", - "url", - "URL", - ".editorconfig", - ".hgrc", - "hgrc", - "java", - "bsh", - "properties", - "jsp", - "js", - "htc", - "js", - "jsx", - "babel", - "es6", - "js.erb", - "json", - "sublime-settings", - "sublime-menu", - "sublime-keymap", - "sublime-mousemap", - "sublime-theme", - "sublime-build", - "sublime-project", - "sublime-completions", - "sublime-commands", - "sublime-macro", - "sublime-color-scheme", - "ipynb", - "Pipfile.lock", - "jsonnet", - "libsonnet", - "libjsonnet", - "jl", - "kt", - "kts", - "tex", - "ltx", - "less", - "css.less", - "lisp", - "cl", - "clisp", - "l", - "mud", - "el", - "scm", - "ss", - "lsp", - "fasl", - "lhs", - "lua", - "make", - "GNUmakefile", - "makefile", - "Makefile", - "makefile.am", - "Makefile.am", - "makefile.in", - "Makefile.in", - "OCamlMakefile", - "mak", - "mk", - "md", - "mdown", - "markdown", - "markdn", - "matlab", - "build", - "nix", - "m", - "h", - "mm", - "M", - "h", - "ml", - "mli", - "mll", - "mly", - "pas", - "p", - "dpr", - "pl", - "pm", - "pod", - "t", - "PL", - "php", - "php3", - "php4", - "php5", - "php7", - "phps", - "phpt", - "phtml", - "txt", - "ps1", - "psm1", - "psd1", - "proto", - "protodevel", - "pb.txt", - "proto.text", - "textpb", - "pbtxt", - "prototxt", - "pp", - "epp", - "purs", - "py", - "py3", - "pyw", - "pyi", - "pyx", - "pyx.in", - "pxd", - "pxd.in", - "pxi", - "pxi.in", - "rpy", - "cpy", - "SConstruct", - "Sconstruct", - "sconstruct", - "SConscript", - "gyp", - "gypi", - "Snakefile", - "wscript", - "R", - "r", - "s", - "S", - "Rprofile", - "rd", - "re", - "rst", - "rest", - "robot", - "rb", - "Appfile", - "Appraisals", - "Berksfile", - "Brewfile", - "capfile", - "cgi", - "Cheffile", - "config.ru", - "Deliverfile", - "Fastfile", - "fcgi", - "Gemfile", - "gemspec", - "Guardfile", - "irbrc", - "jbuilder", - "Podfile", - "podspec", - "prawn", - "rabl", - "rake", - "Rakefile", - "Rantfile", - "rbx", - "rjs", - "ruby.rail", - "Scanfile", - "simplecov", - "Snapfile", - "thor", - "Thorfile", - "Vagrantfile", - "haml", - "sass", - "rxml", - "builder", - "rs", - "scala", - "sbt", - "sql", - "ddl", - "dml", - "erbsql", - "sql.erb", - "swift", - "log", - "tcl", - "tf", - "tfvars", - "hcl", - "sty", - "cls", - "textile", - "toml", - "tml", - "Cargo.lock", - "Gopkg.lock", - "Pipfile", - "ts", - "tsx", - "varlink", - "vim", - ".vimrc", - "xml", - "xsd", - "xslt", - "tld", - "dtml", - "rss", - "opml", - "svg", - "yaml", - "yml", - "sublime-syntax", -]; diff --git a/crates/nu-command/src/commands/viewers/mod.rs b/crates/nu-command/src/commands/viewers/mod.rs deleted file mode 100644 index ddd1b9c67d..0000000000 --- a/crates/nu-command/src/commands/viewers/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod autoview; -mod bat_constants; -mod table; - -pub use autoview::*; -pub use bat_constants::BAT_LANGUAGES; -pub use table::*; diff --git a/crates/nu-command/src/commands/viewers/table/command.rs b/crates/nu-command/src/commands/viewers/table/command.rs deleted file mode 100644 index 6caa91c6d6..0000000000 --- a/crates/nu-command/src/commands/viewers/table/command.rs +++ /dev/null @@ -1,267 +0,0 @@ -use crate::commands::viewers::table::options::{ConfigExtensions, NuConfig}; -use crate::prelude::*; -use crate::primitive::get_color_config; -use futures::executor::block_on; -use nu_data::value::{format_leaf, style_leaf}; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value}; -use nu_table::{draw_table, Alignment, StyledString, TextStyle}; -use std::collections::HashMap; -use std::sync::atomic::Ordering; -use std::time::Instant; - -const STREAM_PAGE_SIZE: usize = 1000; -const STREAM_TIMEOUT_CHECK_INTERVAL: usize = 100; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "table" - } - - fn signature(&self) -> Signature { - Signature::build("table").named( - "start_number", - SyntaxShape::Number, - "row number to start viewing from", - Some('n'), - ) - } - - fn usage(&self) -> &str { - "View the contents of the pipeline as a table." - } - - fn run(&self, args: CommandArgs) -> Result { - table(args) - } -} - -pub fn from_list( - values: &[Value], - configuration: &NuConfig, - starting_idx: usize, - color_hm: &HashMap, -) -> nu_table::Table { - let header_style = configuration.header_style(); - let mut headers: Vec = nu_protocol::merge_descriptors(values) - .into_iter() - .map(|x| StyledString::new(x, header_style)) - .collect(); - let entries = values_to_entries(values, &mut headers, configuration, starting_idx, color_hm); - nu_table::Table { - headers, - data: entries, - theme: configuration.table_mode(), - } -} - -fn values_to_entries( - values: &[Value], - headers: &mut Vec, - configuration: &NuConfig, - starting_idx: usize, - color_hm: &HashMap, -) -> Vec> { - let disable_indexes = configuration.disabled_indexes(); - let mut entries = vec![]; - - if headers.is_empty() { - headers.push(StyledString::new("".to_string(), TextStyle::basic_left())); - } - - for (idx, value) in values.iter().enumerate() { - let mut row: Vec = headers - .iter() - .map(|d: &StyledString| { - if d.contents.is_empty() { - match value { - Value { - value: UntaggedValue::Row(..), - .. - } => StyledString::new( - format_leaf(&UntaggedValue::nothing()).plain_string(100_000), - style_leaf(&UntaggedValue::nothing(), color_hm), - ), - _ => StyledString::new( - format_leaf(value).plain_string(100_000), - style_leaf(value, color_hm), - ), - } - } 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(), color_hm), - ) - } - _ => StyledString::new( - format_leaf(&UntaggedValue::nothing()).plain_string(100_000), - style_leaf(&UntaggedValue::nothing(), color_hm), - ), - } - } - }) - .collect(); - - // Indices are green, bold, right-aligned: - // unless we change them :) - if !disable_indexes { - row.insert( - 0, - StyledString::new( - (starting_idx + idx).to_string(), - TextStyle::new().alignment(Alignment::Right).style( - color_hm - .get("index_color") - .unwrap_or( - &nu_ansi_term::Style::default() - .bold() - .fg(nu_ansi_term::Color::Green), - ) - .to_owned(), - ), - ), - ); - } - - entries.push(row); - } - - if !disable_indexes { - headers.insert( - 0, - StyledString::new( - "#".to_string(), - TextStyle::new().alignment(Alignment::Center).style( - color_hm - .get("header_color") - .unwrap_or( - &nu_ansi_term::Style::default() - .bold() - .fg(nu_ansi_term::Color::Green), - ) - .to_owned(), - ), - ), - ); - } - - entries -} - -fn table(mut args: CommandArgs) -> Result { - let configuration = args.configs().lock().global_config(); - - // Ideally, get_color_config would get all the colors configured in the config.toml - // and create a style based on those settings. However, there are few places where - // this just won't work right now, like header styling, because a style needs to know - // more than just color, it needs fg & bg color, bold, dimmed, italic, underline, - // blink, reverse, hidden, strikethrough and most of those aren't available in the - // config.toml.... yet. - let color_hm = get_color_config(&configuration); - - let mut start_number = if let Some(f) = args.get_flag("start_number")? { - f - } else { - 0 - }; - - let mut delay_slot = None; - - let term_width = args.host().lock().width(); - - let stream_data = async { - let finished = Arc::new(AtomicBool::new(false)); - // we are required to clone finished, for use within the callback, otherwise we get borrow errors - while !finished.clone().load(Ordering::Relaxed) { - let mut new_input: VecDeque = VecDeque::new(); - - let start_time = Instant::now(); - for idx in 0..STREAM_PAGE_SIZE { - if let Some(val) = delay_slot { - new_input.push_back(val); - delay_slot = None; - } else { - match args.input.next() { - Some(a) => { - if !new_input.is_empty() { - if let Some(descs) = new_input.get(0) { - let descs = descs.data_descriptors(); - let compare = a.data_descriptors(); - if descs != compare { - delay_slot = Some(a); - break; - } else { - new_input.push_back(a); - } - } else { - new_input.push_back(a); - } - } else { - new_input.push_back(a); - } - } - _ => { - finished.store(true, Ordering::Relaxed); - break; - } - } - - // Check if we've gone over our buffering threshold - if (idx + 1) % STREAM_TIMEOUT_CHECK_INTERVAL == 0 { - let end_time = Instant::now(); - - // If we've been buffering over a second, go ahead and send out what we have so far - if (end_time - start_time).as_secs() >= 1 { - break; - } - - if finished.load(Ordering::Relaxed) { - break; - } - } - } - } - - let input: Vec = new_input.into(); - - if !input.is_empty() { - let t = from_list(&input, &configuration, start_number, &color_hm); - let output = draw_table(&t, term_width, &color_hm); - - println!("{}", output); - } - - start_number += input.len(); - } - - Result::<_, ShellError>::Ok(()) - }; - - block_on(stream_data) - .map_err(|_| ShellError::untagged_runtime_error("Error streaming data"))?; - - Ok(OutputStream::empty()) -} - -#[cfg(test)] -mod tests { - use super::Command; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(Command {}) - } -} diff --git a/crates/nu-command/src/commands/viewers/table/mod.rs b/crates/nu-command/src/commands/viewers/table/mod.rs deleted file mode 100644 index 96f007aac7..0000000000 --- a/crates/nu-command/src/commands/viewers/table/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod command; -mod options; - -pub use command::Command as Table; diff --git a/crates/nu-command/src/commands/viewers/table/options.rs b/crates/nu-command/src/commands/viewers/table/options.rs deleted file mode 100644 index cafb349516..0000000000 --- a/crates/nu-command/src/commands/viewers/table/options.rs +++ /dev/null @@ -1,108 +0,0 @@ -pub use nu_data::config::NuConfig; -use nu_data::primitive::lookup_ansi_color_style; -use nu_protocol::Value; -use nu_table::{Alignment, TextStyle}; -use std::fmt::Debug; - -pub trait ConfigExtensions: Debug + Send { - fn table_mode(&self) -> nu_table::Theme; - fn disabled_indexes(&self) -> bool; - fn header_style(&self) -> TextStyle; -} - -pub fn header_alignment_from_value(align_value: Option<&Value>) -> nu_table::Alignment { - match align_value { - Some(v) => match v - .as_string() - .unwrap_or_else(|_| "none".to_string()) - .as_ref() - { - "l" | "left" => nu_table::Alignment::Left, - "c" | "center" => nu_table::Alignment::Center, - "r" | "right" => nu_table::Alignment::Right, - _ => nu_table::Alignment::Center, - }, - _ => nu_table::Alignment::Center, - } -} - -pub fn get_color_from_key_and_subkey(config: &NuConfig, key: &str, subkey: &str) -> Option { - let vars = &config.vars; - - if let Some(config_vars) = vars.get(key) { - for (kee, value) in config_vars.row_entries() { - if kee == subkey { - return Some(value.clone()); - } - } - } - - None -} - -pub fn header_bold_from_value(bold_value: Option<&Value>) -> bool { - bold_value - .map(|x| x.as_bool().unwrap_or(true)) - .unwrap_or(true) -} - -pub fn table_mode(config: &NuConfig) -> nu_table::Theme { - let vars = &config.vars; - - vars.get("table_mode") - .map_or(nu_table::Theme::compact(), |mode| match mode.as_string() { - Ok(m) if m == "basic" => nu_table::Theme::basic(), - Ok(m) if m == "compact" => nu_table::Theme::compact(), - Ok(m) if m == "light" => nu_table::Theme::light(), - Ok(m) if m == "thin" => nu_table::Theme::thin(), - Ok(m) if m == "with_love" => nu_table::Theme::with_love(), - Ok(m) if m == "compact_double" => nu_table::Theme::compact_double(), - Ok(m) if m == "rounded" => nu_table::Theme::rounded(), - Ok(m) if m == "reinforced" => nu_table::Theme::reinforced(), - Ok(m) if m == "heavy" => nu_table::Theme::heavy(), - Ok(m) if m == "none" => nu_table::Theme::none(), - _ => nu_table::Theme::compact(), - }) -} - -pub fn disabled_indexes(config: &NuConfig) -> bool { - let vars = &config.vars; - - vars.get("disable_table_indexes") - .map_or(false, |x| x.as_bool().unwrap_or(false)) -} - -impl ConfigExtensions for NuConfig { - fn header_style(&self) -> TextStyle { - // FIXME: I agree, this is the long way around, please suggest and alternative. - let head_color = get_color_from_key_and_subkey(self, "color_config", "header_color"); - let (head_color_style, head_bold_bool) = match head_color { - Some(s) => ( - lookup_ansi_color_style(s.as_string().unwrap_or_else(|_| "green".to_string())), - header_bold_from_value(Some(&s)), - ), - None => (nu_ansi_term::Color::Green.normal(), true), - }; - - let head_align = get_color_from_key_and_subkey(self, "color_config", "header_align"); - let head_alignment = match head_align { - Some(a) => header_alignment_from_value(Some(&a)), - None => Alignment::Center, - }; - - TextStyle::new() - .alignment(head_alignment) - .bold(Some(head_bold_bool)) - .fg(head_color_style - .foreground - .unwrap_or(nu_ansi_term::Color::Green)) - } - - fn table_mode(&self) -> nu_table::Theme { - table_mode(self) - } - - fn disabled_indexes(&self) -> bool { - disabled_indexes(self) - } -} diff --git a/crates/nu-command/src/conversions/fmt.rs b/crates/nu-command/src/conversions/fmt.rs new file mode 100644 index 0000000000..ebd0dd7f8f --- /dev/null +++ b/crates/nu-command/src/conversions/fmt.rs @@ -0,0 +1,176 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::{Call, CellPath}, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; + +#[derive(Clone)] +pub struct Fmt; + +impl Command for Fmt { + fn name(&self) -> &str { + "fmt" + } + + fn usage(&self) -> &str { + "format numbers" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("fmt").category(Category::Conversions) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "format numbers", + example: "42 | fmt", + result: Some(Value::Record { + cols: vec![ + "binary".into(), + "debug".into(), + "display".into(), + "lowerexp".into(), + "lowerhex".into(), + "octal".into(), + "upperexp".into(), + "upperhex".into(), + ], + vals: vec![ + Value::String { + val: "0b101010".to_string(), + span: Span::test_data(), + }, + Value::String { + val: "42".to_string(), + span: Span::test_data(), + }, + Value::String { + val: "42".to_string(), + span: Span::test_data(), + }, + Value::String { + val: "4.2e1".to_string(), + span: Span::test_data(), + }, + Value::String { + val: "0x2a".to_string(), + span: Span::test_data(), + }, + Value::String { + val: "0o52".to_string(), + span: Span::test_data(), + }, + Value::String { + val: "4.2E1".to_string(), + span: Span::test_data(), + }, + Value::String { + val: "0x2A".to_string(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + fmt(engine_state, stack, call, input) + } +} + +fn fmt( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let head = call.head; + let column_paths: Vec = call.rest(engine_state, stack, 0)?; + + input.map( + move |v| { + if column_paths.is_empty() { + action(&v, head) + } else { + let mut ret = v; + for path in &column_paths { + let r = + ret.update_cell_path(&path.members, Box::new(move |old| action(old, head))); + if let Err(error) = r { + return Value::Error { error }; + } + } + + ret + } + }, + engine_state.ctrlc.clone(), + ) +} + +pub fn action(input: &Value, span: Span) -> Value { + match input { + Value::Int { val, .. } => fmt_it(*val, span), + Value::Filesize { val, .. } => fmt_it(*val, span), + _ => Value::Error { + error: ShellError::UnsupportedInput( + format!("unsupported input type: {:?}", input.get_type()), + span, + ), + }, + } +} + +fn fmt_it(num: i64, span: Span) -> Value { + let mut cols = vec![]; + let mut vals = vec![]; + + cols.push("binary".into()); + vals.push(Value::string(format!("{:#b}", num), span)); + + cols.push("debug".into()); + vals.push(Value::string(format!("{:#?}", num), span)); + + cols.push("display".into()); + vals.push(Value::string(format!("{}", num), span)); + + cols.push("lowerexp".into()); + vals.push(Value::string(format!("{:#e}", num), span)); + + cols.push("lowerhex".into()); + vals.push(Value::string(format!("{:#x}", num), span)); + + cols.push("octal".into()); + vals.push(Value::string(format!("{:#o}", num), span)); + + // cols.push("pointer".into()); + // vals.push(Value::string(format!("{:#p}", &num), span)); + + cols.push("upperexp".into()); + vals.push(Value::string(format!("{:#E}", num), span)); + + cols.push("upperhex".into()); + vals.push(Value::string(format!("{:#X}", num), span)); + + Value::Record { cols, vals, span } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Fmt {}) + } +} diff --git a/crates/nu-command/src/conversions/into/binary.rs b/crates/nu-command/src/conversions/into/binary.rs new file mode 100644 index 0000000000..6c635f49da --- /dev/null +++ b/crates/nu-command/src/conversions/into/binary.rs @@ -0,0 +1,195 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::{Call, CellPath}, + engine::{Command, EngineState, Stack}, + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, SyntaxShape, + Value, +}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "into binary" + } + + fn signature(&self) -> Signature { + Signature::build("into binary") + .rest( + "rest", + SyntaxShape::CellPath, + "column paths to convert to binary (for table input)", + ) + .category(Category::Conversions) + } + + fn usage(&self) -> &str { + "Convert value to a binary primitive" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + into_binary(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "convert string to a nushell binary primitive", + example: "'This is a string that is exactly 52 characters long.' | into binary", + result: Some(Value::Binary { + val: "This is a string that is exactly 52 characters long." + .to_string() + .as_bytes() + .to_vec(), + span: Span::test_data(), + }), + }, + Example { + description: "convert a number to a nushell binary primitive", + example: "1 | into binary", + result: Some(Value::Binary { + val: i64::from(1).to_le_bytes().to_vec(), + span: Span::test_data(), + }), + }, + Example { + description: "convert a boolean to a nushell binary primitive", + example: "$true | into binary", + result: Some(Value::Binary { + val: i64::from(1).to_le_bytes().to_vec(), + span: Span::test_data(), + }), + }, + Example { + description: "convert a filesize to a nushell binary primitive", + example: "ls | where name == LICENSE | get size | into binary", + result: None, + }, + Example { + description: "convert a filepath to a nushell binary primitive", + example: "ls | where name == LICENSE | get name | path expand | into binary", + result: None, + }, + Example { + description: "convert a decimal to a nushell binary primitive", + example: "1.234 | into binary", + result: Some(Value::Binary { + val: 1.234f64.to_le_bytes().to_vec(), + span: Span::test_data(), + }), + }, + ] + } +} + +fn into_binary( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let head = call.head; + let column_paths: Vec = call.rest(engine_state, stack, 0)?; + + match input { + PipelineData::RawStream(stream, ..) => { + // TODO: in the future, we may want this to stream out, converting each to bytes + let output = stream.into_bytes()?; + Ok(Value::Binary { + val: output.item, + span: head, + } + .into_pipeline_data()) + } + _ => input.map( + move |v| { + if column_paths.is_empty() { + action(&v, head) + } else { + let mut ret = v; + for path in &column_paths { + let r = ret.update_cell_path( + &path.members, + Box::new(move |old| action(old, head)), + ); + if let Err(error) = r { + return Value::Error { error }; + } + } + + ret + } + }, + engine_state.ctrlc.clone(), + ), + } +} + +fn int_to_endian(n: i64) -> Vec { + if cfg!(target_endian = "little") { + n.to_le_bytes().to_vec() + } else { + n.to_be_bytes().to_vec() + } +} + +fn float_to_endian(n: f64) -> Vec { + if cfg!(target_endian = "little") { + n.to_le_bytes().to_vec() + } else { + n.to_be_bytes().to_vec() + } +} + +pub fn action(input: &Value, span: Span) -> Value { + match input { + Value::Binary { .. } => input.clone(), + Value::Int { val, .. } => Value::Binary { + val: int_to_endian(*val), + span, + }, + Value::Float { val, .. } => Value::Binary { + val: float_to_endian(*val), + span, + }, + Value::Filesize { val, .. } => Value::Binary { + val: int_to_endian(*val), + span, + }, + Value::String { val, .. } => Value::Binary { + val: val.as_bytes().to_vec(), + span, + }, + Value::Bool { val, .. } => Value::Binary { + val: int_to_endian(if *val { 1i64 } else { 0 }), + span, + }, + Value::Date { val, .. } => Value::Binary { + val: val.format("%c").to_string().as_bytes().to_vec(), + span, + }, + + _ => Value::Error { + error: ShellError::UnsupportedInput("'into binary' for unsupported type".into(), span), + }, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/conversions/into/bool.rs b/crates/nu-command/src/conversions/into/bool.rs new file mode 100644 index 0000000000..44e3b43253 --- /dev/null +++ b/crates/nu-command/src/conversions/into/bool.rs @@ -0,0 +1,183 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::{Call, CellPath}, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "into bool" + } + + fn signature(&self) -> Signature { + Signature::build("into bool") + .rest( + "rest", + SyntaxShape::CellPath, + "column paths to convert to boolean (for table input)", + ) + .category(Category::Conversions) + } + + fn usage(&self) -> &str { + "Convert value to boolean" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + into_bool(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + let span = Span::test_data(); + vec![ + Example { + description: "Convert value to boolean in table", + example: "echo [[value]; ['false'] ['1'] [0] [1.0] [$true]] | into bool value", + result: Some(Value::List { + vals: vec![ + Value::Record { + cols: vec!["value".to_string()], + vals: vec![Value::boolean(false, span)], + span, + }, + Value::Record { + cols: vec!["value".to_string()], + vals: vec![Value::boolean(true, span)], + span, + }, + Value::Record { + cols: vec!["value".to_string()], + vals: vec![Value::boolean(false, span)], + span, + }, + Value::Record { + cols: vec!["value".to_string()], + vals: vec![Value::boolean(true, span)], + span, + }, + Value::Record { + cols: vec!["value".to_string()], + vals: vec![Value::boolean(true, span)], + span, + }, + ], + span, + }), + }, + Example { + description: "Convert bool to boolean", + example: "$true | into bool", + result: Some(Value::boolean(true, span)), + }, + Example { + description: "convert decimal to boolean", + example: "1 | into bool", + result: Some(Value::boolean(true, span)), + }, + Example { + description: "convert decimal string to boolean", + example: "'0.0' | into bool", + result: Some(Value::boolean(false, span)), + }, + Example { + description: "convert string to boolean", + example: "'true' | into bool", + result: Some(Value::boolean(true, span)), + }, + ] + } +} + +fn into_bool( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let head = call.head; + let column_paths: Vec = call.rest(engine_state, stack, 0)?; + + input.map( + move |v| { + if column_paths.is_empty() { + action(&v, head) + } else { + let mut ret = v; + for path in &column_paths { + let r = + ret.update_cell_path(&path.members, Box::new(move |old| action(old, head))); + if let Err(error) = r { + return Value::Error { error }; + } + } + + ret + } + }, + engine_state.ctrlc.clone(), + ) +} + +fn string_to_boolean(s: &str, span: Span) -> Result { + match s.trim().to_lowercase().as_str() { + "true" => Ok(true), + "false" => Ok(false), + o => { + let val = o.parse::(); + match val { + Ok(f) => Ok(f.abs() >= f64::EPSILON), + Err(_) => Err(ShellError::CantConvert( + "boolean".to_string(), + "string".to_string(), + span, + )), + } + } + } +} + +fn action(input: &Value, span: Span) -> Value { + match input { + Value::Bool { .. } => input.clone(), + Value::Int { val, .. } => Value::Bool { + val: *val != 0, + span, + }, + Value::Float { val, .. } => Value::Bool { + val: val.abs() >= f64::EPSILON, + span, + }, + Value::String { val, .. } => match string_to_boolean(val, span) { + Ok(val) => Value::Bool { val, span }, + Err(error) => Value::Error { error }, + }, + _ => Value::Error { + error: ShellError::UnsupportedInput( + "'into bool' does not support this input".into(), + span, + ), + }, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/conversions/into/command.rs b/crates/nu-command/src/conversions/into/command.rs new file mode 100644 index 0000000000..3344930d6b --- /dev/null +++ b/crates/nu-command/src/conversions/into/command.rs @@ -0,0 +1,49 @@ +use nu_engine::get_full_help; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, IntoPipelineData, PipelineData, Signature, Value, +}; + +#[derive(Clone)] +pub struct Into; + +impl Command for Into { + fn name(&self) -> &str { + "into" + } + + fn signature(&self) -> Signature { + Signature::build("into").category(Category::Conversions) + } + + fn usage(&self) -> &str { + "Apply into function." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(Value::String { + val: get_full_help(&Into.signature(), &[], engine_state, stack), + span: call.head, + } + .into_pipeline_data()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Into {}) + } +} diff --git a/crates/nu-command/src/conversions/into/datetime.rs b/crates/nu-command/src/conversions/into/datetime.rs new file mode 100644 index 0000000000..fd93bf4bac --- /dev/null +++ b/crates/nu-command/src/conversions/into/datetime.rs @@ -0,0 +1,1313 @@ +use chrono::{DateTime, FixedOffset, Local, LocalResult, Offset, TimeZone, Utc}; +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::ast::CellPath; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Value, +}; + +struct Arguments { + timezone: Option>, + offset: Option>, + format: Option, + column_paths: Vec, +} + +// In case it may be confused with chrono::TimeZone +#[derive(Clone, Debug)] +enum Zone { + Utc, + Local, + East(u8), + West(u8), + Error, // we want the nullshell to cast it instead of rust +} + +impl Zone { + fn new(i: i64) -> Self { + if i.abs() <= 12 { + // guanranteed here + if i >= 0 { + Self::East(i as u8) // won't go out of range + } else { + Self::West(-i as u8) // same here + } + } else { + Self::Error // Out of range + } + } + fn from_string(s: String) -> Self { + match s.to_lowercase().as_str() { + "utc" | "u" => Self::Utc, + "local" | "l" => Self::Local, + _ => Self::Error, + } + } +} + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "into datetime" + } + + fn signature(&self) -> Signature { + Signature::build("into datetime") + .switch( + "list", + "lists strftime cheatsheet", + Some('l'), + ) + .named( + "timezone", + SyntaxShape::String, + "Specify timezone if the input is timestamp, like 'UTC/u' or 'LOCAL/l'", + Some('z'), + ) + .named( + "offset", + SyntaxShape::Int, + "Specify timezone by offset if the input is timestamp, like '+8', '-4', prior than timezone", + Some('o'), + ) + .named( + "format", + SyntaxShape::String, + "Specify date and time formatting", + Some('f'), + ) + .rest( + "rest", + SyntaxShape::CellPath, + "optionally convert text into datetime by column paths", + ) + .category(Category::Conversions) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input) + } + + fn usage(&self) -> &str { + "converts text into datetime" + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Convert to datetime", + example: "'16.11.1984 8:00 am +0000' | into datetime", + result: None, + }, + Example { + description: "Convert to datetime", + example: "'2020-08-04T16:39:18+00:00' | into datetime", + result: None, + }, + Example { + description: "Convert to datetime using a custom format", + example: "'20200904_163918+0000' | into datetime -f '%Y%m%d_%H%M%S%z'", + result: None, + }, + Example { + description: "Convert timestamp (no larger than 8e+12) to datetime using a specified timezone", + example: "'1614434140' | into datetime -z 'UTC'", + result: None, + }, + Example { + description: + "Convert timestamp (no larger than 8e+12) to datetime using a specified timezone offset (between -12 and 12)", + example: "'1614434140' | into datetime -o +9", + result: None, + }, + ] + } +} + +#[derive(Clone)] +struct DatetimeFormat(String); + +fn operate( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let head = call.head; + + let options = Arguments { + timezone: call.get_flag(engine_state, stack, "timezone")?, + offset: call.get_flag(engine_state, stack, "offset")?, + format: call.get_flag(engine_state, stack, "format")?, + column_paths: call.rest(engine_state, stack, 0)?, + }; + + // if zone-offset is specified, then zone will be neglected + let zone_options = match &options.offset { + Some(zone_offset) => Some(Spanned { + item: Zone::new(zone_offset.item), + span: zone_offset.span, + }), + None => options.timezone.as_ref().map(|zone| Spanned { + item: Zone::from_string(zone.item.clone()), + span: zone.span, + }), + }; + + let list_flag = call.has_flag("list"); + + let format_options = options + .format + .as_ref() + .map(|fmt| DatetimeFormat(fmt.to_string())); + + input.map( + move |v| { + if options.column_paths.is_empty() && !list_flag { + action(&v, &zone_options, &format_options, head) + } else if list_flag { + generate_strfttime_list(head) + } else { + let mut ret = v; + for path in &options.column_paths { + let zone_options = zone_options.clone(); + let format_options = format_options.clone(); + let r = ret.update_cell_path( + &path.members, + Box::new(move |old| action(old, &zone_options, &format_options, head)), + ); + if let Err(error) = r { + return Value::Error { error }; + } + } + ret + } + }, + engine_state.ctrlc.clone(), + ) +} + +fn generate_strfttime_list(head: Span) -> Value { + let column_names = vec![ + "Specification".into(), + "Example".into(), + "Description".into(), + ]; + let records = vec![ + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%Y".into(), + span: head, + }, + Value::String { + val: "2001".into(), + span: head, + }, + Value::String { + val: "The full proleptic Gregorian year, zero-padded to 4 digits".into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%C".into(), + span: head, + }, + Value::String { + val: "20".into(), + span: head, + }, + Value::String { + val: "The proleptic Gregorian year divided by 100, zero-padded to 2 digits. +" + .into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%y".into(), + span: head, + }, + Value::String { + val: "01".into(), + span: head, + }, + Value::String { + val: "The proleptic Gregorian year modulo 100, zero-padded to 2 digits. +" + .into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%m".into(), + span: head, + }, + Value::String { + val: "07".into(), + span: head, + }, + Value::String { + val: "Month number (01--12), zero-padded to 2 digits.".into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%b".into(), + span: head, + }, + Value::String { + val: "Jul".into(), + span: head, + }, + Value::String { + val: "Abbreviated month name. Always 3 letters".into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%B".into(), + span: head, + }, + Value::String { + val: "July".into(), + span: head, + }, + Value::String { + val: "Full month name. Also accepts corresponding abbreviation in parsing" + .into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%h".into(), + span: head, + }, + Value::String { + val: "Jul".into(), + span: head, + }, + Value::String { + val: "Same to %b".into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%d".into(), + span: head, + }, + Value::String { + val: "08".into(), + span: head, + }, + Value::String { + val: "Day number (01--31), zero-padded to 2 digits".into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%e".into(), + span: head, + }, + Value::String { + val: "8".into(), + span: head, + }, + Value::String { + val: "Same to %d but space-padded. Same to %_d".into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%a".into(), + span: head, + }, + Value::String { + val: "Sun".into(), + span: head, + }, + Value::String { + val: "Abbreviated weekday name. Always 3 letters".into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%A".into(), + span: head, + }, + Value::String { + val: "Sunday".into(), + span: head, + }, + Value::String { + val: "Full weekday name. Also accepts corresponding abbreviation in parsing" + .into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%w".into(), + span: head, + }, + Value::String { + val: "0".into(), + span: head, + }, + Value::String { + val: "Sunday = 0, Monday = 1, ..., Saturday = 6".into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%u".into(), + span: head, + }, + Value::String { + val: "7".into(), + span: head, + }, + Value::String { + val: "Monday = 1, Tuesday = 2, ..., Sunday = 7. (ISO 8601) +" + .into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%U".into(), + span: head, + }, + Value::String { + val: "28".into(), + span: head, + }, + Value::String { + val: "Week number starting with Sunday (00--53), zero-padded to 2 digits. +" + .into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%W".into(), + span: head, + }, + Value::String { + val: "27".into(), + span: head, + }, + Value::String { + val: "Same to %U, but week 1 starts with the first Monday in that year instead" + .into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%G".into(), + span: head, + }, + Value::String { + val: "2001".into(), + span: head, + }, + Value::String { + val: "Same to %Y but uses the year number in ISO 8601 week date. +" + .into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%g".into(), + span: head, + }, + Value::String { + val: "01".into(), + span: head, + }, + Value::String { + val: "Same to %y but uses the year number in ISO 8601 week date. +" + .into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%V".into(), + span: head, + }, + Value::String { + val: "27".into(), + span: head, + }, + Value::String { + val: "Same to %U but uses the week number in ISO 8601 week date (01--53). +" + .into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%j".into(), + span: head, + }, + Value::String { + val: "189".into(), + span: head, + }, + Value::String { + val: "Day of the year (001--366), zero-padded to 3 digits".into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%D".into(), + span: head, + }, + Value::String { + val: "07/08/01".into(), + span: head, + }, + Value::String { + val: "Month-day-year format. Same to %m/%d/%y".into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%x".into(), + span: head, + }, + Value::String { + val: "07/08/01".into(), + span: head, + }, + Value::String { + val: "Same to %D".into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%F".into(), + span: head, + }, + Value::String { + val: "2001-07-08".into(), + span: head, + }, + Value::String { + val: "Year-month-day format (ISO 8601). Same to %Y-%m-%d".into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%v".into(), + span: head, + }, + Value::String { + val: "8-Jul-2001".into(), + span: head, + }, + Value::String { + val: "Day-month-year format. Same to %e-%b-%Y".into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%H".into(), + span: head, + }, + Value::String { + val: "00".into(), + span: head, + }, + Value::String { + val: "Hour number (00--23), zero-padded to 2 digits".into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%k".into(), + span: head, + }, + Value::String { + val: "0".into(), + span: head, + }, + Value::String { + val: "Same to %H but space-padded. Same to %_H".into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%I".into(), + span: head, + }, + Value::String { + val: "12".into(), + span: head, + }, + Value::String { + val: "Hour number in 12-hour clocks (01--12), zero-padded to 2 digits".into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%l".into(), + span: head, + }, + Value::String { + val: "12".into(), + span: head, + }, + Value::String { + val: "Same to %I but space-padded. Same to %_I".into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%P".into(), + span: head, + }, + Value::String { + val: "am".into(), + span: head, + }, + Value::String { + val: "am or pm in 12-hour clocks".into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%p".into(), + span: head, + }, + Value::String { + val: "AM".into(), + span: head, + }, + Value::String { + val: "AM or PM in 12-hour clocks".into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%M".into(), + span: head, + }, + Value::String { + val: "34".into(), + span: head, + }, + Value::String { + val: "Minute number (00--59), zero-padded to 2 digits".into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%S".into(), + span: head, + }, + Value::String { + val: "60".into(), + span: head, + }, + Value::String { + val: "Second number (00--60), zero-padded to 2 digits. +" + .into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%f".into(), + span: head, + }, + Value::String { + val: "026490000".into(), + span: head, + }, + Value::String { + val: "The fractional seconds (in nanoseconds) since last whole second. +" + .into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%.".into(), + span: head, + }, + Value::String { + val: ".026490".into(), + span: head, + }, + Value::String { + val: "Similar to .%f but left-aligned. +" + .into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%.".into(), + span: head, + }, + Value::String { + val: ".026".into(), + span: head, + }, + Value::String { + val: "Similar to .%f but left-aligned but fixed to a length of 3. +" + .into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%.".into(), + span: head, + }, + Value::String { + val: ".026490".into(), + span: head, + }, + Value::String { + val: "Similar to .%f but left-aligned but fixed to a length of 6. +" + .into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%.".into(), + span: head, + }, + Value::String { + val: ".026490000".into(), + span: head, + }, + Value::String { + val: "Similar to .%f but left-aligned but fixed to a length of 9. +" + .into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%R".into(), + span: head, + }, + Value::String { + val: "00:34".into(), + span: head, + }, + Value::String { + val: "Hour-minute format. Same to %H:%M".into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%T".into(), + span: head, + }, + Value::String { + val: "00:34:60".into(), + span: head, + }, + Value::String { + val: "Hour-minute-second format. Same to %H:%M:%S".into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%X".into(), + span: head, + }, + Value::String { + val: "00:34:60".into(), + span: head, + }, + Value::String { + val: "Same to %T".into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%r".into(), + span: head, + }, + Value::String { + val: "12:34:60".into(), + span: head, + }, + Value::String { + val: "AM Hour-minute-second format in 12-hour clocks. Same to %I:%M:%S %p" + .into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%Z".into(), + span: head, + }, + Value::String { + val: "ACST".into(), + span: head, + }, + Value::String { + val: "Formatting only: Local time zone name".into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%z".into(), + span: head, + }, + Value::String { + val: "+0930".into(), + span: head, + }, + Value::String { + val: "Offset from the local time to UTC (with UTC being +0000)".into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%:".into(), + span: head, + }, + Value::String { + val: "+09:30".into(), + span: head, + }, + Value::String { + val: "Same to %z but with a colon".into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%c".into(), + span: head, + }, + Value::String { + val: "Sun".into(), + span: head, + }, + Value::String { + val: + "Jul 8 00:34:60 2001 ctime date & time format. Same to %a %b %e %T %Y sans" + .into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%s".into(), + span: head, + }, + Value::String { + val: "994518299".into(), + span: head, + }, + Value::String { + val: "UNIX timestamp, the number of seconds since 1970-01-01 00:00 UTC.".into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%t".into(), + span: head, + }, + Value::String { + val: "".into(), + span: head, + }, + Value::String { + val: "Literal tab (\\t)".into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::String { + val: "%n".into(), + span: head, + }, + Value::String { + val: "".into(), + span: head, + }, + Value::String { + val: "Literal newline (\\n)".into(), + span: head, + }, + ], + span: head, + }, + Value::Record { + cols: column_names, + vals: vec![ + Value::String { + val: "%%".into(), + span: head, + }, + Value::String { + val: "".into(), + span: head, + }, + Value::String { + val: "percent sign".into(), + span: head, + }, + ], + span: head, + }, + ]; + + Value::List { + vals: records, + span: head, + } +} + +fn action( + input: &Value, + timezone: &Option>, + dateformat: &Option, + head: Span, +) -> Value { + match input { + Value::String { val: s, span, .. } => { + let ts = s.parse::(); + // if timezone if specified, first check if the input is a timestamp. + if let Some(tz) = timezone { + const TIMESTAMP_BOUND: i64 = 8.2e+12 as i64; + // Since the timestamp method of chrono itself don't throw an error (it just panicked) + // We have to manually guard it. + if let Ok(t) = ts { + if t.abs() > TIMESTAMP_BOUND { + return Value::Error{error: ShellError::UnsupportedInput( + "Given timestamp is out of range, it should between -8e+12 and 8e+12".to_string(), + head, + )}; + } + const HOUR: i32 = 3600; + let stampout = match tz.item { + Zone::Utc => Value::Date { + val: Utc.timestamp(t, 0).into(), + span: head, + }, + Zone::Local => Value::Date { + val: Local.timestamp(t, 0).into(), + span: head, + }, + Zone::East(i) => { + let eastoffset = FixedOffset::east((i as i32) * HOUR); + Value::Date { + val: eastoffset.timestamp(t, 0), + span: head, + } + } + Zone::West(i) => { + let westoffset = FixedOffset::west((i as i32) * HOUR); + Value::Date { + val: westoffset.timestamp(t, 0), + span: head, + } + } + Zone::Error => Value::Error { + error: ShellError::UnsupportedInput( + "Cannot convert given timezone or offset to timestamp".to_string(), + tz.span, + ), + }, + }; + return stampout; + } + }; + // if it's not, continue and negelect the timezone option. + let out = match dateformat { + Some(dt) => match DateTime::parse_from_str(s, &dt.0) { + Ok(d) => Value::Date { val: d, span: head }, + Err(reason) => { + return Value::Error { + error: ShellError::CantConvert( + format!("could not parse as datetime using format '{}'", dt.0), + reason.to_string(), + head, + ), + } + } + }, + None => match dtparse::parse(s) { + Ok((native_dt, fixed_offset)) => { + let offset = match fixed_offset { + Some(fo) => fo, + None => FixedOffset::east(0).fix(), + }; + match offset.from_local_datetime(&native_dt) { + LocalResult::Single(d) => Value::Date { val: d, span: head }, + LocalResult::Ambiguous(d, _) => Value::Date { val: d, span: head }, + LocalResult::None => { + return Value::Error { + error: ShellError::CantConvert( + "could not convert to a timezone-aware datetime" + .to_string(), + "local time representation is invalid".to_string(), + head, + ), + } + } + } + } + Err(_) => { + return Value::Error { + error: ShellError::UnsupportedInput( + "Cannot convert input string as datetime. Might be missing timezone or offset".to_string(), + *span, + ), + } + } + }, + }; + + out + } + other => { + let got = format!("Expected string, got {} instead", other.get_type()); + Value::Error { + error: ShellError::UnsupportedInput(got, head), + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use super::{action, DatetimeFormat, SubCommand, Zone}; + use nu_protocol::Type::Error; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } + + #[test] + fn takes_a_date_format() { + let date_str = Value::test_string("16.11.1984 8:00 am +0000"); + let fmt_options = Some(DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string())); + let actual = action(&date_str, &None, &fmt_options, Span::test_data()); + let expected = Value::Date { + val: DateTime::parse_from_str("16.11.1984 8:00 am +0000", "%d.%m.%Y %H:%M %P %z") + .unwrap(), + span: Span::test_data(), + }; + assert_eq!(actual, expected) + } + + #[test] + fn takes_iso8601_date_format() { + let date_str = Value::test_string("2020-08-04T16:39:18+00:00"); + let actual = action(&date_str, &None, &None, Span::test_data()); + let expected = Value::Date { + val: DateTime::parse_from_str("2020-08-04T16:39:18+00:00", "%Y-%m-%dT%H:%M:%S%z") + .unwrap(), + span: Span::test_data(), + }; + assert_eq!(actual, expected) + } + + #[test] + fn takes_timestamp_offset() { + let date_str = Value::test_string("1614434140"); + let timezone_option = Some(Spanned { + item: Zone::East(8), + span: Span::test_data(), + }); + let actual = action(&date_str, &timezone_option, &None, Span::test_data()); + let expected = Value::Date { + val: DateTime::parse_from_str("2021-02-27 21:55:40 +08:00", "%Y-%m-%d %H:%M:%S %z") + .unwrap(), + span: Span::test_data(), + }; + + assert_eq!(actual, expected) + } + + #[test] + fn takes_timestamp() { + let date_str = Value::test_string("1614434140"); + let timezone_option = Some(Spanned { + item: Zone::Local, + span: Span::test_data(), + }); + let actual = action(&date_str, &timezone_option, &None, Span::test_data()); + let expected = Value::Date { + val: Local.timestamp(1614434140, 0).into(), + span: Span::test_data(), + }; + + assert_eq!(actual, expected) + } + + #[test] + fn takes_invalid_timestamp() { + let date_str = Value::test_string("10440970000000"); + let timezone_option = Some(Spanned { + item: Zone::Utc, + span: Span::test_data(), + }); + let actual = action(&date_str, &timezone_option, &None, Span::test_data()); + + assert_eq!(actual.get_type(), Error); + } + + #[test] + fn communicates_parsing_error_given_an_invalid_datetimelike_string() { + let date_str = Value::test_string("16.11.1984 8:00 am Oops0000"); + let fmt_options = Some(DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string())); + let actual = action(&date_str, &None, &fmt_options, Span::test_data()); + + assert_eq!(actual.get_type(), Error); + } +} diff --git a/crates/nu-command/src/conversions/into/decimal.rs b/crates/nu-command/src/conversions/into/decimal.rs new file mode 100644 index 0000000000..15c5ae7f88 --- /dev/null +++ b/crates/nu-command/src/conversions/into/decimal.rs @@ -0,0 +1,166 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::{Call, CellPath}, + engine::{Command, EngineState, Stack}, + Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "into decimal" + } + + fn signature(&self) -> Signature { + Signature::build("into decimal").rest( + "rest", + SyntaxShape::CellPath, + "optionally convert text into decimal by column paths", + ) + } + + fn usage(&self) -> &str { + "converts text into decimal" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Convert string to integer in table", + example: "[[num]; ['5.01']] | into decimal num", + result: Some(Value::List { + vals: vec![Value::Record { + cols: vec!["num".to_string()], + vals: vec![Value::test_float(5.01)], + span: Span::test_data(), + }], + span: Span::test_data(), + }), + }, + Example { + description: "Convert string to integer", + example: "'1.345' | into decimal", + result: Some(Value::test_float(1.345)), + }, + Example { + description: "Convert decimal to integer", + example: "'-5.9' | into decimal", + result: Some(Value::test_float(-5.9)), + }, + ] + } +} + +fn operate( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let head = call.head; + let column_paths: Vec = call.rest(engine_state, stack, 0)?; + + input.map( + move |v| { + if column_paths.is_empty() { + action(&v, head) + } else { + let mut ret = v; + for path in &column_paths { + let r = + ret.update_cell_path(&path.members, Box::new(move |old| action(old, head))); + if let Err(error) = r { + return Value::Error { error }; + } + } + + ret + } + }, + engine_state.ctrlc.clone(), + ) +} + +fn action(input: &Value, head: Span) -> Value { + match input { + Value::String { val: s, span } => { + let other = s.trim(); + + match other.parse::() { + Ok(x) => Value::Float { val: x, span: head }, + Err(reason) => Value::Error { + error: ShellError::CantConvert("float".to_string(), reason.to_string(), *span), + }, + } + } + Value::Int { val: v, span } => Value::Float { + val: *v as f64, + span: *span, + }, + other => { + let span = other.span(); + match span { + Ok(s) => { + let got = format!("Expected a string, got {} instead", other.get_type()); + Value::Error { + error: ShellError::UnsupportedInput(got, s), + } + } + Err(e) => Value::Error { error: e }, + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use nu_protocol::Type::Error; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } + + #[test] + #[allow(clippy::approx_constant)] + fn string_to_decimal() { + let word = Value::test_string("3.1415"); + let expected = Value::test_float(3.1415); + + let actual = action(&word, Span::test_data()); + assert_eq!(actual, expected); + } + + #[test] + fn communicates_parsing_error_given_an_invalid_decimallike_string() { + let decimal_str = Value::test_string("11.6anra"); + + let actual = action(&decimal_str, Span::test_data()); + + assert_eq!(actual.get_type(), Error); + } + + #[test] + fn int_to_decimal() { + let decimal_str = Value::test_int(10); + let expected = Value::test_float(10.0); + let actual = action(&decimal_str, Span::test_data()); + + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-command/src/conversions/into/filesize.rs b/crates/nu-command/src/conversions/into/filesize.rs new file mode 100644 index 0000000000..4dc27c941c --- /dev/null +++ b/crates/nu-command/src/conversions/into/filesize.rs @@ -0,0 +1,165 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::{Call, CellPath}, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "into filesize" + } + + fn signature(&self) -> Signature { + Signature::build("into filesize") + .rest( + "rest", + SyntaxShape::CellPath, + "column paths to convert to filesize (for table input)", + ) + .category(Category::Conversions) + } + + fn usage(&self) -> &str { + "Convert value to filesize" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + into_filesize(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Convert string to filesize in table", + example: "[[bytes]; ['5'] [3.2] [4] [2kb]] | into filesize bytes", + result: None, + }, + Example { + description: "Convert string to filesize", + example: "'2' | into filesize", + result: Some(Value::Filesize { + val: 2, + span: Span::test_data(), + }), + }, + Example { + description: "Convert decimal to filesize", + example: "8.3 | into filesize", + result: Some(Value::Filesize { + val: 8, + span: Span::test_data(), + }), + }, + Example { + description: "Convert int to filesize", + example: "5 | into filesize", + result: Some(Value::Filesize { + val: 5, + span: Span::test_data(), + }), + }, + Example { + description: "Convert file size to filesize", + example: "4KB | into filesize", + result: Some(Value::Filesize { + val: 4000, + span: Span::test_data(), + }), + }, + ] + } +} + +fn into_filesize( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let head = call.head; + let column_paths: Vec = call.rest(engine_state, stack, 0)?; + + input.map( + move |v| { + if column_paths.is_empty() { + action(&v, head) + } else { + let mut ret = v; + for path in &column_paths { + let r = + ret.update_cell_path(&path.members, Box::new(move |old| action(old, head))); + if let Err(error) = r { + return Value::Error { error }; + } + } + + ret + } + }, + engine_state.ctrlc.clone(), + ) +} + +pub fn action(input: &Value, span: Span) -> Value { + if let Ok(value_span) = input.span() { + match input { + Value::Filesize { .. } => input.clone(), + Value::Int { val, .. } => Value::Filesize { + val: *val, + span: value_span, + }, + Value::Float { val, .. } => Value::Filesize { + val: *val as i64, + span: value_span, + }, + Value::String { val, .. } => match int_from_string(val, value_span) { + Ok(val) => Value::Filesize { + val, + span: value_span, + }, + Err(error) => Value::Error { error }, + }, + _ => Value::Error { + error: ShellError::UnsupportedInput( + "'into filesize' for unsupported type".into(), + value_span, + ), + }, + } + } else { + Value::Error { + error: ShellError::UnsupportedInput( + "'into filesize' for unsupported type".into(), + span, + ), + } + } +} +fn int_from_string(a_string: &str, span: Span) -> Result { + match a_string.trim().parse::() { + Ok(n) => Ok(n.0 as i64), + Err(_) => Err(ShellError::CantConvert("int".into(), "string".into(), span)), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/conversions/into/int.rs b/crates/nu-command/src/conversions/into/int.rs new file mode 100644 index 0000000000..dbffdfdc84 --- /dev/null +++ b/crates/nu-command/src/conversions/into/int.rs @@ -0,0 +1,302 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::{Call, CellPath}, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +struct Arguments { + radix: Option, + column_paths: Vec, +} + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "into int" + } + + fn signature(&self) -> Signature { + Signature::build("into int") + .named("radix", SyntaxShape::Number, "radix of integer", Some('r')) + .rest( + "rest", + SyntaxShape::CellPath, + "column paths to convert to int (for table input)", + ) + .category(Category::Conversions) + } + + fn usage(&self) -> &str { + "Convert value to integer" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + into_int(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Convert string to integer in table", + example: "echo [[num]; ['-5'] [4] [1.5]] | into int num", + result: None, + }, + Example { + description: "Convert string to integer", + example: "'2' | into int", + result: Some(Value::test_int(2)), + }, + Example { + description: "Convert decimal to integer", + example: "5.9 | into int", + result: Some(Value::test_int(5)), + }, + Example { + description: "Convert decimal string to integer", + example: "'5.9' | into int", + result: Some(Value::test_int(5)), + }, + Example { + description: "Convert file size to integer", + example: "4KB | into int", + result: Some(Value::Int { + val: 4000, + span: Span::test_data(), + }), + }, + Example { + description: "Convert bool to integer", + example: "[$false, $true] | into int", + result: Some(Value::List { + vals: vec![Value::test_int(0), Value::test_int(1)], + span: Span::test_data(), + }), + }, + Example { + description: "Convert to integer from binary", + example: "'1101' | into int -r 2", + result: Some(Value::test_int(13)), + }, + Example { + description: "Convert to integer from hex", + example: "'FF' | into int -r 16", + result: Some(Value::test_int(255)), + }, + ] + } +} + +fn into_int( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let head = call.head; + + let options = Arguments { + radix: call.get_flag(engine_state, stack, "radix")?, + column_paths: call.rest(engine_state, stack, 0)?, + }; + + let radix: u32 = match options.radix { + Some(Value::Int { val, .. }) => val as u32, + Some(_) => 10, + None => 10, + }; + + if let Some(val) = &options.radix { + if !(2..=36).contains(&radix) { + return Err(ShellError::UnsupportedInput( + "Radix must lie in the range [2, 36]".to_string(), + val.span()?, + )); + } + } + + input.map( + move |v| { + if options.column_paths.is_empty() { + action(&v, head, radix) + } else { + let mut ret = v; + for path in &options.column_paths { + let r = ret.update_cell_path( + &path.members, + Box::new(move |old| action(old, head, radix)), + ); + if let Err(error) = r { + return Value::Error { error }; + } + } + + ret + } + }, + engine_state.ctrlc.clone(), + ) +} + +pub fn action(input: &Value, span: Span, radix: u32) -> Value { + match input { + Value::Int { val: _, .. } => { + if radix == 10 { + input.clone() + } else { + convert_int(input, span, radix) + } + } + Value::Filesize { val, .. } => Value::Int { val: *val, span }, + Value::Float { val, .. } => Value::Int { + val: *val as i64, + span, + }, + Value::String { val, .. } => { + if radix == 10 { + match int_from_string(val, span) { + Ok(val) => Value::Int { val, span }, + Err(error) => Value::Error { error }, + } + } else { + convert_int(input, span, radix) + } + } + Value::Bool { val, .. } => { + if *val { + Value::Int { val: 1, span } + } else { + Value::Int { val: 0, span } + } + } + _ => Value::Error { + error: ShellError::UnsupportedInput("'into int' for unsupported type".into(), span), + }, + } +} + +fn convert_int(input: &Value, head: Span, radix: u32) -> Value { + let i = match input { + Value::Int { val, .. } => val.to_string(), + Value::String { val, .. } => { + if val.starts_with("0x") || val.starts_with("0b") { + match int_from_string(&val.to_string(), head) { + Ok(x) => return Value::Int { val: x, span: head }, + Err(e) => return Value::Error { error: e }, + } + } + val.to_string() + } + _ => { + return Value::Error { + error: ShellError::UnsupportedInput( + "only strings or integers are supported".to_string(), + head, + ), + } + } + }; + match i64::from_str_radix(&i, radix) { + Ok(n) => Value::Int { val: n, span: head }, + Err(reason) => Value::Error { + error: ShellError::CantConvert("".to_string(), reason.to_string(), head), + }, + } +} + +fn int_from_string(a_string: &str, span: Span) -> Result { + let trimmed = a_string.trim(); + match trimmed { + b if b.starts_with("0b") => { + let num = match i64::from_str_radix(b.trim_start_matches("0b"), 2) { + Ok(n) => n, + Err(reason) => { + return Err(ShellError::CantConvert( + "could not parse as integer".to_string(), + reason.to_string(), + span, + )) + } + }; + Ok(num) + } + h if h.starts_with("0x") => { + let num = match i64::from_str_radix(h.trim_start_matches("0x"), 16) { + Ok(n) => n, + Err(reason) => { + return Err(ShellError::CantConvert( + "could not parse as int".to_string(), + reason.to_string(), + span, + )) + } + }; + Ok(num) + } + _ => match a_string.parse::() { + Ok(n) => Ok(n), + Err(_) => match a_string.parse::() { + Ok(f) => Ok(f as i64), + _ => Err(ShellError::CantConvert( + "into int".to_string(), + "string".to_string(), + span, + )), + }, + }, + } +} + +#[cfg(test)] +mod test { + use super::Value; + use super::*; + use nu_protocol::Type::Error; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } + + #[test] + fn turns_to_integer() { + let word = Value::test_string("10"); + let expected = Value::test_int(10); + + let actual = action(&word, Span::test_data(), 10); + assert_eq!(actual, expected); + } + + #[test] + fn turns_binary_to_integer() { + let s = Value::test_string("0b101"); + let actual = action(&s, Span::test_data(), 10); + assert_eq!(actual, Value::test_int(5)); + } + + #[test] + fn turns_hex_to_integer() { + let s = Value::test_string("0xFF"); + let actual = action(&s, Span::test_data(), 16); + assert_eq!(actual, Value::test_int(255)); + } + + #[test] + fn communicates_parsing_error_given_an_invalid_integerlike_string() { + let integer_str = Value::test_string("36anra"); + + let actual = action(&integer_str, Span::test_data(), 10); + + assert_eq!(actual.get_type(), Error) + } +} diff --git a/crates/nu-command/src/conversions/into/mod.rs b/crates/nu-command/src/conversions/into/mod.rs new file mode 100644 index 0000000000..cdb1003457 --- /dev/null +++ b/crates/nu-command/src/conversions/into/mod.rs @@ -0,0 +1,17 @@ +mod binary; +mod bool; +mod command; +mod datetime; +mod decimal; +mod filesize; +mod int; +mod string; + +pub use self::bool::SubCommand as IntoBool; +pub use self::filesize::SubCommand as IntoFilesize; +pub use binary::SubCommand as IntoBinary; +pub use command::Into; +pub use datetime::SubCommand as IntoDatetime; +pub use decimal::SubCommand as IntoDecimal; +pub use int::SubCommand as IntoInt; +pub use string::SubCommand as IntoString; diff --git a/crates/nu-command/src/conversions/into/string.rs b/crates/nu-command/src/conversions/into/string.rs new file mode 100644 index 0000000000..e57b114726 --- /dev/null +++ b/crates/nu-command/src/conversions/into/string.rs @@ -0,0 +1,284 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::{Call, CellPath}, + engine::{Command, EngineState, Stack}, + Category, Config, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, + SyntaxShape, Value, +}; + +// TODO num_format::SystemLocale once platform-specific dependencies are stable (see Cargo.toml) + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "into string" + } + + fn signature(&self) -> Signature { + Signature::build("into string") + // FIXME - need to support column paths + .rest( + "rest", + SyntaxShape::CellPath, + "column paths to convert to string (for table input)", + ) + .named( + "decimals", + SyntaxShape::Int, + "decimal digits to which to round", + Some('d'), + ) + .category(Category::Conversions) + } + + fn usage(&self) -> &str { + "Convert value to string" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + string_helper(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "convert decimal to string and round to nearest integer", + example: "1.7 | into string -d 0", + result: Some(Value::String { + val: "2".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "convert decimal to string", + example: "1.7 | into string -d 1", + result: Some(Value::String { + val: "1.7".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "convert decimal to string and limit to 2 decimals", + example: "1.734 | into string -d 2", + result: Some(Value::String { + val: "1.73".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "try to convert decimal to string and provide negative decimal points", + example: "1.734 | into string -d -2", + result: None, + // FIXME + // result: Some(Value::Error { + // error: ShellError::UnsupportedInput( + // String::from("Cannot accept negative integers for decimals arguments"), + // Span::test_data(), + // ), + // }), + }, + Example { + description: "convert decimal to string", + example: "4.3 | into string", + result: Some(Value::String { + val: "4.3".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "convert string to string", + example: "'1234' | into string", + result: Some(Value::String { + val: "1234".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "convert boolean to string", + example: "$true | into string", + result: Some(Value::String { + val: "true".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "convert date to string", + example: "date now | into string", + result: None, + }, + Example { + description: "convert filepath to string", + example: "ls Cargo.toml | get name | into string", + result: None, + }, + Example { + description: "convert filesize to string", + example: "ls Cargo.toml | get size | into string", + result: None, + }, + ] + } +} + +fn string_helper( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let decimals = call.has_flag("decimals"); + let head = call.head; + let decimals_value: Option = call.get_flag(engine_state, stack, "decimals")?; + let column_paths: Vec = call.rest(engine_state, stack, 0)?; + let config = stack.get_config().unwrap_or_default(); + + if let Some(decimal_val) = decimals_value { + if decimals && decimal_val.is_negative() { + return Err(ShellError::UnsupportedInput( + "Cannot accept negative integers for decimals arguments".to_string(), + head, + )); + } + } + + match input { + PipelineData::RawStream(stream, ..) => { + // TODO: in the future, we may want this to stream out, converting each to bytes + let output = stream.into_string()?; + Ok(Value::String { + val: output.item, + span: head, + } + .into_pipeline_data()) + } + _ => input.map( + move |v| { + if column_paths.is_empty() { + action(&v, head, decimals, decimals_value, false, &config) + } else { + let mut ret = v; + for path in &column_paths { + let config = config.clone(); + let r = ret.update_cell_path( + &path.members, + Box::new(move |old| { + action(old, head, decimals, decimals_value, false, &config) + }), + ); + if let Err(error) = r { + return Value::Error { error }; + } + } + + ret + } + }, + engine_state.ctrlc.clone(), + ), + } +} + +pub fn action( + input: &Value, + span: Span, + decimals: bool, + digits: Option, + group_digits: bool, + config: &Config, +) -> Value { + match input { + Value::Int { val, .. } => { + let res = if group_digits { + format_int(*val) // int.to_formatted_string(*locale) + } else { + val.to_string() + }; + + Value::String { val: res, span } + } + Value::Float { val, .. } => { + if decimals { + let decimal_value = digits.unwrap_or(2) as usize; + Value::String { + val: format!("{:.*}", decimal_value, val), + span, + } + } else { + Value::String { + val: val.to_string(), + span, + } + } + } + Value::Bool { val, .. } => Value::String { + val: val.to_string(), + span, + }, + Value::Date { val, .. } => Value::String { + val: val.format("%c").to_string(), + span, + }, + Value::String { val, .. } => Value::String { + val: val.to_string(), + span, + }, + + Value::Filesize { val: _, .. } => Value::String { + val: input.into_string(", ", config), + span, + }, + Value::Nothing { .. } => Value::String { + val: "nothing".to_string(), + span, + }, + Value::Record { + cols: _, + vals: _, + span: _, + } => Value::Error { + error: ShellError::UnsupportedInput( + "Cannot convert Record into string".to_string(), + span, + ), + }, + x => Value::Error { + error: ShellError::CantConvert(String::from("string"), x.get_type().to_string(), span), + }, + } +} +fn format_int(int: i64) -> String { + int.to_string() + + // TODO once platform-specific dependencies are stable (see Cargo.toml) + // #[cfg(windows)] + // { + // int.to_formatted_string(&Locale::en) + // } + // #[cfg(not(windows))] + // { + // match SystemLocale::default() { + // Ok(locale) => int.to_formatted_string(&locale), + // Err(_) => int.to_formatted_string(&Locale::en), + // } + // } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/conversions/mod.rs b/crates/nu-command/src/conversions/mod.rs new file mode 100644 index 0000000000..38570d51b0 --- /dev/null +++ b/crates/nu-command/src/conversions/mod.rs @@ -0,0 +1,5 @@ +mod fmt; +pub(crate) mod into; + +pub use fmt::Fmt; +pub use into::*; diff --git a/crates/nu-command/src/core_commands/alias.rs b/crates/nu-command/src/core_commands/alias.rs new file mode 100644 index 0000000000..158a3e9de6 --- /dev/null +++ b/crates/nu-command/src/core_commands/alias.rs @@ -0,0 +1,37 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, PipelineData, Signature, SyntaxShape}; + +#[derive(Clone)] +pub struct Alias; + +impl Command for Alias { + fn name(&self) -> &str { + "alias" + } + + fn usage(&self) -> &str { + "Alias a command (with optional flags) to a new name" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("alias") + .required("name", SyntaxShape::String, "name of the alias") + .required( + "initial_value", + SyntaxShape::Keyword(b"=".to_vec(), Box::new(SyntaxShape::Expression)), + "equals sign followed by value", + ) + .category(Category::Core) + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::new(call.head)) + } +} diff --git a/crates/nu-command/src/core_commands/debug.rs b/crates/nu-command/src/core_commands/debug.rs new file mode 100644 index 0000000000..1a0d618172 --- /dev/null +++ b/crates/nu-command/src/core_commands/debug.rs @@ -0,0 +1,71 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Value}; + +#[derive(Clone)] +pub struct Debug; + +impl Command for Debug { + fn name(&self) -> &str { + "debug" + } + + fn usage(&self) -> &str { + "Debug print the value(s) piped in." + } + + fn signature(&self) -> Signature { + Signature::build("debug").category(Category::Core).switch( + "raw", + "Prints the raw value representation", + Some('r'), + ) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let config = stack.get_config().unwrap_or_default(); + let raw = call.has_flag("raw"); + + input.map( + move |x| { + if raw { + Value::String { + val: x.debug_value(), + span: head, + } + } else { + Value::String { + val: x.debug_string(", ", &config), + span: head, + } + } + }, + engine_state.ctrlc.clone(), + ) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Describe the type of a string", + example: "'hello' | debug", + result: Some(Value::test_string("hello")), + }] + } +} + +#[cfg(test)] +mod test { + #[test] + fn test_examples() { + use super::Debug; + use crate::test_examples; + test_examples(Debug {}) + } +} diff --git a/crates/nu-command/src/core_commands/def.rs b/crates/nu-command/src/core_commands/def.rs new file mode 100644 index 0000000000..63d07c4225 --- /dev/null +++ b/crates/nu-command/src/core_commands/def.rs @@ -0,0 +1,38 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, PipelineData, Signature, SyntaxShape}; + +#[derive(Clone)] +pub struct Def; + +impl Command for Def { + fn name(&self) -> &str { + "def" + } + + fn usage(&self) -> &str { + "Define a custom command" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("def") + .required("def_name", SyntaxShape::String, "definition name") + .required("params", SyntaxShape::Signature, "parameters") + .required( + "block", + SyntaxShape::Block(Some(vec![])), + "body of the definition", + ) + .category(Category::Core) + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::new(call.head)) + } +} diff --git a/crates/nu-command/src/core_commands/def_env.rs b/crates/nu-command/src/core_commands/def_env.rs new file mode 100644 index 0000000000..6f39e72a60 --- /dev/null +++ b/crates/nu-command/src/core_commands/def_env.rs @@ -0,0 +1,38 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, PipelineData, Signature, SyntaxShape}; + +#[derive(Clone)] +pub struct DefEnv; + +impl Command for DefEnv { + fn name(&self) -> &str { + "def-env" + } + + fn usage(&self) -> &str { + "Define a custom command, which participates in the caller environment" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("def-env") + .required("def_name", SyntaxShape::String, "definition name") + .required("params", SyntaxShape::Signature, "parameters") + .required( + "block", + SyntaxShape::Block(Some(vec![])), + "body of the definition", + ) + .category(Category::Core) + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::new(call.head)) + } +} diff --git a/crates/nu-command/src/core_commands/describe.rs b/crates/nu-command/src/core_commands/describe.rs new file mode 100644 index 0000000000..419b1a6d82 --- /dev/null +++ b/crates/nu-command/src/core_commands/describe.rs @@ -0,0 +1,63 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Value, +}; + +#[derive(Clone)] +pub struct Describe; + +impl Command for Describe { + fn name(&self) -> &str { + "describe" + } + + fn usage(&self) -> &str { + "Describe the value(s) piped in." + } + + fn signature(&self) -> Signature { + Signature::build("describe").category(Category::Core) + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + if matches!(input, PipelineData::RawStream(..)) { + Ok(PipelineData::Value( + Value::string("raw input", call.head), + None, + )) + } else { + let value = input.into_value(call.head); + Ok(Value::String { + val: value.get_type().to_string(), + span: head, + } + .into_pipeline_data()) + } + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Describe the type of a string", + example: "'hello' | describe", + result: Some(Value::test_string("string")), + }] + } +} + +#[cfg(test)] +mod test { + #[test] + fn test_examples() { + use super::Describe; + use crate::test_examples; + test_examples(Describe {}) + } +} diff --git a/crates/nu-command/src/core_commands/do_.rs b/crates/nu-command/src/core_commands/do_.rs new file mode 100644 index 0000000000..8d2a87e329 --- /dev/null +++ b/crates/nu-command/src/core_commands/do_.rs @@ -0,0 +1,98 @@ +use nu_engine::{eval_block, CallExt}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{CaptureBlock, Command, EngineState, Stack}; +use nu_protocol::{Category, PipelineData, Signature, SyntaxShape, Value}; + +#[derive(Clone)] +pub struct Do; + +impl Command for Do { + fn name(&self) -> &str { + "do" + } + + fn usage(&self) -> &str { + "Run a block" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("do") + .desc(self.usage()) + .required( + "block", + SyntaxShape::Block(Some(vec![])), + "the block to run", + ) + .switch( + "ignore-errors", + "ignore errors as the block runs", + Some('i'), + ) + .rest("rest", SyntaxShape::Any, "the parameter(s) for the block") + .category(Category::Core) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let block: CaptureBlock = call.req(engine_state, stack, 0)?; + let rest: Vec = call.rest(engine_state, stack, 1)?; + let ignore_errors = call.has_flag("ignore-errors"); + + let mut stack = stack.captures_to_stack(&block.captures); + let block = engine_state.get_block(block.block_id); + + let params: Vec<_> = block + .signature + .required_positional + .iter() + .chain(block.signature.optional_positional.iter()) + .collect(); + + for param in params.iter().zip(&rest) { + if let Some(var_id) = param.0.var_id { + stack.add_var(var_id, param.1.clone()) + } + } + + if let Some(param) = &block.signature.rest_positional { + if rest.len() > params.len() { + let mut rest_items = vec![]; + + for r in rest.into_iter().skip(params.len()) { + rest_items.push(r); + } + + let span = if let Some(rest_item) = rest_items.first() { + rest_item.span()? + } else { + call.head + }; + + stack.add_var( + param + .var_id + .expect("Internal error: rest positional parameter lacks var_id"), + Value::List { + vals: rest_items, + span, + }, + ) + } + } + let result = eval_block(engine_state, &mut stack, block, input); + + if ignore_errors { + match result { + Ok(x) => Ok(x), + Err(_) => Ok(PipelineData::new(call.head)), + } + } else { + result + } + } +} diff --git a/crates/nu-command/src/core_commands/echo.rs b/crates/nu-command/src/core_commands/echo.rs new file mode 100644 index 0000000000..2b7d6c1509 --- /dev/null +++ b/crates/nu-command/src/core_commands/echo.rs @@ -0,0 +1,81 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, ListStream, PipelineData, ShellError, Signature, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Echo; + +impl Command for Echo { + fn name(&self) -> &str { + "echo" + } + + fn usage(&self) -> &str { + "Echo the arguments back to the user." + } + + fn signature(&self) -> Signature { + Signature::build("echo") + .rest("rest", SyntaxShape::Any, "the values to echo") + .category(Category::Core) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + call.rest(engine_state, stack, 0).map(|to_be_echoed| { + let n = to_be_echoed.len(); + match n.cmp(&1usize) { + // More than one value is converted in a stream of values + std::cmp::Ordering::Greater => PipelineData::ListStream( + ListStream::from_stream(to_be_echoed.into_iter(), engine_state.ctrlc.clone()), + None, + ), + + // But a single value can be forwarded as it is + std::cmp::Ordering::Equal => PipelineData::Value(to_be_echoed[0].clone(), None), + + // When there are no elements, we echo the empty string + std::cmp::Ordering::Less => PipelineData::Value( + Value::String { + val: "".to_string(), + span: call.head, + }, + None, + ), + } + }) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Put a hello message in the pipeline", + example: "echo 'hello'", + result: Some(Value::test_string("hello")), + }, + Example { + description: "Print the value of the special '$nu' variable", + example: "echo $nu", + result: None, + }, + ] + } +} + +#[cfg(test)] +mod test { + #[test] + fn test_examples() { + use super::Echo; + use crate::test_examples; + test_examples(Echo {}) + } +} diff --git a/crates/nu-command/src/core_commands/error_make.rs b/crates/nu-command/src/core_commands/error_make.rs new file mode 100644 index 0000000000..b596c0e493 --- /dev/null +++ b/crates/nu-command/src/core_commands/error_make.rs @@ -0,0 +1,113 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, SyntaxShape, + Value, +}; + +#[derive(Clone)] +pub struct ErrorMake; + +impl Command for ErrorMake { + fn name(&self) -> &str { + "error make" + } + + fn signature(&self) -> Signature { + Signature::build("error make") + .optional("error-struct", SyntaxShape::Record, "the error to create") + .category(Category::Core) + } + + fn usage(&self) -> &str { + "Create an error." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let span = call.head; + let ctrlc = engine_state.ctrlc.clone(); + let arg: Option = call.opt(engine_state, stack, 0)?; + + if let Some(arg) = arg { + Ok(make_error(&arg) + .map(|err| Value::Error { error: err }) + .unwrap_or_else(|| Value::Error { + error: ShellError::SpannedLabeledError( + "Creating error value not supported.".into(), + "unsupported error format".into(), + span, + ), + }) + .into_pipeline_data()) + } else { + input.map( + move |value| { + make_error(&value) + .map(|err| Value::Error { error: err }) + .unwrap_or_else(|| Value::Error { + error: ShellError::SpannedLabeledError( + "Creating error value not supported.".into(), + "unsupported error format".into(), + span, + ), + }) + }, + ctrlc, + ) + } + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Create a custom error for a custom command", + example: r#"def foo [x] { + let span = (metadata $x).span; + error make {msg: "this is fishy", label: {text: "fish right here", start: $span.start, end: $span.end } } + }"#, + result: None, + }] + } +} + +fn make_error(value: &Value) -> Option { + if let Value::Record { .. } = &value { + let msg = value.get_data_by_key("msg"); + let label = value.get_data_by_key("label"); + + match (msg, &label) { + (Some(Value::String { val: message, .. }), Some(label)) => { + let label_start = label.get_data_by_key("start"); + let label_end = label.get_data_by_key("end"); + let label_text = label.get_data_by_key("text"); + + match (label_start, label_end, label_text) { + ( + Some(Value::Int { val: start, .. }), + Some(Value::Int { val: end, .. }), + Some(Value::String { + val: label_text, .. + }), + ) => Some(ShellError::SpannedLabeledError( + message, + label_text, + Span { + start: start as usize, + end: end as usize, + }, + )), + _ => None, + } + } + _ => None, + } + } else { + None + } +} diff --git a/crates/nu-command/src/core_commands/export.rs b/crates/nu-command/src/core_commands/export.rs new file mode 100644 index 0000000000..b73337ea45 --- /dev/null +++ b/crates/nu-command/src/core_commands/export.rs @@ -0,0 +1,42 @@ +use nu_engine::get_full_help; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, IntoPipelineData, PipelineData, Signature, Value, +}; + +#[derive(Clone)] +pub struct ExportCommand; + +impl Command for ExportCommand { + fn name(&self) -> &str { + "export" + } + + fn signature(&self) -> Signature { + Signature::build("export").category(Category::Core) + } + + fn usage(&self) -> &str { + "Export custom commands or environment variables from a module." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(Value::String { + val: get_full_help( + &ExportCommand.signature(), + &ExportCommand.examples(), + engine_state, + stack, + ), + span: call.head, + } + .into_pipeline_data()) + } +} diff --git a/crates/nu-command/src/core_commands/export_def.rs b/crates/nu-command/src/core_commands/export_def.rs new file mode 100644 index 0000000000..79d7c2bff7 --- /dev/null +++ b/crates/nu-command/src/core_commands/export_def.rs @@ -0,0 +1,38 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, PipelineData, Signature, SyntaxShape}; + +#[derive(Clone)] +pub struct ExportDef; + +impl Command for ExportDef { + fn name(&self) -> &str { + "export def" + } + + fn usage(&self) -> &str { + "Define a custom command and export it from a module" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("export def") + .required("name", SyntaxShape::String, "definition name") + .required("params", SyntaxShape::Signature, "parameters") + .required( + "block", + SyntaxShape::Block(Some(vec![])), + "body of the definition", + ) + .category(Category::Core) + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::new(call.head)) + } +} diff --git a/crates/nu-command/src/core_commands/export_def_env.rs b/crates/nu-command/src/core_commands/export_def_env.rs new file mode 100644 index 0000000000..c5bc0b3a03 --- /dev/null +++ b/crates/nu-command/src/core_commands/export_def_env.rs @@ -0,0 +1,38 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, PipelineData, Signature, SyntaxShape}; + +#[derive(Clone)] +pub struct ExportDefEnv; + +impl Command for ExportDefEnv { + fn name(&self) -> &str { + "export def-env" + } + + fn usage(&self) -> &str { + "Define a custom command that participates in the environment and export it from a module" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("export def-env") + .required("name", SyntaxShape::String, "definition name") + .required("params", SyntaxShape::Signature, "parameters") + .required( + "block", + SyntaxShape::Block(Some(vec![])), + "body of the definition", + ) + .category(Category::Core) + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::new(call.head)) + } +} diff --git a/crates/nu-command/src/core_commands/export_env.rs b/crates/nu-command/src/core_commands/export_env.rs new file mode 100644 index 0000000000..261a97b3ab --- /dev/null +++ b/crates/nu-command/src/core_commands/export_env.rs @@ -0,0 +1,42 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, PipelineData, Signature, SyntaxShape}; + +#[derive(Clone)] +pub struct ExportEnv; + +impl Command for ExportEnv { + fn name(&self) -> &str { + "export env" + } + + fn usage(&self) -> &str { + "Export a block from a module that will be evaluated as an environment variable when imported." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("export env") + .required( + "name", + SyntaxShape::String, + "name of the environment variable", + ) + .required( + "block", + SyntaxShape::Block(Some(vec![])), + "body of the environment variable definition", + ) + .category(Category::Core) + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + //TODO: Add the env to stack + Ok(PipelineData::new(call.head)) + } +} diff --git a/crates/nu-command/src/core_commands/for_.rs b/crates/nu-command/src/core_commands/for_.rs new file mode 100644 index 0000000000..a686f297cd --- /dev/null +++ b/crates/nu-command/src/core_commands/for_.rs @@ -0,0 +1,216 @@ +use nu_engine::{eval_block_with_redirect, eval_expression, CallExt}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{CaptureBlock, Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoInterruptiblePipelineData, PipelineData, Signature, Span, SyntaxShape, + Value, +}; + +#[derive(Clone)] +pub struct For; + +impl Command for For { + fn name(&self) -> &str { + "for" + } + + fn usage(&self) -> &str { + "Loop over a range" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("for") + .required( + "var_name", + SyntaxShape::VarWithOptType, + "name of the looping variable", + ) + .required( + "range", + SyntaxShape::Keyword(b"in".to_vec(), Box::new(SyntaxShape::Any)), + "range of the loop", + ) + .required( + "block", + SyntaxShape::Block(Some(vec![])), + "the block to run", + ) + .switch( + "numbered", + "returned a numbered item ($it.index and $it.item)", + Some('n'), + ) + .creates_scope() + .category(Category::Core) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let head = call.head; + let var_id = call.positional[0] + .as_var() + .expect("internal error: missing variable"); + + let keyword_expr = call.positional[1] + .as_keyword() + .expect("internal error: missing keyword"); + let values = eval_expression(engine_state, stack, keyword_expr)?; + + let capture_block: CaptureBlock = call.req(engine_state, stack, 2)?; + + let numbered = call.has_flag("numbered"); + + let ctrlc = engine_state.ctrlc.clone(); + let engine_state = engine_state.clone(); + let block = engine_state.get_block(capture_block.block_id).clone(); + let mut stack = stack.captures_to_stack(&capture_block.captures); + let orig_env_vars = stack.env_vars.clone(); + let orig_env_hidden = stack.env_hidden.clone(); + + match values { + Value::List { vals, .. } => Ok(vals + .into_iter() + .enumerate() + .map(move |(idx, x)| { + stack.with_env(&orig_env_vars, &orig_env_hidden); + + stack.add_var( + var_id, + if numbered { + Value::Record { + cols: vec!["index".into(), "item".into()], + vals: vec![ + Value::Int { + val: idx as i64, + span: head, + }, + x, + ], + span: head, + } + } else { + x + }, + ); + + //let block = engine_state.get_block(block_id); + match eval_block_with_redirect( + &engine_state, + &mut stack, + &block, + PipelineData::new(head), + ) { + Ok(pipeline_data) => pipeline_data.into_value(head), + Err(error) => Value::Error { error }, + } + }) + .into_pipeline_data(ctrlc)), + Value::Range { val, .. } => Ok(val + .into_range_iter()? + .enumerate() + .map(move |(idx, x)| { + stack.with_env(&orig_env_vars, &orig_env_hidden); + + stack.add_var( + var_id, + if numbered { + Value::Record { + cols: vec!["index".into(), "item".into()], + vals: vec![ + Value::Int { + val: idx as i64, + span: head, + }, + x, + ], + span: head, + } + } else { + x + }, + ); + + //let block = engine_state.get_block(block_id); + match eval_block_with_redirect( + &engine_state, + &mut stack, + &block, + PipelineData::new(head), + ) { + Ok(pipeline_data) => pipeline_data.into_value(head), + Err(error) => Value::Error { error }, + } + }) + .into_pipeline_data(ctrlc)), + x => { + stack.add_var(var_id, x); + + eval_block_with_redirect(&engine_state, &mut stack, &block, PipelineData::new(head)) + } + } + } + + fn examples(&self) -> Vec { + let span = Span::test_data(); + vec![ + Example { + description: "Echo the square of each integer", + example: "for x in [1 2 3] { $x * $x }", + result: Some(Value::List { + vals: vec![ + Value::Int { val: 1, span }, + Value::Int { val: 4, span }, + Value::Int { val: 9, span }, + ], + span, + }), + }, + Example { + description: "Work with elements of a range", + example: "for $x in 1..3 { $x }", + result: Some(Value::List { + vals: vec![ + Value::Int { val: 1, span }, + Value::Int { val: 2, span }, + Value::Int { val: 3, span }, + ], + span, + }), + }, + Example { + description: "Number each item and echo a message", + example: "for $it in ['bob' 'fred'] --numbered { $\"($it.index) is ($it.item)\" }", + result: Some(Value::List { + vals: vec![ + Value::String { + val: "0 is bob".into(), + span, + }, + Value::String { + val: "1 is fred".into(), + span, + }, + ], + span, + }), + }, + ] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(For {}) + } +} diff --git a/crates/nu-command/src/core_commands/help.rs b/crates/nu-command/src/core_commands/help.rs new file mode 100644 index 0000000000..f0aa181143 --- /dev/null +++ b/crates/nu-command/src/core_commands/help.rs @@ -0,0 +1,261 @@ +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + span, Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, + ShellError, Signature, Spanned, SyntaxShape, Value, +}; + +use nu_engine::{get_full_help, CallExt}; + +#[derive(Clone)] +pub struct Help; + +impl Command for Help { + fn name(&self) -> &str { + "help" + } + + fn signature(&self) -> Signature { + Signature::build("help") + .rest( + "rest", + SyntaxShape::String, + "the name of command to get help on", + ) + .named( + "find", + SyntaxShape::String, + "string to find in command usage", + Some('f'), + ) + .category(Category::Core) + } + + fn usage(&self) -> &str { + "Display help information about commands." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + help(engine_state, stack, call) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "show all commands and sub-commands", + example: "help commands", + result: None, + }, + Example { + description: "generate documentation", + example: "help generate_docs", + result: None, + }, + Example { + description: "show help for single command", + example: "help match", + result: None, + }, + Example { + description: "show help for single sub-command", + example: "help str lpad", + result: None, + }, + Example { + description: "search for string in command usage", + example: "help --find char", + result: None, + }, + ] + } +} + +fn help( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, +) -> Result { + let head = call.head; + let find: Option> = call.get_flag(engine_state, stack, "find")?; + let rest: Vec> = call.rest(engine_state, stack, 0)?; + + let full_commands = engine_state.get_signatures_with_examples(false); + + if let Some(f) = find { + let search_string = f.item.to_lowercase(); + let mut found_cmds_vec = Vec::new(); + + for (sig, _, is_plugin, is_custom) in full_commands { + let mut cols = vec![]; + let mut vals = vec![]; + + let key = sig.name.clone(); + let c = sig.usage.clone(); + let e = sig.extra_usage.clone(); + if key.to_lowercase().contains(&search_string) + || c.to_lowercase().contains(&search_string) + || e.to_lowercase().contains(&search_string) + { + cols.push("name".into()); + vals.push(Value::String { + val: key, + span: head, + }); + + cols.push("category".into()); + vals.push(Value::String { + val: sig.category.to_string(), + span: head, + }); + + cols.push("is_plugin".into()); + vals.push(Value::Bool { + val: is_plugin, + span: head, + }); + + cols.push("is_custom".into()); + vals.push(Value::Bool { + val: is_custom, + span: head, + }); + + cols.push("usage".into()); + vals.push(Value::String { val: c, span: head }); + + cols.push("extra_usage".into()); + vals.push(Value::String { val: e, span: head }); + + found_cmds_vec.push(Value::Record { + cols, + vals, + span: head, + }); + } + } + + return Ok(found_cmds_vec + .into_iter() + .into_pipeline_data(engine_state.ctrlc.clone())); + } + + if !rest.is_empty() { + let mut found_cmds_vec = Vec::new(); + + if rest[0].item == "commands" { + for (sig, _, is_plugin, is_custom) in full_commands { + let mut cols = vec![]; + let mut vals = vec![]; + + let key = sig.name.clone(); + let c = sig.usage.clone(); + let e = sig.extra_usage.clone(); + + cols.push("name".into()); + vals.push(Value::String { + val: key, + span: head, + }); + + cols.push("category".into()); + vals.push(Value::String { + val: sig.category.to_string(), + span: head, + }); + + cols.push("is_plugin".into()); + vals.push(Value::Bool { + val: is_plugin, + span: head, + }); + + cols.push("is_custom".into()); + vals.push(Value::Bool { + val: is_custom, + span: head, + }); + + cols.push("usage".into()); + vals.push(Value::String { val: c, span: head }); + + cols.push("extra_usage".into()); + vals.push(Value::String { val: e, span: head }); + + found_cmds_vec.push(Value::Record { + cols, + vals, + span: head, + }); + } + + Ok(found_cmds_vec + .into_iter() + .into_pipeline_data(engine_state.ctrlc.clone())) + } else { + let mut name = String::new(); + + for r in &rest { + if !name.is_empty() { + name.push(' '); + } + name.push_str(&r.item); + } + + let output = full_commands + .iter() + .filter(|(signature, _, _, _)| signature.name == name) + .map(|(signature, examples, _, _)| { + get_full_help(signature, examples, engine_state, stack) + }) + .collect::>(); + + if !output.is_empty() { + Ok(Value::String { + val: output.join("======================\n\n"), + span: call.head, + } + .into_pipeline_data()) + } else { + Err(ShellError::CommandNotFound(span(&[ + rest[0].span, + rest[rest.len() - 1].span, + ]))) + } + } + } else { + let msg = r#"Welcome to Nushell. + +Here are some tips to help you get started. + * help commands - list all available commands + * help - display help about a particular command + * help --find - search through all of help + +Nushell works on the idea of a "pipeline". Pipelines are commands connected with the '|' character. +Each stage in the pipeline works together to load, parse, and display information to you. + +[Examples] + +List the files in the current directory, sorted by size: + ls | sort-by size + +Get information about the current system: + sys | get host + +Get the processes on your system actively using CPU: + ps | where cpu > 0 + +You can also learn more at https://www.nushell.sh/book/"#; + + Ok(Value::String { + val: msg.into(), + span: head, + } + .into_pipeline_data()) + } +} diff --git a/crates/nu-command/src/core_commands/hide.rs b/crates/nu-command/src/core_commands/hide.rs new file mode 100644 index 0000000000..f8eecad99d --- /dev/null +++ b/crates/nu-command/src/core_commands/hide.rs @@ -0,0 +1,119 @@ +use nu_protocol::ast::{Call, Expr, Expression, ImportPatternMember}; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, PipelineData, ShellError, Signature, SyntaxShape}; + +#[derive(Clone)] +pub struct Hide; + +impl Command for Hide { + fn name(&self) -> &str { + "hide" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("hide") + .required("pattern", SyntaxShape::ImportPattern, "import pattern") + .category(Category::Core) + } + + fn usage(&self) -> &str { + "Hide definitions in the current scope" + } + + fn extra_usage(&self) -> &str { + "If there is a definition and an environment variable with the same name in the current scope, first the definition will be hidden, then the environment variable." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let import_pattern = if let Some(Expression { + expr: Expr::ImportPattern(pat), + .. + }) = call.positional.get(0) + { + pat + } else { + return Err(ShellError::SpannedLabeledError( + "Unexpected import".into(), + "import pattern not supported".into(), + call.head, + )); + }; + + let head_name_str = if let Ok(s) = String::from_utf8(import_pattern.head.name.clone()) { + s + } else { + return Err(ShellError::NonUtf8(import_pattern.head.span)); + }; + + if let Some(overlay_id) = engine_state.find_overlay(&import_pattern.head.name) { + // The first word is a module + let overlay = engine_state.get_overlay(overlay_id); + + let env_vars_to_hide = if import_pattern.members.is_empty() { + overlay.env_vars_with_head(&import_pattern.head.name) + } else { + match &import_pattern.members[0] { + ImportPatternMember::Glob { .. } => overlay.env_vars(), + ImportPatternMember::Name { name, span } => { + let mut output = vec![]; + + if let Some((name, id)) = + overlay.env_var_with_head(name, &import_pattern.head.name) + { + output.push((name, id)); + } else if !overlay.has_decl(name) { + return Err(ShellError::EnvVarNotFoundAtRuntime( + String::from_utf8_lossy(name).into(), + *span, + )); + } + + output + } + ImportPatternMember::List { names } => { + let mut output = vec![]; + + for (name, span) in names { + if let Some((name, id)) = + overlay.env_var_with_head(name, &import_pattern.head.name) + { + output.push((name, id)); + } else if !overlay.has_decl(name) { + return Err(ShellError::EnvVarNotFoundAtRuntime( + String::from_utf8_lossy(name).into(), + *span, + )); + } + } + + output + } + } + }; + + for (name, _) in env_vars_to_hide { + let name = if let Ok(s) = String::from_utf8(name.clone()) { + s + } else { + return Err(ShellError::NonUtf8(import_pattern.span())); + }; + + if stack.remove_env_var(engine_state, &name).is_none() { + return Err(ShellError::NotFound(call.positional[0].span)); + } + } + } else if !import_pattern.hidden.contains(&import_pattern.head.name) + && stack.remove_env_var(engine_state, &head_name_str).is_none() + { + return Err(ShellError::NotFound(call.positional[0].span)); + } + + Ok(PipelineData::new(call.head)) + } +} diff --git a/crates/nu-command/src/core_commands/history.rs b/crates/nu-command/src/core_commands/history.rs new file mode 100644 index 0000000000..fcff71f51b --- /dev/null +++ b/crates/nu-command/src/core_commands/history.rs @@ -0,0 +1,71 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Value, +}; + +const NEWLINE_ESCAPE_CODE: &str = "<\\n>"; + +fn decode_newlines(escaped: &str) -> String { + escaped.replace(NEWLINE_ESCAPE_CODE, "\n") +} + +#[derive(Clone)] +pub struct History; + +impl Command for History { + fn name(&self) -> &str { + "history" + } + + fn usage(&self) -> &str { + "Get the command history" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("history") + .switch("clear", "Clears out the history entries", Some('c')) + .category(Category::Core) + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let head = call.head; + if let Some(config_path) = nu_path::config_dir() { + let clear = call.has_flag("clear"); + let ctrlc = engine_state.ctrlc.clone(); + + let mut history_path = config_path; + history_path.push("nushell"); + history_path.push("history.txt"); + + if clear { + let _ = std::fs::remove_file(history_path); + Ok(PipelineData::new(head)) + } else { + let contents = std::fs::read_to_string(history_path); + + if let Ok(contents) = contents { + Ok(contents + .lines() + .map(move |x| Value::String { + val: decode_newlines(x), + span: head, + }) + .collect::>() + .into_iter() + .into_pipeline_data(ctrlc)) + } else { + Err(ShellError::FileNotFound(head)) + } + } + } else { + Err(ShellError::FileNotFound(head)) + } + } +} diff --git a/crates/nu-command/src/core_commands/if_.rs b/crates/nu-command/src/core_commands/if_.rs new file mode 100644 index 0000000000..2f607eae66 --- /dev/null +++ b/crates/nu-command/src/core_commands/if_.rs @@ -0,0 +1,115 @@ +use nu_engine::{eval_block, eval_expression, CallExt}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{CaptureBlock, Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, FromValue, IntoPipelineData, PipelineData, ShellError, Signature, + SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct If; + +impl Command for If { + fn name(&self) -> &str { + "if" + } + + fn usage(&self) -> &str { + "Conditionally run a block." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("if") + .required("cond", SyntaxShape::Expression, "condition to check") + .required( + "then_block", + SyntaxShape::Block(Some(vec![])), + "block to run if check succeeds", + ) + .optional( + "else_expression", + SyntaxShape::Keyword(b"else".to_vec(), Box::new(SyntaxShape::Expression)), + "expression or block to run if check fails", + ) + .category(Category::Core) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let cond = &call.positional[0]; + let then_block: CaptureBlock = call.req(engine_state, stack, 1)?; + let else_case = call.positional.get(2); + + let result = eval_expression(engine_state, stack, cond)?; + match &result { + Value::Bool { val, .. } => { + if *val { + let block = engine_state.get_block(then_block.block_id); + let mut stack = stack.captures_to_stack(&then_block.captures); + eval_block(engine_state, &mut stack, block, input) + } else if let Some(else_case) = else_case { + if let Some(else_expr) = else_case.as_keyword() { + if let Some(block_id) = else_expr.as_block() { + let result = eval_expression(engine_state, stack, else_expr)?; + let else_block: CaptureBlock = FromValue::from_value(&result)?; + + let mut stack = stack.captures_to_stack(&else_block.captures); + let block = engine_state.get_block(block_id); + eval_block(engine_state, &mut stack, block, input) + } else { + eval_expression(engine_state, stack, else_expr) + .map(|x| x.into_pipeline_data()) + } + } else { + eval_expression(engine_state, stack, else_case) + .map(|x| x.into_pipeline_data()) + } + } else { + Ok(PipelineData::new(call.head)) + } + } + x => Err(ShellError::CantConvert( + "bool".into(), + x.get_type().to_string(), + result.span()?, + )), + } + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Output a value if a condition matches, otherwise return nothing", + example: "if 2 < 3 { 'yes!' }", + result: Some(Value::test_string("yes!")), + }, + Example { + description: "Output a value if a condition matches, else return another value", + example: "if 5 < 3 { 'yes!' } else { 'no!' }", + result: Some(Value::test_string("no!")), + }, + Example { + description: "Chain multiple if's together", + example: "if 5 < 3 { 'yes!' } else if 4 < 5 { 'no!' } else { 'okay!' }", + result: Some(Value::test_string("no!")), + }, + ] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(If {}) + } +} diff --git a/crates/nu-command/src/core_commands/ignore.rs b/crates/nu-command/src/core_commands/ignore.rs new file mode 100644 index 0000000000..679ec156bb --- /dev/null +++ b/crates/nu-command/src/core_commands/ignore.rs @@ -0,0 +1,48 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, Signature}; + +#[derive(Clone)] +pub struct Ignore; + +impl Command for Ignore { + fn name(&self) -> &str { + "ignore" + } + + fn usage(&self) -> &str { + "Ignore the output of the previous command in the pipeline" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("ignore").category(Category::Core) + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::new(call.head)) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Ignore the output of an echo command", + example: "echo done | ignore", + result: None, + }] + } +} + +#[cfg(test)] +mod test { + #[test] + fn test_examples() { + use super::Ignore; + use crate::test_examples; + test_examples(Ignore {}) + } +} diff --git a/crates/nu-command/src/core_commands/let_.rs b/crates/nu-command/src/core_commands/let_.rs new file mode 100644 index 0000000000..9c90c60783 --- /dev/null +++ b/crates/nu-command/src/core_commands/let_.rs @@ -0,0 +1,78 @@ +use nu_engine::eval_expression_with_input; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, Signature, SyntaxShape}; + +#[derive(Clone)] +pub struct Let; + +impl Command for Let { + fn name(&self) -> &str { + "let" + } + + fn usage(&self) -> &str { + "Create a variable and give it a value." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("let") + .required("var_name", SyntaxShape::VarWithOptType, "variable name") + .required( + "initial_value", + SyntaxShape::Keyword(b"=".to_vec(), Box::new(SyntaxShape::Expression)), + "equals sign followed by value", + ) + .category(Category::Core) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let var_id = call.positional[0] + .as_var() + .expect("internal error: missing variable"); + + let keyword_expr = call.positional[1] + .as_keyword() + .expect("internal error: missing keyword"); + + let rhs = eval_expression_with_input(engine_state, stack, keyword_expr, input, false)?; + + //println!("Adding: {:?} to {}", rhs, var_id); + + stack.add_var(var_id, rhs.into_value(call.head)); + Ok(PipelineData::new(call.head)) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Set a variable to a value", + example: "let x = 10", + result: None, + }, + Example { + description: "Set a variable to the result of an expression", + example: "let x = 10 + 100", + result: None, + }, + ] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Let {}) + } +} diff --git a/crates/nu-command/src/core_commands/metadata.rs b/crates/nu-command/src/core_commands/metadata.rs new file mode 100644 index 0000000000..d84d1a4e89 --- /dev/null +++ b/crates/nu-command/src/core_commands/metadata.rs @@ -0,0 +1,169 @@ +use nu_engine::CallExt; +use nu_protocol::ast::{Call, Expr, Expression}; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, DataSource, Example, IntoPipelineData, PipelineData, PipelineMetadata, Signature, + Span, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Metadata; + +impl Command for Metadata { + fn name(&self) -> &str { + "metadata" + } + + fn usage(&self) -> &str { + "Get the metadata for items in the stream" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("metadata") + .optional( + "expression", + SyntaxShape::Any, + "the expression you want metadata for", + ) + .category(Category::Core) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let arg = call.positional.get(0); + let head = call.head; + + match arg { + Some(Expression { + expr: Expr::FullCellPath(full_cell_path), + span, + .. + }) => { + if full_cell_path.tail.is_empty() { + match &full_cell_path.head { + Expression { + expr: Expr::Var(var_id), + .. + } => { + let origin = stack.get_var_with_origin(*var_id, *span)?; + + Ok(build_metadata_record(&origin, &input.metadata(), head) + .into_pipeline_data()) + } + _ => { + let val: Value = call.req(engine_state, stack, 0)?; + Ok(build_metadata_record(&val, &input.metadata(), head) + .into_pipeline_data()) + } + } + } else { + let val: Value = call.req(engine_state, stack, 0)?; + Ok(build_metadata_record(&val, &input.metadata(), head).into_pipeline_data()) + } + } + Some(_) => { + let val: Value = call.req(engine_state, stack, 0)?; + Ok(build_metadata_record(&val, &input.metadata(), head).into_pipeline_data()) + } + None => { + let mut cols = vec![]; + let mut vals = vec![]; + if let Some(x) = &input.metadata() { + match x { + PipelineMetadata { + data_source: DataSource::Ls, + } => { + cols.push("source".into()); + vals.push(Value::String { + val: "ls".into(), + span: head, + }) + } + } + } + + Ok(Value::Record { + cols, + vals, + span: head, + } + .into_pipeline_data()) + } + } + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Get the metadata of a variable", + example: "metadata $a", + result: None, + }, + Example { + description: "Get the metadata of the input", + example: "ls | metadata", + result: None, + }, + ] + } +} + +fn build_metadata_record(arg: &Value, metadata: &Option, head: Span) -> Value { + let mut cols = vec![]; + let mut vals = vec![]; + + if let Ok(span) = arg.span() { + cols.push("span".into()); + vals.push(Value::Record { + cols: vec!["start".into(), "end".into()], + vals: vec![ + Value::Int { + val: span.start as i64, + span, + }, + Value::Int { + val: span.end as i64, + span, + }, + ], + span: head, + }); + } + + if let Some(x) = &metadata { + match x { + PipelineMetadata { + data_source: DataSource::Ls, + } => { + cols.push("source".into()); + vals.push(Value::String { + val: "ls".into(), + span: head, + }) + } + } + } + + Value::Record { + cols, + vals, + span: head, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Metadata {}) + } +} diff --git a/crates/nu-command/src/core_commands/mod.rs b/crates/nu-command/src/core_commands/mod.rs new file mode 100644 index 0000000000..b63275a8bc --- /dev/null +++ b/crates/nu-command/src/core_commands/mod.rs @@ -0,0 +1,56 @@ +mod alias; +mod debug; +mod def; +mod def_env; +mod describe; +mod do_; +mod echo; +mod error_make; +mod export; +mod export_def; +mod export_def_env; +mod export_env; +mod for_; +mod help; +mod hide; +mod history; +mod if_; +mod ignore; +mod let_; +mod metadata; +mod module; +mod source; +mod tutor; +mod use_; +mod version; + +pub use alias::Alias; +pub use debug::Debug; +pub use def::Def; +pub use def_env::DefEnv; +pub use describe::Describe; +pub use do_::Do; +pub use echo::Echo; +pub use error_make::ErrorMake; +pub use export::ExportCommand; +pub use export_def::ExportDef; +pub use export_def_env::ExportDefEnv; +pub use export_env::ExportEnv; +pub use for_::For; +pub use help::Help; +pub use hide::Hide; +pub use history::History; +pub use if_::If; +pub use ignore::Ignore; +pub use let_::Let; +pub use metadata::Metadata; +pub use module::Module; +pub use source::Source; +pub use tutor::Tutor; +pub use use_::Use; +pub use version::Version; +#[cfg(feature = "plugin")] +mod register; + +#[cfg(feature = "plugin")] +pub use register::Register; diff --git a/crates/nu-command/src/core_commands/module.rs b/crates/nu-command/src/core_commands/module.rs new file mode 100644 index 0000000000..fbdc067295 --- /dev/null +++ b/crates/nu-command/src/core_commands/module.rs @@ -0,0 +1,37 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, PipelineData, Signature, SyntaxShape}; + +#[derive(Clone)] +pub struct Module; + +impl Command for Module { + fn name(&self) -> &str { + "module" + } + + fn usage(&self) -> &str { + "Define a custom module" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("module") + .required("module_name", SyntaxShape::String, "module name") + .required( + "block", + SyntaxShape::Block(Some(vec![])), + "body of the module", + ) + .category(Category::Core) + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::new(call.head)) + } +} diff --git a/crates/nu-command/src/core_commands/register.rs b/crates/nu-command/src/core_commands/register.rs new file mode 100644 index 0000000000..ffa6f9d768 --- /dev/null +++ b/crates/nu-command/src/core_commands/register.rs @@ -0,0 +1,53 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, PipelineData, Signature, SyntaxShape}; + +#[derive(Clone)] +pub struct Register; + +impl Command for Register { + fn name(&self) -> &str { + "register" + } + + fn usage(&self) -> &str { + "Register a plugin" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("register") + .required( + "plugin", + SyntaxShape::Filepath, + "path of executable for plugin", + ) + .required_named( + "encoding", + SyntaxShape::String, + "Encoding used to communicate with plugin. Options: [capnp, json]", + Some('e'), + ) + .optional( + "signature", + SyntaxShape::Any, + "Block with signature description as json object", + ) + .named( + "shell", + SyntaxShape::Filepath, + "path of shell used to run plugin (cmd, sh, python, etc)", + Some('s'), + ) + .category(Category::Core) + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::new(call.head)) + } +} diff --git a/crates/nu-command/src/core_commands/source.rs b/crates/nu-command/src/core_commands/source.rs new file mode 100644 index 0000000000..b4626e5e38 --- /dev/null +++ b/crates/nu-command/src/core_commands/source.rs @@ -0,0 +1,43 @@ +use nu_engine::{eval_block, CallExt}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, PipelineData, ShellError, Signature, SyntaxShape}; + +/// Source a file for environment variables. +#[derive(Clone)] +pub struct Source; + +impl Command for Source { + fn name(&self) -> &str { + "source" + } + + fn signature(&self) -> Signature { + Signature::build("source") + .required( + "filename", + SyntaxShape::Filepath, + "the filepath to the script file to source", + ) + .category(Category::Core) + } + + fn usage(&self) -> &str { + "Runs a script file in the current context." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + // Note: this hidden positional is the block_id that corresponded to the 0th position + // it is put here by the parser + let block_id: i64 = call.req(engine_state, stack, 1)?; + + let block = engine_state.get_block(block_id as usize).clone(); + eval_block(engine_state, stack, &block, input) + } +} diff --git a/crates/nu-command/src/core_commands/tutor.rs b/crates/nu-command/src/core_commands/tutor.rs new file mode 100644 index 0000000000..818ccb89db --- /dev/null +++ b/crates/nu-command/src/core_commands/tutor.rs @@ -0,0 +1,466 @@ +use itertools::Itertools; +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, SyntaxShape, + Value, +}; + +#[derive(Clone)] +pub struct Tutor; + +impl Command for Tutor { + fn name(&self) -> &str { + "tutor" + } + + fn signature(&self) -> Signature { + Signature::build("tutor") + .optional( + "search", + SyntaxShape::String, + "item to search for, or 'list' to list available tutorials", + ) + .named( + "find", + SyntaxShape::String, + "Search tutorial for a phrase", + Some('f'), + ) + .category(Category::Core) + } + + fn usage(&self) -> &str { + "Run the tutorial. To begin, run: tutor" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + tutor(engine_state, stack, call) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Begin the tutorial", + example: "tutor begin", + result: None, + }, + Example { + description: "Search a tutorial by phrase", + example: "tutor -f \"$in\"", + result: None, + }, + ] + } +} + +fn tutor( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, +) -> Result { + let span = call.head; + + let search: Option = call.opt(engine_state, stack, 0).unwrap_or(None); + let find: Option = call.get_flag(engine_state, stack, "find")?; + + let search_space = [ + (vec!["begin"], begin_tutor()), + ( + vec!["table", "tables", "row", "rows", "column", "columns"], + table_tutor(), + ), + (vec!["cell", "cells"], cell_tutor()), + ( + vec![ + "expr", + "exprs", + "expressions", + "subexpression", + "subexpressions", + "sub-expression", + "sub-expressions", + ], + expression_tutor(), + ), + (vec!["echo"], echo_tutor()), + (vec!["each", "iteration", "iter"], each_tutor()), + ( + vec!["var", "vars", "variable", "variables"], + variable_tutor(), + ), + (vec!["engine-q", "e-q"], engineq_tutor()), + (vec!["block", "blocks"], block_tutor()), + (vec!["shorthand", "shorthands"], shorthand_tutor()), + ]; + + if let Some(find) = find { + let mut results = vec![]; + for search_group in search_space { + if search_group.1.contains(&find) { + results.push(search_group.0[0].to_string()) + } + } + + let message = format!("You can find '{}' in the following topics:\n{}\n\nYou can learn about a topic using `tutor` followed by the name of the topic.\nFor example: `tutor table` to open the table topic.\n\n", + find, + results.into_iter().map(|x| format!("- {}", x)).join("\n") + ); + + return Ok(display(&message, engine_state, stack, span)); + } else if let Some(search) = search { + for search_group in search_space { + if search_group.0.contains(&search.as_str()) { + return Ok(display(search_group.1, engine_state, stack, span)); + } + } + } + Ok(display(default_tutor(), engine_state, stack, span)) +} + +fn default_tutor() -> &'static str { + r#" +Welcome to the Nushell tutorial! + +With the `tutor` command, you'll be able to learn a lot about how Nushell +works along with many fun tips and tricks to speed up everyday tasks. + +To get started, you can use `tutor begin`. + +"# +} + +fn begin_tutor() -> &'static str { + r#" +Nushell is a structured shell and programming language. One way to begin +using it is to try a few of the commands. + +The first command to try is `ls`. The `ls` command will show you a list +of the files in the current directory. Notice that these files are shown +as a table. Each column of this table not only tells us what is being +shown, but also gives us a way to work with the data. + +You can combine the `ls` command with other commands using the pipeline +symbol '|'. This allows data to flow from one command to the next. + +For example, if we only wanted the name column, we could do: +``` +ls | select name +``` +Notice that we still get a table, but this time it only has one column: +the name column. + +You can continue to learn more about tables by running: +``` +tutor tables +``` +If at any point, you'd like to restart this tutorial, you can run: +``` +tutor begin +``` +"# +} + +fn table_tutor() -> &'static str { + r#" +The most common form of data in Nushell is the table. Tables contain rows and +columns of data. In each cell of the table, there is data that you can access +using Nushell commands. + +To get the 3rd row in the table, you can use the `nth` command: +``` +ls | nth 2 +``` +This will get the 3rd (note that `nth` is zero-based) row in the table created +by the `ls` command. You can use `nth` on any table created by other commands +as well. + +You can also access the column of data in one of two ways. If you want +to keep the column as part of a new table, you can use `select`. +``` +ls | select name +``` +This runs `ls` and returns only the "name" column of the table. + +If, instead, you'd like to get access to the values inside of the column, you +can use the `get` command. +``` +ls | get name +``` +This allows us to get to the list of strings that are the filenames rather +than having a full table. In some cases, this can make the names easier to +work with. + +You can continue to learn more about working with cells of the table by +running: +``` +tutor cells +``` +"# +} + +fn cell_tutor() -> &'static str { + r#" +Working with cells of data in the table is a key part of working with data in +Nushell. Because of this, there is a rich list of commands to work with cells +as well as handy shorthands for accessing cells. + +Cells can hold simple values like strings and numbers, or more complex values +like lists and tables. + +To reach a cell of data from a table, you can combine a row operation and a +column operation. +``` +ls | nth 4 | get name +``` +You can combine these operations into one step using a shortcut. +``` +(ls).4.name +``` +Names/strings represent columns names and numbers represent row numbers. + +The `(ls)` is a form of expression. You can continue to learn more about +expressions by running: +``` +tutor expressions +``` +You can also learn about these cell shorthands by running: +``` +tutor shorthands +``` +"# +} + +fn expression_tutor() -> &'static str { + r#" +Expressions give you the power to mix calls to commands with math. The +simplest expression is a single value like a string or number. +``` +3 +``` +Expressions can also include math operations like addition or division. +``` +10 / 2 +``` +Normally, an expression is one type of operation: math or commands. You can +mix these types by using subexpressions. Subexpressions are just like +expressions, but they're wrapped in parentheses `()`. +``` +10 * (3 + 4) +``` +Here we use parentheses to create a higher math precedence in the math +expression. +``` +echo (2 + 3) +``` +You can continue to learn more about the `echo` command by running: +``` +tutor echo +``` +"# +} + +fn echo_tutor() -> &'static str { + r#" +The `echo` command in Nushell is a powerful tool for not only seeing values, +but also for creating new ones. +``` +echo "Hello" +``` +You can echo output. This output, if it's not redirected using a "|" pipeline +will be displayed to the screen. +``` +echo 1..10 +``` +You can also use echo to work with individual values of a range. In this +example, `echo` will create the values from 1 to 10 as a list. +``` +echo 1 2 3 4 5 +``` +You can also create lists of values by passing `echo` multiple arguments. +This can be helpful if you want to later processes these values. + +The `echo` command can pair well with the `each` command which can run +code on each row, or item, of input. + +You can continue to learn more about the `each` command by running: +``` +tutor each +``` +"# +} + +fn each_tutor() -> &'static str { + r#" +The `each` command gives us a way of working with each individual row or +element of a list one at a time. It reads these in from the pipeline and +runs a block on each element. A block is a group of pipelines. +``` +echo 1 2 3 | each { $it + 10} +``` +This example iterates over each element sent by `echo`, giving us three new +values that are the original value + 10. Here, the `$it` is a variable that +is the name given to the block's parameter by default. + +You can learn more about blocks by running: +``` +tutor blocks +``` +You can also learn more about variables by running: +``` +tutor variables +``` +"# +} + +fn variable_tutor() -> &'static str { + r#" +Variables are an important way to store values to be used later. To create a +variable, you can use the `let` keyword. The `let` command will create a +variable and then assign it a value in one step. +``` +let $x = 3 +``` +Once created, we can refer to this variable by name. +``` +$x +``` +Nushell also comes with built-in variables. The `$nu` variable is a reserved +variable that contains a lot of information about the currently running +instance of Nushell. The `$it` variable is the name given to block parameters +if you don't specify one. And `$in` is the variable that allows you to work +with all of the data coming in from the pipeline in one place. + +"# +} + +fn block_tutor() -> &'static str { + r#" +Blocks are a special form of expression that hold code to be run at a later +time. Often, you'll see blocks as one of the arguments given to commands +like `each` and `if`. +``` +ls | each {|x| $x.name} +``` +The above will create a list of the filenames in the directory. +``` +if $true { echo "it's true" } { echo "it's not true" } +``` +This `if` call will run the first block if the expression is true, or the +second block if the expression is false. + +"# +} + +fn shorthand_tutor() -> &'static str { + r#" +You can access cells in a table using a shorthand notation sometimes called a +"column path" or "cell path". These paths allow you to go from a table to +rows, columns, or cells inside of the table. + +Shorthand paths are made from rows numbers, column names, or both. You can use +them on any variable or subexpression. +``` +$nu.cwd +``` +The above accesses the built-in `$nu` variable, gets its table, and then uses +the shorthand path to retrieve only the cell data inside the "cwd" column. +``` +(ls).name.4 +``` +This will retrieve the cell data in the "name" column on the 5th row (note: +row numbers are zero-based). + +Rows and columns don't need to come in any specific order. You can get the +same value using: +``` +(ls).4.name +``` +"# +} + +fn engineq_tutor() -> &'static str { + r#" +Engine-q is the upcoming engine for Nushell. Build for speed and correctness, +it also comes with a set of changes from Nushell versions prior to 0.60. To +get ready for engine-q look for some of these changes that might impact your +current scripts: + +* Engine-q now uses a few new data structures, including a record syntax + that allows you to model key-value pairs similar to JSON objects. +* Environment variables can now contain more than just strings. Structured + values are converted to strings for external commands using converters. +* `if` will now use an `else` keyword before the else block. +* We're moving from "config.toml" to "config.nu". This means startup will + now be a script file. +* `config` and its subcommands are being replaced by a record that you can + update in the shell which contains all the settings under the variable + `$config`. +* bigint/bigdecimal values are now machine i64 and f64 values +* And more, you can read more about upcoming changes in the up-to-date list + at: https://github.com/nushell/engine-q/issues/522 +"# +} + +fn display(help: &str, engine_state: &EngineState, stack: &mut Stack, span: Span) -> PipelineData { + let help = help.split('`'); + + let mut build = String::new(); + let mut code_mode = false; + + for item in help { + if code_mode { + code_mode = false; + + //TODO: support no-color mode + if let Some(highlighter) = engine_state.find_decl(b"nu-highlight") { + let decl = engine_state.get_decl(highlighter); + + if let Ok(output) = decl.run( + engine_state, + stack, + &Call::new(span), + Value::String { + val: item.to_string(), + span: Span { start: 0, end: 0 }, + } + .into_pipeline_data(), + ) { + let result = output.into_value(Span { start: 0, end: 0 }); + match result.as_string() { + Ok(s) => { + build.push_str(&s); + } + _ => { + build.push_str(item); + } + } + } + } + } else { + code_mode = true; + build.push_str(item); + } + } + + Value::string(build, span).into_pipeline_data() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Tutor) + } +} diff --git a/crates/nu-command/src/core_commands/use_.rs b/crates/nu-command/src/core_commands/use_.rs new file mode 100644 index 0000000000..965200540a --- /dev/null +++ b/crates/nu-command/src/core_commands/use_.rs @@ -0,0 +1,118 @@ +use nu_engine::eval_block; +use nu_protocol::ast::{Call, Expr, Expression, ImportPatternMember}; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, PipelineData, ShellError, Signature, SyntaxShape}; + +#[derive(Clone)] +pub struct Use; + +impl Command for Use { + fn name(&self) -> &str { + "use" + } + + fn usage(&self) -> &str { + "Use definitions from a module" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("use") + .required("pattern", SyntaxShape::ImportPattern, "import pattern") + .category(Category::Core) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let import_pattern = if let Some(Expression { + expr: Expr::ImportPattern(pat), + .. + }) = call.positional.get(0) + { + pat + } else { + return Err(ShellError::SpannedLabeledError( + "Unexpected import".into(), + "import pattern not supported".into(), + call.head, + )); + }; + + if let Some(overlay_id) = engine_state.find_overlay(&import_pattern.head.name) { + let overlay = engine_state.get_overlay(overlay_id); + + let env_vars_to_use = if import_pattern.members.is_empty() { + overlay.env_vars_with_head(&import_pattern.head.name) + } else { + match &import_pattern.members[0] { + ImportPatternMember::Glob { .. } => overlay.env_vars(), + ImportPatternMember::Name { name, span } => { + let mut output = vec![]; + + if let Some(id) = overlay.get_env_var_id(name) { + output.push((name.clone(), id)); + } else if !overlay.has_decl(name) { + return Err(ShellError::EnvVarNotFoundAtRuntime( + String::from_utf8_lossy(name).into(), + *span, + )); + } + + output + } + ImportPatternMember::List { names } => { + let mut output = vec![]; + + for (name, span) in names { + if let Some(id) = overlay.get_env_var_id(name) { + output.push((name.clone(), id)); + } else if !overlay.has_decl(name) { + return Err(ShellError::EnvVarNotFoundAtRuntime( + String::from_utf8_lossy(name).into(), + *span, + )); + } + } + + output + } + } + }; + + for (name, block_id) in env_vars_to_use { + let name = if let Ok(s) = String::from_utf8(name.clone()) { + s + } else { + return Err(ShellError::NonUtf8(import_pattern.head.span)); + }; + + let block = engine_state.get_block(block_id); + + // TODO: Add string conversions (e.g. int to string) + // TODO: Later expand env to take all Values + let val = eval_block(engine_state, stack, block, PipelineData::new(call.head))? + .into_value(call.head); + + stack.add_env_var(name, val); + } + } else { + // TODO: This is a workaround since call.positional[0].span points at 0 for some reason + // when this error is triggered + let bytes = engine_state.get_span_contents(&call.positional[0].span); + return Err(ShellError::SpannedLabeledError( + format!( + "Could not use '{}' import pattern", + String::from_utf8_lossy(bytes) + ), + "called here".to_string(), + call.head, + )); + } + + Ok(PipelineData::new(call.head)) + } +} diff --git a/crates/nu-command/src/core_commands/version.rs b/crates/nu-command/src/core_commands/version.rs new file mode 100644 index 0000000000..d7d74f90a9 --- /dev/null +++ b/crates/nu-command/src/core_commands/version.rs @@ -0,0 +1,352 @@ +use indexmap::IndexMap; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Example, IntoPipelineData, PipelineData, ShellError, Signature, Value}; + +pub mod shadow { + include!(concat!(env!("OUT_DIR"), "/shadow.rs")); +} + +#[derive(Clone)] +pub struct Version; + +impl Command for Version { + fn name(&self) -> &str { + "version" + } + + fn signature(&self) -> Signature { + Signature::build("version") + } + + fn usage(&self) -> &str { + "Display Nu version." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + version(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Display Nu version", + example: "version", + result: None, + }] + } +} + +pub fn version( + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, +) -> Result { + let tag = call.head; + + let mut indexmap = IndexMap::with_capacity(4); + + indexmap.insert( + "version".to_string(), + Value::String { + val: env!("CARGO_PKG_VERSION").to_string(), + span: tag, + }, + ); + + let branch: Option<&str> = Some(shadow::BRANCH).filter(|x| !x.is_empty()); + if let Some(branch) = branch { + indexmap.insert( + "branch".to_string(), + Value::String { + val: branch.to_string(), + span: call.head, + }, + ); + } + + let short_commit: Option<&str> = Some(shadow::SHORT_COMMIT).filter(|x| !x.is_empty()); + if let Some(short_commit) = short_commit { + indexmap.insert( + "short_commit".to_string(), + Value::String { + val: short_commit.to_string(), + span: call.head, + }, + ); + } + let commit_hash: Option<&str> = Some(shadow::COMMIT_HASH).filter(|x| !x.is_empty()); + if let Some(commit_hash) = commit_hash { + indexmap.insert( + "commit_hash".to_string(), + Value::String { + val: commit_hash.to_string(), + span: call.head, + }, + ); + } + let commit_date: Option<&str> = Some(shadow::COMMIT_DATE).filter(|x| !x.is_empty()); + if let Some(commit_date) = commit_date { + indexmap.insert( + "commit_date".to_string(), + Value::String { + val: commit_date.to_string(), + span: call.head, + }, + ); + } + + let build_os: Option<&str> = Some(shadow::BUILD_OS).filter(|x| !x.is_empty()); + if let Some(build_os) = build_os { + indexmap.insert( + "build_os".to_string(), + Value::String { + val: build_os.to_string(), + span: call.head, + }, + ); + } + + let rust_version: Option<&str> = Some(shadow::RUST_VERSION).filter(|x| !x.is_empty()); + if let Some(rust_version) = rust_version { + indexmap.insert( + "rust_version".to_string(), + Value::String { + val: rust_version.to_string(), + span: call.head, + }, + ); + } + + let rust_channel: Option<&str> = Some(shadow::RUST_CHANNEL).filter(|x| !x.is_empty()); + if let Some(rust_channel) = rust_channel { + indexmap.insert( + "rust_channel".to_string(), + Value::String { + val: rust_channel.to_string(), + span: call.head, + }, + ); + } + + let cargo_version: Option<&str> = Some(shadow::CARGO_VERSION).filter(|x| !x.is_empty()); + if let Some(cargo_version) = cargo_version { + indexmap.insert( + "cargo_version".to_string(), + Value::String { + val: cargo_version.to_string(), + span: call.head, + }, + ); + } + + let pkg_version: Option<&str> = Some(shadow::PKG_VERSION).filter(|x| !x.is_empty()); + if let Some(pkg_version) = pkg_version { + indexmap.insert( + "pkg_version".to_string(), + Value::String { + val: pkg_version.to_string(), + span: call.head, + }, + ); + } + + let build_time: Option<&str> = Some(shadow::BUILD_TIME).filter(|x| !x.is_empty()); + if let Some(build_time) = build_time { + indexmap.insert( + "build_time".to_string(), + Value::String { + val: build_time.to_string(), + span: call.head, + }, + ); + } + + let build_rust_channel: Option<&str> = + Some(shadow::BUILD_RUST_CHANNEL).filter(|x| !x.is_empty()); + if let Some(build_rust_channel) = build_rust_channel { + indexmap.insert( + "build_rust_channel".to_string(), + Value::String { + val: build_rust_channel.to_string(), + span: call.head, + }, + ); + } + + indexmap.insert( + "features".to_string(), + Value::String { + val: features_enabled().join(", "), + span: call.head, + }, + ); + + // Get a list of command names and check for plugins + let installed_plugins = engine_state + .plugin_decls() + .into_iter() + .filter(|x| x.is_plugin().is_some()) + .map(|x| x.name()) + .collect::>(); + + indexmap.insert( + "installed_plugins".to_string(), + Value::String { + val: installed_plugins.join(", "), + span: call.head, + }, + ); + + let cols = indexmap.keys().cloned().collect::>(); + let vals = indexmap.values().cloned().collect::>(); + + // Ok(Value::List { + // vals: vec![Value::Record { + // cols, + // vals, + // span: call.head, + // }], + // span: call.head, + // } + // .into_pipeline_data()) + + // List looks better than table, imo + Ok(Value::Record { + cols, + vals, + span: call.head, + } + .into_pipeline_data()) +} + +fn features_enabled() -> Vec { + let mut names = vec!["default".to_string()]; + + // NOTE: There should be another way to know + // features on. + #[cfg(feature = "ctrlc")] + { + names.push("ctrlc".to_string()); + } + + // #[cfg(feature = "rich-benchmark")] + // { + // names.push("rich-benchmark".to_string()); + // } + + #[cfg(feature = "rustyline-support")] + { + names.push("rustyline".to_string()); + } + + #[cfg(feature = "term")] + { + names.push("term".to_string()); + } + + #[cfg(feature = "uuid_crate")] + { + names.push("uuid".to_string()); + } + + #[cfg(feature = "which")] + { + names.push("which".to_string()); + } + + #[cfg(feature = "zip")] + { + names.push("zip".to_string()); + } + + #[cfg(feature = "clipboard-cli")] + { + names.push("clipboard-cli".to_string()); + } + + #[cfg(feature = "trash-support")] + { + names.push("trash".to_string()); + } + + #[cfg(feature = "dataframe")] + { + names.push("dataframe".to_string()); + } + + #[cfg(feature = "table-pager")] + { + names.push("table-pager".to_string()); + } + + // #[cfg(feature = "binaryview")] + // { + // names.push("binaryview".to_string()); + // } + + // #[cfg(feature = "start")] + // { + // names.push("start".to_string()); + // } + + // #[cfg(feature = "bson")] + // { + // names.push("bson".to_string()); + // } + + // #[cfg(feature = "sqlite")] + // { + // names.push("sqlite".to_string()); + // } + + // #[cfg(feature = "s3")] + // { + // names.push("s3".to_string()); + // } + + // #[cfg(feature = "chart")] + // { + // names.push("chart".to_string()); + // } + + // #[cfg(feature = "xpath")] + // { + // names.push("xpath".to_string()); + // } + + // #[cfg(feature = "selector")] + // { + // names.push("selector".to_string()); + // } + + // #[cfg(feature = "extra")] + // { + // names.push("extra".to_string()); + // } + + // #[cfg(feature = "preserve_order")] + // { + // names.push("preserve_order".to_string()); + // } + + // #[cfg(feature = "wee_alloc")] + // { + // names.push("wee_alloc".to_string()); + // } + + // #[cfg(feature = "console_error_panic_hook")] + // { + // names.push("console_error_panic_hook".to_string()); + // } + + names.sort(); + + names +} diff --git a/crates/nu-command/src/dataframe/README.md b/crates/nu-command/src/dataframe/README.md new file mode 100644 index 0000000000..2a50786a0e --- /dev/null +++ b/crates/nu-command/src/dataframe/README.md @@ -0,0 +1,3 @@ +# nu-dataframe + +The nu-dataframe crate holds the definitions of the dataframe structures and commands diff --git a/crates/nu-command/src/dataframe/eager/aggregate.rs b/crates/nu-command/src/dataframe/eager/aggregate.rs new file mode 100644 index 0000000000..66017c41ac --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/aggregate.rs @@ -0,0 +1,375 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + did_you_mean, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Value, +}; +use polars::{frame::groupby::GroupBy, prelude::PolarsError}; + +use crate::dataframe::values::NuGroupBy; + +use super::super::values::{Column, NuDataFrame}; + +enum Operation { + Mean, + Sum, + Min, + Max, + First, + Last, + Nunique, + Quantile(f64), + Median, + Var, + Std, + Count, +} + +impl Operation { + fn from_tagged( + name: &Spanned, + quantile: Option>, + ) -> Result { + match name.item.as_ref() { + "mean" => Ok(Operation::Mean), + "sum" => Ok(Operation::Sum), + "min" => Ok(Operation::Min), + "max" => Ok(Operation::Max), + "first" => Ok(Operation::First), + "last" => Ok(Operation::Last), + "nunique" => Ok(Operation::Nunique), + "quantile" => match quantile { + None => Err(ShellError::SpannedLabeledError( + "Quantile value not fount".into(), + "Quantile operation requires quantile value".into(), + name.span, + )), + Some(value) => { + if (value.item < 0.0) | (value.item > 1.0) { + Err(ShellError::SpannedLabeledError( + "Inappropriate quantile".into(), + "Quantile value should be between 0.0 and 1.0".into(), + value.span, + )) + } else { + Ok(Operation::Quantile(value.item)) + } + } + }, + "median" => Ok(Operation::Median), + "var" => Ok(Operation::Var), + "std" => Ok(Operation::Std), + "count" => Ok(Operation::Count), + selection => { + let possibilities = [ + "mean".to_string(), + "sum".to_string(), + "min".to_string(), + "max".to_string(), + "first".to_string(), + "last".to_string(), + "nunique".to_string(), + "quantile".to_string(), + "median".to_string(), + "var".to_string(), + "std".to_string(), + "count".to_string(), + ]; + + match did_you_mean(&possibilities, selection) { + Some(suggestion) => Err(ShellError::DidYouMean(suggestion, name.span)), + None => Err(ShellError::SpannedLabeledErrorHelp( + "Operation not fount".into(), + "Operation does not exist".into(), + name.span, + "Perhaps you want: mean, sum, min, max, first, last, nunique, quantile, median, var, std, or count".into(), + )) + } + } + } + } + + fn to_str(&self) -> &'static str { + match self { + Self::Mean => "mean", + Self::Sum => "sum", + Self::Min => "min", + Self::Max => "max", + Self::First => "first", + Self::Last => "last", + Self::Nunique => "nunique", + Self::Quantile(_) => "quantile", + Self::Median => "median", + Self::Var => "var", + Self::Std => "std", + Self::Count => "count", + } + } +} + +#[derive(Clone)] +pub struct Aggregate; + +impl Command for Aggregate { + fn name(&self) -> &str { + "dfr aggregate" + } + + fn usage(&self) -> &str { + "Performs an aggregation operation on a dataframe and groupby object" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required( + "operation-name", + SyntaxShape::String, + "\n\tDataframes: mean, sum, min, max, quantile, median, var, std +\tGroupBy: mean, sum, min, max, first, last, nunique, quantile, median, var, std, count", + ) + .named( + "quantile", + SyntaxShape::Number, + "quantile value for quantile operation", + Some('q'), + ) + .switch( + "explicit", + "returns explicit names for groupby aggregations", + Some('e'), + ) + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Aggregate sum by grouping by column a and summing on col b", + example: + "[[a b]; [one 1] [one 2]] | dfr to-df | dfr group-by a | dfr aggregate sum", + result: Some( + NuDataFrame::try_from_columns(vec![ + Column::new("a".to_string(), vec![Value::test_string("one")]), + Column::new("b".to_string(), vec![Value::test_int(3)]), + ]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }, + Example { + description: "Aggregate sum in dataframe columns", + example: "[[a b]; [4 1] [5 2]] | dfr to-df | dfr aggregate sum", + result: Some( + NuDataFrame::try_from_columns(vec![ + Column::new("a".to_string(), vec![Value::test_int(9)]), + Column::new("b".to_string(), vec![Value::test_int(3)]), + ]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }, + Example { + description: "Aggregate sum in series", + example: "[4 1 5 6] | dfr to-df | dfr aggregate sum", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0".to_string(), + vec![Value::test_int(16)], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let operation: Spanned = call.req(engine_state, stack, 0)?; + let quantile: Option> = call.get_flag(engine_state, stack, "quantile")?; + let op = Operation::from_tagged(&operation, quantile)?; + + match input { + PipelineData::Value(Value::CustomValue { val, span }, _) => { + let df = val.as_any().downcast_ref::(); + let groupby = val.as_any().downcast_ref::(); + + match (df, groupby) { + (Some(df), None) => { + let df = df.as_ref(); + let res = perform_dataframe_aggregation(df, op, operation.span)?; + + Ok(PipelineData::Value( + NuDataFrame::dataframe_into_value(res, span), + None, + )) + } + (None, Some(nu_groupby)) => { + let groupby = nu_groupby.to_groupby()?; + + let res = perform_groupby_aggregation( + groupby, + op, + operation.span, + call.head, + call.has_flag("explicit"), + )?; + + Ok(PipelineData::Value( + NuDataFrame::dataframe_into_value(res, span), + None, + )) + } + _ => Err(ShellError::SpannedLabeledError( + "Incorrect datatype".into(), + "no groupby or dataframe found in input stream".into(), + call.head, + )), + } + } + _ => Err(ShellError::SpannedLabeledError( + "Incorrect datatype".into(), + "no groupby or dataframe found in input stream".into(), + call.head, + )), + } +} + +fn perform_groupby_aggregation( + groupby: GroupBy, + operation: Operation, + operation_span: Span, + agg_span: Span, + explicit: bool, +) -> Result { + let mut res = match operation { + Operation::Mean => groupby.mean(), + Operation::Sum => groupby.sum(), + Operation::Min => groupby.min(), + Operation::Max => groupby.max(), + Operation::First => groupby.first(), + Operation::Last => groupby.last(), + Operation::Nunique => groupby.n_unique(), + Operation::Quantile(quantile) => groupby.quantile(quantile), + Operation::Median => groupby.median(), + Operation::Var => groupby.var(), + Operation::Std => groupby.std(), + Operation::Count => groupby.count(), + } + .map_err(|e| { + let span = match &e { + PolarsError::NotFound(_) => agg_span, + _ => operation_span, + }; + + ShellError::SpannedLabeledError("Error calculating aggregation".into(), e.to_string(), span) + })?; + + if !explicit { + let col_names = res + .get_column_names() + .iter() + .map(|name| name.to_string()) + .collect::>(); + + for col in col_names { + let from = match operation { + Operation::Mean => "_mean", + Operation::Sum => "_sum", + Operation::Min => "_min", + Operation::Max => "_max", + Operation::First => "_first", + Operation::Last => "_last", + Operation::Nunique => "_n_unique", + Operation::Quantile(_) => "_quantile", + Operation::Median => "_median", + Operation::Var => "_agg_var", + Operation::Std => "_agg_std", + Operation::Count => "_count", + }; + + let new_col = match col.find(from) { + Some(index) => &col[..index], + None => &col[..], + }; + + res.rename(&col, new_col) + .expect("Column is always there. Looping with known names"); + } + } + + Ok(res) +} + +fn perform_dataframe_aggregation( + dataframe: &polars::prelude::DataFrame, + operation: Operation, + operation_span: Span, +) -> Result { + match operation { + Operation::Mean => Ok(dataframe.mean()), + Operation::Sum => Ok(dataframe.sum()), + Operation::Min => Ok(dataframe.min()), + Operation::Max => Ok(dataframe.max()), + Operation::Quantile(quantile) => dataframe.quantile(quantile).map_err(|e| { + ShellError::SpannedLabeledError( + "Error calculating quantile".into(), + e.to_string(), + operation_span, + ) + }), + Operation::Median => Ok(dataframe.median()), + Operation::Var => Ok(dataframe.var()), + Operation::Std => Ok(dataframe.std()), + operation => { + let possibilities = [ + "mean".to_string(), + "sum".to_string(), + "min".to_string(), + "max".to_string(), + "quantile".to_string(), + "median".to_string(), + "var".to_string(), + "std".to_string(), + ]; + + match did_you_mean(&possibilities, operation.to_str()) { + Some(suggestion) => Err(ShellError::DidYouMean(suggestion, operation_span)), + None => Err(ShellError::SpannedLabeledErrorHelp( + "Operation not fount".into(), + "Operation does not exist".into(), + operation_span, + "Perhaps you want: mean, sum, min, max, quantile, median, var, or std".into(), + )), + } + } + } +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::super::CreateGroupBy; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(Aggregate {}), Box::new(CreateGroupBy {})]) + } +} diff --git a/crates/nu-command/src/dataframe/eager/append.rs b/crates/nu-command/src/dataframe/eager/append.rs new file mode 100644 index 0000000000..a57ed9e990 --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/append.rs @@ -0,0 +1,130 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +use super::super::values::{Axis, Column, NuDataFrame}; + +#[derive(Clone)] +pub struct AppendDF; + +impl Command for AppendDF { + fn name(&self) -> &str { + "dfr append" + } + + fn usage(&self) -> &str { + "Appends a new dataframe" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required("other", SyntaxShape::Any, "dataframe to be appended") + .switch("col", "appends in col orientation", Some('c')) + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Appends a dataframe as new columns", + example: r#"let a = ([[a b]; [1 2] [3 4]] | dfr to-df); + $a | dfr append $a"#, + result: Some( + NuDataFrame::try_from_columns(vec![ + Column::new( + "a".to_string(), + vec![Value::test_int(1), Value::test_int(3)], + ), + Column::new( + "b".to_string(), + vec![Value::test_int(2), Value::test_int(4)], + ), + Column::new( + "a_x".to_string(), + vec![Value::test_int(1), Value::test_int(3)], + ), + Column::new( + "b_x".to_string(), + vec![Value::test_int(2), Value::test_int(4)], + ), + ]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }, + Example { + description: "Appends a dataframe merging at the end of columns", + example: r#"let a = ([[a b]; [1 2] [3 4]] | dfr to-df); + $a | dfr append $a --col"#, + result: Some( + NuDataFrame::try_from_columns(vec![ + Column::new( + "a".to_string(), + vec![ + Value::test_int(1), + Value::test_int(3), + Value::test_int(1), + Value::test_int(3), + ], + ), + Column::new( + "b".to_string(), + vec![ + Value::test_int(2), + Value::test_int(4), + Value::test_int(2), + Value::test_int(4), + ], + ), + ]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let other: Value = call.req(engine_state, stack, 0)?; + + let axis = if call.has_flag("col") { + Axis::Column + } else { + Axis::Row + }; + let df_other = NuDataFrame::try_from_value(other)?; + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + df.append_df(&df_other, axis, call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(AppendDF {})]) + } +} diff --git a/crates/nu-command/src/dataframe/eager/column.rs b/crates/nu-command/src/dataframe/eager/column.rs new file mode 100644 index 0000000000..9d3df4344d --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/column.rs @@ -0,0 +1,81 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Value, +}; + +use super::super::values::{Column, NuDataFrame}; + +#[derive(Clone)] +pub struct ColumnDF; + +impl Command for ColumnDF { + fn name(&self) -> &str { + "dfr column" + } + + fn usage(&self) -> &str { + "Returns the selected column" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required("column", SyntaxShape::String, "column name") + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Returns the selected column as series", + example: "[[a b]; [1 2] [3 4]] | dfr to-df | dfr column a", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "a".to_string(), + vec![Value::test_int(1), Value::test_int(3)], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let column: Spanned = call.req(engine_state, stack, 0)?; + + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + let res = df.as_ref().column(&column.item).map_err(|e| { + ShellError::SpannedLabeledError("Error selecting column".into(), e.to_string(), column.span) + })?; + + NuDataFrame::try_from_series(vec![res.clone()], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(ColumnDF {})]) + } +} diff --git a/crates/nu-command/src/dataframe/eager/command.rs b/crates/nu-command/src/dataframe/eager/command.rs new file mode 100644 index 0000000000..f8ca63593e --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/command.rs @@ -0,0 +1,42 @@ +use nu_engine::get_full_help; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, IntoPipelineData, PipelineData, ShellError, Signature, Value, +}; + +#[derive(Clone)] +pub struct Dataframe; + +impl Command for Dataframe { + fn name(&self) -> &str { + "dfr" + } + + fn usage(&self) -> &str { + "Dataframe commands" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(Value::String { + val: get_full_help( + &Dataframe.signature(), + &Dataframe.examples(), + engine_state, + stack, + ), + span: call.head, + } + .into_pipeline_data()) + } +} diff --git a/crates/nu-command/src/dataframe/eager/describe.rs b/crates/nu-command/src/dataframe/eager/describe.rs new file mode 100644 index 0000000000..2d9791b034 --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/describe.rs @@ -0,0 +1,241 @@ +use super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; +use polars::{ + chunked_array::ChunkedArray, + prelude::{ + AnyValue, DataFrame, DataType, Float64Type, IntoSeries, NewChunkedArray, Series, Utf8Type, + }, +}; + +#[derive(Clone)] +pub struct DescribeDF; + +impl Command for DescribeDF { + fn name(&self) -> &str { + "dfr describe" + } + + fn usage(&self) -> &str { + "Describes dataframes numeric columns" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "dataframe description", + example: "[[a b]; [1 1] [1 1]] | dfr to-df | dfr describe", + result: Some( + NuDataFrame::try_from_columns(vec![ + Column::new( + "descriptor".to_string(), + vec![ + Value::test_string("count"), + Value::test_string("sum"), + Value::test_string("mean"), + Value::test_string("median"), + Value::test_string("std"), + Value::test_string("min"), + Value::test_string("25%"), + Value::test_string("50%"), + Value::test_string("75%"), + Value::test_string("max"), + ], + ), + Column::new( + "a (i64)".to_string(), + vec![ + Value::test_float(2.0), + Value::test_float(2.0), + Value::test_float(1.0), + Value::test_float(1.0), + Value::test_float(0.0), + Value::test_float(1.0), + Value::test_float(1.0), + Value::test_float(1.0), + Value::test_float(1.0), + Value::test_float(1.0), + ], + ), + Column::new( + "b (i64)".to_string(), + vec![ + Value::test_float(2.0), + Value::test_float(2.0), + Value::test_float(1.0), + Value::test_float(1.0), + Value::test_float(0.0), + Value::test_float(1.0), + Value::test_float(1.0), + Value::test_float(1.0), + Value::test_float(1.0), + Value::test_float(1.0), + ], + ), + ]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + let names = ChunkedArray::::new_from_opt_slice( + "descriptor", + &[ + Some("count"), + Some("sum"), + Some("mean"), + Some("median"), + Some("std"), + Some("min"), + Some("25%"), + Some("50%"), + Some("75%"), + Some("max"), + ], + ) + .into_series(); + + let head = std::iter::once(names); + + let tail = df + .as_ref() + .get_columns() + .iter() + .filter(|col| col.dtype() != &DataType::Object("object")) + .map(|col| { + let count = col.len() as f64; + + let sum = col + .sum_as_series() + .cast(&DataType::Float64) + .ok() + .and_then(|ca| match ca.get(0) { + AnyValue::Float64(v) => Some(v), + _ => None, + }); + + let mean = match col.mean_as_series().get(0) { + AnyValue::Float64(v) => Some(v), + _ => None, + }; + + let median = match col.median_as_series().get(0) { + AnyValue::Float64(v) => Some(v), + _ => None, + }; + + let std = match col.std_as_series().get(0) { + AnyValue::Float64(v) => Some(v), + _ => None, + }; + + let min = col + .min_as_series() + .cast(&DataType::Float64) + .ok() + .and_then(|ca| match ca.get(0) { + AnyValue::Float64(v) => Some(v), + _ => None, + }); + + let q_25 = col + .quantile_as_series(0.25) + .ok() + .and_then(|ca| ca.cast(&DataType::Float64).ok()) + .and_then(|ca| match ca.get(0) { + AnyValue::Float64(v) => Some(v), + _ => None, + }); + + let q_50 = col + .quantile_as_series(0.50) + .ok() + .and_then(|ca| ca.cast(&DataType::Float64).ok()) + .and_then(|ca| match ca.get(0) { + AnyValue::Float64(v) => Some(v), + _ => None, + }); + + let q_75 = col + .quantile_as_series(0.75) + .ok() + .and_then(|ca| ca.cast(&DataType::Float64).ok()) + .and_then(|ca| match ca.get(0) { + AnyValue::Float64(v) => Some(v), + _ => None, + }); + + let max = col + .max_as_series() + .cast(&DataType::Float64) + .ok() + .and_then(|ca| match ca.get(0) { + AnyValue::Float64(v) => Some(v), + _ => None, + }); + + let name = format!("{} ({})", col.name(), col.dtype()); + ChunkedArray::::new_from_opt_slice( + &name, + &[ + Some(count), + sum, + mean, + median, + std, + min, + q_25, + q_50, + q_75, + max, + ], + ) + .into_series() + }); + + let res = head.chain(tail).collect::>(); + + DataFrame::new(res) + .map_err(|e| { + ShellError::SpannedLabeledError("Dataframe Error".into(), e.to_string(), call.head) + }) + .map(|df| PipelineData::Value(NuDataFrame::dataframe_into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(DescribeDF {})]) + } +} diff --git a/crates/nu-command/src/dataframe/eager/drop.rs b/crates/nu-command/src/dataframe/eager/drop.rs new file mode 100644 index 0000000000..df6946d86b --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/drop.rs @@ -0,0 +1,111 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +use super::super::values::utils::convert_columns; +use super::super::values::{Column, NuDataFrame}; + +#[derive(Clone)] +pub struct DropDF; + +impl Command for DropDF { + fn name(&self) -> &str { + "dfr drop" + } + + fn usage(&self) -> &str { + "Creates a new dataframe by dropping the selected columns" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .rest("rest", SyntaxShape::Any, "column names to be dropped") + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "drop column a", + example: "[[a b]; [1 2] [3 4]] | dfr to-df | dfr drop a", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "b".to_string(), + vec![Value::test_int(2), Value::test_int(4)], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let columns: Vec = call.rest(engine_state, stack, 0)?; + let (col_string, col_span) = convert_columns(columns, call.head)?; + + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + let new_df = col_string + .get(0) + .ok_or_else(|| { + ShellError::SpannedLabeledError( + "Empty names list".into(), + "No column names where found".into(), + col_span, + ) + }) + .and_then(|col| { + df.as_ref().drop(&col.item).map_err(|e| { + ShellError::SpannedLabeledError( + "Error dropping column".into(), + e.to_string(), + col.span, + ) + }) + })?; + + // If there are more columns in the drop selection list, these + // are added from the resulting dataframe + col_string + .iter() + .skip(1) + .try_fold(new_df, |new_df, col| { + new_df.drop(&col.item).map_err(|e| { + ShellError::SpannedLabeledError( + "Error dropping column".into(), + e.to_string(), + col.span, + ) + }) + }) + .map(|df| PipelineData::Value(NuDataFrame::dataframe_into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(DropDF {})]) + } +} diff --git a/crates/nu-command/src/dataframe/eager/drop_duplicates.rs b/crates/nu-command/src/dataframe/eager/drop_duplicates.rs new file mode 100644 index 0000000000..4d61a95e11 --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/drop_duplicates.rs @@ -0,0 +1,106 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +use super::super::values::utils::convert_columns_string; +use super::super::values::{Column, NuDataFrame}; + +#[derive(Clone)] +pub struct DropDuplicates; + +impl Command for DropDuplicates { + fn name(&self) -> &str { + "dfr drop-duplicates" + } + + fn usage(&self) -> &str { + "Drops duplicate values in dataframe" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .optional( + "subset", + SyntaxShape::Table, + "subset of columns to drop duplicates", + ) + .switch("maintain", "maintain order", Some('m')) + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "drop duplicates", + example: "[[a b]; [1 2] [3 4] [1 2]] | dfr to-df | dfr drop-duplicates", + result: Some( + NuDataFrame::try_from_columns(vec![ + Column::new( + "a".to_string(), + vec![Value::test_int(1), Value::test_int(3)], + ), + Column::new( + "b".to_string(), + vec![Value::test_int(2), Value::test_int(4)], + ), + ]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let columns: Option> = call.opt(engine_state, stack, 0)?; + let (subset, col_span) = match columns { + Some(cols) => { + let (agg_string, col_span) = convert_columns_string(cols, call.head)?; + (Some(agg_string), col_span) + } + None => (None, call.head), + }; + + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + let subset_slice = subset.as_ref().map(|cols| &cols[..]); + + df.as_ref() + .drop_duplicates(call.has_flag("maintain"), subset_slice) + .map_err(|e| { + ShellError::SpannedLabeledError( + "Error dropping duplicates".into(), + e.to_string(), + col_span, + ) + }) + .map(|df| PipelineData::Value(NuDataFrame::dataframe_into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(DropDuplicates {})]) + } +} diff --git a/crates/nu-command/src/dataframe/eager/drop_nulls.rs b/crates/nu-command/src/dataframe/eager/drop_nulls.rs new file mode 100644 index 0000000000..e91b57edfa --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/drop_nulls.rs @@ -0,0 +1,130 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +use super::super::values::utils::convert_columns_string; +use super::super::values::{Column, NuDataFrame}; + +#[derive(Clone)] +pub struct DropNulls; + +impl Command for DropNulls { + fn name(&self) -> &str { + "dfr drop-nulls" + } + + fn usage(&self) -> &str { + "Drops null values in dataframe" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .optional( + "subset", + SyntaxShape::Table, + "subset of columns to drop nulls", + ) + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "drop null values in dataframe", + example: r#"let df = ([[a b]; [1 2] [3 0] [1 2]] | dfr to-df); + let res = ($df.b / $df.b); + let a = ($df | dfr with-column $res --name res); + $a | dfr drop-nulls"#, + result: Some( + NuDataFrame::try_from_columns(vec![ + Column::new( + "a".to_string(), + vec![Value::test_int(1), Value::test_int(1)], + ), + Column::new( + "b".to_string(), + vec![Value::test_int(2), Value::test_int(2)], + ), + Column::new( + "res".to_string(), + vec![Value::test_int(1), Value::test_int(1)], + ), + ]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }, + Example { + description: "drop null values in dataframe", + example: r#"let s = ([1 2 0 0 3 4] | dfr to-df); + ($s / $s) | dfr drop-nulls"#, + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "div_0_0".to_string(), + vec![ + Value::test_int(1), + Value::test_int(1), + Value::test_int(1), + Value::test_int(1), + ], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + let columns: Option> = call.opt(engine_state, stack, 0)?; + + let (subset, col_span) = match columns { + Some(cols) => { + let (agg_string, col_span) = convert_columns_string(cols, call.head)?; + (Some(agg_string), col_span) + } + None => (None, call.head), + }; + + let subset_slice = subset.as_ref().map(|cols| &cols[..]); + + df.as_ref() + .drop_nulls(subset_slice) + .map_err(|e| { + ShellError::SpannedLabeledError("Error dropping nulls".into(), e.to_string(), col_span) + }) + .map(|df| PipelineData::Value(NuDataFrame::dataframe_into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::super::WithColumn; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(DropNulls {}), Box::new(WithColumn {})]) + } +} diff --git a/crates/nu-command/src/dataframe/eager/dtypes.rs b/crates/nu-command/src/dataframe/eager/dtypes.rs new file mode 100644 index 0000000000..923a032bc4 --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/dtypes.rs @@ -0,0 +1,106 @@ +use super::super::values::{Column, NuDataFrame}; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; + +#[derive(Clone)] +pub struct DataTypes; + +impl Command for DataTypes { + fn name(&self) -> &str { + "dfr dtypes" + } + + fn usage(&self) -> &str { + "Show dataframe data types" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Dataframe dtypes", + example: "[[a b]; [1 2] [3 4]] | dfr to-df | dfr dtypes", + result: Some( + NuDataFrame::try_from_columns(vec![ + Column::new( + "column".to_string(), + vec![Value::test_string("a"), Value::test_string("b")], + ), + Column::new( + "dtype".to_string(), + vec![Value::test_string("i64"), Value::test_string("i64")], + ), + ]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +#[allow(clippy::needless_collect)] +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + let mut dtypes: Vec = Vec::new(); + let names: Vec = df + .as_ref() + .get_column_names() + .iter() + .map(|v| { + let dtype = df + .as_ref() + .column(v) + .expect("using name from list of names from dataframe") + .dtype(); + + let dtype_str = dtype.to_string(); + dtypes.push(Value::String { + val: dtype_str, + span: call.head, + }); + + Value::String { + val: v.to_string(), + span: call.head, + } + }) + .collect(); + + let names_col = Column::new("column".to_string(), names); + let dtypes_col = Column::new("dtype".to_string(), dtypes); + + NuDataFrame::try_from_columns(vec![names_col, dtypes_col]) + .map(|df| PipelineData::Value(df.into_value(call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(DataTypes {})]) + } +} diff --git a/crates/nu-command/src/dataframe/eager/dummies.rs b/crates/nu-command/src/dataframe/eager/dummies.rs new file mode 100644 index 0000000000..ee1744e6be --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/dummies.rs @@ -0,0 +1,137 @@ +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; + +use super::super::values::{Column, NuDataFrame}; + +#[derive(Clone)] +pub struct Dummies; + +impl Command for Dummies { + fn name(&self) -> &str { + "dfr to-dummies" + } + + fn usage(&self) -> &str { + "Creates a new dataframe with dummy variables" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Create new dataframe with dummy variables from a dataframe", + example: "[[a b]; [1 2] [3 4]] | dfr to-df | dfr to-dummies", + result: Some( + NuDataFrame::try_from_columns(vec![ + Column::new( + "a_1".to_string(), + vec![Value::test_int(1), Value::test_int(0)], + ), + Column::new( + "a_3".to_string(), + vec![Value::test_int(0), Value::test_int(1)], + ), + Column::new( + "b_2".to_string(), + vec![Value::test_int(1), Value::test_int(0)], + ), + Column::new( + "b_4".to_string(), + vec![Value::test_int(0), Value::test_int(1)], + ), + ]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }, + Example { + description: "Create new dataframe with dummy variables from a series", + example: "[1 2 2 3 3] | dfr to-df | dfr to-dummies", + result: Some( + NuDataFrame::try_from_columns(vec![ + Column::new( + "0_1".to_string(), + vec![ + Value::test_int(1), + Value::test_int(0), + Value::test_int(0), + Value::test_int(0), + Value::test_int(0), + ], + ), + Column::new( + "0_2".to_string(), + vec![ + Value::test_int(0), + Value::test_int(1), + Value::test_int(1), + Value::test_int(0), + Value::test_int(0), + ], + ), + Column::new( + "0_3".to_string(), + vec![ + Value::test_int(0), + Value::test_int(0), + Value::test_int(0), + Value::test_int(1), + Value::test_int(1), + ], + ), + ]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + df.as_ref() + .to_dummies() + .map_err(|e| { + ShellError::SpannedLabeledErrorHelp( + "Error calculating dummies".into(), + e.to_string(), + call.head, + "The only allowed column types for dummies are String or Int".into(), + ) + }) + .map(|df| PipelineData::Value(NuDataFrame::dataframe_into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(Dummies {})]) + } +} diff --git a/crates/nu-command/src/dataframe/eager/filter_with.rs b/crates/nu-command/src/dataframe/eager/filter_with.rs new file mode 100644 index 0000000000..cdaf1cb456 --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/filter_with.rs @@ -0,0 +1,98 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +use super::super::values::{Column, NuDataFrame}; + +#[derive(Clone)] +pub struct FilterWith; + +impl Command for FilterWith { + fn name(&self) -> &str { + "dfr filter-with" + } + + fn usage(&self) -> &str { + "Filters dataframe using a mask as reference" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required("mask", SyntaxShape::Any, "boolean mask used to filter data") + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Filter dataframe using a bool mask", + example: r#"let mask = ([$true $false] | dfr to-df); + [[a b]; [1 2] [3 4]] | dfr to-df | dfr filter-with $mask"#, + result: Some( + NuDataFrame::try_from_columns(vec![ + Column::new("a".to_string(), vec![Value::test_int(1)]), + Column::new("b".to_string(), vec![Value::test_int(2)]), + ]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let mask_value: Value = call.req(engine_state, stack, 0)?; + + let mask_span = mask_value.span()?; + let mask = NuDataFrame::try_from_value(mask_value)?.as_series(mask_span)?; + let mask = mask.bool().map_err(|e| { + ShellError::SpannedLabeledErrorHelp( + "Error casting to bool".into(), + e.to_string(), + mask_span, + "Perhaps you want to use a series with booleans as mask".into(), + ) + })?; + + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + df.as_ref() + .filter(mask) + .map_err(|e| { + ShellError::SpannedLabeledErrorHelp( + "Error calculating dummies".into(), + e.to_string(), + call.head, + "The only allowed column types for dummies are String or Int".into(), + ) + }) + .map(|df| PipelineData::Value(NuDataFrame::dataframe_into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(FilterWith {})]) + } +} diff --git a/crates/nu-command/src/dataframe/eager/first.rs b/crates/nu-command/src/dataframe/eager/first.rs new file mode 100644 index 0000000000..1371f869cc --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/first.rs @@ -0,0 +1,80 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +use super::super::values::{utils::DEFAULT_ROWS, Column, NuDataFrame}; + +#[derive(Clone)] +pub struct FirstDF; + +impl Command for FirstDF { + fn name(&self) -> &str { + "dfr first" + } + + fn usage(&self) -> &str { + "Creates new dataframe with first rows" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .optional("rows", SyntaxShape::Int, "Number of rows for head") + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Create new dataframe with head rows", + example: "[[a b]; [1 2] [3 4]] | dfr to-df | dfr first 1", + result: Some( + NuDataFrame::try_from_columns(vec![ + Column::new("a".to_string(), vec![Value::test_int(1)]), + Column::new("b".to_string(), vec![Value::test_int(2)]), + ]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let rows: Option = call.opt(engine_state, stack, 0)?; + let rows = rows.unwrap_or(DEFAULT_ROWS); + + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let res = df.as_ref().head(Some(rows)); + Ok(PipelineData::Value( + NuDataFrame::dataframe_into_value(res, call.head), + None, + )) +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(FirstDF {})]) + } +} diff --git a/crates/nu-command/src/dataframe/eager/get.rs b/crates/nu-command/src/dataframe/eager/get.rs new file mode 100644 index 0000000000..8c1eab5897 --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/get.rs @@ -0,0 +1,88 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +use crate::dataframe::values::utils::convert_columns_string; + +use super::super::values::{Column, NuDataFrame}; + +#[derive(Clone)] +pub struct GetDF; + +impl Command for GetDF { + fn name(&self) -> &str { + "dfr get" + } + + fn usage(&self) -> &str { + "Creates dataframe with the selected columns" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .rest("rest", SyntaxShape::Any, "column names to sort dataframe") + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Creates dataframe with selected columns", + example: "[[a b]; [1 2] [3 4]] | dfr to-df | dfr get a", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "a".to_string(), + vec![Value::test_int(1), Value::test_int(3)], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let columns: Vec = call.rest(engine_state, stack, 0)?; + let (col_string, col_span) = convert_columns_string(columns, call.head)?; + + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + df.as_ref() + .select(&col_string) + .map_err(|e| { + ShellError::SpannedLabeledError( + "Error selecting columns".into(), + e.to_string(), + col_span, + ) + }) + .map(|df| PipelineData::Value(NuDataFrame::dataframe_into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(GetDF {})]) + } +} diff --git a/crates/nu-command/src/dataframe/eager/groupby.rs b/crates/nu-command/src/dataframe/eager/groupby.rs new file mode 100644 index 0000000000..e471b20f6b --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/groupby.rs @@ -0,0 +1,71 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Value, +}; + +use super::super::values::{utils::convert_columns_string, NuDataFrame, NuGroupBy}; + +#[derive(Clone)] +pub struct CreateGroupBy; + +impl Command for CreateGroupBy { + fn name(&self) -> &str { + "dfr group-by" + } + + fn usage(&self) -> &str { + "Creates a groupby object that can be used for other aggregations" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .rest("rest", SyntaxShape::Any, "groupby columns") + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Grouping by column a", + example: "[[a b]; [one 1] [one 2]] | dfr to-df | dfr group-by a", + result: None, + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + // Extracting the names of the columns to perform the groupby + let columns: Vec = call.rest(engine_state, stack, 0)?; + let (col_string, col_span) = convert_columns_string(columns, call.head)?; + + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + // This is the expensive part of the groupby; to create the + // groups that will be used for grouping the data in the + // dataframe. Once it has been done these values can be stored + // in a NuGroupBy + let groupby = df.as_ref().groupby(&col_string).map_err(|e| { + ShellError::SpannedLabeledError("Error creating groupby".into(), e.to_string(), col_span) + })?; + + let groups = groupby.get_groups().to_vec(); + let groupby = NuGroupBy::new(df.as_ref().clone(), col_string, groups); + + Ok(PipelineData::Value(groupby.into_value(call.head), None)) +} diff --git a/crates/nu-command/src/dataframe/eager/join.rs b/crates/nu-command/src/dataframe/eager/join.rs new file mode 100644 index 0000000000..732b9c93ca --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/join.rs @@ -0,0 +1,226 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Value, +}; +use polars::prelude::JoinType; + +use crate::dataframe::values::utils::convert_columns_string; + +use super::super::values::{Column, NuDataFrame}; + +#[derive(Clone)] +pub struct JoinDF; + +impl Command for JoinDF { + fn name(&self) -> &str { + "dfr join" + } + + fn usage(&self) -> &str { + "Joins a dataframe using columns as reference" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required("dataframe", SyntaxShape::Any, "right dataframe to join") + .required_named( + "left", + SyntaxShape::Table, + "left column names to perform join", + Some('l'), + ) + .required_named( + "right", + SyntaxShape::Table, + "right column names to perform join", + Some('r'), + ) + .named( + "type", + SyntaxShape::String, + "type of join. Inner by default", + Some('t'), + ) + .named( + "suffix", + SyntaxShape::String, + "suffix for the columns of the right dataframe", + Some('s'), + ) + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "inner join dataframe", + example: r#"let right = ([[a b c]; [1 2 5] [3 4 5] [5 6 6]] | dfr to-df); + $right | dfr join $right -l [a b] -r [a b]"#, + result: Some( + NuDataFrame::try_from_columns(vec![ + Column::new( + "a".to_string(), + vec![Value::test_int(1), Value::test_int(3), Value::test_int(5)], + ), + Column::new( + "b".to_string(), + vec![Value::test_int(2), Value::test_int(4), Value::test_int(6)], + ), + Column::new( + "c".to_string(), + vec![Value::test_int(5), Value::test_int(5), Value::test_int(6)], + ), + Column::new( + "c_right".to_string(), + vec![Value::test_int(5), Value::test_int(5), Value::test_int(6)], + ), + ]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let r_df: Value = call.req(engine_state, stack, 0)?; + let l_col: Vec = call + .get_flag(engine_state, stack, "left")? + .expect("required value in syntax"); + let r_col: Vec = call + .get_flag(engine_state, stack, "right")? + .expect("required value in syntax"); + let suffix: Option = call.get_flag(engine_state, stack, "suffix")?; + let join_type_op: Option> = call.get_flag(engine_state, stack, "type")?; + + let join_type = match join_type_op { + None => JoinType::Inner, + Some(val) => match val.item.as_ref() { + "inner" => JoinType::Inner, + "outer" => JoinType::Outer, + "left" => JoinType::Left, + _ => { + return Err(ShellError::SpannedLabeledErrorHelp( + "Incorrect join type".into(), + "Invalid join type".into(), + val.span, + "Options: inner, outer or left".into(), + )) + } + }, + }; + + let (l_col_string, l_col_span) = convert_columns_string(l_col, call.head)?; + let (r_col_string, r_col_span) = convert_columns_string(r_col, call.head)?; + + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let r_df = NuDataFrame::try_from_value(r_df)?; + + check_column_datatypes( + df.as_ref(), + r_df.as_ref(), + &l_col_string, + l_col_span, + &r_col_string, + r_col_span, + )?; + + df.as_ref() + .join( + r_df.as_ref(), + &l_col_string, + &r_col_string, + join_type, + suffix, + ) + .map_err(|e| { + ShellError::SpannedLabeledError( + "Error joining dataframes".into(), + e.to_string(), + l_col_span, + ) + }) + .map(|df| PipelineData::Value(NuDataFrame::dataframe_into_value(df, call.head), None)) +} + +fn check_column_datatypes>( + df_l: &polars::prelude::DataFrame, + df_r: &polars::prelude::DataFrame, + l_cols: &[T], + l_col_span: Span, + r_cols: &[T], + r_col_span: Span, +) -> Result<(), ShellError> { + if l_cols.len() != r_cols.len() { + return Err(ShellError::SpannedLabeledErrorHelp( + "Mismatched number of column names".into(), + format!( + "found {} left names vs {} right names", + l_cols.len(), + r_cols.len() + ), + l_col_span, + "perhaps you need to change the number of columns to join".into(), + )); + } + + for (l, r) in l_cols.iter().zip(r_cols) { + let l_series = df_l.column(l.as_ref()).map_err(|e| { + ShellError::SpannedLabeledError( + "Error selecting the columns".into(), + e.to_string(), + l_col_span, + ) + })?; + + let r_series = df_r.column(r.as_ref()).map_err(|e| { + ShellError::SpannedLabeledError( + "Error selecting the columns".into(), + e.to_string(), + r_col_span, + ) + })?; + + if l_series.dtype() != r_series.dtype() { + return Err(ShellError::SpannedLabeledErrorHelp( + "Mismatched datatypes".into(), + format!( + "left column type '{}' doesn't match '{}' right column match", + l_series.dtype(), + r_series.dtype() + ), + l_col_span, + "perhaps you need to select other column to match".into(), + )); + } + } + + Ok(()) +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(JoinDF {})]) + } +} diff --git a/crates/nu-command/src/dataframe/eager/last.rs b/crates/nu-command/src/dataframe/eager/last.rs new file mode 100644 index 0000000000..39294fae3c --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/last.rs @@ -0,0 +1,80 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +use super::super::values::{utils::DEFAULT_ROWS, Column, NuDataFrame}; + +#[derive(Clone)] +pub struct LastDF; + +impl Command for LastDF { + fn name(&self) -> &str { + "dfr last" + } + + fn usage(&self) -> &str { + "Creates new dataframe with tail rows" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .optional("rows", SyntaxShape::Int, "Number of rows for tail") + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Create new dataframe with last rows", + example: "[[a b]; [1 2] [3 4]] | dfr to-df | dfr last 1", + result: Some( + NuDataFrame::try_from_columns(vec![ + Column::new("a".to_string(), vec![Value::test_int(3)]), + Column::new("b".to_string(), vec![Value::test_int(4)]), + ]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let rows: Option = call.opt(engine_state, stack, 0)?; + let rows = rows.unwrap_or(DEFAULT_ROWS); + + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let res = df.as_ref().tail(Some(rows)); + Ok(PipelineData::Value( + NuDataFrame::dataframe_into_value(res, call.head), + None, + )) +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(LastDF {})]) + } +} diff --git a/crates/nu-command/src/dataframe/eager/melt.rs b/crates/nu-command/src/dataframe/eager/melt.rs new file mode 100644 index 0000000000..5956fbb416 --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/melt.rs @@ -0,0 +1,243 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Value, +}; + +use crate::dataframe::values::utils::convert_columns_string; + +use super::super::values::{Column, NuDataFrame}; + +#[derive(Clone)] +pub struct MeltDF; + +impl Command for MeltDF { + fn name(&self) -> &str { + "dfr melt" + } + + fn usage(&self) -> &str { + "Unpivot a DataFrame from wide to long format" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required_named( + "columns", + SyntaxShape::Table, + "column names for melting", + Some('c'), + ) + .required_named( + "values", + SyntaxShape::Table, + "column names used as value columns", + Some('v'), + ) + .named( + "variable-name", + SyntaxShape::String, + "optional name for variable column", + Some('r'), + ) + .named( + "value-name", + SyntaxShape::String, + "optional name for value column", + Some('l'), + ) + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "melt dataframe", + example: + "[[a b c d]; [x 1 4 a] [y 2 5 b] [z 3 6 c]] | dfr to-df | dfr melt -c [b c] -v [a d]", + result: Some( + NuDataFrame::try_from_columns(vec![ + Column::new( + "b".to_string(), + vec![ + Value::test_int(1), + Value::test_int(2), + Value::test_int(3), + Value::test_int(1), + Value::test_int(2), + Value::test_int(3), + ], + ), + Column::new( + "c".to_string(), + vec![ + Value::test_int(4), + Value::test_int(5), + Value::test_int(6), + Value::test_int(4), + Value::test_int(5), + Value::test_int(6), + ], + ), + Column::new( + "variable".to_string(), + vec![ + Value::test_string("a"), + Value::test_string("a"), + Value::test_string("a"), + Value::test_string("d"), + Value::test_string("d"), + Value::test_string("d"), + ], + ), + Column::new( + "value".to_string(), + vec![ + Value::test_string("x"), + Value::test_string("y"), + Value::test_string("z"), + Value::test_string("a"), + Value::test_string("b"), + Value::test_string("c"), + ], + ), + ]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let id_col: Vec = call + .get_flag(engine_state, stack, "columns")? + .expect("required value"); + let val_col: Vec = call + .get_flag(engine_state, stack, "values")? + .expect("required value"); + + let value_name: Option> = call.get_flag(engine_state, stack, "value-name")?; + let variable_name: Option> = + call.get_flag(engine_state, stack, "variable-name")?; + + let (id_col_string, id_col_span) = convert_columns_string(id_col, call.head)?; + let (val_col_string, val_col_span) = convert_columns_string(val_col, call.head)?; + + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + check_column_datatypes(df.as_ref(), &id_col_string, id_col_span)?; + check_column_datatypes(df.as_ref(), &val_col_string, val_col_span)?; + + let mut res = df + .as_ref() + .melt(&id_col_string, &val_col_string) + .map_err(|e| { + ShellError::SpannedLabeledError( + "Error calculating melt".into(), + e.to_string(), + call.head, + ) + })?; + + if let Some(name) = &variable_name { + res.rename("variable", &name.item).map_err(|e| { + ShellError::SpannedLabeledError( + "Error renaming column".into(), + e.to_string(), + name.span, + ) + })?; + } + + if let Some(name) = &value_name { + res.rename("value", &name.item).map_err(|e| { + ShellError::SpannedLabeledError( + "Error renaming column".into(), + e.to_string(), + name.span, + ) + })?; + } + + Ok(PipelineData::Value( + NuDataFrame::dataframe_into_value(res, call.head), + None, + )) +} + +fn check_column_datatypes>( + df: &polars::prelude::DataFrame, + cols: &[T], + col_span: Span, +) -> Result<(), ShellError> { + if cols.is_empty() { + return Err(ShellError::SpannedLabeledError( + "Merge error".into(), + "empty column list".into(), + col_span, + )); + } + + // Checking if they are same type + if cols.len() > 1 { + for w in cols.windows(2) { + let l_series = df.column(w[0].as_ref()).map_err(|e| { + ShellError::SpannedLabeledError( + "Error selecting columns".into(), + e.to_string(), + col_span, + ) + })?; + + let r_series = df.column(w[1].as_ref()).map_err(|e| { + ShellError::SpannedLabeledError( + "Error selecting columns".into(), + e.to_string(), + col_span, + ) + })?; + + if l_series.dtype() != r_series.dtype() { + return Err(ShellError::SpannedLabeledErrorHelp( + "Merge error".into(), + "found different column types in list".into(), + col_span, + format!( + "datatypes {} and {} are incompatible", + l_series.dtype(), + r_series.dtype() + ), + )); + } + } + } + + Ok(()) +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(MeltDF {})]) + } +} diff --git a/crates/nu-command/src/dataframe/eager/mod.rs b/crates/nu-command/src/dataframe/eager/mod.rs new file mode 100644 index 0000000000..1177bd2059 --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/mod.rs @@ -0,0 +1,107 @@ +mod aggregate; +mod append; +mod column; +mod command; +mod describe; +mod drop; +mod drop_duplicates; +mod drop_nulls; +mod dtypes; +mod dummies; +mod filter_with; +mod first; +mod get; +mod groupby; +mod join; +mod last; +mod melt; +mod open; +mod pivot; +mod rename; +mod sample; +mod shape; +mod slice; +mod sort; +mod take; +mod to_csv; +mod to_df; +mod to_nu; +mod to_parquet; +mod with_column; + +use nu_protocol::engine::StateWorkingSet; + +pub use aggregate::Aggregate; +pub use append::AppendDF; +pub use column::ColumnDF; +pub use command::Dataframe; +pub use describe::DescribeDF; +pub use drop::DropDF; +pub use drop_duplicates::DropDuplicates; +pub use drop_nulls::DropNulls; +pub use dtypes::DataTypes; +pub use dummies::Dummies; +pub use filter_with::FilterWith; +pub use first::FirstDF; +pub use get::GetDF; +pub use groupby::CreateGroupBy; +pub use join::JoinDF; +pub use last::LastDF; +pub use melt::MeltDF; +pub use open::OpenDataFrame; +pub use pivot::PivotDF; +pub use rename::RenameDF; +pub use sample::SampleDF; +pub use shape::ShapeDF; +pub use slice::SliceDF; +pub use sort::SortDF; +pub use take::TakeDF; +pub use to_csv::ToCSV; +pub use to_df::ToDataFrame; +pub use to_nu::ToNu; +pub use to_parquet::ToParquet; +pub use with_column::WithColumn; + +pub fn add_eager_decls(working_set: &mut StateWorkingSet) { + macro_rules! bind_command { + ( $command:expr ) => { + working_set.add_decl(Box::new($command)); + }; + ( $( $command:expr ),* ) => { + $( working_set.add_decl(Box::new($command)); )* + }; + } + + // Dataframe commands + bind_command!( + Aggregate, + AppendDF, + ColumnDF, + CreateGroupBy, + Dataframe, + DataTypes, + DescribeDF, + DropDF, + DropNulls, + Dummies, + FilterWith, + FirstDF, + GetDF, + JoinDF, + LastDF, + MeltDF, + OpenDataFrame, + PivotDF, + RenameDF, + SampleDF, + ShapeDF, + SliceDF, + SortDF, + TakeDF, + ToCSV, + ToDataFrame, + ToNu, + ToParquet, + WithColumn + ); +} diff --git a/crates/nu-command/src/dataframe/eager/open.rs b/crates/nu-command/src/dataframe/eager/open.rs new file mode 100644 index 0000000000..0bc069a7fd --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/open.rs @@ -0,0 +1,218 @@ +use super::super::values::NuDataFrame; +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape, +}; +use std::{fs::File, path::PathBuf}; + +use polars::prelude::{CsvEncoding, CsvReader, JsonReader, ParquetReader, SerReader}; + +#[derive(Clone)] +pub struct OpenDataFrame; + +impl Command for OpenDataFrame { + fn name(&self) -> &str { + "dfr open" + } + + fn usage(&self) -> &str { + "Opens csv, json or parquet file to create dataframe" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required( + "file", + SyntaxShape::Filepath, + "file path to load values from", + ) + .named( + "delimiter", + SyntaxShape::String, + "file delimiter character. CSV file", + Some('d'), + ) + .switch( + "no-header", + "Indicates if file doesn't have header. CSV file", + None, + ) + .named( + "infer-schema", + SyntaxShape::Number, + "Number of rows to infer the schema of the file. CSV file", + None, + ) + .named( + "skip-rows", + SyntaxShape::Number, + "Number of rows to skip from file. CSV file", + None, + ) + .named( + "columns", + SyntaxShape::List(Box::new(SyntaxShape::String)), + "Columns to be selected from csv file. CSV and Parquet file", + None, + ) + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Takes a file name and creates a dataframe", + example: "dfr open test.csv", + result: None, + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + command(engine_state, stack, call) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, +) -> Result { + let span = call.head; + let file: Spanned = call.req(engine_state, stack, 0)?; + + match file.item.extension() { + Some(e) => match e.to_str() { + Some("csv") => from_csv(engine_state, stack, call), + Some("parquet") => from_parquet(engine_state, stack, call), + Some("json") => from_json(engine_state, stack, call), + _ => Err(ShellError::FileNotFoundCustom( + "Not a csv, parquet or json file".into(), + file.span, + )), + }, + None => Err(ShellError::FileNotFoundCustom( + "File without extension".into(), + file.span, + )), + } + .map(|df| PipelineData::Value(NuDataFrame::dataframe_into_value(df, span), None)) +} + +fn from_parquet( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, +) -> Result { + let file: Spanned = call.req(engine_state, stack, 0)?; + let columns: Option> = call.get_flag(engine_state, stack, "columns")?; + + let r = File::open(&file.item).map_err(|e| { + ShellError::SpannedLabeledError("Error opening file".into(), e.to_string(), file.span) + })?; + let reader = ParquetReader::new(r); + + let reader = match columns { + None => reader, + Some(columns) => reader.with_columns(Some(columns)), + }; + + reader.finish().map_err(|e| { + ShellError::SpannedLabeledError( + "Parquet reader error".into(), + format!("{:?}", e), + call.head, + ) + }) +} + +fn from_json( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, +) -> Result { + let file: Spanned = call.req(engine_state, stack, 0)?; + + let r = File::open(&file.item).map_err(|e| { + ShellError::SpannedLabeledError("Error opening file".into(), e.to_string(), file.span) + })?; + + let reader = JsonReader::new(r); + + reader.finish().map_err(|e| { + ShellError::SpannedLabeledError("Json reader error".into(), format!("{:?}", e), call.head) + }) +} + +fn from_csv( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, +) -> Result { + let file: Spanned = call.req(engine_state, stack, 0)?; + let delimiter: Option> = call.get_flag(engine_state, stack, "delimiter")?; + let no_header: bool = call.has_flag("no_header"); + let infer_schema: Option = call.get_flag(engine_state, stack, "infer_schema")?; + let skip_rows: Option = call.get_flag(engine_state, stack, "skip_rows")?; + let columns: Option> = call.get_flag(engine_state, stack, "columns")?; + + let csv_reader = CsvReader::from_path(&file.item) + .map_err(|e| { + ShellError::SpannedLabeledError( + "Error creating CSV reader".into(), + e.to_string(), + file.span, + ) + })? + .with_encoding(CsvEncoding::LossyUtf8); + + let csv_reader = match delimiter { + None => csv_reader, + Some(d) => { + if d.item.len() != 1 { + return Err(ShellError::SpannedLabeledError( + "Incorrect delimiter".into(), + "Delimiter has to be one character".into(), + d.span, + )); + } else { + let delimiter = match d.item.chars().next() { + Some(d) => d as u8, + None => unreachable!(), + }; + csv_reader.with_delimiter(delimiter) + } + } + }; + + let csv_reader = csv_reader.has_header(!no_header); + + let csv_reader = match infer_schema { + None => csv_reader, + Some(r) => csv_reader.infer_schema(Some(r)), + }; + + let csv_reader = match skip_rows { + None => csv_reader, + Some(r) => csv_reader.with_skip_rows(r), + }; + + let csv_reader = match columns { + None => csv_reader, + Some(columns) => csv_reader.with_columns(Some(columns)), + }; + + csv_reader.finish().map_err(|e| { + ShellError::SpannedLabeledError( + "Parquet reader error".into(), + format!("{:?}", e), + call.head, + ) + }) +} diff --git a/crates/nu-command/src/dataframe/eager/pivot.rs b/crates/nu-command/src/dataframe/eager/pivot.rs new file mode 100644 index 0000000000..d2feb84618 --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/pivot.rs @@ -0,0 +1,175 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape, +}; +use polars::prelude::DataType; + +use crate::dataframe::values::NuGroupBy; + +use super::super::values::NuDataFrame; + +enum Operation { + First, + Sum, + Min, + Max, + Mean, + Median, +} + +impl Operation { + fn from_tagged(name: Spanned) -> Result { + match name.item.as_ref() { + "first" => Ok(Operation::First), + "sum" => Ok(Operation::Sum), + "min" => Ok(Operation::Min), + "max" => Ok(Operation::Max), + "mean" => Ok(Operation::Mean), + "median" => Ok(Operation::Median), + _ => Err(ShellError::SpannedLabeledErrorHelp( + "Operation not fount".into(), + "Operation does not exist for pivot".into(), + name.span, + "Options: first, sum, min, max, mean, median".into(), + )), + } + } +} + +#[derive(Clone)] +pub struct PivotDF; + +impl Command for PivotDF { + fn name(&self) -> &str { + "dfr pivot" + } + + fn usage(&self) -> &str { + "Performs a pivot operation on a groupby object" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required( + "pivot-column", + SyntaxShape::String, + "pivot column to perform pivot", + ) + .required( + "value-column", + SyntaxShape::String, + "value column to perform pivot", + ) + .required("operation", SyntaxShape::String, "aggregate operation") + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Pivot a dataframe on b and aggregation on col c", + example: + "[[a b c]; [one x 1] [two y 2]] | dfr to-df | dfr group-by a | dfr pivot b c sum", + result: None, + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let pivot_col: Spanned = call.req(engine_state, stack, 0)?; + let value_col: Spanned = call.req(engine_state, stack, 1)?; + let operation: Spanned = call.req(engine_state, stack, 2)?; + let op = Operation::from_tagged(operation)?; + + let nu_groupby = NuGroupBy::try_from_pipeline(input, call.head)?; + let df_ref = nu_groupby.as_ref(); + + check_pivot_column(df_ref, &pivot_col)?; + check_value_column(df_ref, &value_col)?; + + let mut groupby = nu_groupby.to_groupby()?; + + let pivot = groupby.pivot(&pivot_col.item, &value_col.item); + + match op { + Operation::Mean => pivot.mean(), + Operation::Sum => pivot.sum(), + Operation::Min => pivot.min(), + Operation::Max => pivot.max(), + Operation::First => pivot.first(), + Operation::Median => pivot.median(), + } + .map_err(|e| { + ShellError::SpannedLabeledError("Error creating pivot".into(), e.to_string(), call.head) + }) + .map(|df| PipelineData::Value(NuDataFrame::dataframe_into_value(df, call.head), None)) +} + +fn check_pivot_column( + df: &polars::prelude::DataFrame, + col: &Spanned, +) -> Result<(), ShellError> { + let series = df.column(&col.item).map_err(|e| { + ShellError::SpannedLabeledError("Column not found".into(), e.to_string(), col.span) + })?; + + match series.dtype() { + DataType::UInt8 + | DataType::UInt16 + | DataType::UInt32 + | DataType::UInt64 + | DataType::Int8 + | DataType::Int16 + | DataType::Int32 + | DataType::Int64 + | DataType::Utf8 => Ok(()), + _ => Err(ShellError::SpannedLabeledError( + "Pivot error".into(), + format!("Unsupported datatype {}", series.dtype()), + col.span, + )), + } +} + +fn check_value_column( + df: &polars::prelude::DataFrame, + col: &Spanned, +) -> Result<(), ShellError> { + let series = df.column(&col.item).map_err(|e| { + ShellError::SpannedLabeledError("Column not found".into(), e.to_string(), col.span) + })?; + + match series.dtype() { + DataType::UInt8 + | DataType::UInt16 + | DataType::UInt32 + | DataType::UInt64 + | DataType::Int8 + | DataType::Int16 + | DataType::Int32 + | DataType::Int64 + | DataType::Float32 + | DataType::Float64 => Ok(()), + _ => Err(ShellError::SpannedLabeledError( + "Pivot error".into(), + format!("Unsupported datatype {}", series.dtype()), + col.span, + )), + } +} diff --git a/crates/nu-command/src/dataframe/eager/rename.rs b/crates/nu-command/src/dataframe/eager/rename.rs new file mode 100644 index 0000000000..91815d2998 --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/rename.rs @@ -0,0 +1,94 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +use super::super::values::{Column, NuDataFrame}; + +#[derive(Clone)] +pub struct RenameDF; + +impl Command for RenameDF { + fn name(&self) -> &str { + "dfr rename-col" + } + + fn usage(&self) -> &str { + "rename a dataframe column" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required("from", SyntaxShape::String, "column name to be renamed") + .required("to", SyntaxShape::String, "new column name") + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Renames a dataframe column", + example: "[[a b]; [1 2] [3 4]] | dfr to-df | dfr rename-col a a_new", + result: Some( + NuDataFrame::try_from_columns(vec![ + Column::new( + "a_new".to_string(), + vec![Value::test_int(1), Value::test_int(3)], + ), + Column::new( + "b".to_string(), + vec![Value::test_int(2), Value::test_int(4)], + ), + ]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let from: String = call.req(engine_state, stack, 0)?; + let to: String = call.req(engine_state, stack, 1)?; + + let mut df = NuDataFrame::try_from_pipeline(input, call.head)?; + + df.as_mut() + .rename(&from, &to) + .map_err(|e| { + ShellError::SpannedLabeledError("Error renaming".into(), e.to_string(), call.head) + }) + .map(|df| { + PipelineData::Value( + NuDataFrame::dataframe_into_value(df.clone(), call.head), + None, + ) + }) +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(RenameDF {})]) + } +} diff --git a/crates/nu-command/src/dataframe/eager/sample.rs b/crates/nu-command/src/dataframe/eager/sample.rs new file mode 100644 index 0000000000..b0fcc54192 --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/sample.rs @@ -0,0 +1,106 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape, +}; + +use super::super::values::NuDataFrame; + +#[derive(Clone)] +pub struct SampleDF; + +impl Command for SampleDF { + fn name(&self) -> &str { + "dfr sample" + } + + fn usage(&self) -> &str { + "Create sample dataframe" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .named( + "n-rows", + SyntaxShape::Int, + "number of rows to be taken from dataframe", + Some('n'), + ) + .named( + "fraction", + SyntaxShape::Number, + "fraction of dataframe to be taken", + Some('f'), + ) + .switch("replace", "sample with replace", Some('e')) + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Sample rows from dataframe", + example: "[[a b]; [1 2] [3 4]] | dfr to-df | dfr sample -n 1", + result: None, // No expected value because sampling is random + }, + Example { + description: "Shows sample row using fraction and replace", + example: "[[a b]; [1 2] [3 4] [5 6]] | dfr to-df | dfr sample -f 0.5 -e", + result: None, // No expected value because sampling is random + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let rows: Option> = call.get_flag(engine_state, stack, "n-rows")?; + let fraction: Option> = call.get_flag(engine_state, stack, "fraction")?; + let replace: bool = call.has_flag("replace"); + + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + match (rows, fraction) { + (Some(rows), None) => df.as_ref().sample_n(rows.item, replace).map_err(|e| { + ShellError::SpannedLabeledError( + "Error creating sample".into(), + e.to_string(), + rows.span, + ) + }), + (None, Some(frac)) => df.as_ref().sample_frac(frac.item, replace).map_err(|e| { + ShellError::SpannedLabeledError( + "Error creating sample".into(), + e.to_string(), + frac.span, + ) + }), + (Some(_), Some(_)) => Err(ShellError::SpannedLabeledError( + "Incompatible flags".into(), + "Only one selection criterion allowed".into(), + call.head, + )), + (None, None) => Err(ShellError::SpannedLabeledErrorHelp( + "No selection".into(), + "No selection criterion was found".into(), + call.head, + "Perhaps you want to use the flag -n or -f".into(), + )), + } + .map(|df| PipelineData::Value(NuDataFrame::dataframe_into_value(df, call.head), None)) +} diff --git a/crates/nu-command/src/dataframe/eager/shape.rs b/crates/nu-command/src/dataframe/eager/shape.rs new file mode 100644 index 0000000000..32cda93a38 --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/shape.rs @@ -0,0 +1,87 @@ +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; + +use crate::dataframe::values::Column; + +use super::super::values::NuDataFrame; + +#[derive(Clone)] +pub struct ShapeDF; + +impl Command for ShapeDF { + fn name(&self) -> &str { + "dfr shape" + } + + fn usage(&self) -> &str { + "Shows column and row size for a dataframe" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Shows row and column shape", + example: "[[a b]; [1 2] [3 4]] | dfr to-df | dfr shape", + result: Some( + NuDataFrame::try_from_columns(vec![ + Column::new("rows".to_string(), vec![Value::test_int(2)]), + Column::new("columns".to_string(), vec![Value::test_int(2)]), + ]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + let rows = Value::Int { + val: df.as_ref().height() as i64, + span: call.head, + }; + + let cols = Value::Int { + val: df.as_ref().width() as i64, + span: call.head, + }; + + let rows_col = Column::new("rows".to_string(), vec![rows]); + let cols_col = Column::new("columns".to_string(), vec![cols]); + + NuDataFrame::try_from_columns(vec![rows_col, cols_col]) + .map(|df| PipelineData::Value(df.into_value(call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(ShapeDF {})]) + } +} diff --git a/crates/nu-command/src/dataframe/eager/slice.rs b/crates/nu-command/src/dataframe/eager/slice.rs new file mode 100644 index 0000000000..087fe23aac --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/slice.rs @@ -0,0 +1,85 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +use crate::dataframe::values::Column; + +use super::super::values::NuDataFrame; + +#[derive(Clone)] +pub struct SliceDF; + +impl Command for SliceDF { + fn name(&self) -> &str { + "dfr slice" + } + + fn usage(&self) -> &str { + "Creates new dataframe from a slice of rows" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required("offset", SyntaxShape::Int, "start of slice") + .required("size", SyntaxShape::Int, "size of slice") + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Create new dataframe from a slice of the rows", + example: "[[a b]; [1 2] [3 4]] | dfr to-df | dfr slice 0 1", + result: Some( + NuDataFrame::try_from_columns(vec![ + Column::new("a".to_string(), vec![Value::test_int(1)]), + Column::new("b".to_string(), vec![Value::test_int(2)]), + ]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let offset: i64 = call.req(engine_state, stack, 0)?; + let size: usize = call.req(engine_state, stack, 1)?; + + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + let res = df.as_ref().slice(offset, size); + + Ok(PipelineData::Value( + NuDataFrame::dataframe_into_value(res, call.head), + None, + )) +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(SliceDF {})]) + } +} diff --git a/crates/nu-command/src/dataframe/eager/sort.rs b/crates/nu-command/src/dataframe/eager/sort.rs new file mode 100644 index 0000000000..d5bded8904 --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/sort.rs @@ -0,0 +1,142 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +use crate::dataframe::values::{utils::convert_columns_string, Column}; + +use super::super::values::NuDataFrame; + +#[derive(Clone)] +pub struct SortDF; + +impl Command for SortDF { + fn name(&self) -> &str { + "dfr sort" + } + + fn usage(&self) -> &str { + "Creates new sorted dataframe or series" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .switch("reverse", "invert sort", Some('r')) + .rest("rest", SyntaxShape::Any, "column names to sort dataframe") + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Create new sorted dataframe", + example: "[[a b]; [3 4] [1 2]] | dfr to-df | dfr sort a", + result: Some( + NuDataFrame::try_from_columns(vec![ + Column::new( + "a".to_string(), + vec![Value::test_int(1), Value::test_int(3)], + ), + Column::new( + "b".to_string(), + vec![Value::test_int(2), Value::test_int(4)], + ), + ]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }, + Example { + description: "Create new sorted series", + example: "[3 4 1 2] | dfr to-df | dfr sort", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0".to_string(), + vec![ + Value::test_int(1), + Value::test_int(2), + Value::test_int(3), + Value::test_int(4), + ], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let reverse = call.has_flag("reverse"); + + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + if df.is_series() { + let columns = df.as_ref().get_column_names(); + + df.as_ref() + .sort(columns, reverse) + .map_err(|e| { + ShellError::SpannedLabeledError( + "Error sorting dataframe".into(), + e.to_string(), + call.head, + ) + }) + .map(|df| PipelineData::Value(NuDataFrame::dataframe_into_value(df, call.head), None)) + } else { + let columns: Vec = call.rest(engine_state, stack, 0)?; + + if !columns.is_empty() { + let (col_string, col_span) = convert_columns_string(columns, call.head)?; + + df.as_ref() + .sort(&col_string, reverse) + .map_err(|e| { + ShellError::SpannedLabeledError( + "Error sorting dataframe".into(), + e.to_string(), + col_span, + ) + }) + .map(|df| { + PipelineData::Value(NuDataFrame::dataframe_into_value(df, call.head), None) + }) + } else { + Err(ShellError::SpannedLabeledError( + "Missing columns".into(), + "missing column name to perform sort".into(), + call.head, + )) + } + } +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(SortDF {})]) + } +} diff --git a/crates/nu-command/src/dataframe/eager/take.rs b/crates/nu-command/src/dataframe/eager/take.rs new file mode 100644 index 0000000000..0b3a68cce2 --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/take.rs @@ -0,0 +1,144 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; +use polars::prelude::DataType; + +use crate::dataframe::values::Column; + +use super::super::values::NuDataFrame; + +#[derive(Clone)] +pub struct TakeDF; + +impl Command for TakeDF { + fn name(&self) -> &str { + "dfr take" + } + + fn usage(&self) -> &str { + "Creates new dataframe using the given indices" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required( + "indices", + SyntaxShape::Any, + "list of indices used to take data", + ) + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Takes selected rows from dataframe", + example: r#"let df = ([[a b]; [4 1] [5 2] [4 3]] | dfr to-df); + let indices = ([0 2] | dfr to-df); + $df | dfr take $indices"#, + result: Some( + NuDataFrame::try_from_columns(vec![ + Column::new( + "a".to_string(), + vec![Value::test_int(4), Value::test_int(4)], + ), + Column::new( + "b".to_string(), + vec![Value::test_int(1), Value::test_int(3)], + ), + ]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }, + Example { + description: "Takes selected rows from series", + example: r#"let series = ([4 1 5 2 4 3] | dfr to-df); + let indices = ([0 2] | dfr to-df); + $series | dfr take $indices"#, + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0".to_string(), + vec![Value::test_int(4), Value::test_int(5)], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let index_value: Value = call.req(engine_state, stack, 0)?; + let index_span = index_value.span()?; + let index = NuDataFrame::try_from_value(index_value)?.as_series(index_span)?; + + let casted = match index.dtype() { + DataType::UInt32 | DataType::UInt64 | DataType::Int32 | DataType::Int64 => { + index.cast(&DataType::UInt32).map_err(|e| { + ShellError::SpannedLabeledError( + "Error casting index list".into(), + e.to_string(), + index_span, + ) + }) + } + _ => Err(ShellError::SpannedLabeledErrorHelp( + "Incorrect type".into(), + "Series with incorrect type".into(), + call.head, + "Consider using a Series with type int type".into(), + )), + }?; + + let indices = casted.u32().map_err(|e| { + ShellError::SpannedLabeledError( + "Error casting index list".into(), + e.to_string(), + index_span, + ) + })?; + + NuDataFrame::try_from_pipeline(input, call.head).and_then(|df| { + df.as_ref() + .take(indices) + .map_err(|e| { + ShellError::SpannedLabeledError( + "Error taking values".into(), + e.to_string(), + call.head, + ) + }) + .map(|df| PipelineData::Value(NuDataFrame::dataframe_into_value(df, call.head), None)) + }) +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(TakeDF {})]) + } +} diff --git a/crates/nu-command/src/dataframe/eager/to_csv.rs b/crates/nu-command/src/dataframe/eager/to_csv.rs new file mode 100644 index 0000000000..5db04a11f9 --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/to_csv.rs @@ -0,0 +1,132 @@ +use std::{fs::File, path::PathBuf}; + +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Value, +}; +use polars::prelude::{CsvWriter, SerWriter}; + +use super::super::values::NuDataFrame; + +#[derive(Clone)] +pub struct ToCSV; + +impl Command for ToCSV { + fn name(&self) -> &str { + "dfr to-csv" + } + + fn usage(&self) -> &str { + "Saves dataframe to csv file" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required("file", SyntaxShape::Filepath, "file path to save dataframe") + .named( + "delimiter", + SyntaxShape::String, + "file delimiter character", + Some('d'), + ) + .switch("no-header", "Indicates if file doesn't have header", None) + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Saves dataframe to csv file", + example: "[[a b]; [1 2] [3 4]] | dfr to-df | dfr to-csv test.csv", + result: None, + }, + Example { + description: "Saves dataframe to csv file using other delimiter", + example: "[[a b]; [1 2] [3 4]] | dfr to-df | dfr to-csv test.csv -d '|'", + result: None, + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let file_name: Spanned = call.req(engine_state, stack, 0)?; + let delimiter: Option> = call.get_flag(engine_state, stack, "delimiter")?; + let no_header: bool = call.has_flag("no_header"); + + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + let mut file = File::create(&file_name.item).map_err(|e| { + ShellError::SpannedLabeledError( + "Error with file name".into(), + e.to_string(), + file_name.span, + ) + })?; + + let writer = CsvWriter::new(&mut file); + + let writer = if no_header { + writer.has_header(false) + } else { + writer.has_header(true) + }; + + let writer = match delimiter { + None => writer, + Some(d) => { + if d.item.len() != 1 { + return Err(ShellError::SpannedLabeledError( + "Incorrect delimiter".into(), + "Delimiter has to be one char".into(), + d.span, + )); + } else { + let delimiter = match d.item.chars().next() { + Some(d) => d as u8, + None => unreachable!(), + }; + + writer.with_delimiter(delimiter) + } + } + }; + + writer.finish(df.as_ref()).map_err(|e| { + ShellError::SpannedLabeledError( + "Error writing to file".into(), + e.to_string(), + file_name.span, + ) + })?; + + let file_value = Value::String { + val: format!("saved {:?}", &file_name.item), + span: file_name.span, + }; + + Ok(PipelineData::Value( + Value::List { + vals: vec![file_value], + span: call.head, + }, + None, + )) +} diff --git a/crates/nu-command/src/dataframe/eager/to_df.rs b/crates/nu-command/src/dataframe/eager/to_df.rs new file mode 100644 index 0000000000..4feee1856d --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/to_df.rs @@ -0,0 +1,127 @@ +use super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; + +#[derive(Clone)] +pub struct ToDataFrame; + +impl Command for ToDataFrame { + fn name(&self) -> &str { + "dfr to-df" + } + + fn usage(&self) -> &str { + "Converts a List, Table or Dictionary into a dataframe" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Takes a dictionary and creates a dataframe", + example: "[[a b];[1 2] [3 4]] | dfr to-df", + result: Some( + NuDataFrame::try_from_columns(vec![ + Column::new( + "a".to_string(), + vec![Value::test_int(1), Value::test_int(3)], + ), + Column::new( + "b".to_string(), + vec![Value::test_int(2), Value::test_int(4)], + ), + ]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }, + Example { + description: "Takes a list of tables and creates a dataframe", + example: "[[1 2 a] [3 4 b] [5 6 c]] | dfr to-df", + result: Some( + NuDataFrame::try_from_columns(vec![ + Column::new( + "0".to_string(), + vec![Value::test_int(1), Value::test_int(3), Value::test_int(5)], + ), + Column::new( + "1".to_string(), + vec![Value::test_int(2), Value::test_int(4), Value::test_int(6)], + ), + Column::new( + "2".to_string(), + vec![ + Value::test_string("a"), + Value::test_string("b"), + Value::test_string("c"), + ], + ), + ]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }, + Example { + description: "Takes a list and creates a dataframe", + example: "[a b c] | dfr to-df", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0".to_string(), + vec![ + Value::test_string("a"), + Value::test_string("b"), + Value::test_string("c"), + ], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }, + Example { + description: "Takes a list of booleans and creates a dataframe", + example: "[$true $true $false] | dfr to-df", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0".to_string(), + vec![ + Value::test_bool(true), + Value::test_bool(true), + Value::test_bool(false), + ], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }, + ] + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + NuDataFrame::try_from_iter(input.into_iter()) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) + } +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(ToDataFrame {})]) + } +} diff --git a/crates/nu-command/src/dataframe/eager/to_nu.rs b/crates/nu-command/src/dataframe/eager/to_nu.rs new file mode 100644 index 0000000000..2f8026999d --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/to_nu.rs @@ -0,0 +1,83 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Value, +}; + +use super::super::values::NuDataFrame; + +#[derive(Clone)] +pub struct ToNu; + +impl Command for ToNu { + fn name(&self) -> &str { + "dfr to-nu" + } + + fn usage(&self) -> &str { + "Converts a section of the dataframe to Nushell Table" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .named( + "n-rows", + SyntaxShape::Number, + "number of rows to be shown", + Some('n'), + ) + .switch("tail", "shows tail rows", Some('t')) + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Shows head rows from dataframe", + example: "[[a b]; [1 2] [3 4]] | dfr to-df | dfr to-nu", + result: None, + }, + Example { + description: "Shows tail rows from dataframe", + example: "[[a b]; [1 2] [3 4] [5 6]] | dfr to-df | dfr to-nu -t -n 1", + result: None, + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let rows: Option = call.get_flag(engine_state, stack, "n-rows")?; + let tail: bool = call.has_flag("tail"); + + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + let values = if tail { + df.tail(rows, call.head)? + } else { + df.head(rows, call.head)? + }; + + let value = Value::List { + vals: values, + span: call.head, + }; + + Ok(PipelineData::Value(value, None)) +} diff --git a/crates/nu-command/src/dataframe/eager/to_parquet.rs b/crates/nu-command/src/dataframe/eager/to_parquet.rs new file mode 100644 index 0000000000..12db49b361 --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/to_parquet.rs @@ -0,0 +1,84 @@ +use std::{fs::File, path::PathBuf}; + +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Value, +}; +use polars::prelude::ParquetWriter; + +use super::super::values::NuDataFrame; + +#[derive(Clone)] +pub struct ToParquet; + +impl Command for ToParquet { + fn name(&self) -> &str { + "dfr to-parquet" + } + + fn usage(&self) -> &str { + "Saves dataframe to parquet file" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required("file", SyntaxShape::Filepath, "file path to save dataframe") + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Saves dataframe to csv file", + example: "[[a b]; [1 2] [3 4]] | dfr to-df | dfr to-parquet test.parquet", + result: None, + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let file_name: Spanned = call.req(engine_state, stack, 0)?; + + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + let file = File::create(&file_name.item).map_err(|e| { + ShellError::SpannedLabeledError( + "Error with file name".into(), + e.to_string(), + file_name.span, + ) + })?; + + ParquetWriter::new(file).finish(df.as_ref()).map_err(|e| { + ShellError::SpannedLabeledError("Error saving file".into(), e.to_string(), file_name.span) + })?; + + let file_value = Value::String { + val: format!("saved {:?}", &file_name.item), + span: file_name.span, + }; + + Ok(PipelineData::Value( + Value::List { + vals: vec![file_value], + span: call.head, + }, + None, + )) +} diff --git a/crates/nu-command/src/dataframe/eager/with_column.rs b/crates/nu-command/src/dataframe/eager/with_column.rs new file mode 100644 index 0000000000..8be389e1bc --- /dev/null +++ b/crates/nu-command/src/dataframe/eager/with_column.rs @@ -0,0 +1,109 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Value, +}; + +use super::super::values::{Column, NuDataFrame}; + +#[derive(Clone)] +pub struct WithColumn; + +impl Command for WithColumn { + fn name(&self) -> &str { + "dfr with-column" + } + + fn usage(&self) -> &str { + "Adds a series to the dataframe" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required("series", SyntaxShape::Any, "series to be added") + .required_named("name", SyntaxShape::String, "column name", Some('n')) + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Adds a series to the dataframe", + example: + "[[a b]; [1 2] [3 4]] | dfr to-df | dfr with-column ([5 6] | dfr to-df) --name c", + result: Some( + NuDataFrame::try_from_columns(vec![ + Column::new( + "a".to_string(), + vec![Value::test_int(1), Value::test_int(3)], + ), + Column::new( + "b".to_string(), + vec![Value::test_int(2), Value::test_int(4)], + ), + Column::new( + "c".to_string(), + vec![Value::test_int(5), Value::test_int(6)], + ), + ]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let name: Spanned = call + .get_flag(engine_state, stack, "name")? + .expect("required named value"); + + let other_value: Value = call.req(engine_state, stack, 0)?; + let other_span = other_value.span()?; + let mut other = NuDataFrame::try_from_value(other_value)?.as_series(other_span)?; + let series = other.rename(&name.item).clone(); + + let mut df = NuDataFrame::try_from_pipeline(input, call.head)?; + + df.as_mut() + .with_column(series) + .map_err(|e| { + ShellError::SpannedLabeledError( + "Error adding column to dataframe".into(), + e.to_string(), + other_span, + ) + }) + .map(|df| { + PipelineData::Value( + NuDataFrame::dataframe_into_value(df.clone(), call.head), + None, + ) + }) +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(WithColumn {})]) + } +} diff --git a/crates/nu-command/src/dataframe/mod.rs b/crates/nu-command/src/dataframe/mod.rs new file mode 100644 index 0000000000..61abdea795 --- /dev/null +++ b/crates/nu-command/src/dataframe/mod.rs @@ -0,0 +1,16 @@ +mod eager; +mod series; +mod values; + +pub use eager::add_eager_decls; +pub use series::add_series_decls; + +use nu_protocol::engine::StateWorkingSet; + +pub fn add_dataframe_decls(working_set: &mut StateWorkingSet) { + add_series_decls(working_set); + add_eager_decls(working_set); +} + +#[cfg(test)] +mod test_dataframe; diff --git a/crates/nu-command/src/dataframe/series/all_false.rs b/crates/nu-command/src/dataframe/series/all_false.rs new file mode 100644 index 0000000000..de94fbb5b1 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/all_false.rs @@ -0,0 +1,102 @@ +use super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; + +#[derive(Clone)] +pub struct AllFalse; + +impl Command for AllFalse { + fn name(&self) -> &str { + "dfr all-false" + } + + fn usage(&self) -> &str { + "Returns true if all values are false" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Returns true if all values are false", + example: "[$false $false $false] | dfr to-df | dfr all-false", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "all_false".to_string(), + vec![Value::test_bool(true)], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }, + Example { + description: "Checks the result from a comparison", + example: r#"let s = ([5 6 2 10] | dfr to-df); + let res = ($s > 9); + $res | dfr all-false"#, + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "all_false".to_string(), + vec![Value::test_bool(false)], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + let series = df.as_series(call.head)?; + let bool = series.bool().map_err(|_| { + ShellError::SpannedLabeledError( + "Error converting to bool".into(), + "all-false only works with series of type bool".into(), + call.head, + ) + })?; + + let value = Value::Bool { + val: bool.all_false(), + span: call.head, + }; + + NuDataFrame::try_from_columns(vec![Column::new("all_false".to_string(), vec![value])]) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(AllFalse {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/all_true.rs b/crates/nu-command/src/dataframe/series/all_true.rs new file mode 100644 index 0000000000..0818f42c22 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/all_true.rs @@ -0,0 +1,102 @@ +use super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; + +#[derive(Clone)] +pub struct AllTrue; + +impl Command for AllTrue { + fn name(&self) -> &str { + "dfr all-true" + } + + fn usage(&self) -> &str { + "Returns true if all values are true" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Returns true if all values are true", + example: "[$true $true $true] | dfr to-df | dfr all-true", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "all_true".to_string(), + vec![Value::test_bool(true)], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }, + Example { + description: "Checks the result from a comparison", + example: r#"let s = ([5 6 2 8] | dfr to-df); + let res = ($s > 9); + $res | dfr all-true"#, + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "all_true".to_string(), + vec![Value::test_bool(false)], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + let series = df.as_series(call.head)?; + let bool = series.bool().map_err(|_| { + ShellError::SpannedLabeledError( + "Error converting to bool".into(), + "all-false only works with series of type bool".into(), + call.head, + ) + })?; + + let value = Value::Bool { + val: bool.all_true(), + span: call.head, + }; + + NuDataFrame::try_from_columns(vec![Column::new("all_true".to_string(), vec![value])]) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(AllTrue {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/arg_max.rs b/crates/nu-command/src/dataframe/series/arg_max.rs new file mode 100644 index 0000000000..774ba401aa --- /dev/null +++ b/crates/nu-command/src/dataframe/series/arg_max.rs @@ -0,0 +1,81 @@ +use super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; +use polars::prelude::{IntoSeries, NewChunkedArray, UInt32Chunked}; + +#[derive(Clone)] +pub struct ArgMax; + +impl Command for ArgMax { + fn name(&self) -> &str { + "dfr arg-max" + } + + fn usage(&self) -> &str { + "Return index for max value in series" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Returns index for max value", + example: "[1 3 2] | dfr to-df | dfr arg-max", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "arg_max".to_string(), + vec![Value::test_int(1)], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let series = df.as_series(call.head)?; + + let res = series.arg_max(); + let chunked = match res { + Some(index) => UInt32Chunked::new_from_slice("arg_max", &[index as u32]), + None => UInt32Chunked::new_from_slice("arg_max", &[]), + }; + + let res = chunked.into_series(); + NuDataFrame::try_from_series(vec![res], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(ArgMax {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/arg_min.rs b/crates/nu-command/src/dataframe/series/arg_min.rs new file mode 100644 index 0000000000..bacdbf2768 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/arg_min.rs @@ -0,0 +1,81 @@ +use super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; +use polars::prelude::{IntoSeries, NewChunkedArray, UInt32Chunked}; + +#[derive(Clone)] +pub struct ArgMin; + +impl Command for ArgMin { + fn name(&self) -> &str { + "dfr arg-min" + } + + fn usage(&self) -> &str { + "Return index for min value in series" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Returns index for min value", + example: "[1 3 2] | dfr to-df | dfr arg-min", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "arg_min".to_string(), + vec![Value::test_int(0)], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let series = df.as_series(call.head)?; + + let res = series.arg_min(); + let chunked = match res { + Some(index) => UInt32Chunked::new_from_slice("arg_min", &[index as u32]), + None => UInt32Chunked::new_from_slice("arg_min", &[]), + }; + + let res = chunked.into_series(); + NuDataFrame::try_from_series(vec![res], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(ArgMin {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/cumulative.rs b/crates/nu-command/src/dataframe/series/cumulative.rs new file mode 100644 index 0000000000..a61484f8dc --- /dev/null +++ b/crates/nu-command/src/dataframe/series/cumulative.rs @@ -0,0 +1,135 @@ +use super::super::values::{Column, NuDataFrame}; + +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Value, +}; +use polars::prelude::{DataType, IntoSeries}; + +enum CumType { + Min, + Max, + Sum, +} + +impl CumType { + fn from_str(roll_type: &str, span: Span) -> Result { + match roll_type { + "min" => Ok(Self::Min), + "max" => Ok(Self::Max), + "sum" => Ok(Self::Sum), + _ => Err(ShellError::SpannedLabeledErrorHelp( + "Wrong operation".into(), + "Operation not valid for cumulative".into(), + span, + "Allowed values: max, min, sum".into(), + )), + } + } + + fn to_str(&self) -> &'static str { + match self { + CumType::Min => "cumulative_min", + CumType::Max => "cumulative_max", + CumType::Sum => "cumulative_sum", + } + } +} + +#[derive(Clone)] +pub struct Cumulative; + +impl Command for Cumulative { + fn name(&self) -> &str { + "dfr cumulative" + } + + fn usage(&self) -> &str { + "Cumulative calculation for a series" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required("type", SyntaxShape::String, "rolling operation") + .switch("reverse", "Reverse cumulative calculation", Some('r')) + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Cumulative sum for a series", + example: "[1 2 3 4 5] | dfr to-df | dfr cumulative sum", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0_cumulative_sum".to_string(), + vec![ + Value::test_int(1), + Value::test_int(3), + Value::test_int(6), + Value::test_int(10), + Value::test_int(15), + ], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let cum_type: Spanned = call.req(engine_state, stack, 0)?; + let reverse = call.has_flag("reverse"); + + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let series = df.as_series(call.head)?; + + if let DataType::Object(_) = series.dtype() { + return Err(ShellError::SpannedLabeledError( + "Found object series".into(), + "Series of type object cannot be used for cumulative operation".into(), + call.head, + )); + } + + let cum_type = CumType::from_str(&cum_type.item, cum_type.span)?; + let mut res = match cum_type { + CumType::Max => series.cummax(reverse), + CumType::Min => series.cummin(reverse), + CumType::Sum => series.cumsum(reverse), + }; + + let name = format!("{}_{}", series.name(), cum_type.to_str()); + res.rename(&name); + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(Cumulative {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/date/get_day.rs b/crates/nu-command/src/dataframe/series/date/get_day.rs new file mode 100644 index 0000000000..fe840121a6 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/date/get_day.rs @@ -0,0 +1,87 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; +use polars::prelude::IntoSeries; + +#[derive(Clone)] +pub struct GetDay; + +impl Command for GetDay { + fn name(&self) -> &str { + "dfr get-day" + } + + fn usage(&self) -> &str { + "Gets day from date" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Returns day from a date", + example: r#"let dt = ('2020-08-04T16:39:18+00:00' | into datetime -z 'UTC'); + let df = ([$dt $dt] | dfr to-df); + $df | dfr get-day"#, + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0".to_string(), + vec![Value::test_int(4), Value::test_int(4)], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let series = df.as_series(call.head)?; + + let casted = series.datetime().map_err(|e| { + ShellError::SpannedLabeledError( + "Error casting to datetime type".into(), + e.to_string(), + call.head, + ) + })?; + + let res = casted.day().into_series(); + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::super::IntoDatetime; + use super::super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(GetDay {}), Box::new(IntoDatetime {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/date/get_hour.rs b/crates/nu-command/src/dataframe/series/date/get_hour.rs new file mode 100644 index 0000000000..4b0ea98ee2 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/date/get_hour.rs @@ -0,0 +1,87 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; +use polars::prelude::IntoSeries; + +#[derive(Clone)] +pub struct GetHour; + +impl Command for GetHour { + fn name(&self) -> &str { + "dfr get-hour" + } + + fn usage(&self) -> &str { + "Gets hour from date" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Returns hour from a date", + example: r#"let dt = ('2020-08-04T16:39:18+00:00' | into datetime -z 'UTC'); + let df = ([$dt $dt] | dfr to-df); + $df | dfr get-hour"#, + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0".to_string(), + vec![Value::test_int(16), Value::test_int(16)], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let series = df.as_series(call.head)?; + + let casted = series.datetime().map_err(|e| { + ShellError::SpannedLabeledError( + "Error casting to datetime type".into(), + e.to_string(), + call.head, + ) + })?; + + let res = casted.hour().into_series(); + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::super::IntoDatetime; + use super::super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(GetHour {}), Box::new(IntoDatetime {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/date/get_minute.rs b/crates/nu-command/src/dataframe/series/date/get_minute.rs new file mode 100644 index 0000000000..161a85f464 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/date/get_minute.rs @@ -0,0 +1,87 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; +use polars::prelude::IntoSeries; + +#[derive(Clone)] +pub struct GetMinute; + +impl Command for GetMinute { + fn name(&self) -> &str { + "dfr get-minute" + } + + fn usage(&self) -> &str { + "Gets minute from date" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Returns minute from a date", + example: r#"let dt = ('2020-08-04T16:39:18+00:00' | into datetime -z 'UTC'); + let df = ([$dt $dt] | dfr to-df); + $df | dfr get-minute"#, + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0".to_string(), + vec![Value::test_int(39), Value::test_int(39)], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let series = df.as_series(call.head)?; + + let casted = series.datetime().map_err(|e| { + ShellError::SpannedLabeledError( + "Error casting to datetime type".into(), + e.to_string(), + call.head, + ) + })?; + + let res = casted.minute().into_series(); + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::super::IntoDatetime; + use super::super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(GetMinute {}), Box::new(IntoDatetime {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/date/get_month.rs b/crates/nu-command/src/dataframe/series/date/get_month.rs new file mode 100644 index 0000000000..a5f9f0fc98 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/date/get_month.rs @@ -0,0 +1,87 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; +use polars::prelude::IntoSeries; + +#[derive(Clone)] +pub struct GetMonth; + +impl Command for GetMonth { + fn name(&self) -> &str { + "dfr get-month" + } + + fn usage(&self) -> &str { + "Gets month from date" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Returns month from a date", + example: r#"let dt = ('2020-08-04T16:39:18+00:00' | into datetime -z 'UTC'); + let df = ([$dt $dt] | dfr to-df); + $df | dfr get-month"#, + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0".to_string(), + vec![Value::test_int(8), Value::test_int(8)], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let series = df.as_series(call.head)?; + + let casted = series.datetime().map_err(|e| { + ShellError::SpannedLabeledError( + "Error casting to datetime type".into(), + e.to_string(), + call.head, + ) + })?; + + let res = casted.month().into_series(); + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::super::IntoDatetime; + use super::super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(GetMonth {}), Box::new(IntoDatetime {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/date/get_nanosecond.rs b/crates/nu-command/src/dataframe/series/date/get_nanosecond.rs new file mode 100644 index 0000000000..445398d49e --- /dev/null +++ b/crates/nu-command/src/dataframe/series/date/get_nanosecond.rs @@ -0,0 +1,87 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; +use polars::prelude::IntoSeries; + +#[derive(Clone)] +pub struct GetNanosecond; + +impl Command for GetNanosecond { + fn name(&self) -> &str { + "dfr get-nanosecond" + } + + fn usage(&self) -> &str { + "Gets nanosecond from date" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Returns nanosecond from a date", + example: r#"let dt = ('2020-08-04T16:39:18+00:00' | into datetime -z 'UTC'); + let df = ([$dt $dt] | dfr to-df); + $df | dfr get-nanosecond"#, + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0".to_string(), + vec![Value::test_int(0), Value::test_int(0)], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let series = df.as_series(call.head)?; + + let casted = series.datetime().map_err(|e| { + ShellError::SpannedLabeledError( + "Error casting to datetime type".into(), + e.to_string(), + call.head, + ) + })?; + + let res = casted.nanosecond().into_series(); + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::super::IntoDatetime; + use super::super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(GetNanosecond {}), Box::new(IntoDatetime {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/date/get_ordinal.rs b/crates/nu-command/src/dataframe/series/date/get_ordinal.rs new file mode 100644 index 0000000000..95b46c24a0 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/date/get_ordinal.rs @@ -0,0 +1,87 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; +use polars::prelude::IntoSeries; + +#[derive(Clone)] +pub struct GetOrdinal; + +impl Command for GetOrdinal { + fn name(&self) -> &str { + "dfr get-ordinal" + } + + fn usage(&self) -> &str { + "Gets ordinal from date" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Returns ordinal from a date", + example: r#"let dt = ('2020-08-04T16:39:18+00:00' | into datetime -z 'UTC'); + let df = ([$dt $dt] | dfr to-df); + $df | dfr get-ordinal"#, + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0".to_string(), + vec![Value::test_int(217), Value::test_int(217)], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let series = df.as_series(call.head)?; + + let casted = series.datetime().map_err(|e| { + ShellError::SpannedLabeledError( + "Error casting to datetime type".into(), + e.to_string(), + call.head, + ) + })?; + + let res = casted.ordinal().into_series(); + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::super::IntoDatetime; + use super::super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(GetOrdinal {}), Box::new(IntoDatetime {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/date/get_second.rs b/crates/nu-command/src/dataframe/series/date/get_second.rs new file mode 100644 index 0000000000..ba5ecf8e48 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/date/get_second.rs @@ -0,0 +1,87 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; +use polars::prelude::IntoSeries; + +#[derive(Clone)] +pub struct GetSecond; + +impl Command for GetSecond { + fn name(&self) -> &str { + "dfr get-second" + } + + fn usage(&self) -> &str { + "Gets second from date" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Returns second from a date", + example: r#"let dt = ('2020-08-04T16:39:18+00:00' | into datetime -z 'UTC'); + let df = ([$dt $dt] | dfr to-df); + $df | dfr get-second"#, + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0".to_string(), + vec![Value::test_int(18), Value::test_int(18)], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let series = df.as_series(call.head)?; + + let casted = series.datetime().map_err(|e| { + ShellError::SpannedLabeledError( + "Error casting to datetime type".into(), + e.to_string(), + call.head, + ) + })?; + + let res = casted.second().into_series(); + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::super::IntoDatetime; + use super::super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(GetSecond {}), Box::new(IntoDatetime {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/date/get_week.rs b/crates/nu-command/src/dataframe/series/date/get_week.rs new file mode 100644 index 0000000000..a671d3229c --- /dev/null +++ b/crates/nu-command/src/dataframe/series/date/get_week.rs @@ -0,0 +1,87 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; +use polars::prelude::IntoSeries; + +#[derive(Clone)] +pub struct GetWeek; + +impl Command for GetWeek { + fn name(&self) -> &str { + "dfr get-week" + } + + fn usage(&self) -> &str { + "Gets week from date" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Returns week from a date", + example: r#"let dt = ('2020-08-04T16:39:18+00:00' | into datetime -z 'UTC'); + let df = ([$dt $dt] | dfr to-df); + $df | dfr get-week"#, + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0".to_string(), + vec![Value::test_int(32), Value::test_int(32)], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let series = df.as_series(call.head)?; + + let casted = series.datetime().map_err(|e| { + ShellError::SpannedLabeledError( + "Error casting to datetime type".into(), + e.to_string(), + call.head, + ) + })?; + + let res = casted.week().into_series(); + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::super::IntoDatetime; + use super::super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(GetWeek {}), Box::new(IntoDatetime {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/date/get_weekday.rs b/crates/nu-command/src/dataframe/series/date/get_weekday.rs new file mode 100644 index 0000000000..b4e370665e --- /dev/null +++ b/crates/nu-command/src/dataframe/series/date/get_weekday.rs @@ -0,0 +1,87 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; +use polars::prelude::IntoSeries; + +#[derive(Clone)] +pub struct GetWeekDay; + +impl Command for GetWeekDay { + fn name(&self) -> &str { + "dfr get-weekday" + } + + fn usage(&self) -> &str { + "Gets weekday from date" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Returns weekday from a date", + example: r#"let dt = ('2020-08-04T16:39:18+00:00' | into datetime -z 'UTC'); + let df = ([$dt $dt] | dfr to-df); + $df | dfr get-weekday"#, + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0".to_string(), + vec![Value::test_int(1), Value::test_int(1)], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let series = df.as_series(call.head)?; + + let casted = series.datetime().map_err(|e| { + ShellError::SpannedLabeledError( + "Error casting to datetime type".into(), + e.to_string(), + call.head, + ) + })?; + + let res = casted.weekday().into_series(); + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::super::IntoDatetime; + use super::super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(GetWeekDay {}), Box::new(IntoDatetime {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/date/get_year.rs b/crates/nu-command/src/dataframe/series/date/get_year.rs new file mode 100644 index 0000000000..ed248c2af1 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/date/get_year.rs @@ -0,0 +1,87 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; +use polars::prelude::IntoSeries; + +#[derive(Clone)] +pub struct GetYear; + +impl Command for GetYear { + fn name(&self) -> &str { + "dfr get-year" + } + + fn usage(&self) -> &str { + "Gets year from date" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Returns year from a date", + example: r#"let dt = ('2020-08-04T16:39:18+00:00' | into datetime -z 'UTC'); + let df = ([$dt $dt] | dfr to-df); + $df | dfr get-year"#, + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0".to_string(), + vec![Value::test_int(2020), Value::test_int(2020)], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let series = df.as_series(call.head)?; + + let casted = series.datetime().map_err(|e| { + ShellError::SpannedLabeledError( + "Error casting to datetime type".into(), + e.to_string(), + call.head, + ) + })?; + + let res = casted.year().into_series(); + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::super::IntoDatetime; + use super::super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(GetYear {}), Box::new(IntoDatetime {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/date/mod.rs b/crates/nu-command/src/dataframe/series/date/mod.rs new file mode 100644 index 0000000000..fbbb75f5d3 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/date/mod.rs @@ -0,0 +1,21 @@ +mod get_day; +mod get_hour; +mod get_minute; +mod get_month; +mod get_nanosecond; +mod get_ordinal; +mod get_second; +mod get_week; +mod get_weekday; +mod get_year; + +pub use get_day::GetDay; +pub use get_hour::GetHour; +pub use get_minute::GetMinute; +pub use get_month::GetMonth; +pub use get_nanosecond::GetNanosecond; +pub use get_ordinal::GetOrdinal; +pub use get_second::GetSecond; +pub use get_week::GetWeek; +pub use get_weekday::GetWeekDay; +pub use get_year::GetYear; diff --git a/crates/nu-command/src/dataframe/series/indexes/arg_sort.rs b/crates/nu-command/src/dataframe/series/indexes/arg_sort.rs new file mode 100644 index 0000000000..d1ff8f9963 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/indexes/arg_sort.rs @@ -0,0 +1,107 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; +use polars::prelude::IntoSeries; + +#[derive(Clone)] +pub struct ArgSort; + +impl Command for ArgSort { + fn name(&self) -> &str { + "dfr arg-sort" + } + + fn usage(&self) -> &str { + "Returns indexes for a sorted series" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .switch("reverse", "reverse order", Some('r')) + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Returns indexes for a sorted series", + example: "[1 2 2 3 3] | dfr to-df | dfr arg-sort", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "arg_sort".to_string(), + vec![ + Value::test_int(0), + Value::test_int(1), + Value::test_int(2), + Value::test_int(3), + Value::test_int(4), + ], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }, + Example { + description: "Returns indexes for a sorted series", + example: "[1 2 2 3 3] | dfr to-df | dfr arg-sort -r", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "arg_sort".to_string(), + vec![ + Value::test_int(3), + Value::test_int(4), + Value::test_int(1), + Value::test_int(2), + Value::test_int(0), + ], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + let mut res = df + .as_series(call.head)? + .argsort(call.has_flag("reverse")) + .into_series(); + res.rename("arg_sort"); + + NuDataFrame::try_from_series(vec![res], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(ArgSort {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/indexes/arg_true.rs b/crates/nu-command/src/dataframe/series/indexes/arg_true.rs new file mode 100644 index 0000000000..12af3ed086 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/indexes/arg_true.rs @@ -0,0 +1,85 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; +use polars::prelude::IntoSeries; + +#[derive(Clone)] +pub struct ArgTrue; + +impl Command for ArgTrue { + fn name(&self) -> &str { + "dfr arg-true" + } + + fn usage(&self) -> &str { + "Returns indexes where values are true" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Returns indexes where values are true", + example: "[$false $true $false] | dfr to-df | dfr arg-true", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "arg_true".to_string(), + vec![Value::test_int(1)], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + let series = df.as_series(call.head)?; + let bool = series.bool().map_err(|_| { + ShellError::SpannedLabeledError( + "Error converting to bool".into(), + "all-false only works with series of type bool".into(), + call.head, + ) + })?; + + let mut res = bool.arg_true().into_series(); + res.rename("arg_true"); + + NuDataFrame::try_from_series(vec![res], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(ArgTrue {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/indexes/arg_unique.rs b/crates/nu-command/src/dataframe/series/indexes/arg_unique.rs new file mode 100644 index 0000000000..6ea52f0e42 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/indexes/arg_unique.rs @@ -0,0 +1,86 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; +use polars::prelude::IntoSeries; + +#[derive(Clone)] +pub struct ArgUnique; + +impl Command for ArgUnique { + fn name(&self) -> &str { + "dfr arg-unique" + } + + fn usage(&self) -> &str { + "Returns indexes for unique values" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Returns indexes for unique values", + example: "[1 2 2 3 3] | dfr to-df | dfr arg-unique", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "arg_unique".to_string(), + vec![Value::test_int(0), Value::test_int(1), Value::test_int(3)], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + let mut res = df + .as_series(call.head)? + .arg_unique() + .map_err(|e| { + ShellError::SpannedLabeledError( + "Error extracting unique values".into(), + e.to_string(), + call.head, + ) + })? + .into_series(); + res.rename("arg_unique"); + + NuDataFrame::try_from_series(vec![res], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(ArgUnique {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/indexes/mod.rs b/crates/nu-command/src/dataframe/series/indexes/mod.rs new file mode 100644 index 0000000000..c0af8c8653 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/indexes/mod.rs @@ -0,0 +1,9 @@ +mod arg_sort; +mod arg_true; +mod arg_unique; +mod set_with_idx; + +pub use arg_sort::ArgSort; +pub use arg_true::ArgTrue; +pub use arg_unique::ArgUnique; +pub use set_with_idx::SetWithIndex; diff --git a/crates/nu-command/src/dataframe/series/indexes/set_with_idx.rs b/crates/nu-command/src/dataframe/series/indexes/set_with_idx.rs new file mode 100644 index 0000000000..cb3abb1d88 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/indexes/set_with_idx.rs @@ -0,0 +1,186 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; +use polars::prelude::{ChunkSet, DataType, IntoSeries}; + +#[derive(Clone)] +pub struct SetWithIndex; + +impl Command for SetWithIndex { + fn name(&self) -> &str { + "dfr set-with-idx" + } + + fn usage(&self) -> &str { + "Sets value in the given index" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required("value", SyntaxShape::Any, "value to be inserted in series") + .required_named( + "indices", + SyntaxShape::Any, + "list of indices indicating where to set the value", + Some('i'), + ) + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Set value in selected rows from series", + example: r#"let series = ([4 1 5 2 4 3] | dfr to-df); + let indices = ([0 2] | dfr to-df); + $series | dfr set-with-idx 6 -i $indices"#, + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0".to_string(), + vec![ + Value::test_int(6), + Value::test_int(1), + Value::test_int(6), + Value::test_int(2), + Value::test_int(4), + Value::test_int(3), + ], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let value: Value = call.req(engine_state, stack, 0)?; + + let indices_value: Value = call + .get_flag(engine_state, stack, "indices")? + .expect("required named value"); + let indices_span = indices_value.span()?; + let indices = NuDataFrame::try_from_value(indices_value)?.as_series(indices_span)?; + + let casted = match indices.dtype() { + DataType::UInt32 | DataType::UInt64 | DataType::Int32 | DataType::Int64 => { + indices.as_ref().cast(&DataType::UInt32).map_err(|e| { + ShellError::SpannedLabeledError( + "Error casting indices".into(), + e.to_string(), + indices_span, + ) + }) + } + _ => Err(ShellError::SpannedLabeledErrorHelp( + "Incorrect type".into(), + "Series with incorrect type".into(), + indices_span, + "Consider using a Series with type int type".into(), + )), + }?; + + let indices = casted + .u32() + .map_err(|e| { + ShellError::SpannedLabeledError( + "Error casting indices".into(), + e.to_string(), + indices_span, + ) + })? + .into_iter() + .filter_map(|val| val.map(|v| v as usize)); + + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let series = df.as_series(call.head)?; + + let res = match value { + Value::Int { val, span } => { + let chunked = series.i64().map_err(|e| { + ShellError::SpannedLabeledError("Error casting to i64".into(), e.to_string(), span) + })?; + + let res = chunked.set_at_idx(indices, Some(val)).map_err(|e| { + ShellError::SpannedLabeledError("Error setting value".into(), e.to_string(), span) + })?; + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + } + Value::Float { val, span } => { + let chunked = series.as_ref().f64().map_err(|e| { + ShellError::SpannedLabeledError("Error casting to f64".into(), e.to_string(), span) + })?; + + let res = chunked.set_at_idx(indices, Some(val)).map_err(|e| { + ShellError::SpannedLabeledError("Error setting value".into(), e.to_string(), span) + })?; + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + } + Value::String { val, span } => { + let chunked = series.as_ref().utf8().map_err(|e| { + ShellError::SpannedLabeledError( + "Error casting to string".into(), + e.to_string(), + span, + ) + })?; + + let res = chunked + .set_at_idx(indices, Some(val.as_ref())) + .map_err(|e| { + ShellError::SpannedLabeledError( + "Error setting value".into(), + e.to_string(), + span, + ) + })?; + + let mut res = res.into_series(); + res.rename("string"); + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + } + _ => Err(ShellError::SpannedLabeledError( + "Incorrect value type".into(), + format!( + "this value cannot be set in a series of type '{}'", + series.dtype() + ), + value.span()?, + )), + }; + + res.map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(SetWithIndex {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/masks/is_duplicated.rs b/crates/nu-command/src/dataframe/series/masks/is_duplicated.rs new file mode 100644 index 0000000000..b237ebe94f --- /dev/null +++ b/crates/nu-command/src/dataframe/series/masks/is_duplicated.rs @@ -0,0 +1,95 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; +use polars::prelude::IntoSeries; + +#[derive(Clone)] +pub struct IsDuplicated; + +impl Command for IsDuplicated { + fn name(&self) -> &str { + "dfr is-duplicated" + } + + fn usage(&self) -> &str { + "Creates mask indicating duplicated values" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Create mask indicating duplicated values", + example: "[5 6 6 6 8 8 8] | dfr to-df | dfr is-duplicated", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "is_duplicated".to_string(), + vec![ + Value::test_bool(false), + Value::test_bool(true), + Value::test_bool(true), + Value::test_bool(true), + Value::test_bool(true), + Value::test_bool(true), + Value::test_bool(true), + ], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + let mut res = df + .as_series(call.head)? + .is_duplicated() + .map_err(|e| { + ShellError::SpannedLabeledError( + "Error finding duplicates".into(), + e.to_string(), + call.head, + ) + })? + .into_series(); + + res.rename("is_duplicated"); + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(IsDuplicated {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/masks/is_in.rs b/crates/nu-command/src/dataframe/series/masks/is_in.rs new file mode 100644 index 0000000000..dfe4cec4f8 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/masks/is_in.rs @@ -0,0 +1,104 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; +use polars::prelude::IntoSeries; + +#[derive(Clone)] +pub struct IsIn; + +impl Command for IsIn { + fn name(&self) -> &str { + "dfr is-in" + } + + fn usage(&self) -> &str { + "Checks if elements from a series are contained in right series" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required("other", SyntaxShape::Any, "right series") + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Checks if elements from a series are contained in right series", + example: r#"let other = ([1 3 6] | dfr to-df); + [5 6 6 6 8 8 8] | dfr to-df | dfr is-in $other"#, + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "is_in".to_string(), + vec![ + Value::test_bool(false), + Value::test_bool(true), + Value::test_bool(true), + Value::test_bool(true), + Value::test_bool(false), + Value::test_bool(false), + Value::test_bool(false), + ], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + let other_value: Value = call.req(engine_state, stack, 0)?; + let other_span = other_value.span()?; + let other_df = NuDataFrame::try_from_value(other_value)?; + let other = other_df.as_series(other_span)?; + + let mut res = df + .as_series(call.head)? + .is_in(&other) + .map_err(|e| { + ShellError::SpannedLabeledError( + "Error finding in other".into(), + e.to_string(), + call.head, + ) + })? + .into_series(); + + res.rename("is_in"); + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(IsIn {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/masks/is_not_null.rs b/crates/nu-command/src/dataframe/series/masks/is_not_null.rs new file mode 100644 index 0000000000..c8226c13e2 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/masks/is_not_null.rs @@ -0,0 +1,83 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; +use polars::prelude::IntoSeries; + +#[derive(Clone)] +pub struct IsNotNull; + +impl Command for IsNotNull { + fn name(&self) -> &str { + "dfr is-not-null" + } + + fn usage(&self) -> &str { + "Creates mask where value is not null" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Create mask where values are not null", + example: r#"let s = ([5 6 0 8] | dfr to-df); + let res = ($s / $s); + $res | dfr is-not-null"#, + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "is_not_null".to_string(), + vec![ + Value::test_bool(true), + Value::test_bool(true), + Value::test_bool(false), + Value::test_bool(true), + ], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + let mut res = df.as_series(call.head)?.is_not_null(); + res.rename("is_not_null"); + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(IsNotNull {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/masks/is_null.rs b/crates/nu-command/src/dataframe/series/masks/is_null.rs new file mode 100644 index 0000000000..397a87fe1d --- /dev/null +++ b/crates/nu-command/src/dataframe/series/masks/is_null.rs @@ -0,0 +1,83 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; +use polars::prelude::IntoSeries; + +#[derive(Clone)] +pub struct IsNull; + +impl Command for IsNull { + fn name(&self) -> &str { + "dfr is-null" + } + + fn usage(&self) -> &str { + "Creates mask where value is null" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Create mask where values are null", + example: r#"let s = ([5 6 0 8] | dfr to-df); + let res = ($s / $s); + $res | dfr is-null"#, + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "is_null".to_string(), + vec![ + Value::test_bool(false), + Value::test_bool(false), + Value::test_bool(true), + Value::test_bool(false), + ], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + let mut res = df.as_series(call.head)?.is_null(); + res.rename("is_null"); + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(IsNull {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/masks/is_unique.rs b/crates/nu-command/src/dataframe/series/masks/is_unique.rs new file mode 100644 index 0000000000..d0272084a0 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/masks/is_unique.rs @@ -0,0 +1,90 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; +use polars::prelude::IntoSeries; + +#[derive(Clone)] +pub struct IsUnique; + +impl Command for IsUnique { + fn name(&self) -> &str { + "dfr is-unique" + } + + fn usage(&self) -> &str { + "Creates mask indicating unique values" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Create mask indicating unique values", + example: "[5 6 6 6 8 8 8] | dfr to-df | dfr is-unique", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "is_unique".to_string(), + vec![ + Value::test_bool(true), + Value::test_bool(false), + Value::test_bool(false), + Value::test_bool(false), + Value::test_bool(false), + Value::test_bool(false), + Value::test_bool(false), + ], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + let mut res = df.as_series(call.head)?.is_unique().map_err(|e| { + ShellError::SpannedLabeledError( + "Error finding unique values".into(), + e.to_string(), + call.head, + ) + })?; + res.rename("is_unique"); + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(IsUnique {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/masks/mod.rs b/crates/nu-command/src/dataframe/series/masks/mod.rs new file mode 100644 index 0000000000..80c98b5ef0 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/masks/mod.rs @@ -0,0 +1,15 @@ +mod is_duplicated; +mod is_in; +mod is_not_null; +mod is_null; +mod is_unique; +mod not; +mod set; + +pub use is_duplicated::IsDuplicated; +pub use is_in::IsIn; +pub use is_not_null::IsNotNull; +pub use is_null::IsNull; +pub use is_unique::IsUnique; +pub use not::NotSeries; +pub use set::SetSeries; diff --git a/crates/nu-command/src/dataframe/series/masks/not.rs b/crates/nu-command/src/dataframe/series/masks/not.rs new file mode 100644 index 0000000000..636ee2818f --- /dev/null +++ b/crates/nu-command/src/dataframe/series/masks/not.rs @@ -0,0 +1,86 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; +use polars::prelude::IntoSeries; + +use std::ops::Not; + +#[derive(Clone)] +pub struct NotSeries; + +impl Command for NotSeries { + fn name(&self) -> &str { + "dfr not" + } + + fn usage(&self) -> &str { + "Inverts boolean mask" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Inverts boolean mask", + example: "[$true $false $true] | dfr to-df | dfr not", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0".to_string(), + vec![ + Value::test_bool(false), + Value::test_bool(true), + Value::test_bool(false), + ], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let series = df.as_series(call.head)?; + + let bool = series.bool().map_err(|e| { + ShellError::SpannedLabeledError("Error inverting mask".into(), e.to_string(), call.head) + })?; + + let res = bool.not(); + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(NotSeries {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/masks/set.rs b/crates/nu-command/src/dataframe/series/masks/set.rs new file mode 100644 index 0000000000..8711728655 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/masks/set.rs @@ -0,0 +1,169 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; +use polars::prelude::{ChunkSet, DataType, IntoSeries}; + +#[derive(Clone)] +pub struct SetSeries; + +impl Command for SetSeries { + fn name(&self) -> &str { + "dfr set" + } + + fn usage(&self) -> &str { + "Sets value where given mask is true" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required("value", SyntaxShape::Any, "value to be inserted in series") + .required_named( + "mask", + SyntaxShape::Any, + "mask indicating insertions", + Some('m'), + ) + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Shifts the values by a given period", + example: r#"let s = ([1 2 2 3 3] | dfr to-df | dfr shift 2); + let mask = ($s | dfr is-null); + $s | dfr set 0 --mask $mask"#, + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0".to_string(), + vec![ + Value::test_int(0), + Value::test_int(0), + Value::test_int(1), + Value::test_int(2), + Value::test_int(2), + ], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let value: Value = call.req(engine_state, stack, 0)?; + + let mask_value: Value = call + .get_flag(engine_state, stack, "mask")? + .expect("required named value"); + let mask_span = mask_value.span()?; + let mask = NuDataFrame::try_from_value(mask_value)?.as_series(mask_span)?; + + let bool_mask = match mask.dtype() { + DataType::Boolean => mask.bool().map_err(|e| { + ShellError::SpannedLabeledError( + "Error casting to bool".into(), + e.to_string(), + mask_span, + ) + }), + _ => Err(ShellError::SpannedLabeledError( + "Incorrect type".into(), + "can only use bool series as mask".into(), + mask_span, + )), + }?; + + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let series = df.as_series(call.head)?; + + let res = match value { + Value::Int { val, span } => { + let chunked = series.i64().map_err(|e| { + ShellError::SpannedLabeledError("Error casting to i64".into(), e.to_string(), span) + })?; + + let res = chunked.set(bool_mask, Some(val)).map_err(|e| { + ShellError::SpannedLabeledError("Error setting value".into(), e.to_string(), span) + })?; + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + } + Value::Float { val, span } => { + let chunked = series.as_ref().f64().map_err(|e| { + ShellError::SpannedLabeledError("Error casting to f64".into(), e.to_string(), span) + })?; + + let res = chunked.set(bool_mask, Some(val)).map_err(|e| { + ShellError::SpannedLabeledError("Error setting value".into(), e.to_string(), span) + })?; + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + } + Value::String { val, span } => { + let chunked = series.as_ref().utf8().map_err(|e| { + ShellError::SpannedLabeledError( + "Error casting to string".into(), + e.to_string(), + span, + ) + })?; + + let res = chunked.set(bool_mask, Some(val.as_ref())).map_err(|e| { + ShellError::SpannedLabeledError("Error setting value".into(), e.to_string(), span) + })?; + + let mut res = res.into_series(); + res.rename("string"); + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + } + _ => Err(ShellError::SpannedLabeledError( + "Incorrect value type".into(), + format!( + "this value cannot be set in a series of type '{}'", + series.dtype() + ), + value.span()?, + )), + }; + + res.map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::test_dataframe::test_dataframe; + use super::super::super::{IsNull, Shift}; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![ + Box::new(SetSeries {}), + Box::new(IsNull {}), + Box::new(Shift {}), + ]) + } +} diff --git a/crates/nu-command/src/dataframe/series/mod.rs b/crates/nu-command/src/dataframe/series/mod.rs new file mode 100644 index 0000000000..fbe81ebcfa --- /dev/null +++ b/crates/nu-command/src/dataframe/series/mod.rs @@ -0,0 +1,96 @@ +mod date; +pub use date::*; + +mod string; +pub use string::*; + +mod masks; +pub use masks::*; + +mod indexes; +pub use indexes::*; + +mod all_false; +mod all_true; +mod arg_max; +mod arg_min; +mod cumulative; +mod n_null; +mod n_unique; +mod rename; +mod rolling; +mod shift; +mod unique; +mod value_counts; + +use nu_protocol::engine::StateWorkingSet; + +pub use all_false::AllFalse; +pub use all_true::AllTrue; +pub use arg_max::ArgMax; +pub use arg_min::ArgMin; +pub use cumulative::Cumulative; +pub use n_null::NNull; +pub use n_unique::NUnique; +pub use rename::Rename; +pub use rolling::Rolling; +pub use shift::Shift; +pub use unique::Unique; +pub use value_counts::ValueCount; + +pub fn add_series_decls(working_set: &mut StateWorkingSet) { + macro_rules! bind_command { + ( $command:expr ) => { + working_set.add_decl(Box::new($command)); + }; + ( $( $command:expr ),* ) => { + $( working_set.add_decl(Box::new($command)); )* + }; + } + + // Series commands + bind_command!( + AllFalse, + AllTrue, + ArgMax, + ArgMin, + ArgSort, + ArgTrue, + ArgUnique, + Concatenate, + Contains, + Cumulative, + GetDay, + GetHour, + GetMinute, + GetMonth, + GetNanosecond, + GetOrdinal, + GetSecond, + GetWeek, + GetWeekDay, + GetYear, + IsDuplicated, + IsIn, + IsNotNull, + IsNull, + IsUnique, + NNull, + NUnique, + NotSeries, + Rename, + Replace, + ReplaceAll, + Rolling, + SetSeries, + SetWithIndex, + Shift, + StrLengths, + StrSlice, + StrFTime, + ToLowerCase, + ToUpperCase, + Unique, + ValueCount + ); +} diff --git a/crates/nu-command/src/dataframe/series/n_null.rs b/crates/nu-command/src/dataframe/series/n_null.rs new file mode 100644 index 0000000000..9bee66005c --- /dev/null +++ b/crates/nu-command/src/dataframe/series/n_null.rs @@ -0,0 +1,79 @@ +use super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; + +#[derive(Clone)] +pub struct NNull; + +impl Command for NNull { + fn name(&self) -> &str { + "dfr count-null" + } + + fn usage(&self) -> &str { + "Counts null values" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Counts null values", + example: r#"let s = ([1 1 0 0 3 3 4] | dfr to-df); + ($s / $s) | dfr count-null"#, + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "count_null".to_string(), + vec![Value::test_int(2)], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + let res = df.as_series(call.head)?.null_count(); + let value = Value::Int { + val: res as i64, + span: call.head, + }; + + NuDataFrame::try_from_columns(vec![Column::new("count_null".to_string(), vec![value])]) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(NNull {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/n_unique.rs b/crates/nu-command/src/dataframe/series/n_unique.rs new file mode 100644 index 0000000000..3a5a511b66 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/n_unique.rs @@ -0,0 +1,85 @@ +use super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; + +#[derive(Clone)] +pub struct NUnique; + +impl Command for NUnique { + fn name(&self) -> &str { + "dfr count-unique" + } + + fn usage(&self) -> &str { + "Counts unique values" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Counts unique values", + example: "[1 1 2 2 3 3 4] | dfr to-df | dfr count-unique", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "count_unique".to_string(), + vec![Value::test_int(4)], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + let res = df.as_series(call.head)?.n_unique().map_err(|e| { + ShellError::SpannedLabeledError( + "Error counting unique values".into(), + e.to_string(), + call.head, + ) + })?; + + let value = Value::Int { + val: res as i64, + span: call.head, + }; + + NuDataFrame::try_from_columns(vec![Column::new("count_unique".to_string(), vec![value])]) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(NUnique {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/rename.rs b/crates/nu-command/src/dataframe/series/rename.rs new file mode 100644 index 0000000000..9de4d86a3c --- /dev/null +++ b/crates/nu-command/src/dataframe/series/rename.rs @@ -0,0 +1,84 @@ +use super::super::values::{Column, NuDataFrame}; + +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Rename; + +impl Command for Rename { + fn name(&self) -> &str { + "dfr rename" + } + + fn usage(&self) -> &str { + "Renames a series" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required("name", SyntaxShape::String, "new series name") + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Renames a series", + example: "[5 6 7 8] | dfr to-df | dfr rename new_name", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "new_name".to_string(), + vec![ + Value::test_int(5), + Value::test_int(6), + Value::test_int(7), + Value::test_int(8), + ], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let name: String = call.req(engine_state, stack, 0)?; + + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let mut series = df.as_series(call.head)?; + series.rename(&name); + + NuDataFrame::try_from_series(vec![series], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(Rename {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/rolling.rs b/crates/nu-command/src/dataframe/series/rolling.rs new file mode 100644 index 0000000000..d2ee02abc2 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/rolling.rs @@ -0,0 +1,173 @@ +use super::super::values::{Column, NuDataFrame}; + +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Value, +}; +use polars::prelude::{DataType, IntoSeries, RollingOptions}; + +enum RollType { + Min, + Max, + Sum, + Mean, +} + +impl RollType { + fn from_str(roll_type: &str, span: Span) -> Result { + match roll_type { + "min" => Ok(Self::Min), + "max" => Ok(Self::Max), + "sum" => Ok(Self::Sum), + "mean" => Ok(Self::Mean), + _ => Err(ShellError::SpannedLabeledErrorHelp( + "Wrong operation".into(), + "Operation not valid for cumulative".into(), + span, + "Allowed values: min, max, sum, mean".into(), + )), + } + } + + fn to_str(&self) -> &'static str { + match self { + RollType::Min => "rolling_min", + RollType::Max => "rolling_max", + RollType::Sum => "rolling_sum", + RollType::Mean => "rolling_mean", + } + } +} + +#[derive(Clone)] +pub struct Rolling; + +impl Command for Rolling { + fn name(&self) -> &str { + "dfr rolling" + } + + fn usage(&self) -> &str { + "Rolling calculation for a series" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required("type", SyntaxShape::String, "rolling operation") + .required("window", SyntaxShape::Int, "Window size for rolling") + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Rolling sum for a series", + example: "[1 2 3 4 5] | dfr to-df | dfr rolling sum 2 | dfr drop-nulls", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0_rolling_sum".to_string(), + vec![ + Value::test_int(3), + Value::test_int(5), + Value::test_int(7), + Value::test_int(9), + ], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }, + Example { + description: "Rolling max for a series", + example: "[1 2 3 4 5] | dfr to-df | dfr rolling max 2 | dfr drop-nulls", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0_rolling_max".to_string(), + vec![ + Value::test_int(2), + Value::test_int(3), + Value::test_int(4), + Value::test_int(5), + ], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let roll_type: Spanned = call.req(engine_state, stack, 0)?; + let window_size: usize = call.req(engine_state, stack, 1)?; + + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let series = df.as_series(call.head)?; + + if let DataType::Object(_) = series.dtype() { + return Err(ShellError::SpannedLabeledError( + "Found object series".into(), + "Series of type object cannot be used for rolling operation".into(), + call.head, + )); + } + + let roll_type = RollType::from_str(&roll_type.item, roll_type.span)?; + + let rolling_opts = RollingOptions { + window_size, + min_periods: window_size, + weights: None, + center: false, + }; + let res = match roll_type { + RollType::Max => series.rolling_max(rolling_opts), + RollType::Min => series.rolling_min(rolling_opts), + RollType::Sum => series.rolling_sum(rolling_opts), + RollType::Mean => series.rolling_mean(rolling_opts), + }; + + let mut res = res.map_err(|e| { + ShellError::SpannedLabeledError( + "Error calculating rolling values".into(), + e.to_string(), + call.head, + ) + })?; + + let name = format!("{}_{}", series.name(), roll_type.to_str()); + res.rename(&name); + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::eager::DropNulls; + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(Rolling {}), Box::new(DropNulls {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/shift.rs b/crates/nu-command/src/dataframe/series/shift.rs new file mode 100644 index 0000000000..cc35a2ca3f --- /dev/null +++ b/crates/nu-command/src/dataframe/series/shift.rs @@ -0,0 +1,79 @@ +use super::super::values::{Column, NuDataFrame}; + +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Shift; + +impl Command for Shift { + fn name(&self) -> &str { + "dfr shift" + } + + fn usage(&self) -> &str { + "Shifts the values by a given period" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required("period", SyntaxShape::Int, "shift period") + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Shifts the values by a given period", + example: "[1 2 2 3 3] | dfr to-df | dfr shift 2 | dfr drop-nulls", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0".to_string(), + vec![Value::test_int(1), Value::test_int(2), Value::test_int(2)], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let period: i64 = call.req(engine_state, stack, 0)?; + + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let series = df.as_series(call.head)?.shift(period); + + NuDataFrame::try_from_series(vec![series], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::eager::DropNulls; + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(Shift {}), Box::new(DropNulls {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/string/concatenate.rs b/crates/nu-command/src/dataframe/series/string/concatenate.rs new file mode 100644 index 0000000000..37d5f73128 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/string/concatenate.rs @@ -0,0 +1,111 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; +use polars::prelude::IntoSeries; + +#[derive(Clone)] +pub struct Concatenate; + +impl Command for Concatenate { + fn name(&self) -> &str { + "dfr concatenate" + } + + fn usage(&self) -> &str { + "Concatenates strings with other array" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required( + "other", + SyntaxShape::Any, + "Other array with string to be concatenated", + ) + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Concatenate string", + example: r#"let other = ([za xs cd] | dfr to-df); + [abc abc abc] | dfr to-df | dfr concatenate $other"#, + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0".to_string(), + vec![ + Value::test_string("abcza"), + Value::test_string("abcxs"), + Value::test_string("abccd"), + ], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + + let other: Value = call.req(engine_state, stack, 0)?; + let other_span = other.span()?; + let other_df = NuDataFrame::try_from_value(other)?; + + let other_series = other_df.as_series(other_span)?; + let other_chunked = other_series.utf8().map_err(|e| { + ShellError::SpannedLabeledError( + "The concatenate only with string columns".into(), + e.to_string(), + other_span, + ) + })?; + + let series = df.as_series(call.head)?; + let chunked = series.utf8().map_err(|e| { + ShellError::SpannedLabeledError( + "The concatenate only with string columns".into(), + e.to_string(), + call.head, + ) + })?; + + let mut res = chunked.concat(other_chunked); + + res.rename(series.name()); + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(Concatenate {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/string/contains.rs b/crates/nu-command/src/dataframe/series/string/contains.rs new file mode 100644 index 0000000000..2c0fc58959 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/string/contains.rs @@ -0,0 +1,102 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; +use polars::prelude::IntoSeries; + +#[derive(Clone)] +pub struct Contains; + +impl Command for Contains { + fn name(&self) -> &str { + "dfr contains" + } + + fn usage(&self) -> &str { + "Checks if a pattern is contained in a string" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required( + "pattern", + SyntaxShape::String, + "Regex pattern to be searched", + ) + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Returns boolean indicating if pattern was found", + example: "[abc acb acb] | dfr to-df | dfr contains ab", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0".to_string(), + vec![ + Value::test_bool(true), + Value::test_bool(false), + Value::test_bool(false), + ], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let pattern: String = call.req(engine_state, stack, 0)?; + + let series = df.as_series(call.head)?; + let chunked = series.utf8().map_err(|e| { + ShellError::SpannedLabeledError( + "The contains command only with string columns".into(), + e.to_string(), + call.head, + ) + })?; + + let res = chunked.contains(&pattern).map_err(|e| { + ShellError::SpannedLabeledError( + "Error searching in series".into(), + e.to_string(), + call.head, + ) + })?; + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(Contains {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/string/mod.rs b/crates/nu-command/src/dataframe/series/string/mod.rs new file mode 100644 index 0000000000..f2fa19cbaf --- /dev/null +++ b/crates/nu-command/src/dataframe/series/string/mod.rs @@ -0,0 +1,19 @@ +mod concatenate; +mod contains; +mod replace; +mod replace_all; +mod str_lengths; +mod str_slice; +mod strftime; +mod to_lowercase; +mod to_uppercase; + +pub use concatenate::Concatenate; +pub use contains::Contains; +pub use replace::Replace; +pub use replace_all::ReplaceAll; +pub use str_lengths::StrLengths; +pub use str_slice::StrSlice; +pub use strftime::StrFTime; +pub use to_lowercase::ToLowerCase; +pub use to_uppercase::ToUpperCase; diff --git a/crates/nu-command/src/dataframe/series/string/replace.rs b/crates/nu-command/src/dataframe/series/string/replace.rs new file mode 100644 index 0000000000..ac95901031 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/string/replace.rs @@ -0,0 +1,116 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; +use polars::prelude::IntoSeries; + +#[derive(Clone)] +pub struct Replace; + +impl Command for Replace { + fn name(&self) -> &str { + "dfr replace" + } + + fn usage(&self) -> &str { + "Replace the leftmost (sub)string by a regex pattern" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required_named( + "pattern", + SyntaxShape::String, + "Regex pattern to be matched", + Some('p'), + ) + .required_named( + "replace", + SyntaxShape::String, + "replacing string", + Some('r'), + ) + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Replaces string", + example: "[abc abc abc] | dfr to-df | dfr replace -p ab -r AB", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0".to_string(), + vec![ + Value::test_string("ABc"), + Value::test_string("ABc"), + Value::test_string("ABc"), + ], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let pattern: String = call + .get_flag(engine_state, stack, "pattern")? + .expect("required value"); + let replace: String = call + .get_flag(engine_state, stack, "replace")? + .expect("required value"); + + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let series = df.as_series(call.head)?; + let chunked = series.utf8().map_err(|e| { + ShellError::SpannedLabeledError( + "Error convertion to string".into(), + e.to_string(), + call.head, + ) + })?; + + let mut res = chunked.replace(&pattern, &replace).map_err(|e| { + ShellError::SpannedLabeledError( + "Error finding pattern other".into(), + e.to_string(), + call.head, + ) + })?; + + res.rename(series.name()); + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(Replace {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/string/replace_all.rs b/crates/nu-command/src/dataframe/series/string/replace_all.rs new file mode 100644 index 0000000000..383bba4848 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/string/replace_all.rs @@ -0,0 +1,116 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; +use polars::prelude::IntoSeries; + +#[derive(Clone)] +pub struct ReplaceAll; + +impl Command for ReplaceAll { + fn name(&self) -> &str { + "dfr replace-all" + } + + fn usage(&self) -> &str { + "Replace all (sub)strings by a regex pattern" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required_named( + "pattern", + SyntaxShape::String, + "Regex pattern to be matched", + Some('p'), + ) + .required_named( + "replace", + SyntaxShape::String, + "replacing string", + Some('r'), + ) + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Replaces string", + example: "[abac abac abac] | dfr to-df | dfr replace-all -p a -r A", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0".to_string(), + vec![ + Value::test_string("AbAc"), + Value::test_string("AbAc"), + Value::test_string("AbAc"), + ], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let pattern: String = call + .get_flag(engine_state, stack, "pattern")? + .expect("required value"); + let replace: String = call + .get_flag(engine_state, stack, "replace")? + .expect("required value"); + + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let series = df.as_series(call.head)?; + let chunked = series.utf8().map_err(|e| { + ShellError::SpannedLabeledError( + "Error convertion to string".into(), + e.to_string(), + call.head, + ) + })?; + + let mut res = chunked.replace_all(&pattern, &replace).map_err(|e| { + ShellError::SpannedLabeledError( + "Error finding pattern other".into(), + e.to_string(), + call.head, + ) + })?; + + res.rename(series.name()); + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(ReplaceAll {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/string/str_lengths.rs b/crates/nu-command/src/dataframe/series/string/str_lengths.rs new file mode 100644 index 0000000000..f3afad973b --- /dev/null +++ b/crates/nu-command/src/dataframe/series/string/str_lengths.rs @@ -0,0 +1,85 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; +use polars::prelude::IntoSeries; + +#[derive(Clone)] +pub struct StrLengths; + +impl Command for StrLengths { + fn name(&self) -> &str { + "dfr str-lengths" + } + + fn usage(&self) -> &str { + "Get lengths of all strings" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Returns string lengths", + example: "[a ab abc] | dfr to-df | dfr str-lengths", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0".to_string(), + vec![Value::test_int(1), Value::test_int(2), Value::test_int(3)], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let series = df.as_series(call.head)?; + + let chunked = series.utf8().map_err(|e| { + ShellError::SpannedLabeledErrorHelp( + "Error casting to string".into(), + e.to_string(), + call.head, + "The str-lengths command can only be used with string columns".into(), + ) + })?; + + let res = chunked.as_ref().str_lengths().into_series(); + + NuDataFrame::try_from_series(vec![res], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(StrLengths {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/string/str_slice.rs b/crates/nu-command/src/dataframe/series/string/str_slice.rs new file mode 100644 index 0000000000..af69b47cdb --- /dev/null +++ b/crates/nu-command/src/dataframe/series/string/str_slice.rs @@ -0,0 +1,101 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; +use polars::prelude::IntoSeries; + +#[derive(Clone)] +pub struct StrSlice; + +impl Command for StrSlice { + fn name(&self) -> &str { + "dfr str-slice" + } + + fn usage(&self) -> &str { + "Slices the string from the start position until the selected length" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required("start", SyntaxShape::Int, "start of slice") + .named("length", SyntaxShape::Int, "optional length", Some('l')) + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Creates slices from the strings", + example: "[abcded abc321 abc123] | dfr to-df | dfr str-slice 1 -l 2", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0".to_string(), + vec![ + Value::test_string("bc"), + Value::test_string("bc"), + Value::test_string("bc"), + ], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let start: i64 = call.req(engine_state, stack, 0)?; + + let length: Option = call.get_flag(engine_state, stack, "length")?; + let length = length.map(|v| v as u64); + + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let series = df.as_series(call.head)?; + + let chunked = series.utf8().map_err(|e| { + ShellError::SpannedLabeledErrorHelp( + "Error casting to string".into(), + e.to_string(), + call.head, + "The str-slice command can only be used with string columns".into(), + ) + })?; + + let mut res = chunked.str_slice(start, length).map_err(|e| { + ShellError::SpannedLabeledError("Error slicing series".into(), e.to_string(), call.head) + })?; + res.rename(series.name()); + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(StrSlice {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/string/strftime.rs b/crates/nu-command/src/dataframe/series/string/strftime.rs new file mode 100644 index 0000000000..e57c44a6c8 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/string/strftime.rs @@ -0,0 +1,96 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; +use polars::prelude::IntoSeries; + +#[derive(Clone)] +pub struct StrFTime; + +impl Command for StrFTime { + fn name(&self) -> &str { + "dfr strftime" + } + + fn usage(&self) -> &str { + "Formats date based on string rule" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required("fmt", SyntaxShape::String, "Format rule") + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Formats date", + example: r#"let dt = ('2020-08-04T16:39:18+00:00' | into datetime -z 'UTC'); + let df = ([$dt $dt] | dfr to-df); + $df | dfr strftime "%Y/%m/%d""#, + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0".to_string(), + vec![ + Value::test_string("2020/08/04"), + Value::test_string("2020/08/04"), + ], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let fmt: String = call.req(engine_state, stack, 0)?; + + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let series = df.as_series(call.head)?; + + let casted = series.datetime().map_err(|e| { + ShellError::SpannedLabeledErrorHelp( + "Error casting to date".into(), + e.to_string(), + call.head, + "The str-slice command can only be used with string columns".into(), + ) + })?; + + let res = casted.strftime(&fmt).into_series(); + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::super::IntoDatetime; + use super::super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(StrFTime {}), Box::new(IntoDatetime {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/string/to_lowercase.rs b/crates/nu-command/src/dataframe/series/string/to_lowercase.rs new file mode 100644 index 0000000000..7ecd8e58c5 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/string/to_lowercase.rs @@ -0,0 +1,90 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; +use polars::prelude::IntoSeries; + +#[derive(Clone)] +pub struct ToLowerCase; + +impl Command for ToLowerCase { + fn name(&self) -> &str { + "dfr to-lowercase" + } + + fn usage(&self) -> &str { + "Lowercase the strings in the column" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Modifies strings to lowercase", + example: "[Abc aBc abC] | dfr to-df | dfr to-lowercase", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0".to_string(), + vec![ + Value::test_string("abc"), + Value::test_string("abc"), + Value::test_string("abc"), + ], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let series = df.as_series(call.head)?; + + let casted = series.utf8().map_err(|e| { + ShellError::SpannedLabeledErrorHelp( + "Error casting to string".into(), + e.to_string(), + call.head, + "The str-slice command can only be used with string columns".into(), + ) + })?; + + let mut res = casted.to_lowercase(); + res.rename(series.name()); + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(ToLowerCase {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/string/to_uppercase.rs b/crates/nu-command/src/dataframe/series/string/to_uppercase.rs new file mode 100644 index 0000000000..568d76378c --- /dev/null +++ b/crates/nu-command/src/dataframe/series/string/to_uppercase.rs @@ -0,0 +1,90 @@ +use super::super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; +use polars::prelude::IntoSeries; + +#[derive(Clone)] +pub struct ToUpperCase; + +impl Command for ToUpperCase { + fn name(&self) -> &str { + "dfr to-uppercase" + } + + fn usage(&self) -> &str { + "Uppercase the strings in the column" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Modifies strings to uppercase", + example: "[Abc aBc abC] | dfr to-df | dfr to-uppercase", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0".to_string(), + vec![ + Value::test_string("ABC"), + Value::test_string("ABC"), + Value::test_string("ABC"), + ], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let series = df.as_series(call.head)?; + + let casted = series.utf8().map_err(|e| { + ShellError::SpannedLabeledErrorHelp( + "Error casting to string".into(), + e.to_string(), + call.head, + "The str-slice command can only be used with string columns".into(), + ) + })?; + + let mut res = casted.to_uppercase(); + res.rename(series.name()); + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(ToUpperCase {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/unique.rs b/crates/nu-command/src/dataframe/series/unique.rs new file mode 100644 index 0000000000..c9aad6f174 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/unique.rs @@ -0,0 +1,83 @@ +use super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; +use polars::prelude::IntoSeries; + +#[derive(Clone)] +pub struct Unique; + +impl Command for Unique { + fn name(&self) -> &str { + "dfr unique" + } + + fn usage(&self) -> &str { + "Returns unique values from a series" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Returns unique values from a series", + example: "[2 2 2 2 2] | dfr to-df | dfr unique", + result: Some( + NuDataFrame::try_from_columns(vec![Column::new( + "0".to_string(), + vec![Value::test_int(2)], + )]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let series = df.as_series(call.head)?; + + let res = series.unique().map_err(|e| { + ShellError::SpannedLabeledErrorHelp( + "Error calculating unique values".into(), + e.to_string(), + call.head, + "The str-slice command can only be used with string columns".into(), + ) + })?; + + NuDataFrame::try_from_series(vec![res.into_series()], call.head) + .map(|df| PipelineData::Value(NuDataFrame::into_value(df, call.head), None)) +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(Unique {})]) + } +} diff --git a/crates/nu-command/src/dataframe/series/value_counts.rs b/crates/nu-command/src/dataframe/series/value_counts.rs new file mode 100644 index 0000000000..714595a693 --- /dev/null +++ b/crates/nu-command/src/dataframe/series/value_counts.rs @@ -0,0 +1,90 @@ +use super::super::values::{Column, NuDataFrame}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; + +#[derive(Clone)] +pub struct ValueCount; + +impl Command for ValueCount { + fn name(&self) -> &str { + "dfr value-counts" + } + + fn usage(&self) -> &str { + "Returns a dataframe with the counts for unique values in series" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Calculates value counts", + example: "[5 5 5 5 6 6] | dfr to-df | dfr value-counts", + result: Some( + NuDataFrame::try_from_columns(vec![ + Column::new( + "0".to_string(), + vec![Value::test_int(5), Value::test_int(6)], + ), + Column::new( + "counts".to_string(), + vec![Value::test_int(4), Value::test_int(2)], + ), + ]) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + command(engine_state, stack, call, input) + } +} + +fn command( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let df = NuDataFrame::try_from_pipeline(input, call.head)?; + let series = df.as_series(call.head)?; + + let res = series.value_counts().map_err(|e| { + ShellError::SpannedLabeledErrorHelp( + "Error calculating value counts values".into(), + e.to_string(), + call.head, + "The str-slice command can only be used with string columns".into(), + ) + })?; + + Ok(PipelineData::Value( + NuDataFrame::dataframe_into_value(res, call.head), + None, + )) +} + +#[cfg(test)] +mod test { + use super::super::super::test_dataframe::test_dataframe; + use super::*; + + #[test] + fn test_examples() { + test_dataframe(vec![Box::new(ValueCount {})]) + } +} diff --git a/crates/nu-command/src/dataframe/test_dataframe.rs b/crates/nu-command/src/dataframe/test_dataframe.rs new file mode 100644 index 0000000000..3b78750a28 --- /dev/null +++ b/crates/nu-command/src/dataframe/test_dataframe.rs @@ -0,0 +1,97 @@ +use nu_engine::eval_block; +use nu_parser::parse; +use nu_protocol::{ + engine::{Command, EngineState, Stack, StateWorkingSet}, + PipelineData, Span, Value, CONFIG_VARIABLE_ID, +}; + +use super::eager::ToDataFrame; +use crate::Let; + +pub fn test_dataframe(cmds: Vec>) { + if cmds.is_empty() { + panic!("Empty commands vector") + } + + // The first element in the cmds vector must be the one tested + let examples = cmds[0].examples(); + let mut engine_state = Box::new(EngineState::new()); + + let delta = { + // Base functions that are needed for testing + // Try to keep this working set small to keep tests running as fast as possible + let mut working_set = StateWorkingSet::new(&*engine_state); + working_set.add_decl(Box::new(Let)); + working_set.add_decl(Box::new(ToDataFrame)); + + // Adding the command that is being tested to the working set + for cmd in cmds { + working_set.add_decl(cmd); + } + + working_set.render() + }; + + let cwd = std::env::current_dir().expect("Could not get current working directory."); + let _ = engine_state.merge_delta(delta, None, &cwd); + + for example in examples { + // Skip tests that don't have results to compare to + if example.result.is_none() { + continue; + } + let start = std::time::Instant::now(); + + let (block, delta) = { + let mut working_set = StateWorkingSet::new(&*engine_state); + let (output, err) = parse(&mut working_set, None, example.example.as_bytes(), false); + + if let Some(err) = err { + panic!("test parse error in `{}`: {:?}", example.example, err) + } + + (output, working_set.render()) + }; + + let _ = engine_state.merge_delta(delta, None, &cwd); + + let mut stack = Stack::new(); + + // Set up our initial config to start from + stack.vars.insert( + CONFIG_VARIABLE_ID, + Value::Record { + cols: vec![], + vals: vec![], + span: Span::test_data(), + }, + ); + + match eval_block( + &engine_state, + &mut stack, + &block, + PipelineData::new(Span::test_data()), + ) { + Err(err) => panic!("test eval error in `{}`: {:?}", example.example, err), + Ok(result) => { + let result = result.into_value(Span::test_data()); + println!("input: {}", example.example); + println!("result: {:?}", result); + println!("done: {:?}", start.elapsed()); + + // Note. Value implements PartialEq for Bool, Int, Float, String and Block + // If the command you are testing requires to compare another case, then + // you need to define its equality in the Value struct + if let Some(expected) = example.result { + if result != expected { + panic!( + "the example result is different to expected value: {:?} != {:?}", + result, expected + ) + } + } + } + } + } +} diff --git a/crates/nu-command/src/dataframe/values/mod.rs b/crates/nu-command/src/dataframe/values/mod.rs new file mode 100644 index 0000000000..b952137a0c --- /dev/null +++ b/crates/nu-command/src/dataframe/values/mod.rs @@ -0,0 +1,6 @@ +mod nu_dataframe; +mod nu_groupby; +pub mod utils; + +pub use nu_dataframe::{Axis, Column, NuDataFrame}; +pub use nu_groupby::NuGroupBy; diff --git a/crates/nu-command/src/dataframe/values/nu_dataframe/between_values.rs b/crates/nu-command/src/dataframe/values/nu_dataframe/between_values.rs new file mode 100644 index 0000000000..65a4ba218c --- /dev/null +++ b/crates/nu-command/src/dataframe/values/nu_dataframe/between_values.rs @@ -0,0 +1,630 @@ +use super::{operations::Axis, NuDataFrame}; + +use nu_protocol::{ast::Operator, span, ShellError, Span, Spanned, Value}; +use num::Zero; +use polars::prelude::{ + BooleanType, ChunkCompare, ChunkedArray, DataType, Float64Type, Int64Type, IntoSeries, + NumOpsDispatchChecked, PolarsError, Series, +}; +use std::ops::{Add, BitAnd, BitOr, Div, Mul, Sub}; + +pub(super) fn between_dataframes( + operator: Spanned, + left: &Value, + lhs: &NuDataFrame, + right: &Value, + rhs: &NuDataFrame, +) -> Result { + let operation_span = span(&[left.span()?, right.span()?]); + match operator.item { + Operator::Plus => match lhs.append_df(rhs, Axis::Row, operation_span) { + Ok(df) => Ok(df.into_value(operation_span)), + Err(e) => Err(e), + }, + _ => Err(ShellError::OperatorMismatch { + op_span: operator.span, + lhs_ty: left.get_type(), + lhs_span: left.span()?, + rhs_ty: right.get_type(), + rhs_span: right.span()?, + }), + } +} + +pub(super) fn compute_between_series( + operator: Spanned, + left: &Value, + lhs: &Series, + right: &Value, + rhs: &Series, +) -> Result { + let operation_span = span(&[left.span()?, right.span()?]); + match operator.item { + Operator::Plus => { + let mut res = lhs + rhs; + let name = format!("sum_{}_{}", lhs.name(), rhs.name()); + res.rename(&name); + NuDataFrame::series_to_value(res, operation_span) + } + Operator::Minus => { + let mut res = lhs - rhs; + let name = format!("sub_{}_{}", lhs.name(), rhs.name()); + res.rename(&name); + NuDataFrame::series_to_value(res, operation_span) + } + Operator::Multiply => { + let mut res = lhs * rhs; + let name = format!("mul_{}_{}", lhs.name(), rhs.name()); + res.rename(&name); + NuDataFrame::series_to_value(res, operation_span) + } + Operator::Divide => { + let res = lhs.checked_div(rhs); + match res { + Ok(mut res) => { + let name = format!("div_{}_{}", lhs.name(), rhs.name()); + res.rename(&name); + NuDataFrame::series_to_value(res, operation_span) + } + Err(e) => Err(ShellError::SpannedLabeledError( + "Division error".into(), + e.to_string(), + right.span()?, + )), + } + } + Operator::Equal => { + let mut res = Series::equal(lhs, rhs).into_series(); + let name = format!("eq_{}_{}", lhs.name(), rhs.name()); + res.rename(&name); + NuDataFrame::series_to_value(res, operation_span) + } + Operator::NotEqual => { + let mut res = Series::not_equal(lhs, rhs).into_series(); + let name = format!("neq_{}_{}", lhs.name(), rhs.name()); + res.rename(&name); + NuDataFrame::series_to_value(res, operation_span) + } + Operator::LessThan => { + let mut res = Series::lt(lhs, rhs).into_series(); + let name = format!("lt_{}_{}", lhs.name(), rhs.name()); + res.rename(&name); + NuDataFrame::series_to_value(res, operation_span) + } + Operator::LessThanOrEqual => { + let mut res = Series::lt_eq(lhs, rhs).into_series(); + let name = format!("lte_{}_{}", lhs.name(), rhs.name()); + res.rename(&name); + NuDataFrame::series_to_value(res, operation_span) + } + Operator::GreaterThan => { + let mut res = Series::gt(lhs, rhs).into_series(); + let name = format!("gt_{}_{}", lhs.name(), rhs.name()); + res.rename(&name); + NuDataFrame::series_to_value(res, operation_span) + } + Operator::GreaterThanOrEqual => { + let mut res = Series::gt_eq(lhs, rhs).into_series(); + let name = format!("gte_{}_{}", lhs.name(), rhs.name()); + res.rename(&name); + NuDataFrame::series_to_value(res, operation_span) + } + Operator::And => match lhs.dtype() { + DataType::Boolean => { + let lhs_cast = lhs.bool(); + let rhs_cast = rhs.bool(); + + match (lhs_cast, rhs_cast) { + (Ok(l), Ok(r)) => { + let mut res = l.bitand(r).into_series(); + let name = format!("and_{}_{}", lhs.name(), rhs.name()); + res.rename(&name); + NuDataFrame::series_to_value(res, operation_span) + } + _ => Err(ShellError::SpannedLabeledError( + "Incompatible types".into(), + "unable to cast to boolean".into(), + right.span()?, + )), + } + } + _ => Err(ShellError::IncompatibleParametersSingle( + format!( + "Operation {} can only be done with boolean values", + operator.item + ), + operation_span, + )), + }, + Operator::Or => match lhs.dtype() { + DataType::Boolean => { + let lhs_cast = lhs.bool(); + let rhs_cast = rhs.bool(); + + match (lhs_cast, rhs_cast) { + (Ok(l), Ok(r)) => { + let mut res = l.bitor(r).into_series(); + let name = format!("or_{}_{}", lhs.name(), rhs.name()); + res.rename(&name); + NuDataFrame::series_to_value(res, operation_span) + } + _ => Err(ShellError::SpannedLabeledError( + "Incompatible types".into(), + "unable to cast to boolean".into(), + right.span()?, + )), + } + } + _ => Err(ShellError::IncompatibleParametersSingle( + format!( + "Operation {} can only be done with boolean values", + operator.item + ), + operation_span, + )), + }, + _ => Err(ShellError::OperatorMismatch { + op_span: operator.span, + lhs_ty: left.get_type(), + lhs_span: left.span()?, + rhs_ty: right.get_type(), + rhs_span: right.span()?, + }), + } +} + +pub(super) fn compute_series_single_value( + operator: Spanned, + left: &Value, + lhs: &NuDataFrame, + right: &Value, +) -> Result { + if !lhs.is_series() { + return Err(ShellError::OperatorMismatch { + op_span: operator.span, + lhs_ty: left.get_type(), + lhs_span: left.span()?, + rhs_ty: right.get_type(), + rhs_span: right.span()?, + }); + } + + let lhs_span = left.span()?; + let lhs = lhs.as_series(lhs_span)?; + + match operator.item { + Operator::Plus => match &right { + Value::Int { val, .. } => { + compute_series_i64(&lhs, *val, >::add, lhs_span) + } + Value::Float { val, .. } => { + compute_series_decimal(&lhs, *val, >::add, lhs_span) + } + _ => Err(ShellError::OperatorMismatch { + op_span: operator.span, + lhs_ty: left.get_type(), + lhs_span: left.span()?, + rhs_ty: right.get_type(), + rhs_span: right.span()?, + }), + }, + Operator::Minus => match &right { + Value::Int { val, .. } => { + compute_series_i64(&lhs, *val, >::sub, lhs_span) + } + Value::Float { val, .. } => { + compute_series_decimal(&lhs, *val, >::sub, lhs_span) + } + _ => Err(ShellError::OperatorMismatch { + op_span: operator.span, + lhs_ty: left.get_type(), + lhs_span: left.span()?, + rhs_ty: right.get_type(), + rhs_span: right.span()?, + }), + }, + Operator::Multiply => match &right { + Value::Int { val, .. } => { + compute_series_i64(&lhs, *val, >::mul, lhs_span) + } + Value::Float { val, .. } => { + compute_series_decimal(&lhs, *val, >::mul, lhs_span) + } + _ => Err(ShellError::OperatorMismatch { + op_span: operator.span, + lhs_ty: left.get_type(), + lhs_span: left.span()?, + rhs_ty: right.get_type(), + rhs_span: right.span()?, + }), + }, + Operator::Divide => match &right { + Value::Int { val, span } => { + if *val == 0 { + Err(ShellError::DivisionByZero(*span)) + } else { + compute_series_i64(&lhs, *val, >::div, lhs_span) + } + } + Value::Float { val, span } => { + if val.is_zero() { + Err(ShellError::DivisionByZero(*span)) + } else { + compute_series_decimal(&lhs, *val, >::div, lhs_span) + } + } + _ => Err(ShellError::OperatorMismatch { + op_span: operator.span, + lhs_ty: left.get_type(), + lhs_span: left.span()?, + rhs_ty: right.get_type(), + rhs_span: right.span()?, + }), + }, + Operator::Equal => match &right { + Value::Int { val, .. } => compare_series_i64(&lhs, *val, ChunkedArray::equal, lhs_span), + Value::Float { val, .. } => { + compare_series_decimal(&lhs, *val, ChunkedArray::equal, lhs_span) + } + _ => Err(ShellError::OperatorMismatch { + op_span: operator.span, + lhs_ty: left.get_type(), + lhs_span: left.span()?, + rhs_ty: right.get_type(), + rhs_span: right.span()?, + }), + }, + Operator::NotEqual => match &right { + Value::Int { val, .. } => { + compare_series_i64(&lhs, *val, ChunkedArray::not_equal, lhs_span) + } + Value::Float { val, .. } => { + compare_series_decimal(&lhs, *val, ChunkedArray::not_equal, lhs_span) + } + _ => Err(ShellError::OperatorMismatch { + op_span: operator.span, + lhs_ty: left.get_type(), + lhs_span: left.span()?, + rhs_ty: right.get_type(), + rhs_span: right.span()?, + }), + }, + Operator::LessThan => match &right { + Value::Int { val, .. } => compare_series_i64(&lhs, *val, ChunkedArray::lt, lhs_span), + Value::Float { val, .. } => { + compare_series_decimal(&lhs, *val, ChunkedArray::lt, lhs_span) + } + _ => Err(ShellError::OperatorMismatch { + op_span: operator.span, + lhs_ty: left.get_type(), + lhs_span: left.span()?, + rhs_ty: right.get_type(), + rhs_span: right.span()?, + }), + }, + Operator::LessThanOrEqual => match &right { + Value::Int { val, .. } => compare_series_i64(&lhs, *val, ChunkedArray::lt_eq, lhs_span), + Value::Float { val, .. } => { + compare_series_decimal(&lhs, *val, ChunkedArray::lt_eq, lhs_span) + } + _ => Err(ShellError::OperatorMismatch { + op_span: operator.span, + lhs_ty: left.get_type(), + lhs_span: left.span()?, + rhs_ty: right.get_type(), + rhs_span: right.span()?, + }), + }, + Operator::GreaterThan => match &right { + Value::Int { val, .. } => compare_series_i64(&lhs, *val, ChunkedArray::gt, lhs_span), + Value::Float { val, .. } => { + compare_series_decimal(&lhs, *val, ChunkedArray::gt, lhs_span) + } + _ => Err(ShellError::OperatorMismatch { + op_span: operator.span, + lhs_ty: left.get_type(), + lhs_span: left.span()?, + rhs_ty: right.get_type(), + rhs_span: right.span()?, + }), + }, + Operator::GreaterThanOrEqual => match &right { + Value::Int { val, .. } => compare_series_i64(&lhs, *val, ChunkedArray::gt_eq, lhs_span), + Value::Float { val, .. } => { + compare_series_decimal(&lhs, *val, ChunkedArray::gt_eq, lhs_span) + } + _ => Err(ShellError::OperatorMismatch { + op_span: operator.span, + lhs_ty: left.get_type(), + lhs_span: left.span()?, + rhs_ty: right.get_type(), + rhs_span: right.span()?, + }), + }, + Operator::Contains => match &right { + Value::String { val, .. } => contains_series_pat(&lhs, val, lhs_span), + _ => Err(ShellError::OperatorMismatch { + op_span: operator.span, + lhs_ty: left.get_type(), + lhs_span: left.span()?, + rhs_ty: right.get_type(), + rhs_span: right.span()?, + }), + }, + _ => Err(ShellError::OperatorMismatch { + op_span: operator.span, + lhs_ty: left.get_type(), + lhs_span: left.span()?, + rhs_ty: right.get_type(), + rhs_span: right.span()?, + }), + } +} + +fn compute_series_i64(series: &Series, val: i64, f: F, span: Span) -> Result +where + F: Fn(ChunkedArray, i64) -> ChunkedArray, +{ + match series.dtype() { + DataType::UInt32 | DataType::Int32 | DataType::UInt64 => { + let to_i64 = series.cast(&DataType::Int64); + + match to_i64 { + Ok(series) => { + let casted = series.i64(); + compute_casted_i64(casted, val, f, span) + } + Err(e) => Err(ShellError::SpannedLabeledError( + "Unable to cast to i64".into(), + e.to_string(), + span, + )), + } + } + DataType::Int64 => { + let casted = series.i64(); + compute_casted_i64(casted, val, f, span) + } + _ => Err(ShellError::SpannedLabeledError( + "Incorrect type".into(), + format!( + "Series of type {} can not be used for operations with an i64 value", + series.dtype() + ), + span, + )), + } +} + +fn compute_casted_i64( + casted: Result<&ChunkedArray, PolarsError>, + val: i64, + f: F, + span: Span, +) -> Result +where + F: Fn(ChunkedArray, i64) -> ChunkedArray, +{ + match casted { + Ok(casted) => { + let res = f(casted.clone(), val); + let res = res.into_series(); + NuDataFrame::series_to_value(res, span) + } + Err(e) => Err(ShellError::SpannedLabeledError( + "Unable to cast to i64".into(), + e.to_string(), + span, + )), + } +} + +fn compute_series_decimal( + series: &Series, + val: f64, + f: F, + span: Span, +) -> Result +where + F: Fn(ChunkedArray, f64) -> ChunkedArray, +{ + match series.dtype() { + DataType::Float32 => { + let to_f64 = series.cast(&DataType::Float64); + + match to_f64 { + Ok(series) => { + let casted = series.f64(); + compute_casted_f64(casted, val, f, span) + } + Err(e) => Err(ShellError::SpannedLabeledError( + "Unable to cast to f64".into(), + e.to_string(), + span, + )), + } + } + DataType::Float64 => { + let casted = series.f64(); + compute_casted_f64(casted, val, f, span) + } + _ => Err(ShellError::SpannedLabeledError( + "Incorrect type".into(), + format!( + "Series of type {} can not be used for operations with a decimal value", + series.dtype() + ), + span, + )), + } +} + +fn compute_casted_f64( + casted: Result<&ChunkedArray, PolarsError>, + val: f64, + f: F, + span: Span, +) -> Result +where + F: Fn(ChunkedArray, f64) -> ChunkedArray, +{ + match casted { + Ok(casted) => { + let res = f(casted.clone(), val); + let res = res.into_series(); + NuDataFrame::series_to_value(res, span) + } + Err(e) => Err(ShellError::SpannedLabeledError( + "Unable to cast to f64".into(), + e.to_string(), + span, + )), + } +} + +fn compare_series_i64(series: &Series, val: i64, f: F, span: Span) -> Result +where + F: Fn(&ChunkedArray, i64) -> ChunkedArray, +{ + match series.dtype() { + DataType::UInt32 | DataType::Int32 | DataType::UInt64 => { + let to_i64 = series.cast(&DataType::Int64); + + match to_i64 { + Ok(series) => { + let casted = series.i64(); + compare_casted_i64(casted, val, f, span) + } + Err(e) => Err(ShellError::SpannedLabeledError( + "Unable to cast to f64".into(), + e.to_string(), + span, + )), + } + } + DataType::Int64 => { + let casted = series.i64(); + compare_casted_i64(casted, val, f, span) + } + _ => Err(ShellError::SpannedLabeledError( + "Incorrect type".into(), + format!( + "Series of type {} can not be used for operations with an i64 value", + series.dtype() + ), + span, + )), + } +} + +fn compare_casted_i64( + casted: Result<&ChunkedArray, PolarsError>, + val: i64, + f: F, + span: Span, +) -> Result +where + F: Fn(&ChunkedArray, i64) -> ChunkedArray, +{ + match casted { + Ok(casted) => { + let res = f(casted, val); + let res = res.into_series(); + NuDataFrame::series_to_value(res, span) + } + Err(e) => Err(ShellError::SpannedLabeledError( + "Unable to cast to i64".into(), + e.to_string(), + span, + )), + } +} + +fn compare_series_decimal( + series: &Series, + val: f64, + f: F, + span: Span, +) -> Result +where + F: Fn(&ChunkedArray, f64) -> ChunkedArray, +{ + match series.dtype() { + DataType::Float32 => { + let to_f64 = series.cast(&DataType::Float64); + + match to_f64 { + Ok(series) => { + let casted = series.f64(); + compare_casted_f64(casted, val, f, span) + } + Err(e) => Err(ShellError::SpannedLabeledError( + "Unable to cast to i64".into(), + e.to_string(), + span, + )), + } + } + DataType::Float64 => { + let casted = series.f64(); + compare_casted_f64(casted, val, f, span) + } + _ => Err(ShellError::SpannedLabeledError( + "Incorrect type".into(), + format!( + "Series of type {} can not be used for operations with a decimal value", + series.dtype() + ), + span, + )), + } +} + +fn compare_casted_f64( + casted: Result<&ChunkedArray, PolarsError>, + val: f64, + f: F, + span: Span, +) -> Result +where + F: Fn(&ChunkedArray, f64) -> ChunkedArray, +{ + match casted { + Ok(casted) => { + let res = f(casted, val); + let res = res.into_series(); + NuDataFrame::series_to_value(res, span) + } + Err(e) => Err(ShellError::SpannedLabeledError( + "Unable to cast to f64".into(), + e.to_string(), + span, + )), + } +} + +fn contains_series_pat(series: &Series, pat: &str, span: Span) -> Result { + let casted = series.utf8(); + match casted { + Ok(casted) => { + let res = casted.contains(pat); + + match res { + Ok(res) => { + let res = res.into_series(); + NuDataFrame::series_to_value(res, span) + } + Err(e) => Err(ShellError::SpannedLabeledError( + "Error using contains".into(), + e.to_string(), + span, + )), + } + } + Err(e) => Err(ShellError::SpannedLabeledError( + "Unable to cast to string".into(), + e.to_string(), + span, + )), + } +} diff --git a/crates/nu-command/src/dataframe/values/nu_dataframe/conversion.rs b/crates/nu-command/src/dataframe/values/nu_dataframe/conversion.rs new file mode 100644 index 0000000000..5d6be647b9 --- /dev/null +++ b/crates/nu-command/src/dataframe/values/nu_dataframe/conversion.rs @@ -0,0 +1,623 @@ +use super::{DataFrameValue, NuDataFrame}; + +use chrono::{DateTime, FixedOffset, NaiveDateTime}; +use indexmap::map::{Entry, IndexMap}; +use nu_protocol::{ShellError, Span, Value}; +use polars::chunked_array::object::builder::ObjectChunkedBuilder; +use polars::chunked_array::ChunkedArray; +use polars::prelude::{ + DataFrame, DataType, DatetimeChunked, Int64Type, IntoSeries, NamedFrom, NewChunkedArray, + ObjectType, Series, +}; +use std::ops::{Deref, DerefMut}; + +const SECS_PER_DAY: i64 = 86_400; + +#[derive(Debug)] +pub struct Column { + name: String, + values: Vec, +} + +impl Column { + pub fn new(name: String, values: Vec) -> Self { + Self { name, values } + } + + pub fn new_empty(name: String) -> Self { + Self { + name, + values: Vec::new(), + } + } + + pub fn name(&self) -> &str { + self.name.as_str() + } + + //pub fn iter(&self) -> impl Iterator { + // self.values.iter() + //} +} + +impl IntoIterator for Column { + type Item = Value; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.values.into_iter() + } +} + +impl Deref for Column { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.values + } +} + +impl DerefMut for Column { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.values + } +} + +#[derive(Debug)] +pub enum InputType { + Integer, + Float, + String, + Boolean, + Object, + Date, + Duration, +} + +#[derive(Debug)] +pub struct TypedColumn { + column: Column, + column_type: Option, +} + +impl TypedColumn { + fn new_empty(name: String) -> Self { + Self { + column: Column::new_empty(name), + column_type: None, + } + } +} + +impl Deref for TypedColumn { + type Target = Column; + + fn deref(&self) -> &Self::Target { + &self.column + } +} + +impl DerefMut for TypedColumn { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.column + } +} + +pub type ColumnMap = IndexMap; + +pub fn create_column( + series: &Series, + from_row: usize, + to_row: usize, + span: Span, +) -> Result { + let size = to_row - from_row; + match series.dtype() { + DataType::Null => { + let values = std::iter::repeat(Value::Nothing { span }) + .take(size) + .collect::>(); + + Ok(Column::new(series.name().into(), values)) + } + DataType::UInt8 => { + let casted = series.u8().map_err(|e| { + ShellError::LabeledError("Error casting column to u8".into(), e.to_string()) + })?; + let values = casted + .into_iter() + .skip(from_row) + .take(size) + .map(|v| match v { + Some(a) => Value::Int { + val: a as i64, + span, + }, + None => Value::Nothing { span }, + }) + .collect::>(); + + Ok(Column::new(casted.name().into(), values)) + } + DataType::UInt16 => { + let casted = series.u16().map_err(|e| { + ShellError::LabeledError("Error casting column to u16".into(), e.to_string()) + })?; + let values = casted + .into_iter() + .skip(from_row) + .take(size) + .map(|v| match v { + Some(a) => Value::Int { + val: a as i64, + span, + }, + None => Value::Nothing { span }, + }) + .collect::>(); + + Ok(Column::new(casted.name().into(), values)) + } + DataType::UInt32 => { + let casted = series.u32().map_err(|e| { + ShellError::LabeledError("Error casting column to u32".into(), e.to_string()) + })?; + let values = casted + .into_iter() + .skip(from_row) + .take(size) + .map(|v| match v { + Some(a) => Value::Int { + val: a as i64, + span, + }, + None => Value::Nothing { span }, + }) + .collect::>(); + + Ok(Column::new(casted.name().into(), values)) + } + DataType::UInt64 => { + let casted = series.u64().map_err(|e| { + ShellError::LabeledError("Error casting column to u64".into(), e.to_string()) + })?; + let values = casted + .into_iter() + .skip(from_row) + .take(size) + .map(|v| match v { + Some(a) => Value::Int { + val: a as i64, + span, + }, + None => Value::Nothing { span }, + }) + .collect::>(); + + Ok(Column::new(casted.name().into(), values)) + } + DataType::Int8 => { + let casted = series.i8().map_err(|e| { + ShellError::LabeledError("Error casting column to i8".into(), e.to_string()) + })?; + let values = casted + .into_iter() + .skip(from_row) + .take(size) + .map(|v| match v { + Some(a) => Value::Int { + val: a as i64, + span, + }, + None => Value::Nothing { span }, + }) + .collect::>(); + + Ok(Column::new(casted.name().into(), values)) + } + DataType::Int16 => { + let casted = series.i16().map_err(|e| { + ShellError::LabeledError("Error casting column to i16".into(), e.to_string()) + })?; + let values = casted + .into_iter() + .skip(from_row) + .take(size) + .map(|v| match v { + Some(a) => Value::Int { + val: a as i64, + span, + }, + None => Value::Nothing { span }, + }) + .collect::>(); + + Ok(Column::new(casted.name().into(), values)) + } + DataType::Int32 => { + let casted = series.i32().map_err(|e| { + ShellError::LabeledError("Error casting column to i32".into(), e.to_string()) + })?; + let values = casted + .into_iter() + .skip(from_row) + .take(size) + .map(|v| match v { + Some(a) => Value::Int { + val: a as i64, + span, + }, + None => Value::Nothing { span }, + }) + .collect::>(); + + Ok(Column::new(casted.name().into(), values)) + } + DataType::Int64 => { + let casted = series.i64().map_err(|e| { + ShellError::LabeledError("Error casting column to i64".into(), e.to_string()) + })?; + let values = casted + .into_iter() + .skip(from_row) + .take(size) + .map(|v| match v { + Some(a) => Value::Int { + val: a as i64, + span, + }, + None => Value::Nothing { span }, + }) + .collect::>(); + + Ok(Column::new(casted.name().into(), values)) + } + DataType::Float32 => { + let casted = series.f32().map_err(|e| { + ShellError::LabeledError("Error casting column to f32".into(), e.to_string()) + })?; + let values = casted + .into_iter() + .skip(from_row) + .take(size) + .map(|v| match v { + Some(a) => Value::Float { + val: a as f64, + span, + }, + None => Value::Nothing { span }, + }) + .collect::>(); + + Ok(Column::new(casted.name().into(), values)) + } + DataType::Float64 => { + let casted = series.f64().map_err(|e| { + ShellError::LabeledError("Error casting column to f64".into(), e.to_string()) + })?; + let values = casted + .into_iter() + .skip(from_row) + .take(size) + .map(|v| match v { + Some(a) => Value::Float { val: a, span }, + None => Value::Nothing { span }, + }) + .collect::>(); + + Ok(Column::new(casted.name().into(), values)) + } + DataType::Boolean => { + let casted = series.bool().map_err(|e| { + ShellError::LabeledError("Error casting column to bool".into(), e.to_string()) + })?; + + let values = casted + .into_iter() + .skip(from_row) + .take(size) + .map(|v| match v { + Some(a) => Value::Bool { val: a, span }, + None => Value::Nothing { span }, + }) + .collect::>(); + + Ok(Column::new(casted.name().into(), values)) + } + DataType::Utf8 => { + let casted = series.utf8().map_err(|e| { + ShellError::LabeledError("Error casting column to string".into(), e.to_string()) + })?; + + let values = casted + .into_iter() + .skip(from_row) + .take(size) + .map(|v| match v { + Some(a) => Value::String { + val: a.into(), + span, + }, + None => Value::Nothing { span }, + }) + .collect::>(); + + Ok(Column::new(casted.name().into(), values)) + } + DataType::Object(x) => { + let casted = series + .as_any() + .downcast_ref::>>(); + + match casted { + None => Err(ShellError::LabeledError( + "Error casting object from series".into(), + format!("Object not supported for conversion: {}", x), + )), + Some(ca) => { + let values = ca + .into_iter() + .skip(from_row) + .take(size) + .map(|v| match v { + Some(a) => a.get_value(), + None => Value::Nothing { span }, + }) + .collect::>(); + + Ok(Column::new(ca.name().into(), values)) + } + } + } + DataType::Date => { + let casted = series.date().map_err(|e| { + ShellError::LabeledError("Error casting column to date".into(), e.to_string()) + })?; + + let values = casted + .into_iter() + .skip(from_row) + .take(size) + .map(|v| match v { + Some(a) => { + // elapsed time in day since 1970-01-01 + let seconds = a as i64 * SECS_PER_DAY; + let naive_datetime = NaiveDateTime::from_timestamp(seconds, 0); + + // Zero length offset + let offset = FixedOffset::east(0); + let datetime = DateTime::::from_utc(naive_datetime, offset); + + Value::Date { + val: datetime, + span, + } + } + None => Value::Nothing { span }, + }) + .collect::>(); + + Ok(Column::new(casted.name().into(), values)) + } + DataType::Datetime => { + let casted = series.datetime().map_err(|e| { + ShellError::LabeledError("Error casting column to datetime".into(), e.to_string()) + })?; + + let values = casted + .into_iter() + .skip(from_row) + .take(size) + .map(|v| match v { + Some(a) => { + // elapsed time in milliseconds since 1970-01-01 + let seconds = a / 1000; + let naive_datetime = NaiveDateTime::from_timestamp(seconds, 0); + + // Zero length offset + let offset = FixedOffset::east(0); + let datetime = DateTime::::from_utc(naive_datetime, offset); + + Value::Date { + val: datetime, + span, + } + } + None => Value::Nothing { span }, + }) + .collect::>(); + + Ok(Column::new(casted.name().into(), values)) + } + DataType::Time => { + let casted = series.time().map_err(|e| { + ShellError::LabeledError("Error casting column to time".into(), e.to_string()) + })?; + + let values = casted + .into_iter() + .skip(from_row) + .take(size) + .map(|v| match v { + Some(nanoseconds) => Value::Duration { + val: nanoseconds, + span, + }, + None => Value::Nothing { span }, + }) + .collect::>(); + + Ok(Column::new(casted.name().into(), values)) + } + e => Err(ShellError::LabeledError( + "Error creating Dataframe".into(), + format!("Value not supported in nushell: {}", e), + )), + } +} + +// Adds a separator to the vector of values using the column names from the +// dataframe to create the Values Row +pub fn add_separator(values: &mut Vec, df: &DataFrame, span: Span) { + let mut cols = vec![]; + let mut vals = vec![]; + + for name in df.get_column_names() { + cols.push(name.to_string()); + vals.push(Value::String { + val: "...".into(), + span, + }) + } + + let extra_record = Value::Record { cols, vals, span }; + + values.push(extra_record); +} + +// Inserting the values found in a Value::List +pub fn insert_record( + column_values: &mut ColumnMap, + cols: &[String], + values: &[Value], +) -> Result<(), ShellError> { + for (col, value) in cols.iter().zip(values.iter()) { + insert_value(value.clone(), col.clone(), column_values)?; + } + + Ok(()) +} + +pub fn insert_value( + value: Value, + key: String, + column_values: &mut ColumnMap, +) -> Result<(), ShellError> { + let col_val = match column_values.entry(key.clone()) { + Entry::Vacant(entry) => entry.insert(TypedColumn::new_empty(key)), + Entry::Occupied(entry) => entry.into_mut(), + }; + + // Checking that the type for the value is the same + // for the previous value in the column + if col_val.values.is_empty() { + match &value { + Value::Int { .. } => { + col_val.column_type = Some(InputType::Integer); + } + Value::Float { .. } => { + col_val.column_type = Some(InputType::Float); + } + Value::String { .. } => { + col_val.column_type = Some(InputType::String); + } + Value::Bool { .. } => { + col_val.column_type = Some(InputType::Boolean); + } + Value::Date { .. } => { + col_val.column_type = Some(InputType::Date); + } + Value::Duration { .. } => { + col_val.column_type = Some(InputType::Duration); + } + _ => col_val.column_type = Some(InputType::Object), + } + col_val.values.push(value); + } else { + let prev_value = &col_val.values[col_val.values.len() - 1]; + + match (&prev_value, &value) { + (Value::Int { .. }, Value::Int { .. }) + | (Value::Float { .. }, Value::Float { .. }) + | (Value::String { .. }, Value::String { .. }) + | (Value::Bool { .. }, Value::Bool { .. }) + | (Value::Date { .. }, Value::Date { .. }) + | (Value::Duration { .. }, Value::Duration { .. }) => col_val.values.push(value), + _ => { + col_val.column_type = Some(InputType::Object); + col_val.values.push(value); + } + } + } + + Ok(()) +} + +// The ColumnMap has the parsed data from the StreamInput +// This data can be used to create a Series object that can initialize +// the dataframe based on the type of data that is found +pub fn from_parsed_columns(column_values: ColumnMap) -> Result { + let mut df_series: Vec = Vec::new(); + for (name, column) in column_values { + if let Some(column_type) = &column.column_type { + match column_type { + InputType::Float => { + let series_values: Result, _> = + column.values.iter().map(|v| v.as_f64()).collect(); + let series = Series::new(&name, series_values?); + df_series.push(series) + } + InputType::Integer => { + let series_values: Result, _> = + column.values.iter().map(|v| v.as_i64()).collect(); + let series = Series::new(&name, series_values?); + df_series.push(series) + } + InputType::String => { + let series_values: Result, _> = + column.values.iter().map(|v| v.as_string()).collect(); + let series = Series::new(&name, series_values?); + df_series.push(series) + } + InputType::Boolean => { + let series_values: Result, _> = + column.values.iter().map(|v| v.as_bool()).collect(); + let series = Series::new(&name, series_values?); + df_series.push(series) + } + InputType::Object => { + let mut builder = + ObjectChunkedBuilder::::new(&name, column.values.len()); + + for v in &column.values { + builder.append_value(DataFrameValue::new(v.clone())); + } + + let res = builder.finish(); + df_series.push(res.into_series()) + } + InputType::Date => { + let it = column.values.iter().map(|v| { + if let Value::Date { val, .. } = &v { + Some(val.timestamp_millis()) + } else { + None + } + }); + + let res: DatetimeChunked = + ChunkedArray::::new_from_opt_iter(&name, it).into(); + + df_series.push(res.into_series()) + } + InputType::Duration => { + let it = column.values.iter().map(|v| { + if let Value::Duration { val, .. } = &v { + Some(*val) + } else { + None + } + }); + + let res = ChunkedArray::::new_from_opt_iter(&name, it); + + df_series.push(res.into_series()) + } + } + } + } + + DataFrame::new(df_series) + .map(NuDataFrame::new) + .map_err(|e| ShellError::LabeledError("Error creating dataframe".into(), e.to_string())) +} diff --git a/crates/nu-command/src/dataframe/values/nu_dataframe/custom_value.rs b/crates/nu-command/src/dataframe/values/nu_dataframe/custom_value.rs new file mode 100644 index 0000000000..67fd59fd85 --- /dev/null +++ b/crates/nu-command/src/dataframe/values/nu_dataframe/custom_value.rs @@ -0,0 +1,69 @@ +use super::NuDataFrame; +use nu_protocol::{ast::Operator, CustomValue, ShellError, Span, Value}; + +// CustomValue implementation for NuDataFrame +impl CustomValue for NuDataFrame { + fn typetag_name(&self) -> &'static str { + "dataframe" + } + + fn typetag_deserialize(&self) { + unimplemented!("typetag_deserialize") + } + + fn clone_value(&self, span: nu_protocol::Span) -> Value { + let cloned = NuDataFrame(self.0.clone()); + + Value::CustomValue { + val: Box::new(cloned), + span, + } + } + + fn value_string(&self) -> String { + self.typetag_name().to_string() + } + + fn to_base_value(&self, span: Span) -> Result { + let vals = self.print(span)?; + + Ok(Value::List { vals, span }) + } + + fn to_json(&self) -> nu_json::Value { + nu_json::Value::Null + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn follow_path_int(&self, count: usize, span: Span) -> Result { + self.get_value(count, span) + } + + fn follow_path_string(&self, column_name: String, span: Span) -> Result { + let column = self.column(&column_name, span)?; + Ok(column.into_value(span)) + } + + fn partial_cmp(&self, other: &Value) -> Option { + match other { + Value::CustomValue { val, .. } => val + .as_any() + .downcast_ref::() + .and_then(|other| self.is_equal(other)), + _ => None, + } + } + + fn operation( + &self, + lhs_span: Span, + operator: Operator, + op: Span, + right: &Value, + ) -> Result { + self.compute_with_value(lhs_span, operator, op, right) + } +} diff --git a/crates/nu-command/src/dataframe/values/nu_dataframe/mod.rs b/crates/nu-command/src/dataframe/values/nu_dataframe/mod.rs new file mode 100644 index 0000000000..ba7e246e0a --- /dev/null +++ b/crates/nu-command/src/dataframe/values/nu_dataframe/mod.rs @@ -0,0 +1,407 @@ +mod between_values; +mod conversion; +mod custom_value; +mod operations; + +pub use conversion::{Column, ColumnMap}; +pub use operations::Axis; + +use indexmap::map::IndexMap; +use nu_protocol::{did_you_mean, PipelineData, ShellError, Span, Value}; +use polars::prelude::{DataFrame, DataType, PolarsObject, Series}; +use serde::{Deserialize, Serialize}; +use std::{cmp::Ordering, fmt::Display, hash::Hasher}; + +use super::utils::DEFAULT_ROWS; + +// DataFrameValue is an encapsulation of Nushell Value that can be used +// to define the PolarsObject Trait. The polars object trait allows to +// create dataframes with mixed datatypes +#[derive(Clone, Debug)] +pub struct DataFrameValue(Value); + +impl DataFrameValue { + fn new(value: Value) -> Self { + Self(value) + } + + fn get_value(&self) -> Value { + self.0.clone() + } +} + +impl Display for DataFrameValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.get_type()) + } +} + +impl Default for DataFrameValue { + fn default() -> Self { + Self(Value::Nothing { + span: Span { start: 0, end: 0 }, + }) + } +} + +impl PartialEq for DataFrameValue { + fn eq(&self, other: &Self) -> bool { + self.0.partial_cmp(&other.0).map_or(false, Ordering::is_eq) + } +} +impl Eq for DataFrameValue {} + +impl std::hash::Hash for DataFrameValue { + fn hash(&self, state: &mut H) { + match &self.0 { + Value::Nothing { .. } => 0.hash(state), + Value::Int { val, .. } => val.hash(state), + Value::String { val, .. } => val.hash(state), + // TODO. Define hash for the rest of types + _ => {} + } + } +} + +impl PolarsObject for DataFrameValue { + fn type_name() -> &'static str { + "object" + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct NuDataFrame(DataFrame); + +impl AsRef for NuDataFrame { + fn as_ref(&self) -> &polars::prelude::DataFrame { + &self.0 + } +} + +impl AsMut for NuDataFrame { + fn as_mut(&mut self) -> &mut polars::prelude::DataFrame { + &mut self.0 + } +} + +impl NuDataFrame { + pub fn new(dataframe: DataFrame) -> Self { + Self(dataframe) + } + + fn default_value(span: Span) -> Value { + let dataframe = DataFrame::default(); + NuDataFrame::dataframe_into_value(dataframe, span) + } + + pub fn dataframe_into_value(dataframe: DataFrame, span: Span) -> Value { + Value::CustomValue { + val: Box::new(Self::new(dataframe)), + span, + } + } + + pub fn into_value(self, span: Span) -> Value { + Value::CustomValue { + val: Box::new(self), + span, + } + } + + pub fn series_to_value(series: Series, span: Span) -> Result { + match DataFrame::new(vec![series]) { + Ok(dataframe) => Ok(NuDataFrame::dataframe_into_value(dataframe, span)), + Err(e) => Err(ShellError::SpannedLabeledError( + "Error creating dataframe".into(), + e.to_string(), + span, + )), + } + } + + pub fn try_from_iter(iter: T) -> Result + where + T: Iterator, + { + // Dictionary to store the columnar data extracted from + // the input. During the iteration we check if the values + // have different type + let mut column_values: ColumnMap = IndexMap::new(); + + for value in iter { + match value { + Value::List { vals, .. } => { + let cols = (0..vals.len()) + .map(|i| format!("{}", i)) + .collect::>(); + + conversion::insert_record(&mut column_values, &cols, &vals)? + } + Value::Record { cols, vals, .. } => { + conversion::insert_record(&mut column_values, &cols, &vals)? + } + _ => { + let key = "0".to_string(); + conversion::insert_value(value, key, &mut column_values)? + } + } + } + + conversion::from_parsed_columns(column_values) + } + + pub fn try_from_series(columns: Vec, span: Span) -> Result { + let dataframe = DataFrame::new(columns).map_err(|e| { + ShellError::SpannedLabeledError( + "Error creating dataframe".into(), + format!("Unable to create DataFrame: {}", e), + span, + ) + })?; + + Ok(Self::new(dataframe)) + } + + pub fn try_from_columns(columns: Vec) -> Result { + let mut column_values: ColumnMap = IndexMap::new(); + + for column in columns { + let name = column.name().to_string(); + for value in column { + conversion::insert_value(value, name.clone(), &mut column_values)?; + } + } + + conversion::from_parsed_columns(column_values) + } + + pub fn try_from_value(value: Value) -> Result { + match value { + Value::CustomValue { val, span } => match val.as_any().downcast_ref::() { + Some(df) => Ok(NuDataFrame(df.0.clone())), + None => Err(ShellError::CantConvert( + "dataframe".into(), + "non-dataframe".into(), + span, + )), + }, + x => Err(ShellError::CantConvert( + "dataframe".into(), + x.get_type().to_string(), + x.span()?, + )), + } + } + + pub fn try_from_pipeline(input: PipelineData, span: Span) -> Result { + let value = input.into_value(span); + NuDataFrame::try_from_value(value) + } + + pub fn column(&self, column: &str, span: Span) -> Result { + let s = self.0.column(column).map_err(|_| { + let possibilities = self + .0 + .get_column_names() + .iter() + .map(|name| name.to_string()) + .collect::>(); + + let option = did_you_mean(&possibilities, column).unwrap_or_else(|| column.to_string()); + ShellError::DidYouMean(option, span) + })?; + + let dataframe = DataFrame::new(vec![s.clone()]).map_err(|e| { + ShellError::SpannedLabeledError("Error creating dataframe".into(), e.to_string(), span) + })?; + + Ok(Self(dataframe)) + } + + pub fn is_series(&self) -> bool { + self.0.width() == 1 + } + + pub fn as_series(&self, span: Span) -> Result { + if !self.is_series() { + return Err(ShellError::SpannedLabeledError( + "Error using as series".into(), + "dataframe has more than one column".into(), + span, + )); + } + + let series = self + .0 + .get_columns() + .get(0) + .expect("We have already checked that the width is 1"); + + Ok(series.clone()) + } + + pub fn get_value(&self, row: usize, span: Span) -> Result { + let series = self.as_series(span)?; + let column = conversion::create_column(&series, row, row + 1, span)?; + + if column.len() == 0 { + Err(ShellError::AccessBeyondEnd(series.len(), span)) + } else { + let value = column + .into_iter() + .next() + .expect("already checked there is a value"); + Ok(value) + } + } + + // Print is made out a head and if the dataframe is too large, then a tail + pub fn print(&self, span: Span) -> Result, ShellError> { + let df = &self.0; + let size: usize = 20; + + if df.height() > size { + let sample_size = size / 2; + let mut values = self.head(Some(sample_size), span)?; + conversion::add_separator(&mut values, df, span); + let remaining = df.height() - sample_size; + let tail_size = remaining.min(sample_size); + let mut tail_values = self.tail(Some(tail_size), span)?; + values.append(&mut tail_values); + + Ok(values) + } else { + Ok(self.head(Some(size), span)?) + } + } + + pub fn head(&self, rows: Option, span: Span) -> Result, ShellError> { + let to_row = rows.unwrap_or(5); + let values = self.to_rows(0, to_row, span)?; + + Ok(values) + } + + pub fn tail(&self, rows: Option, span: Span) -> Result, ShellError> { + let df = &self.0; + let to_row = df.height(); + let size = rows.unwrap_or(DEFAULT_ROWS); + let from_row = to_row.saturating_sub(size); + + let values = self.to_rows(from_row, to_row, span)?; + + Ok(values) + } + + pub fn to_rows( + &self, + from_row: usize, + to_row: usize, + span: Span, + ) -> Result, ShellError> { + let df = &self.0; + let upper_row = to_row.min(df.height()); + + let mut size: usize = 0; + let columns = self + .0 + .get_columns() + .iter() + .map( + |col| match conversion::create_column(col, from_row, upper_row, span) { + Ok(col) => { + size = col.len(); + Ok(col) + } + Err(e) => Err(e), + }, + ) + .collect::, ShellError>>()?; + + let mut iterators = columns + .into_iter() + .map(|col| (col.name().to_string(), col.into_iter())) + .collect::)>>(); + + let values = (0..size) + .into_iter() + .map(|_| { + let mut cols = vec![]; + let mut vals = vec![]; + + for (name, col) in &mut iterators { + cols.push(name.clone()); + + match col.next() { + Some(v) => vals.push(v), + None => vals.push(Value::Nothing { span }), + }; + } + + Value::Record { cols, vals, span } + }) + .collect::>(); + + Ok(values) + } + + // Dataframes are considered equal if they have the same shape, column name and values + pub fn is_equal(&self, other: &Self) -> Option { + if self.as_ref().width() == 0 { + // checking for empty dataframe + return None; + } + + if self.as_ref().get_column_names() != other.as_ref().get_column_names() { + // checking both dataframes share the same names + return None; + } + + if self.as_ref().height() != other.as_ref().height() { + // checking both dataframes have the same row size + return None; + } + + // sorting dataframe by the first column + let column_names = self.as_ref().get_column_names(); + let first_col = column_names + .get(0) + .expect("already checked that dataframe is different than 0"); + + // if unable to sort, then unable to compare + let lhs = match self.as_ref().sort(*first_col, false) { + Ok(df) => df, + Err(_) => return None, + }; + + let rhs = match other.as_ref().sort(*first_col, false) { + Ok(df) => df, + Err(_) => return None, + }; + + for name in self.as_ref().get_column_names() { + let self_series = lhs.column(name).expect("name from dataframe names"); + + let other_series = rhs + .column(name) + .expect("already checked that name in other"); + + let self_series = match self_series.dtype() { + // Casting needed to compare other numeric types with nushell numeric type. + // In nushell we only have i64 integer numeric types and any array created + // with nushell untagged primitives will be of type i64 + DataType::UInt32 => match self_series.cast(&DataType::Int64) { + Ok(series) => series, + Err(_) => return None, + }, + _ => self_series.clone(), + }; + + if !self_series.series_equal(other_series) { + return None; + } + } + + Some(Ordering::Equal) + } +} diff --git a/crates/nu-command/src/dataframe/values/nu_dataframe/operations.rs b/crates/nu-command/src/dataframe/values/nu_dataframe/operations.rs new file mode 100644 index 0000000000..95258c5da7 --- /dev/null +++ b/crates/nu-command/src/dataframe/values/nu_dataframe/operations.rs @@ -0,0 +1,208 @@ +use nu_protocol::{ast::Operator, ShellError, Span, Spanned, Value}; +use polars::prelude::{DataFrame, Series}; + +use super::between_values::{ + between_dataframes, compute_between_series, compute_series_single_value, +}; + +use super::NuDataFrame; + +pub enum Axis { + Row, + Column, +} + +impl NuDataFrame { + pub fn compute_with_value( + &self, + lhs_span: Span, + operator: Operator, + op_span: Span, + right: &Value, + ) -> Result { + match right { + Value::CustomValue { + val: rhs, + span: rhs_span, + } => { + let rhs = rhs.as_any().downcast_ref::().ok_or_else(|| { + ShellError::DowncastNotPossible( + "Unable to create dataframe".to_string(), + *rhs_span, + ) + })?; + + match (self.is_series(), rhs.is_series()) { + (true, true) => { + let lhs = &self + .as_series(lhs_span) + .expect("Already checked that is a series"); + let rhs = &rhs + .as_series(*rhs_span) + .expect("Already checked that is a series"); + + if lhs.dtype() != rhs.dtype() { + return Err(ShellError::IncompatibleParameters { + left_message: format!("datatype {}", lhs.dtype()), + left_span: lhs_span, + right_message: format!("datatype {}", lhs.dtype()), + right_span: *rhs_span, + }); + } + + if lhs.len() != rhs.len() { + return Err(ShellError::IncompatibleParameters { + left_message: format!("len {}", lhs.len()), + left_span: lhs_span, + right_message: format!("len {}", rhs.len()), + right_span: *rhs_span, + }); + } + + let op = Spanned { + item: operator, + span: op_span, + }; + + compute_between_series( + op, + &NuDataFrame::default_value(lhs_span), + lhs, + right, + rhs, + ) + } + _ => { + if self.0.height() != rhs.0.height() { + return Err(ShellError::IncompatibleParameters { + left_message: format!("rows {}", self.0.height()), + left_span: lhs_span, + right_message: format!("rows {}", rhs.0.height()), + right_span: *rhs_span, + }); + } + + let op = Spanned { + item: operator, + span: op_span, + }; + + between_dataframes( + op, + &NuDataFrame::default_value(lhs_span), + self, + right, + rhs, + ) + } + } + } + _ => { + let op = Spanned { + item: operator, + span: op_span, + }; + + compute_series_single_value(op, &NuDataFrame::default_value(lhs_span), self, right) + } + } + } + + pub fn append_df( + &self, + other: &NuDataFrame, + axis: Axis, + span: Span, + ) -> Result { + match axis { + Axis::Row => { + let mut columns: Vec<&str> = Vec::new(); + + let new_cols = self + .0 + .get_columns() + .iter() + .chain(other.0.get_columns()) + .map(|s| { + let name = if columns.contains(&s.name()) { + format!("{}_{}", s.name(), "x") + } else { + columns.push(s.name()); + s.name().to_string() + }; + + let mut series = s.clone(); + series.rename(&name); + series + }) + .collect::>(); + + let df_new = DataFrame::new(new_cols).map_err(|e| { + ShellError::SpannedLabeledError( + "Error creating dataframe".into(), + e.to_string(), + span, + ) + })?; + + Ok(NuDataFrame::new(df_new)) + } + Axis::Column => { + if self.0.width() != other.0.width() { + return Err(ShellError::IncompatibleParametersSingle( + "Dataframes with different number of columns".into(), + span, + )); + } + + if !self + .0 + .get_column_names() + .iter() + .all(|col| other.0.get_column_names().contains(col)) + { + return Err(ShellError::IncompatibleParametersSingle( + "Dataframes with different columns names".into(), + span, + )); + } + + let new_cols = self + .0 + .get_columns() + .iter() + .map(|s| { + let other_col = other + .0 + .column(s.name()) + .expect("Already checked that dataframes have same columns"); + + let mut tmp = s.clone(); + let res = tmp.append(other_col); + + match res { + Ok(s) => Ok(s.clone()), + Err(e) => Err({ + ShellError::SpannedLabeledError( + "Error appending dataframe".into(), + format!("Unable to append: {}", e), + span, + ) + }), + } + }) + .collect::, ShellError>>()?; + + let df_new = DataFrame::new(new_cols).map_err(|e| { + ShellError::SpannedLabeledError( + "Error appending dataframe".into(), + format!("Unable to append dataframes: {}", e), + span, + ) + })?; + + Ok(NuDataFrame::new(df_new)) + } + } + } +} diff --git a/crates/nu-command/src/dataframe/values/nu_groupby/custom_value.rs b/crates/nu-command/src/dataframe/values/nu_groupby/custom_value.rs new file mode 100644 index 0000000000..f60a6bff7a --- /dev/null +++ b/crates/nu-command/src/dataframe/values/nu_groupby/custom_value.rs @@ -0,0 +1,44 @@ +use super::NuGroupBy; +use nu_protocol::{CustomValue, ShellError, Span, Value}; + +// CustomValue implementation for NuDataFrame +impl CustomValue for NuGroupBy { + fn typetag_name(&self) -> &'static str { + "groupby" + } + + fn typetag_deserialize(&self) { + unimplemented!("typetag_deserialize") + } + + fn clone_value(&self, span: nu_protocol::Span) -> Value { + let cloned = NuGroupBy { + dataframe: self.dataframe.clone(), + by: self.by.clone(), + groups: self.groups.clone(), + }; + + Value::CustomValue { + val: Box::new(cloned), + span, + } + } + + fn value_string(&self) -> String { + self.typetag_name().to_string() + } + + fn to_base_value(&self, span: Span) -> Result { + let vals = self.print(span)?; + + Ok(Value::List { vals, span }) + } + + fn to_json(&self) -> nu_json::Value { + nu_json::Value::Null + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} diff --git a/crates/nu-command/src/dataframe/values/nu_groupby/mod.rs b/crates/nu-command/src/dataframe/values/nu_groupby/mod.rs new file mode 100644 index 0000000000..5d3dbb5e41 --- /dev/null +++ b/crates/nu-command/src/dataframe/values/nu_groupby/mod.rs @@ -0,0 +1,89 @@ +mod custom_value; + +use nu_protocol::{PipelineData, ShellError, Span, Value}; +use polars::frame::groupby::{GroupBy, GroupTuples}; +use polars::prelude::DataFrame; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct NuGroupBy { + dataframe: DataFrame, + by: Vec, + groups: GroupTuples, +} + +impl NuGroupBy { + pub fn new(dataframe: DataFrame, by: Vec, groups: GroupTuples) -> Self { + NuGroupBy { + dataframe, + by, + groups, + } + } + + pub fn into_value(self, span: Span) -> Value { + Value::CustomValue { + val: Box::new(self), + span, + } + } + + pub fn try_from_value(value: Value) -> Result { + match value { + Value::CustomValue { val, span } => match val.as_any().downcast_ref::() { + Some(groupby) => Ok(NuGroupBy { + dataframe: groupby.dataframe.clone(), + by: groupby.by.clone(), + groups: groupby.groups.clone(), + }), + None => Err(ShellError::CantConvert( + "groupby".into(), + "non-dataframe".into(), + span, + )), + }, + x => Err(ShellError::CantConvert( + "groupby".into(), + x.get_type().to_string(), + x.span()?, + )), + } + } + + pub fn try_from_pipeline(input: PipelineData, span: Span) -> Result { + let value = input.into_value(span); + NuGroupBy::try_from_value(value) + } + + pub fn to_groupby(&self) -> Result { + let by = self.dataframe.select_series(&self.by).map_err(|e| { + ShellError::LabeledError("Error creating groupby".into(), e.to_string()) + })?; + + Ok(GroupBy::new(&self.dataframe, by, self.groups.clone(), None)) + } + + pub fn print(&self, span: Span) -> Result, ShellError> { + let values = self + .by + .iter() + .map(|col| { + let cols = vec!["group by".to_string()]; + let vals = vec![Value::String { + val: col.into(), + span, + }]; + + Value::Record { cols, vals, span } + }) + .collect::>(); + + Ok(values) + } +} + +impl AsRef for NuGroupBy { + fn as_ref(&self) -> &polars::prelude::DataFrame { + &self.dataframe + } +} diff --git a/crates/nu-command/src/dataframe/values/utils.rs b/crates/nu-command/src/dataframe/values/utils.rs new file mode 100644 index 0000000000..947c68a3dd --- /dev/null +++ b/crates/nu-command/src/dataframe/values/utils.rs @@ -0,0 +1,76 @@ +use nu_protocol::{span as span_join, ShellError, Span, Spanned, Value}; + +// Default value used when selecting rows from dataframe +pub const DEFAULT_ROWS: usize = 5; + +// Converts a Vec to a Vec> with a Span marking the whole +// location of the columns for error referencing +pub(crate) fn convert_columns( + columns: Vec, + span: Span, +) -> Result<(Vec>, Span), ShellError> { + // First column span + let mut col_span = columns + .get(0) + .ok_or_else(|| { + ShellError::SpannedLabeledError( + "Empty column list".into(), + "Empty list found for command".into(), + span, + ) + }) + .and_then(|v| v.span())?; + + let res = columns + .into_iter() + .map(|value| match value { + Value::String { val, span } => { + col_span = span_join(&[col_span, span]); + Ok(Spanned { item: val, span }) + } + _ => Err(ShellError::SpannedLabeledError( + "Incorrect column format".into(), + "Only string as column name".into(), + span, + )), + }) + .collect::>, _>>()?; + + Ok((res, col_span)) +} + +// Converts a Vec to a Vec with a Span marking the whole +// location of the columns for error referencing +pub(crate) fn convert_columns_string( + columns: Vec, + span: Span, +) -> Result<(Vec, Span), ShellError> { + // First column span + let mut col_span = columns + .get(0) + .ok_or_else(|| { + ShellError::SpannedLabeledError( + "Empty column list".into(), + "Empty list found for command".into(), + span, + ) + }) + .and_then(|v| v.span())?; + + let res = columns + .into_iter() + .map(|value| match value { + Value::String { val, span } => { + col_span = span_join(&[col_span, span]); + Ok(val) + } + _ => Err(ShellError::SpannedLabeledError( + "Incorrect column format".into(), + "Only string as column name".into(), + span, + )), + }) + .collect::, _>>()?; + + Ok((res, col_span)) +} diff --git a/crates/nu-command/src/date/date_.rs b/crates/nu-command/src/date/date_.rs new file mode 100644 index 0000000000..ed68f35b7b --- /dev/null +++ b/crates/nu-command/src/date/date_.rs @@ -0,0 +1,47 @@ +use nu_engine::get_full_help; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, IntoPipelineData, PipelineData, ShellError, Signature, Value, +}; + +#[derive(Clone)] +pub struct Date; + +impl Command for Date { + fn name(&self) -> &str { + "date" + } + + fn signature(&self) -> Signature { + Signature::build("date").category(Category::Date) + } + + fn usage(&self) -> &str { + "date" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + date(engine_state, stack, call) + } +} + +fn date( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, +) -> Result { + let head = call.head; + + Ok(Value::String { + val: get_full_help(&Date.signature(), &Date.examples(), engine_state, stack), + span: head, + } + .into_pipeline_data()) +} diff --git a/crates/nu-command/src/date/format.rs b/crates/nu-command/src/date/format.rs new file mode 100644 index 0000000000..b4c78f5081 --- /dev/null +++ b/crates/nu-command/src/date/format.rs @@ -0,0 +1,112 @@ +use chrono::Local; +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, Signature, Span, Spanned, SyntaxShape, Value, +}; + +use super::utils::{parse_date_from_string, unsupported_input_error}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "date format" + } + + fn signature(&self) -> Signature { + Signature::build("date format") + .required( + "format string", + SyntaxShape::String, + "the desired date format", + ) + .category(Category::Date) + } + + fn usage(&self) -> &str { + "Format a given date using the given format string." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let formatter: Spanned = call.req(engine_state, stack, 0)?; + input.map( + move |value| format_helper(value, &formatter, head), + engine_state.ctrlc.clone(), + ) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Format a given date using the given format string.", + example: "date format '%Y-%m-%d'", + result: None, + }, + Example { + description: "Format a given date using the given format string.", + example: r#"date format "%Y-%m-%d %H:%M:%S""#, + result: None, + }, + Example { + description: "Format a given date using the given format string.", + example: r#""2021-10-22 20:00:12 +01:00" | date format "%Y-%m-%d""#, + result: None, + }, + ] + } +} + +fn format_helper(value: Value, formatter: &Spanned, span: Span) -> Value { + match value { + Value::Date { val, span: _ } => Value::String { + val: val.format(formatter.item.as_str()).to_string(), + span, + }, + Value::String { + val, + span: val_span, + } => { + let dt = parse_date_from_string(val, val_span); + match dt { + Ok(x) => Value::String { + val: x.format(formatter.item.as_str()).to_string(), + span, + }, + Err(e) => e, + } + } + Value::Nothing { span: _ } => { + let dt = Local::now(); + Value::String { + val: dt + .with_timezone(dt.offset()) + .format(formatter.item.as_str()) + .to_string(), + span, + } + } + _ => unsupported_input_error(span), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/date/humanize.rs b/crates/nu-command/src/date/humanize.rs new file mode 100644 index 0000000000..f03d525970 --- /dev/null +++ b/crates/nu-command/src/date/humanize.rs @@ -0,0 +1,102 @@ +use crate::date::utils::parse_date_from_string; +use chrono::{DateTime, FixedOffset, Local}; +use chrono_humanize::HumanTime; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Span, Value}; +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "date humanize" + } + + fn signature(&self) -> Signature { + Signature::build("date humanize").category(Category::Date) + } + + fn usage(&self) -> &str { + "Print a 'humanized' format for the date, relative to now." + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + input.map(move |value| helper(value, head), engine_state.ctrlc.clone()) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Print a 'humanized' format for the date, relative to now.", + example: "date humanize", + result: Some(Value::String { + val: "now".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "Print a 'humanized' format for the date, relative to now.", + example: r#""2021-10-22 20:00:12 +01:00" | date humanize"#, + result: None, + }, + ] + } +} + +fn helper(value: Value, head: Span) -> Value { + match value { + Value::Nothing { span: _ } => { + let dt = Local::now(); + Value::String { + val: humanize_date(dt.with_timezone(dt.offset())), + span: head, + } + } + Value::String { + val, + span: val_span, + } => { + let dt = parse_date_from_string(val, val_span); + match dt { + Ok(x) => Value::String { + val: humanize_date(x), + span: head, + }, + Err(e) => e, + } + } + Value::Date { val, span: _ } => Value::String { + val: humanize_date(val), + span: head, + }, + _ => Value::Error { + error: ShellError::UnsupportedInput( + String::from("Date cannot be parsed / date format is not supported"), + head, + ), + }, + } +} + +fn humanize_date(dt: DateTime) -> String { + HumanTime::from(dt).to_string() +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/date/list_timezone.rs b/crates/nu-command/src/date/list_timezone.rs new file mode 100644 index 0000000000..1114398174 --- /dev/null +++ b/crates/nu-command/src/date/list_timezone.rs @@ -0,0 +1,44 @@ +use chrono_tz::TZ_VARIANTS; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, IntoInterruptiblePipelineData, PipelineData, Signature, Value}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "date list-timezone" + } + + fn signature(&self) -> Signature { + Signature::build("date list-timezone").category(Category::Date) + } + + fn usage(&self) -> &str { + "List supported time zones." + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let span = call.head; + + Ok(TZ_VARIANTS + .iter() + .map(move |x| { + let cols = vec!["timezone".into()]; + let vals = vec![Value::String { + val: x.name().to_string(), + span, + }]; + Value::Record { cols, vals, span } + }) + .into_iter() + .into_pipeline_data(engine_state.ctrlc.clone())) + } +} diff --git a/crates/nu-command/src/date/mod.rs b/crates/nu-command/src/date/mod.rs new file mode 100644 index 0000000000..6d0e3727e7 --- /dev/null +++ b/crates/nu-command/src/date/mod.rs @@ -0,0 +1,17 @@ +mod date_; +mod format; +mod humanize; +mod list_timezone; +mod now; +mod parser; +mod to_table; +mod to_timezone; +mod utils; + +pub use date_::Date; +pub use format::SubCommand as DateFormat; +pub use humanize::SubCommand as DateHumanize; +pub use list_timezone::SubCommand as DateListTimezones; +pub use now::SubCommand as DateNow; +pub use to_table::SubCommand as DateToTable; +pub use to_timezone::SubCommand as DateToTimezone; diff --git a/crates/nu-command/src/date/now.rs b/crates/nu-command/src/date/now.rs new file mode 100644 index 0000000000..4069938338 --- /dev/null +++ b/crates/nu-command/src/date/now.rs @@ -0,0 +1,36 @@ +use chrono::Local; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, IntoPipelineData, PipelineData, Signature, Value}; +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "date now" + } + + fn signature(&self) -> Signature { + Signature::build("date now").category(Category::Date) + } + + fn usage(&self) -> &str { + "Get the current date." + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let head = call.head; + let dt = Local::now(); + Ok(Value::Date { + val: dt.with_timezone(dt.offset()), + span: head, + } + .into_pipeline_data()) + } +} diff --git a/crates/nu-command/src/commands/generators/date/parser.rs b/crates/nu-command/src/date/parser.rs similarity index 100% rename from crates/nu-command/src/commands/generators/date/parser.rs rename to crates/nu-command/src/date/parser.rs diff --git a/crates/nu-command/src/date/to_table.rs b/crates/nu-command/src/date/to_table.rs new file mode 100644 index 0000000000..efdc40f22d --- /dev/null +++ b/crates/nu-command/src/date/to_table.rs @@ -0,0 +1,166 @@ +use crate::date::utils::{parse_date_from_string, unsupported_input_error}; +use chrono::{DateTime, Datelike, FixedOffset, Local, Timelike}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, Signature, Span, Value}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "date to-table" + } + + fn signature(&self) -> Signature { + Signature::build("date to-table").category(Category::Date) + } + + fn usage(&self) -> &str { + "Print the date in a structured table." + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + input.map(move |value| helper(value, head), engine_state.ctrlc.clone()) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Print the date in a structured table.", + example: "date to-table", + result: None, + }, + Example { + description: "Print the date in a structured table.", + example: "date now | date to-table", + result: None, + }, + Example { + description: "Print the date in a structured table.", + example: " '2020-04-12 22:10:57 +0200' | date to-table", + result: { + let span = Span::test_data(); + let cols = vec![ + "year".into(), + "month".into(), + "day".into(), + "hour".into(), + "minute".into(), + "second".into(), + "timezone".into(), + ]; + let vals = vec![ + Value::Int { val: 2020, span }, + Value::Int { val: 4, span }, + Value::Int { val: 12, span }, + Value::Int { val: 22, span }, + Value::Int { val: 10, span }, + Value::Int { val: 57, span }, + Value::String { + val: "+02:00".to_string(), + span, + }, + ]; + Some(Value::List { + vals: vec![Value::Record { cols, vals, span }], + span, + }) + }, + }, + ] + } +} + +fn parse_date_into_table(date: Result, Value>, head: Span) -> Value { + let cols = vec![ + "year".into(), + "month".into(), + "day".into(), + "hour".into(), + "minute".into(), + "second".into(), + "timezone".into(), + ]; + match date { + Ok(x) => { + let vals = vec![ + Value::Int { + val: x.year() as i64, + span: head, + }, + Value::Int { + val: x.month() as i64, + span: head, + }, + Value::Int { + val: x.day() as i64, + span: head, + }, + Value::Int { + val: x.hour() as i64, + span: head, + }, + Value::Int { + val: x.minute() as i64, + span: head, + }, + Value::Int { + val: x.second() as i64, + span: head, + }, + Value::String { + val: x.offset().to_string(), + span: head, + }, + ]; + Value::List { + vals: vec![Value::Record { + cols, + vals, + span: head, + }], + span: head, + } + } + Err(e) => e, + } +} + +fn helper(val: Value, head: Span) -> Value { + match val { + Value::String { + val, + span: val_span, + } => { + let date = parse_date_from_string(val, val_span); + parse_date_into_table(date, head) + } + Value::Nothing { span: _ } => { + let now = Local::now(); + let n = now.with_timezone(now.offset()); + parse_date_into_table(Ok(n), head) + } + Value::Date { val, span: _ } => parse_date_into_table(Ok(val), head), + _ => unsupported_input_error(head), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/date/to_timezone.rs b/crates/nu-command/src/date/to_timezone.rs new file mode 100644 index 0000000000..0a6261873d --- /dev/null +++ b/crates/nu-command/src/date/to_timezone.rs @@ -0,0 +1,130 @@ +use super::parser::datetime_in_timezone; +use crate::date::utils::{parse_date_from_string, unsupported_input_error}; +use chrono::{DateTime, Local}; +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Value, +}; + +use chrono::{FixedOffset, TimeZone}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "date to-timezone" + } + + fn signature(&self) -> Signature { + Signature::build("date to-timezone") + .required("time zone", SyntaxShape::String, "time zone description") + .category(Category::Date) + } + + fn usage(&self) -> &str { + "Convert a date to a given time zone." + } + + fn extra_usage(&self) -> &str { + "Use 'date list-timezone' to list all supported time zones." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let timezone: Spanned = call.req(engine_state, stack, 0)?; + + //Ok(PipelineData::new()) + input.map( + move |value| helper(value, head, &timezone), + engine_state.ctrlc.clone(), + ) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Get the current date in UTC+05:00", + example: "date now | date to-timezone +0500", + result: None, + }, + Example { + description: "Get the current local date", + example: "date now | date to-timezone local", + result: None, + }, + Example { + description: "Get the current date in Hawaii", + example: "date now | date to-timezone US/Hawaii", + result: None, + }, + Example { + description: "Get the current date in Hawaii", + example: r#""2020-10-10 10:00:00 +02:00" | date to-timezone "+0500""#, + // result: None + // The following should be the result of the test, but it fails. Cannot figure it out why. + result: { + let dt = FixedOffset::east(5 * 3600) + .ymd(2020, 10, 10) + .and_hms(13, 00, 00); + + Some(Value::Date { + val: dt, + span: Span::test_data(), + }) + }, + }, + ] + } +} + +fn helper(value: Value, head: Span, timezone: &Spanned) -> Value { + match value { + Value::Date { val, span: _ } => _to_timezone(val, timezone, head), + Value::String { + val, + span: val_span, + } => { + let time = parse_date_from_string(val, val_span); + match time { + Ok(dt) => _to_timezone(dt, timezone, head), + Err(e) => e, + } + } + + Value::Nothing { span: _ } => { + let dt = Local::now(); + _to_timezone(dt.with_timezone(dt.offset()), timezone, head) + } + _ => unsupported_input_error(head), + } +} + +fn _to_timezone(dt: DateTime, timezone: &Spanned, span: Span) -> Value { + match datetime_in_timezone(&dt, timezone.item.as_str()) { + Ok(dt) => Value::Date { val: dt, span }, + Err(_) => Value::Error { + error: ShellError::UnsupportedInput(String::from("invalid time zone"), span), + }, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/date/utils.rs b/crates/nu-command/src/date/utils.rs new file mode 100644 index 0000000000..25a4f6a016 --- /dev/null +++ b/crates/nu-command/src/date/utils.rs @@ -0,0 +1,43 @@ +use chrono::{DateTime, FixedOffset}; +use nu_protocol::{ShellError, Span, Value}; + +pub fn unsupported_input_error(span: Span) -> Value { + Value::Error { + error: ShellError::UnsupportedInput( + String::from( + "Only dates with timezones are supported. The following formats are allowed \n + * %Y-%m-%d %H:%M:%S %z -- 2020-04-12 22:10:57 +02:00 \n + * %Y-%m-%d %H:%M:%S%.6f %z -- 2020-04-12 22:10:57.213231 +02:00 \n + * rfc3339 -- 2020-04-12T22:10:57+02:00 \n + * rfc2822 -- Tue, 1 Jul 2003 10:52:37 +0200", + ), + span, + ), + } +} + +pub fn parse_date_from_string(input: String, span: Span) -> Result, Value> { + let datetime = DateTime::parse_from_str(&input, "%Y-%m-%d %H:%M:%S %z"); // "2020-04-12 22:10:57 +02:00"; + match datetime { + Ok(x) => Ok(x), + Err(_) => { + let datetime = DateTime::parse_from_str(&input, "%Y-%m-%d %H:%M:%S%.6f %z"); // "2020-04-12 22:10:57.213231 +02:00"; + match datetime { + Ok(x) => Ok(x), + Err(_) => { + let datetime = DateTime::parse_from_rfc3339(&input); // "2020-04-12T22:10:57+02:00"; + match datetime { + Ok(x) => Ok(x), + Err(_) => { + let datetime = DateTime::parse_from_rfc2822(&input); // "Tue, 1 Jul 2003 10:52:37 +0200"; + match datetime { + Ok(x) => Ok(x), + Err(_) => Err(unsupported_input_error(span)), + } + } + } + } + } + } + } +} diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 9d091957ee..1e53a6bac3 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -1,372 +1,359 @@ -use crate::prelude::*; -use nu_engine::whole_stream_command; -use std::error::Error; +use nu_protocol::engine::{EngineState, StateWorkingSet}; -pub fn create_default_context(interactive: bool) -> Result> { - let context = EvaluationContext::basic(); +use std::path::Path; - { - use crate::commands::*; +use crate::*; - context.add_commands(vec![ - // Fundamentals - whole_stream_command(NuPlugin), - whole_stream_command(Let), - whole_stream_command(LetEnv), - whole_stream_command(UnletEnv), - whole_stream_command(LoadEnv), - whole_stream_command(Def), - whole_stream_command(Source), - whole_stream_command(Alias), - whole_stream_command(Unalias), - whole_stream_command(Ignore), - whole_stream_command(Tutor), - whole_stream_command(Find), - // System/file operations - whole_stream_command(ErrorMake), - whole_stream_command(Exec), - whole_stream_command(Pwd), - whole_stream_command(Ls), - whole_stream_command(Du), - whole_stream_command(Cd), - whole_stream_command(Remove), - whole_stream_command(Open), - whole_stream_command(Pathvar), - whole_stream_command(PathvarAdd), - whole_stream_command(PathvarRemove), - whole_stream_command(PathvarReset), - whole_stream_command(PathvarAppend), - whole_stream_command(PathvarSave), - whole_stream_command(Config), - whole_stream_command(ConfigGet), - whole_stream_command(ConfigSet), - whole_stream_command(ConfigSetInto), - whole_stream_command(ConfigClear), - whole_stream_command(ConfigRemove), - whole_stream_command(ConfigPath), - whole_stream_command(Help), - whole_stream_command(History), - whole_stream_command(Save), - whole_stream_command(Touch), - whole_stream_command(Cpy), - whole_stream_command(Date), - whole_stream_command(DateListTimeZone), - whole_stream_command(DateNow), - whole_stream_command(DateToTable), - whole_stream_command(DateToTimeZone), - whole_stream_command(DateFormat), - whole_stream_command(DateHumanize), - whole_stream_command(Cal), - whole_stream_command(Mkdir), - whole_stream_command(Mv), - whole_stream_command(Kill), - whole_stream_command(Version), - whole_stream_command(Clear), - whole_stream_command(Describe), - whole_stream_command(Which), - whole_stream_command(Debug), - whole_stream_command(WithEnv), - whole_stream_command(Do), - whole_stream_command(Sleep), - // Statistics - whole_stream_command(Size), - whole_stream_command(Length), - whole_stream_command(Benchmark), - // Metadata - whole_stream_command(Tags), - // Shells - whole_stream_command(Next), - whole_stream_command(Previous), - whole_stream_command(Goto), - whole_stream_command(Shells), - whole_stream_command(Enter), - whole_stream_command(Exit), - // Viz - whole_stream_command(Chart), - // Viewers - whole_stream_command(Autoview), - whole_stream_command(Table), - // Text manipulation - whole_stream_command(Hash), - whole_stream_command(HashBase64), - whole_stream_command(HashMd5::default()), - whole_stream_command(HashSha256::default()), - whole_stream_command(Split), - whole_stream_command(SplitColumn), - whole_stream_command(SplitRow), - whole_stream_command(SplitChars), - whole_stream_command(Lines), - whole_stream_command(Echo), - whole_stream_command(Parse), - whole_stream_command(Str), - whole_stream_command(StrToDecimal), - whole_stream_command(StrToInteger), - whole_stream_command(StrDowncase), - whole_stream_command(StrUpcase), - whole_stream_command(StrCapitalize), - whole_stream_command(StrFindReplace), - whole_stream_command(StrSubstring), - whole_stream_command(StrToDatetime), - whole_stream_command(StrContains), - whole_stream_command(StrIndexOf), - whole_stream_command(StrTrim), - whole_stream_command(StrStartsWith), - whole_stream_command(StrEndsWith), - whole_stream_command(StrCollect), - whole_stream_command(StrLength), - whole_stream_command(StrLPad), - whole_stream_command(StrReverse), - whole_stream_command(StrRPad), - whole_stream_command(StrCamelCase), - whole_stream_command(StrPascalCase), - whole_stream_command(StrKebabCase), - whole_stream_command(StrSnakeCase), - whole_stream_command(StrScreamingSnakeCase), - whole_stream_command(BuildString), - whole_stream_command(Ansi), - whole_stream_command(AnsiStrip), - whole_stream_command(AnsiGradient), - whole_stream_command(Char), - whole_stream_command(DetectColumns), - // Column manipulation - whole_stream_command(DropColumn), - whole_stream_command(MoveColumn), - whole_stream_command(Reject), - whole_stream_command(Select), - whole_stream_command(Get), - whole_stream_command(Update), - whole_stream_command(UpdateCells), - whole_stream_command(Insert), - whole_stream_command(Into), - whole_stream_command(IntoBinary), - whole_stream_command(IntoColumnPath), - whole_stream_command(IntoInt), - whole_stream_command(IntoFilepath), - whole_stream_command(IntoFilesize), - whole_stream_command(IntoString), - whole_stream_command(SplitBy), - // Row manipulation - whole_stream_command(All), - whole_stream_command(Any), - whole_stream_command(Reverse), - whole_stream_command(Append), - whole_stream_command(Prepend), - whole_stream_command(SortBy), - whole_stream_command(GroupBy), - 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(DropNth), - whole_stream_command(Format), - whole_stream_command(FileSize), - whole_stream_command(Where), - whole_stream_command(If), - whole_stream_command(Compact), - whole_stream_command(Default), - whole_stream_command(Skip), - whole_stream_command(SkipUntil), - whole_stream_command(SkipWhile), - whole_stream_command(Keep), - whole_stream_command(KeepUntil), - whole_stream_command(KeepWhile), - whole_stream_command(Range), - whole_stream_command(Rename), - whole_stream_command(Uniq), - whole_stream_command(Each), - whole_stream_command(EachGroup), - whole_stream_command(EachWindow), - whole_stream_command(Empty), - whole_stream_command(ForIn), - // Table manipulation - whole_stream_command(Flatten), - whole_stream_command(Merge), - whole_stream_command(Shuffle), - whole_stream_command(Wrap), - whole_stream_command(Pivot), - whole_stream_command(Headers), - whole_stream_command(Reduce), - whole_stream_command(Roll), - whole_stream_command(RollColumn), - whole_stream_command(RollUp), - whole_stream_command(Rotate), - whole_stream_command(RotateCounterClockwise), - whole_stream_command(Zip), - whole_stream_command(Collect), - // Data processing - whole_stream_command(Histogram), - whole_stream_command(Autoenv), - whole_stream_command(AutoenvTrust), - whole_stream_command(AutoenvUntrust), - whole_stream_command(Math), - whole_stream_command(MathAbs), - whole_stream_command(MathAverage), - whole_stream_command(MathEval), - whole_stream_command(MathMedian), - whole_stream_command(MathMinimum), - whole_stream_command(MathMode), - whole_stream_command(MathMaximum), - whole_stream_command(MathStddev), - whole_stream_command(MathSummation), - whole_stream_command(MathVariance), - whole_stream_command(MathProduct), - whole_stream_command(MathRound), - whole_stream_command(MathFloor), - whole_stream_command(MathCeil), - whole_stream_command(MathSqrt), - // File format output - whole_stream_command(To), - whole_stream_command(ToCsv), - whole_stream_command(ToHtml), - whole_stream_command(ToJson), - whole_stream_command(ToMarkdown), - whole_stream_command(ToToml), - whole_stream_command(ToTsv), - whole_stream_command(ToUrl), - whole_stream_command(ToYaml), - whole_stream_command(ToXml), - // File format input - whole_stream_command(From), - whole_stream_command(FromCsv), - whole_stream_command(FromEml), - whole_stream_command(FromTsv), - whole_stream_command(FromSsv), - whole_stream_command(FromIni), - whole_stream_command(FromJson), - whole_stream_command(FromOds), - whole_stream_command(FromToml), - whole_stream_command(FromUrl), - whole_stream_command(FromXlsx), - whole_stream_command(FromXml), - whole_stream_command(FromYaml), - whole_stream_command(FromYml), - whole_stream_command(FromIcs), - whole_stream_command(FromVcf), - // "Private" commands (not intended to be accessed directly) - whole_stream_command(RunExternalCommand { interactive }), - // Random value generation - whole_stream_command(Random), - whole_stream_command(RandomBool), - whole_stream_command(RandomDice), - #[cfg(feature = "uuid_crate")] - whole_stream_command(RandomUUID), - whole_stream_command(RandomInteger), - whole_stream_command(RandomDecimal), - whole_stream_command(RandomChars), - // Path - whole_stream_command(PathBasename), - whole_stream_command(PathCommand), - whole_stream_command(PathDirname), - whole_stream_command(PathExists), - whole_stream_command(PathExpand), - whole_stream_command(PathJoin), - whole_stream_command(PathParse), - whole_stream_command(PathRelativeTo), - whole_stream_command(PathSplit), - whole_stream_command(PathType), - // Url - whole_stream_command(UrlCommand), - whole_stream_command(UrlScheme), - whole_stream_command(UrlPath), - whole_stream_command(UrlHost), - whole_stream_command(UrlQuery), - whole_stream_command(Seq), - whole_stream_command(SeqDates), - whole_stream_command(TermSize), - // Network - #[cfg(feature = "fetch")] - whole_stream_command(Fetch), - #[cfg(feature = "post")] - whole_stream_command(Post), - // System - #[cfg(feature = "ps")] - whole_stream_command(Ps), - #[cfg(feature = "sys")] - whole_stream_command(Sys), - ]); +pub fn create_default_context(cwd: impl AsRef) -> EngineState { + let mut engine_state = EngineState::new(); - //Dataframe commands + let delta = { + let mut working_set = StateWorkingSet::new(&engine_state); + + macro_rules! bind_command { + ( $( $command:expr ),* $(,)? ) => { + $( working_set.add_decl(Box::new($command)); )* + }; + } + + // If there are commands that have the same name as default declarations, + // they have to be registered before the main declarations. This helps to make + // them only accessible if the correct input value category is used with the + // declaration #[cfg(feature = "dataframe")] - context.add_commands(vec![ - whole_stream_command(DataFrame), - whole_stream_command(DataFrameOpen), - whole_stream_command(DataFrameList), - whole_stream_command(DataFrameGroupBy), - whole_stream_command(DataFrameAggregate), - whole_stream_command(DataFrameShow), - whole_stream_command(DataFrameSample), - whole_stream_command(DataFrameJoin), - whole_stream_command(DataFrameDrop), - whole_stream_command(DataFrameSelect), - whole_stream_command(DataFrameDTypes), - whole_stream_command(DataFrameDummies), - whole_stream_command(DataFrameFirst), - whole_stream_command(DataFrameLast), - whole_stream_command(DataFrameSlice), - whole_stream_command(DataFrameMelt), - whole_stream_command(DataFramePivot), - whole_stream_command(DataFrameWhere), - whole_stream_command(DataFrameToDF), - whole_stream_command(DataFrameToParquet), - whole_stream_command(DataFrameToCsv), - whole_stream_command(DataFrameSort), - whole_stream_command(DataFrameGet), - whole_stream_command(DataFrameDropDuplicates), - whole_stream_command(DataFrameDropNulls), - whole_stream_command(DataFrameColumn), - whole_stream_command(DataFrameWithColumn), - whole_stream_command(DataFrameFilter), - whole_stream_command(DataFrameSeriesRename), - whole_stream_command(DataFrameValueCounts), - whole_stream_command(DataFrameIsNull), - whole_stream_command(DataFrameIsNotNull), - whole_stream_command(DataFrameAllTrue), - whole_stream_command(DataFrameAllFalse), - whole_stream_command(DataFrameArgMax), - whole_stream_command(DataFrameArgMin), - whole_stream_command(DataFrameArgTrue), - whole_stream_command(DataFrameArgUnique), - whole_stream_command(DataFrameArgSort), - whole_stream_command(DataFrameUnique), - whole_stream_command(DataFrameNUnique), - whole_stream_command(DataFrameNNull), - whole_stream_command(DataFrameIsUnique), - whole_stream_command(DataFrameIsDuplicated), - whole_stream_command(DataFrameIsIn), - whole_stream_command(DataFrameShift), - whole_stream_command(DataFrameSet), - whole_stream_command(DataFrameNot), - whole_stream_command(DataFrameTake), - whole_stream_command(DataFrameSetWithIdx), - whole_stream_command(DataFrameShape), - whole_stream_command(DataFrameReplace), - whole_stream_command(DataFrameReplaceAll), - whole_stream_command(DataFrameStringLengths), - whole_stream_command(DataFrameContains), - whole_stream_command(DataFrameToLowercase), - whole_stream_command(DataFrameToUppercase), - whole_stream_command(DataFrameStringSlice), - whole_stream_command(DataFrameConcatenate), - whole_stream_command(DataFrameAppend), - whole_stream_command(DataFrameGetHour), - whole_stream_command(DataFrameGetMinute), - whole_stream_command(DataFrameGetSecond), - whole_stream_command(DataFrameGetDay), - whole_stream_command(DataFrameGetMonth), - whole_stream_command(DataFrameGetYear), - whole_stream_command(DataFrameGetWeek), - whole_stream_command(DataFrameGetWeekDay), - whole_stream_command(DataFrameGetOrdinal), - whole_stream_command(DataFrameGetNanoSecond), - whole_stream_command(DataFrameStrFTime), - whole_stream_command(DataFrameDescribe), - whole_stream_command(DataFrameRolling), - whole_stream_command(DataFrameCumulative), - whole_stream_command(DataFrameRename), - ]); - } + add_dataframe_decls(&mut working_set); - Ok(context) + // Core + bind_command! { + Alias, + Debug, + Def, + DefEnv, + Describe, + Do, + Du, + Echo, + ErrorMake, + ExportCommand, + ExportDef, + ExportDefEnv, + ExportEnv, + For, + Help, + Hide, + History, + If, + Ignore, + Let, + Metadata, + Module, + Source, + Tutor, + Use, + Version, + }; + + // Filters + bind_command! { + All, + Any, + Append, + Collect, + Columns, + Compact, + Default, + Drop, + DropColumn, + DropNth, + Each, + EachGroup, + EachWindow, + Empty, + Every, + Find, + First, + Flatten, + Get, + GroupBy, + SplitBy, + Keep, + Merge, + Move, + KeepUntil, + KeepWhile, + Last, + Length, + Lines, + Nth, + ParEach, + ParEachGroup, + Prepend, + Range, + Reduce, + Reject, + Rename, + Reverse, + Rotate, + Select, + Shuffle, + Skip, + SkipUntil, + SkipWhile, + SortBy, + Transpose, + Uniq, + Update, + UpdateCells, + Where, + Wrap, + Zip, + }; + + // Path + bind_command! { + Path, + PathBasename, + PathDirname, + PathExists, + PathExpand, + PathJoin, + PathParse, + PathRelativeTo, + PathSplit, + PathType, + }; + + // System + bind_command! { + Benchmark, + Exec, + External, + Ps, + Sys, + }; + + #[cfg(feature = "which")] + bind_command! { Which }; + + // Strings + bind_command! { + BuildString, + Char, + Decode, + DetectColumns, + Format, + Parse, + Size, + Split, + SplitChars, + SplitColumn, + SplitRow, + Str, + StrCamelCase, + StrCapitalize, + StrCollect, + StrContains, + StrDowncase, + StrEndswith, + StrFindReplace, + StrIndexOf, + StrKebabCase, + StrLength, + StrLpad, + StrPascalCase, + StrReverse, + StrRpad, + StrScreamingSnakeCase, + StrSnakeCase, + StrStartsWith, + StrSubstring, + StrTrim, + StrUpcase + }; + + // FileSystem + bind_command! { + Cd, + Cp, + Ls, + Mkdir, + Mv, + Open, + Rm, + Save, + Touch, + }; + + // Platform + bind_command! { + Ansi, + AnsiGradient, + AnsiStrip, + Clear, + KeybindingsDefault, + Input, + KeybindingsListen, + Keybindings, + Kill, + KeybindingsList, + Sleep, + TermSize, + }; + + // Date + bind_command! { + Date, + DateFormat, + DateHumanize, + DateListTimezones, + DateNow, + DateToTable, + DateToTimezone, + }; + + // Shells + bind_command! { + Enter, + Exit, + GotoShell, + NextShell, + PrevShell, + Shells, + }; + + // Formats + bind_command! { + From, + FromCsv, + FromEml, + FromIcs, + FromIni, + FromJson, + FromOds, + FromSsv, + FromToml, + FromTsv, + FromUrl, + FromVcf, + FromXlsx, + FromXml, + FromYaml, + FromYml, + To, + ToCsv, + ToHtml, + ToJson, + ToMd, + ToToml, + ToTsv, + ToCsv, + Touch, + Use, + Update, + Where, + ToUrl, + ToXml, + ToYaml, + }; + + // Viewers + bind_command! { + Griddle, + Table, + }; + + // Conversions + bind_command! { + Fmt, + Into, + IntoBool, + IntoBinary, + IntoDatetime, + IntoDecimal, + IntoFilesize, + IntoInt, + IntoString, + }; + + // Env + bind_command! { + Env, + LetEnv, + LoadEnv, + WithEnv, + }; + + // Math + bind_command! { + Math, + MathAbs, + MathAvg, + MathCeil, + MathEval, + MathFloor, + MathMax, + MathMedian, + MathMin, + MathMode, + MathProduct, + MathRound, + MathSqrt, + MathStddev, + MathSum, + MathVariance, + }; + + // Network + bind_command! { + Fetch, + Url, + UrlHost, + UrlPath, + UrlQuery, + UrlScheme, + } + + // Random + bind_command! { + Random, + RandomBool, + RandomChars, + RandomDecimal, + RandomDice, + RandomInteger, + RandomUuid, + }; + + // Generators + bind_command! { + Cal, + Seq, + SeqDate, + }; + + // Hash + bind_command! { + Hash, + HashMd5::default(), + HashSha256::default(), + Base64, + }; + + // Experimental + bind_command! { + ViewSource, + }; + + #[cfg(feature = "plugin")] + bind_command!(Register); + + // This is a WIP proof of concept + // bind_command!(ListGitBranches, Git, GitCheckout, Source); + + working_set.render() + }; + + let _ = engine_state.merge_delta(delta, None, &cwd); + + engine_state } diff --git a/crates/nu-command/src/env/env_command.rs b/crates/nu-command/src/env/env_command.rs new file mode 100644 index 0000000000..5d1f34fa8f --- /dev/null +++ b/crates/nu-command/src/env/env_command.rs @@ -0,0 +1,62 @@ +use nu_engine::env_to_string; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, IntoPipelineData, PipelineData, Signature, Value}; + +#[derive(Clone)] +pub struct Env; + +impl Command for Env { + fn name(&self) -> &str { + "env" + } + + fn usage(&self) -> &str { + "Display current environment variables" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("env").category(Category::Env) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let span = call.head; + let config = stack.get_config().unwrap_or_default(); + + let mut env_vars: Vec<(String, Value)> = + stack.get_env_vars(engine_state).into_iter().collect(); + env_vars.sort_by(|(name1, _), (name2, _)| name1.cmp(name2)); + + let mut values = vec![]; + + for (name, val) in env_vars { + let mut cols = vec![]; + let mut vals = vec![]; + + let raw = env_to_string(&name, val.clone(), engine_state, stack, &config)?; + let val_type = val.get_type(); + + cols.push("name".into()); + vals.push(Value::string(name, span)); + + cols.push("type".into()); + vals.push(Value::string(format!("{}", val_type), span)); + + cols.push("value".into()); + vals.push(val); + + cols.push("raw".into()); + vals.push(Value::string(raw, span)); + + values.push(Value::Record { cols, vals, span }); + } + + Ok(Value::List { vals: values, span }.into_pipeline_data()) + } +} diff --git a/crates/nu-command/src/env/let_env.rs b/crates/nu-command/src/env/let_env.rs new file mode 100644 index 0000000000..4035959cca --- /dev/null +++ b/crates/nu-command/src/env/let_env.rs @@ -0,0 +1,61 @@ +use nu_engine::{current_dir, eval_expression_with_input, CallExt}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, PipelineData, Signature, SyntaxShape, Value}; + +#[derive(Clone)] +pub struct LetEnv; + +impl Command for LetEnv { + fn name(&self) -> &str { + "let-env" + } + + fn usage(&self) -> &str { + "Create an environment variable and give it a value." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("let-env") + .required("var_name", SyntaxShape::String, "variable name") + .required( + "initial_value", + SyntaxShape::Keyword(b"=".to_vec(), Box::new(SyntaxShape::Any)), + "equals sign followed by value", + ) + .category(Category::Env) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let env_var = call.req(engine_state, stack, 0)?; + + let keyword_expr = call.positional[1] + .as_keyword() + .expect("internal error: missing keyword"); + + let rhs = eval_expression_with_input(engine_state, stack, keyword_expr, input, false)? + .into_value(call.head); + + if env_var == "PWD" { + let cwd = current_dir(engine_state, stack)?; + let rhs = rhs.as_string()?; + let rhs = nu_path::expand_path_with(rhs, cwd); + stack.add_env_var( + env_var, + Value::String { + val: rhs.to_string_lossy().to_string(), + span: call.head, + }, + ); + } else { + stack.add_env_var(env_var, rhs); + } + Ok(PipelineData::new(call.head)) + } +} diff --git a/crates/nu-command/src/env/load_env.rs b/crates/nu-command/src/env/load_env.rs new file mode 100644 index 0000000000..c4edfebc82 --- /dev/null +++ b/crates/nu-command/src/env/load_env.rs @@ -0,0 +1,109 @@ +use nu_engine::{current_dir, CallExt}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Value}; + +#[derive(Clone)] +pub struct LoadEnv; + +impl Command for LoadEnv { + fn name(&self) -> &str { + "load-env" + } + + fn usage(&self) -> &str { + "Loads an environment update from a record." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("load-env") + .optional( + "update", + SyntaxShape::Record, + "the record to use for updates", + ) + .category(Category::FileSystem) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let arg: Option<(Vec, Vec)> = call.opt(engine_state, stack, 0)?; + let span = call.head; + + match arg { + Some((cols, vals)) => { + for (env_var, rhs) in cols.into_iter().zip(vals) { + if env_var == "PWD" { + let cwd = current_dir(engine_state, stack)?; + let rhs = rhs.as_string()?; + let rhs = nu_path::expand_path_with(rhs, cwd); + stack.add_env_var( + env_var, + Value::String { + val: rhs.to_string_lossy().to_string(), + span: call.head, + }, + ); + } else { + stack.add_env_var(env_var, rhs); + } + } + Ok(PipelineData::new(call.head)) + } + None => match input { + PipelineData::Value(Value::Record { cols, vals, .. }, ..) => { + for (env_var, rhs) in cols.into_iter().zip(vals) { + if env_var == "PWD" { + let cwd = current_dir(engine_state, stack)?; + let rhs = rhs.as_string()?; + let rhs = nu_path::expand_path_with(rhs, cwd); + stack.add_env_var( + env_var, + Value::String { + val: rhs.to_string_lossy().to_string(), + span: call.head, + }, + ); + } else { + stack.add_env_var(env_var, rhs); + } + } + Ok(PipelineData::new(call.head)) + } + _ => Err(ShellError::UnsupportedInput("Record".into(), span)), + }, + } + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Load variables from an input stream", + example: r#"{NAME: ABE, AGE: UNKNOWN} | load-env; echo $env.NAME"#, + result: Some(Value::test_string("ABE")), + }, + Example { + description: "Load variables from an argument", + example: r#"load-env {NAME: ABE, AGE: UNKNOWN}; echo $env.NAME"#, + result: Some(Value::test_string("ABE")), + }, + ] + } +} + +#[cfg(test)] +mod tests { + use super::LoadEnv; + + #[test] + fn examples_work_as_expected() { + use crate::test_examples; + + test_examples(LoadEnv {}) + } +} diff --git a/crates/nu-command/src/env/mod.rs b/crates/nu-command/src/env/mod.rs new file mode 100644 index 0000000000..3b40657fb4 --- /dev/null +++ b/crates/nu-command/src/env/mod.rs @@ -0,0 +1,9 @@ +mod env_command; +mod let_env; +mod load_env; +mod with_env; + +pub use env_command::Env; +pub use let_env::LetEnv; +pub use load_env::LoadEnv; +pub use with_env::WithEnv; diff --git a/crates/nu-command/src/env/with_env.rs b/crates/nu-command/src/env/with_env.rs new file mode 100644 index 0000000000..265796fc18 --- /dev/null +++ b/crates/nu-command/src/env/with_env.rs @@ -0,0 +1,148 @@ +use std::collections::HashMap; + +use nu_engine::{eval_block, CallExt}; +use nu_protocol::{ + ast::Call, + engine::{CaptureBlock, Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct WithEnv; + +impl Command for WithEnv { + fn name(&self) -> &str { + "with-env" + } + + fn signature(&self) -> Signature { + Signature::build("with-env") + .required( + "variable", + SyntaxShape::Any, + "the environment variable to temporarily set", + ) + .required( + "block", + SyntaxShape::Block(Some(vec![SyntaxShape::Any])), + "the block to run once the variable is set", + ) + .category(Category::Env) + } + + fn usage(&self) -> &str { + "Runs a block with an environment variable set." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + with_env(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Set the MYENV environment variable", + example: r#"with-env [MYENV "my env value"] { $env.MYENV }"#, + result: Some(Value::test_string("my env value")), + }, + Example { + description: "Set by primitive value list", + example: r#"with-env [X Y W Z] { $env.X }"#, + result: Some(Value::test_string("Y")), + }, + Example { + description: "Set by single row table", + example: r#"with-env [[X W]; [Y Z]] { $env.W }"#, + result: Some(Value::test_string("Z")), + }, + Example { + description: "Set by row(e.g. `open x.json` or `from json`)", + example: r#"echo '{"X":"Y","W":"Z"}'|from json|with-env $it { echo $env.X $env.W }"#, + result: None, + }, + ] + } +} + +fn with_env( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + // let external_redirection = args.call_info.args.external_redirection; + let variable: Value = call.req(engine_state, stack, 0)?; + + let capture_block: CaptureBlock = call.req(engine_state, stack, 1)?; + let block = engine_state.get_block(capture_block.block_id); + let mut stack = stack.captures_to_stack(&capture_block.captures); + + let mut env: HashMap = HashMap::new(); + + match &variable { + Value::List { vals: table, .. } => { + if table.len() == 1 { + // single row([[X W]; [Y Z]]) + match &table[0] { + Value::Record { cols, vals, .. } => { + for (k, v) in cols.iter().zip(vals.iter()) { + env.insert(k.to_string(), v.clone()); + } + } + x => { + return Err(ShellError::CantConvert( + "string list or single row".into(), + x.get_type().to_string(), + call.positional[1].span, + )); + } + } + } else { + // primitive values([X Y W Z]) + for row in table.chunks(2) { + if row.len() == 2 { + env.insert(row[0].as_string()?, (&row[1]).clone()); + } + // TODO: else error? + } + } + } + // when get object by `open x.json` or `from json` + Value::Record { cols, vals, .. } => { + for (k, v) in cols.iter().zip(vals) { + env.insert(k.clone(), v.clone()); + } + } + x => { + return Err(ShellError::CantConvert( + "string list or single row".into(), + x.get_type().to_string(), + call.positional[1].span, + )); + } + }; + + for (k, v) in env { + stack.add_env_var(k, v); + } + + eval_block(engine_state, &mut stack, block, input) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(WithEnv {}) + } +} diff --git a/crates/nu-command/src/example_test.rs b/crates/nu-command/src/example_test.rs new file mode 100644 index 0000000000..c3bd6fd514 --- /dev/null +++ b/crates/nu-command/src/example_test.rs @@ -0,0 +1,144 @@ +#[cfg(test)] +use nu_engine::eval_block; +#[cfg(test)] +use nu_parser::parse; +#[cfg(test)] +use nu_protocol::{ + engine::{Command, EngineState, Stack, StateWorkingSet}, + PipelineData, Span, Value, CONFIG_VARIABLE_ID, +}; + +#[cfg(test)] +use crate::To; + +#[cfg(test)] +use super::{ + Ansi, Date, From, If, Into, Math, Path, Random, Split, Str, StrCollect, StrFindReplace, + StrLength, Url, Wrap, +}; + +#[cfg(test)] +pub fn test_examples(cmd: impl Command + 'static) { + use crate::BuildString; + + let examples = cmd.examples(); + let mut engine_state = Box::new(EngineState::new()); + + let delta = { + // Base functions that are needed for testing + // Try to keep this working set small to keep tests running as fast as possible + let mut working_set = StateWorkingSet::new(&*engine_state); + working_set.add_decl(Box::new(Str)); + working_set.add_decl(Box::new(StrCollect)); + working_set.add_decl(Box::new(StrLength)); + working_set.add_decl(Box::new(StrFindReplace)); + working_set.add_decl(Box::new(BuildString)); + working_set.add_decl(Box::new(From)); + working_set.add_decl(Box::new(If)); + working_set.add_decl(Box::new(To)); + working_set.add_decl(Box::new(Into)); + working_set.add_decl(Box::new(Random)); + working_set.add_decl(Box::new(Split)); + working_set.add_decl(Box::new(Math)); + working_set.add_decl(Box::new(Path)); + working_set.add_decl(Box::new(Date)); + working_set.add_decl(Box::new(Url)); + working_set.add_decl(Box::new(Ansi)); + working_set.add_decl(Box::new(Wrap)); + + use super::Echo; + working_set.add_decl(Box::new(Echo)); + // Adding the command that is being tested to the working set + working_set.add_decl(Box::new(cmd)); + + working_set.render() + }; + + let cwd = std::env::current_dir().expect("Could not get current working directory."); + let _ = engine_state.merge_delta(delta, None, &cwd); + + for example in examples { + // Skip tests that don't have results to compare to + if example.result.is_none() { + continue; + } + let start = std::time::Instant::now(); + + let mut stack = Stack::new(); + + // Set up PWD + stack.add_env_var( + "PWD".to_string(), + Value::String { + val: cwd.to_string_lossy().to_string(), + span: Span::test_data(), + }, + ); + let _ = engine_state.merge_delta( + StateWorkingSet::new(&*engine_state).render(), + Some(&mut stack), + &cwd, + ); + + let (block, delta) = { + let mut working_set = StateWorkingSet::new(&*engine_state); + let (output, err) = parse(&mut working_set, None, example.example.as_bytes(), false); + + if let Some(err) = err { + panic!("test parse error in `{}`: {:?}", example.example, err) + } + + (output, working_set.render()) + }; + + let _ = engine_state.merge_delta(delta, None, &cwd); + + let mut stack = Stack::new(); + + // Set up PWD + stack.add_env_var( + "PWD".to_string(), + Value::String { + val: cwd.to_string_lossy().to_string(), + span: Span::test_data(), + }, + ); + + // Set up our initial config to start from + stack.vars.insert( + CONFIG_VARIABLE_ID, + Value::Record { + cols: vec![], + vals: vec![], + span: Span::test_data(), + }, + ); + + match eval_block( + &engine_state, + &mut stack, + &block, + PipelineData::new(Span::test_data()), + ) { + Err(err) => panic!("test eval error in `{}`: {:?}", example.example, err), + Ok(result) => { + let result = result.into_value(Span::test_data()); + println!("input: {}", example.example); + println!("result: {:?}", result); + println!("done: {:?}", start.elapsed()); + + // Note. Value implements PartialEq for Bool, Int, Float, String and Block + // If the command you are testing requires to compare another case, then + // you need to define its equality in the Value struct + if let Some(expected) = example.result { + if result != expected { + panic!( + "the example result is different to expected value: {:?} != {:?}", + result, expected + ) + } + } + } + } + } +} diff --git a/crates/nu-command/src/examples.rs b/crates/nu-command/src/examples.rs deleted file mode 100644 index 3ef28d77bb..0000000000 --- a/crates/nu-command/src/examples.rs +++ /dev/null @@ -1,368 +0,0 @@ -mod sample; - -mod double_echo; -mod double_ls; -mod stub_generate; - -use double_echo::Command as DoubleEcho; -use double_ls::Command as DoubleLs; -use stub_generate::{mock_path, Command as StubOpen}; - -use nu_errors::ShellError; -use nu_parser::ParserScope; -use nu_protocol::hir::{ClassifiedBlock, ExternalRedirection}; -use nu_protocol::{ShellTypeName, Value}; -use nu_source::AnchorLocation; - -#[cfg(feature = "dataframe")] -use crate::commands::{ - DataFrameDropNulls, DataFrameGroupBy, DataFrameIsNull, DataFrameShift, DataFrameToDF, - DataFrameWithColumn, StrToDatetime, -}; - -use crate::commands::{ - Append, BuildString, Collect, Each, Echo, First, Get, If, IntoInt, Keep, Last, Let, Math, - MathMode, Nth, Select, StrCollect, Wrap, -}; -use nu_engine::{run_block, whole_stream_command, Command, EvaluationContext, WholeStreamCommand}; -use nu_stream::InputStream; - -pub fn test_examples(cmd: Command) -> Result<(), ShellError> { - let examples = cmd.examples(); - - let base_context = EvaluationContext::basic(); - - base_context.add_commands(vec![ - // Command Doubles - whole_stream_command(DoubleLs {}), - // Minimal restricted commands to aid in testing - whole_stream_command(Append {}), - whole_stream_command(Echo {}), - whole_stream_command(BuildString {}), - whole_stream_command(First {}), - whole_stream_command(Get {}), - whole_stream_command(If {}), - whole_stream_command(IntoInt {}), - whole_stream_command(Keep {}), - whole_stream_command(Each {}), - whole_stream_command(Last {}), - whole_stream_command(Nth {}), - whole_stream_command(Let {}), - whole_stream_command(Select), - whole_stream_command(StrCollect), - whole_stream_command(Collect), - whole_stream_command(Wrap), - cmd, - ]); - - for sample_pipeline in examples { - let mut ctx = base_context.clone(); - - let block = parse_line(sample_pipeline.example, &ctx)?; - - if let Some(expected) = &sample_pipeline.result { - let result = evaluate_block(block, &mut ctx)?; - - ctx.with_errors(|reasons| reasons.iter().cloned().next()) - .map_or(Ok(()), Err)?; - - if expected.len() != result.len() { - let rows_returned = - format!("expected: {}\nactual: {}", expected.len(), result.len()); - let failed_call = format!("command: {}\n", sample_pipeline.example); - - panic!( - "example command produced unexpected number of results.\n {} {}", - failed_call, rows_returned - ); - } - - for (e, a) in expected.iter().zip(&result) { - if !values_equal(e, a) { - let row_errored = format!("expected: {:#?}\nactual: {:#?}", e, a); - let failed_call = format!("command: {}\n", sample_pipeline.example); - - panic!( - "example command produced unexpected result.\n {} {}", - failed_call, row_errored - ); - } - } - } - } - - Ok(()) -} - -pub fn test(cmd: impl WholeStreamCommand + 'static) -> Result<(), ShellError> { - let examples = cmd.examples(); - - let base_context = EvaluationContext::basic(); - - base_context.add_commands(vec![ - whole_stream_command(Math), - whole_stream_command(MathMode {}), - whole_stream_command(Echo {}), - whole_stream_command(BuildString {}), - whole_stream_command(Get {}), - whole_stream_command(Keep {}), - whole_stream_command(Each {}), - whole_stream_command(Let {}), - whole_stream_command(cmd), - whole_stream_command(Select), - whole_stream_command(StrCollect), - whole_stream_command(Collect), - whole_stream_command(Wrap), - ]); - - for sample_pipeline in examples { - let mut ctx = base_context.clone(); - - let block = parse_line(sample_pipeline.example, &ctx)?; - - if let Some(expected) = &sample_pipeline.result { - let start = std::time::Instant::now(); - let result = evaluate_block(block, &mut ctx)?; - - println!("input: {}", sample_pipeline.example); - println!("result: {:?}", result); - println!("done: {:?}", start.elapsed()); - - ctx.with_errors(|reasons| reasons.iter().cloned().take(1).next()) - .map_or(Ok(()), Err)?; - - if expected.len() != result.len() { - let rows_returned = - format!("expected: {}\nactual: {}", expected.len(), result.len()); - let failed_call = format!("command: {}\n", sample_pipeline.example); - - panic!( - "example command produced unexpected number of results.\n {} {}", - failed_call, rows_returned - ); - } - - for (e, a) in expected.iter().zip(&result) { - if !values_equal(e, a) { - let row_errored = format!("expected: {:#?}\nactual: {:#?}", e, a); - let failed_call = format!("command: {}\n", sample_pipeline.example); - - panic!( - "example command produced unexpected result.\n {} {}", - failed_call, row_errored - ); - } - } - } - } - - Ok(()) -} - -#[cfg(feature = "dataframe")] -pub fn test_dataframe(cmd: impl WholeStreamCommand + 'static) -> Result<(), ShellError> { - use nu_protocol::UntaggedValue; - - let examples = cmd.examples(); - - let base_context = EvaluationContext::basic(); - - base_context.add_commands(vec![ - whole_stream_command(cmd), - // Commands used with dataframe - whole_stream_command(DataFrameToDF), - whole_stream_command(DataFrameShift), - whole_stream_command(DataFrameIsNull), - whole_stream_command(DataFrameGroupBy), - whole_stream_command(DataFrameWithColumn), - whole_stream_command(DataFrameDropNulls), - // Base commands for context - whole_stream_command(Math), - whole_stream_command(MathMode {}), - whole_stream_command(Echo {}), - whole_stream_command(BuildString {}), - whole_stream_command(Get {}), - whole_stream_command(Keep {}), - whole_stream_command(Each {}), - whole_stream_command(Let {}), - whole_stream_command(Select), - whole_stream_command(StrCollect), - whole_stream_command(Collect), - whole_stream_command(Wrap), - whole_stream_command(StrToDatetime), - ]); - - for sample_pipeline in examples { - let mut ctx = base_context.clone(); - - println!("{:?}", &sample_pipeline.example); - let block = parse_line(sample_pipeline.example, &ctx)?; - - if let Some(expected) = &sample_pipeline.result { - let start = std::time::Instant::now(); - let result = evaluate_block(block, &mut ctx)?; - - println!("input: {}", sample_pipeline.example); - println!("result: {:?}", result); - println!("done: {:?}", start.elapsed()); - - let value = match result.get(0) { - Some(v) => v, - None => panic!( - "Unable to extract a value after parsing example: {}", - sample_pipeline.example - ), - }; - - let df = match &value.value { - UntaggedValue::DataFrame(df) => df, - _ => panic!( - "Unable to extract dataframe from parsed example: {}", - sample_pipeline.example - ), - }; - - let expected = match expected.get(0) { - Some(v) => v, - None => panic!("Empty vector in result example"), - }; - - let df_expected = match &expected.value { - UntaggedValue::DataFrame(df) => df, - _ => panic!("Unable to extract dataframe from example result"), - }; - - println!("expected: {:?}", df_expected); - - assert_eq!(df, df_expected) - } - } - - Ok(()) -} - -pub fn test_anchors(cmd: Command) -> Result<(), ShellError> { - let examples = cmd.examples(); - - let base_context = EvaluationContext::basic(); - - base_context.add_commands(vec![ - // Minimal restricted commands to aid in testing - whole_stream_command(StubOpen {}), - whole_stream_command(DoubleEcho {}), - whole_stream_command(DoubleLs {}), - whole_stream_command(Append {}), - whole_stream_command(BuildString {}), - whole_stream_command(First {}), - whole_stream_command(Get {}), - whole_stream_command(If {}), - whole_stream_command(IntoInt {}), - whole_stream_command(Keep {}), - whole_stream_command(Each {}), - whole_stream_command(Last {}), - whole_stream_command(Nth {}), - whole_stream_command(Let {}), - whole_stream_command(Select), - whole_stream_command(StrCollect), - whole_stream_command(Collect), - whole_stream_command(Wrap), - cmd, - ]); - - for sample_pipeline in examples { - let pipeline_with_anchor = format!("stub open --path | {}", sample_pipeline.example); - - let mut ctx = base_context.clone(); - - let block = parse_line(&pipeline_with_anchor, &ctx)?; - - if sample_pipeline.result.is_some() { - let result = evaluate_block(block, &mut ctx)?; - - ctx.with_errors(|reasons| reasons.iter().cloned().next()) - .map_or(Ok(()), Err)?; - - for actual in &result { - if !is_anchor_carried(actual, mock_path()) { - let failed_call = format!("command: {}\n", pipeline_with_anchor); - - panic!( - "example command didn't carry anchor tag correctly.\n {} {:#?} {:#?}", - failed_call, - actual, - mock_path() - ); - } - } - } - } - - Ok(()) -} - -/// Parse and run a nushell pipeline -fn parse_line(line: &str, ctx: &EvaluationContext) -> Result { - //FIXME: do we still need this? - let line = if let Some(line) = line.strip_suffix('\n') { - line - } else { - line - }; - - let (lite_result, err) = nu_parser::lex(line, 0, nu_parser::NewlineMode::Normal); - if let Some(err) = err { - return Err(err.into()); - } - let (lite_result, err) = nu_parser::parse_block(lite_result); - if let Some(err) = err { - return Err(err.into()); - } - - // TODO ensure the command whose examples we're testing is actually in the pipeline - let (block, err) = nu_parser::classify_block(&lite_result, &ctx.scope); - Ok(ClassifiedBlock { block, failed: err }) -} - -fn evaluate_block( - block: ClassifiedBlock, - ctx: &mut EvaluationContext, -) -> Result, ShellError> { - let input_stream = InputStream::empty(); - - ctx.scope.enter_scope(); - - let result = run_block(&block.block, ctx, input_stream, ExternalRedirection::Stdout); - - ctx.scope.exit_scope(); - - let result = result?.drain_vec(); - Ok(result) -} - -// TODO probably something already available to do this -// TODO perhaps better panic messages when things don't compare - -// Deep value comparisons that ignore tags -fn values_equal(expected: &Value, actual: &Value) -> bool { - use nu_protocol::UntaggedValue::*; - - match (&expected.value, &actual.value) { - (Primitive(e), Primitive(a)) => e == a, - (Row(e), Row(a)) => { - if e.entries.len() != a.entries.len() { - return false; - } - - e.entries - .iter() - .zip(&a.entries) - .all(|((ek, ev), (ak, av))| ek == ak && values_equal(ev, av)) - } - (Table(e), Table(a)) => e.iter().zip(a).all(|(e, a)| values_equal(e, a)), - (e, a) => unimplemented!("{} {}", e.type_name(), a.type_name()), - } -} - -fn is_anchor_carried(actual: &Value, anchor: AnchorLocation) -> bool { - actual.tag.anchor() == Some(anchor) -} diff --git a/crates/nu-command/src/examples/double_echo.rs b/crates/nu-command/src/examples/double_echo.rs deleted file mode 100644 index e06e3ae27b..0000000000 --- a/crates/nu-command/src/examples/double_echo.rs +++ /dev/null @@ -1,86 +0,0 @@ -use nu_errors::ShellError; - -use nu_engine::{CommandArgs, WholeStreamCommand}; -use nu_protocol::{Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value}; -use nu_stream::{ActionStream, IntoActionStream}; - -use serde::Deserialize; - -pub struct Command; - -#[derive(Deserialize)] -struct Arguments { - #[allow(unused)] - pub rest: Vec, -} - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "echo" - } - - fn signature(&self) -> Signature { - Signature::build("echo").rest("rest", SyntaxShape::Any, "the values to echo") - } - - fn usage(&self) -> &str { - "Mock echo." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - let name_tag = args.call_info.name_tag.clone(); - let rest: Vec = args.rest(0)?; - - let mut base_value = UntaggedValue::string("Yehuda Katz in Ecuador").into_value(name_tag); - let input: Vec = args.input.collect(); - - if let Some(first) = input.get(0) { - base_value = first.clone() - } - - let stream = rest.into_iter().flat_map(move |i| { - let base_value = base_value.clone(); - match i.as_string() { - Ok(s) => ActionStream::one(Ok(ReturnSuccess::Value(Value { - value: UntaggedValue::Primitive(Primitive::String(s)), - tag: base_value.tag, - }))), - _ => match i { - Value { - value: UntaggedValue::Table(table), - .. - } => { - if table.len() == 1 && table[0].is_table() { - let mut values: Vec = - table[0].table_entries().cloned().collect(); - - for v in &mut values { - v.tag = base_value.tag(); - } - - let subtable = - vec![UntaggedValue::Table(values).into_value(base_value.tag())]; - - (subtable.into_iter().map(ReturnSuccess::value)).into_action_stream() - } else { - (table - .into_iter() - .map(move |mut v| { - v.tag = base_value.tag(); - v - }) - .map(ReturnSuccess::value)) - .into_action_stream() - } - } - _ => ActionStream::one(Ok(ReturnSuccess::Value(Value { - value: i.value.clone(), - tag: base_value.tag, - }))), - }, - } - }); - - Ok(stream.into_action_stream()) - } -} diff --git a/crates/nu-command/src/examples/double_ls.rs b/crates/nu-command/src/examples/double_ls.rs deleted file mode 100644 index 2628f3dce0..0000000000 --- a/crates/nu-command/src/examples/double_ls.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::examples::sample::ls::file_listing; - -use nu_engine::{CommandArgs, WholeStreamCommand}; -use nu_errors::ShellError; -use nu_protocol::{ReturnSuccess, Signature, UntaggedValue, Value}; -use nu_stream::{ActionStream, IntoActionStream}; - -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "ls" - } - - fn signature(&self) -> Signature { - Signature::build("ls") - } - - fn usage(&self) -> &str { - "Mock ls." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - let name_tag = args.call_info.name_tag.clone(); - - let mut base_value = - UntaggedValue::string("Andrés N. Robalino in Portland").into_value(name_tag); - let input: Vec = args.input.collect(); - - if let Some(first) = input.get(0) { - base_value = first.clone() - } - - Ok((file_listing() - .iter() - .map(|row| Value { - value: row.value.clone(), - tag: base_value.tag.clone(), - }) - .collect::>() - .into_iter() - .map(ReturnSuccess::value)) - .into_action_stream()) - } -} diff --git a/crates/nu-command/src/examples/sample.rs b/crates/nu-command/src/examples/sample.rs deleted file mode 100644 index d3a592c676..0000000000 --- a/crates/nu-command/src/examples/sample.rs +++ /dev/null @@ -1,35 +0,0 @@ -use nu_protocol::{row, Value}; -use nu_test_support::value::{date, int, string}; - -pub mod ls { - use super::*; - - pub fn file_listing() -> Vec { - vec![ - row! { - "name".to_string() => string("Andres.txt"), - "type".to_string() => string("File"), - "chickens".to_string() => int(10), - "modified".to_string() => date("2019-07-23") - }, - row! { - "name".to_string() => string("Jonathan"), - "type".to_string() => string("Dir"), - "chickens".to_string() => int(5), - "modified".to_string() => date("2019-07-23") - }, - row! { - "name".to_string() => string("Darren.txt"), - "type".to_string() => string("File"), - "chickens".to_string() => int(20), - "modified".to_string() => date("2019-09-24") - }, - row! { - "name".to_string() => string("Yehuda"), - "type".to_string() => string("Dir"), - "chickens".to_string() => int(4), - "modified".to_string() => date("2019-09-24") - }, - ] - } -} diff --git a/crates/nu-command/src/examples/stub_generate.rs b/crates/nu-command/src/examples/stub_generate.rs deleted file mode 100644 index 4dbe8abb39..0000000000 --- a/crates/nu-command/src/examples/stub_generate.rs +++ /dev/null @@ -1,48 +0,0 @@ -use nu_engine::{CommandArgs, WholeStreamCommand}; -use nu_errors::ShellError; -use nu_protocol::{ReturnSuccess, Signature, UntaggedValue, Value}; -use nu_source::{AnchorLocation, Tag}; -use nu_stream::ActionStream; -pub struct Command; - -impl WholeStreamCommand for Command { - fn name(&self) -> &str { - "stub open" - } - - fn signature(&self) -> Signature { - Signature::build("stub open").switch("path", "Add a mocked path", Some('p')) - } - - fn usage(&self) -> &str { - "Generates tables and metadata that mimics behavior of real commands in controlled ways." - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - let name_tag = args.call_info.name_tag.clone(); - - let mocked_path = args.call_info.switch_present("path"); - - let out = UntaggedValue::string("Yehuda Katz in Ecuador"); - - if mocked_path { - Ok(ActionStream::one(Ok(ReturnSuccess::Value(Value { - value: out, - tag: Tag { - anchor: Some(mock_path()), - span: name_tag.span, - }, - })))) - } else { - Ok(ActionStream::one(Ok(ReturnSuccess::Value( - out.into_value(name_tag), - )))) - } - } -} - -pub fn mock_path() -> AnchorLocation { - let path = String::from("path/to/las_best_arepas_in_the_world.txt"); - - AnchorLocation::File(path) -} diff --git a/crates/nu-command/src/experimental/git.rs b/crates/nu-command/src/experimental/git.rs new file mode 100644 index 0000000000..ab3843133f --- /dev/null +++ b/crates/nu-command/src/experimental/git.rs @@ -0,0 +1,57 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, IntoPipelineData, PipelineData, Signature, Value}; + +#[derive(Clone)] +pub struct Git; + +impl Command for Git { + fn name(&self) -> &str { + "git" + } + + fn usage(&self) -> &str { + "Run a block" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("git").category(Category::Experimental) + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + use std::process::Command as ProcessCommand; + use std::process::Stdio; + + let proc = ProcessCommand::new("git").stdout(Stdio::piped()).spawn(); + + match proc { + Ok(child) => { + match child.wait_with_output() { + Ok(val) => { + let result = val.stdout; + + Ok(Value::String { + val: String::from_utf8_lossy(&result).to_string(), + span: call.head, + } + .into_pipeline_data()) + } + Err(_err) => { + // FIXME: Move this to an external signature and add better error handling + Ok(PipelineData::new(call.head)) + } + } + } + Err(_err) => { + // FIXME: Move this to an external signature and add better error handling + Ok(PipelineData::new(call.head)) + } + } + } +} diff --git a/crates/nu-command/src/experimental/git_checkout.rs b/crates/nu-command/src/experimental/git_checkout.rs new file mode 100644 index 0000000000..ba3834e225 --- /dev/null +++ b/crates/nu-command/src/experimental/git_checkout.rs @@ -0,0 +1,74 @@ +use nu_engine::eval_expression; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, IntoPipelineData, PipelineData, Signature, SyntaxShape, Value}; + +#[derive(Clone)] +pub struct GitCheckout; + +impl Command for GitCheckout { + fn name(&self) -> &str { + "git checkout" + } + + fn usage(&self) -> &str { + "Checkout a git revision" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("git checkout") + .required( + "branch", + SyntaxShape::Custom(Box::new(SyntaxShape::String), "list-git-branches".into()), + "the branch to checkout", + ) + .category(Category::Experimental) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + use std::process::Command as ProcessCommand; + use std::process::Stdio; + + let block = &call.positional[0]; + + let out = eval_expression(engine_state, stack, block)?; + + let out = out.as_string()?; + + let proc = ProcessCommand::new("git") + .arg("checkout") + .arg(out) + .stdout(Stdio::piped()) + .spawn(); + + match proc { + Ok(child) => { + match child.wait_with_output() { + Ok(val) => { + let result = val.stdout; + + Ok(Value::String { + val: String::from_utf8_lossy(&result).to_string(), + span: call.head, + } + .into_pipeline_data()) + } + Err(_err) => { + // FIXME: Move this to an external signature and add better error handling + Ok(PipelineData::new(call.head)) + } + } + } + Err(_err) => { + // FIXME: Move this to an external signature and add better error handling + Ok(PipelineData::new(call.head)) + } + } + } +} diff --git a/crates/nu-command/src/experimental/list_git_branches.rs b/crates/nu-command/src/experimental/list_git_branches.rs new file mode 100644 index 0000000000..9a2fed5152 --- /dev/null +++ b/crates/nu-command/src/experimental/list_git_branches.rs @@ -0,0 +1,76 @@ +// Note: this is a temporary command that later will be converted into a pipeline + +use std::process::Command as ProcessCommand; +use std::process::Stdio; + +use nu_protocol::ast::Call; +use nu_protocol::engine::Command; +use nu_protocol::engine::EngineState; +use nu_protocol::engine::Stack; +use nu_protocol::Category; +use nu_protocol::IntoInterruptiblePipelineData; +use nu_protocol::PipelineData; +use nu_protocol::{Signature, Value}; + +#[derive(Clone)] +pub struct ListGitBranches; + +//NOTE: this is not a real implementation :D. It's just a simple one to test with until we port the real one. +impl Command for ListGitBranches { + fn name(&self) -> &str { + "list-git-branches" + } + + fn usage(&self) -> &str { + "List the git branches of the current directory." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("list-git-branches").category(Category::Experimental) + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let list_branches = ProcessCommand::new("git") + .arg("branch") + .stdout(Stdio::piped()) + .spawn(); + + if let Ok(child) = list_branches { + if let Ok(output) = child.wait_with_output() { + let val = output.stdout; + + let s = String::from_utf8_lossy(&val).to_string(); + + #[allow(clippy::needless_collect)] + let lines: Vec<_> = s + .lines() + .filter_map(|x| { + if x.starts_with("* ") { + None + } else { + Some(x.trim()) + } + }) + .map(|x| Value::String { + val: x.into(), + span: call.head, + }) + .collect(); + + Ok(lines + .into_iter() + .into_pipeline_data(engine_state.ctrlc.clone())) + } else { + Ok(PipelineData::new(call.head)) + } + } else { + Ok(PipelineData::new(call.head)) + } + } +} diff --git a/crates/nu-command/src/experimental/mod.rs b/crates/nu-command/src/experimental/mod.rs new file mode 100644 index 0000000000..6f3c70ea5d --- /dev/null +++ b/crates/nu-command/src/experimental/mod.rs @@ -0,0 +1,9 @@ +mod git; +mod git_checkout; +mod list_git_branches; +mod view_source; + +pub use git::Git; +pub use git_checkout::GitCheckout; +pub use list_git_branches::ListGitBranches; +pub use view_source::ViewSource; diff --git a/crates/nu-command/src/experimental/view_source.rs b/crates/nu-command/src/experimental/view_source.rs new file mode 100644 index 0000000000..8958cb2f55 --- /dev/null +++ b/crates/nu-command/src/experimental/view_source.rs @@ -0,0 +1,98 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct ViewSource; + +impl Command for ViewSource { + fn name(&self) -> &str { + "view-source" + } + + fn usage(&self) -> &str { + "View a block, module, or a definition" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("view-source") + .desc(self.usage()) + .required("item", SyntaxShape::Any, "name or block to view") + .category(Category::Core) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let arg: Value = call.req(engine_state, stack, 0)?; + let arg_span = arg.span()?; + + match arg { + Value::Block { span, .. } => { + let contents = engine_state.get_span_contents(&span); + Ok( + Value::string(String::from_utf8_lossy(contents), call.head) + .into_pipeline_data(), + ) + } + Value::String { val, .. } => { + if let Some(decl_id) = engine_state.find_decl(val.as_bytes()) { + // arg is a command + let decl = engine_state.get_decl(decl_id); + if let Some(block_id) = decl.get_block_id() { + let block = engine_state.get_block(block_id); + if let Some(block_span) = block.span { + let contents = engine_state.get_span_contents(&block_span); + Ok(Value::string(String::from_utf8_lossy(contents), call.head) + .into_pipeline_data()) + } else { + Err(ShellError::SpannedLabeledError( + "Cannot view value".to_string(), + "the command does not have a viewable block".to_string(), + arg_span, + )) + } + } else { + Err(ShellError::SpannedLabeledError( + "Cannot view value".to_string(), + "the command does not have a viewable block".to_string(), + arg_span, + )) + } + } else if let Some(overlay_id) = engine_state.find_overlay(val.as_bytes()) { + // arg is a module + let overlay = engine_state.get_overlay(overlay_id); + if let Some(overlay_span) = overlay.span { + let contents = engine_state.get_span_contents(&overlay_span); + Ok(Value::string(String::from_utf8_lossy(contents), call.head) + .into_pipeline_data()) + } else { + Err(ShellError::SpannedLabeledError( + "Cannot view value".to_string(), + "the module does not have a viewable block".to_string(), + arg_span, + )) + } + } else { + Err(ShellError::SpannedLabeledError( + "Cannot view value".to_string(), + "this name does not correspond to a viewable value".to_string(), + arg_span, + )) + } + } + _ => Err(ShellError::SpannedLabeledError( + "Cannot view value".to_string(), + "this value cannot be viewed".to_string(), + arg_span, + )), + } + } +} diff --git a/crates/nu-command/src/filesystem/cd.rs b/crates/nu-command/src/filesystem/cd.rs new file mode 100644 index 0000000000..2ad032db81 --- /dev/null +++ b/crates/nu-command/src/filesystem/cd.rs @@ -0,0 +1,141 @@ +use nu_engine::{current_dir, CallExt}; +use nu_protocol::ast::{Call, Expr, Expression}; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, PipelineData, ShellError, Signature, SyntaxShape, Value}; + +#[derive(Clone)] +pub struct Cd; + +impl Command for Cd { + fn name(&self) -> &str { + "cd" + } + + fn usage(&self) -> &str { + "Change directory." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("cd") + .optional("path", SyntaxShape::Filepath, "the path to change to") + .category(Category::FileSystem) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let raw_path = call.nth(0); + let path_val: Option = call.opt(engine_state, stack, 0)?; + let cwd = current_dir(engine_state, stack)?; + + let (path, span) = match raw_path { + Some(v) => match &v { + Expression { + expr: Expr::Filepath(val), + span, + .. + } if val == "-" => { + let oldpwd = stack.get_env_var(engine_state, "OLDPWD"); + + if let Some(oldpwd) = oldpwd { + let path = oldpwd.as_path()?; + let path = match nu_path::canonicalize_with(path, &cwd) { + Ok(p) => p, + Err(e) => { + return Err(ShellError::DirectoryNotFoundHelp( + *span, + format!("IO Error: {:?}", e), + )) + } + }; + (path.to_string_lossy().to_string(), *span) + } else { + (cwd.to_string_lossy().to_string(), *span) + } + } + _ => match path_val { + Some(v) => { + let path = v.as_path()?; + let path = match nu_path::canonicalize_with(path, &cwd) { + Ok(p) => { + if !p.is_dir() { + return Err(ShellError::NotADirectory(v.span()?)); + } + p + } + + Err(e) => { + return Err(ShellError::DirectoryNotFoundHelp( + v.span()?, + format!("IO Error: {:?}", e), + )) + } + }; + (path.to_string_lossy().to_string(), v.span()?) + } + None => { + let path = nu_path::expand_tilde("~"); + (path.to_string_lossy().to_string(), call.head) + } + }, + }, + None => { + let path = nu_path::expand_tilde("~"); + (path.to_string_lossy().to_string(), call.head) + } + }; + + let path_value = Value::String { val: path, span }; + let cwd = Value::String { + val: cwd.to_string_lossy().to_string(), + span: call.head, + }; + + let shells = stack.get_env_var(engine_state, "NUSHELL_SHELLS"); + let mut shells = if let Some(v) = shells { + v.as_list() + .map(|x| x.to_vec()) + .unwrap_or_else(|_| vec![cwd]) + } else { + vec![cwd] + }; + + let current_shell = stack.get_env_var(engine_state, "NUSHELL_CURRENT_SHELL"); + let current_shell = if let Some(v) = current_shell { + v.as_integer().unwrap_or_default() as usize + } else { + 0 + }; + + shells[current_shell] = path_value.clone(); + + stack.add_env_var( + "NUSHELL_SHELLS".into(), + Value::List { + vals: shells, + span: call.head, + }, + ); + stack.add_env_var( + "NUSHELL_CURRENT_SHELL".into(), + Value::Int { + val: current_shell as i64, + span: call.head, + }, + ); + + if let Some(oldpwd) = stack.get_env_var(engine_state, "PWD") { + stack.add_env_var("OLDPWD".into(), oldpwd) + } + + //FIXME: this only changes the current scope, but instead this environment variable + //should probably be a block that loads the information from the state in the overlay + + stack.add_env_var("PWD".into(), path_value); + Ok(PipelineData::new(call.head)) + } +} diff --git a/crates/nu-command/src/filesystem/cp.rs b/crates/nu-command/src/filesystem/cp.rs new file mode 100644 index 0000000000..060246b333 --- /dev/null +++ b/crates/nu-command/src/filesystem/cp.rs @@ -0,0 +1,234 @@ +use std::path::PathBuf; + +use super::util::get_interactive_confirmation; +use nu_engine::env::current_dir; +use nu_engine::CallExt; +use nu_path::canonicalize_with; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, PipelineData, ShellError, Signature, SyntaxShape}; + +use crate::filesystem::util::FileStructure; + +#[derive(Clone)] +pub struct Cp; + +#[allow(unused_must_use)] +impl Command for Cp { + fn name(&self) -> &str { + "cp" + } + + fn usage(&self) -> &str { + "Copy files." + } + + fn signature(&self) -> Signature { + Signature::build("cp") + .required("source", SyntaxShape::GlobPattern, "the place to copy from") + .required("destination", SyntaxShape::Filepath, "the place to copy to") + .switch( + "recursive", + "copy recursively through subdirectories", + Some('r'), + ) + .switch("force", "suppress error when no file", Some('f')) + .switch("interactive", "ask user to confirm action", Some('i')) + .category(Category::FileSystem) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let source: String = call.req(engine_state, stack, 0)?; + let destination: String = call.req(engine_state, stack, 1)?; + let interactive = call.has_flag("interactive"); + let force = call.has_flag("force"); + + let path = current_dir(engine_state, stack)?; + let source = path.join(source.as_str()); + let destination = path.join(destination.as_str()); + + let mut sources = + glob::glob(&source.to_string_lossy()).map_or_else(|_| Vec::new(), Iterator::collect); + if sources.is_empty() { + return Err(ShellError::FileNotFound(call.positional[0].span)); + } + + if sources.len() > 1 && !destination.is_dir() { + return Err(ShellError::MoveNotPossible { + source_message: "Can't move many files".to_string(), + source_span: call.positional[0].span, + destination_message: "into single file".to_string(), + destination_span: call.positional[1].span, + }); + } + + let any_source_is_dir = sources.iter().any(|f| matches!(f, Ok(f) if f.is_dir())); + let recursive: bool = call.has_flag("recursive"); + if any_source_is_dir && !recursive { + return Err(ShellError::MoveNotPossibleSingle( + "Directories must be copied using \"--recursive\"".to_string(), + call.positional[0].span, + )); + } + + if interactive && !force { + let mut remove: Vec = vec![]; + for (index, file) in sources.iter().enumerate() { + let prompt = format!( + "Are you shure that you want to copy {} to {}?", + file.as_ref() + .map_err(|err| ShellError::SpannedLabeledError( + "Reference error".into(), + err.to_string(), + call.head + ))? + .file_name() + .ok_or_else(|| ShellError::SpannedLabeledError( + "File name error".into(), + "Unable to get file name".into(), + call.head + ))? + .to_str() + .ok_or_else(|| ShellError::SpannedLabeledError( + "Unable to get str error".into(), + "Unable to convert to str file name".into(), + call.head + ))?, + destination + .file_name() + .ok_or_else(|| ShellError::SpannedLabeledError( + "File name error".into(), + "Unable to get file name".into(), + call.head + ))? + .to_str() + .ok_or_else(|| ShellError::SpannedLabeledError( + "Unable to get str error".into(), + "Unable to convert to str file name".into(), + call.head + ))?, + ); + + let input = get_interactive_confirmation(prompt)?; + + if !input { + remove.push(index); + } + } + + remove.reverse(); + + for index in remove { + sources.remove(index); + } + + if sources.is_empty() { + return Err(ShellError::NoFileToBeCopied()); + } + } + + for entry in sources.into_iter().flatten() { + let mut sources = FileStructure::new(); + sources.walk_decorate(&entry, engine_state, stack)?; + + if entry.is_file() { + let sources = sources.paths_applying_with(|(source_file, _depth_level)| { + if destination.is_dir() { + let mut dest = canonicalize_with(&destination, &path)?; + if let Some(name) = entry.file_name() { + dest.push(name); + } + Ok((source_file, dest)) + } else { + Ok((source_file, destination.clone())) + } + })?; + + for (src, dst) in sources { + if src.is_file() { + std::fs::copy(&src, dst).map_err(|e| { + ShellError::MoveNotPossibleSingle( + format!( + "failed to move containing file \"{}\": {}", + src.to_string_lossy(), + e + ), + call.positional[0].span, + ) + })?; + } + } + } else if entry.is_dir() { + let destination = if !destination.exists() { + destination.clone() + } else { + match entry.file_name() { + Some(name) => destination.join(name), + None => { + return Err(ShellError::FileNotFoundCustom( + format!("containing \"{:?}\" is not a valid path", entry), + call.positional[0].span, + )) + } + } + }; + + std::fs::create_dir_all(&destination).map_err(|e| { + ShellError::MoveNotPossibleSingle( + format!("failed to recursively fill destination: {}", e), + call.positional[1].span, + ) + })?; + + let sources = sources.paths_applying_with(|(source_file, depth_level)| { + let mut dest = destination.clone(); + let path = canonicalize_with(&source_file, &path)?; + let components = path + .components() + .map(|fragment| fragment.as_os_str()) + .rev() + .take(1 + depth_level); + + components.for_each(|fragment| dest.push(fragment)); + Ok((PathBuf::from(&source_file), dest)) + })?; + + for (src, dst) in sources { + if src.is_dir() && !dst.exists() { + std::fs::create_dir_all(&dst).map_err(|e| { + ShellError::MoveNotPossibleSingle( + format!( + "failed to create containing directory \"{}\": {}", + dst.to_string_lossy(), + e + ), + call.positional[1].span, + ) + })?; + } + + if src.is_file() { + std::fs::copy(&src, &dst).map_err(|e| { + ShellError::MoveNotPossibleSingle( + format!( + "failed to move containing file \"{}\": {}", + src.to_string_lossy(), + e + ), + call.positional[0].span, + ) + })?; + } + } + } + } + + Ok(PipelineData::new(call.head)) + } +} diff --git a/crates/nu-command/src/filesystem/ls.rs b/crates/nu-command/src/filesystem/ls.rs new file mode 100644 index 0000000000..87cddb16f3 --- /dev/null +++ b/crates/nu-command/src/filesystem/ls.rs @@ -0,0 +1,481 @@ +use crate::DirBuilder; +use crate::DirInfo; +use chrono::{DateTime, Utc}; +use nu_engine::env::current_dir; +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, DataSource, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, + PipelineMetadata, ShellError, Signature, Span, Spanned, SyntaxShape, Value, +}; +use pathdiff::diff_paths; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; +use std::path::PathBuf; +use std::sync::Arc; + +#[derive(Clone)] +pub struct Ls; + +impl Command for Ls { + fn name(&self) -> &str { + "ls" + } + + fn usage(&self) -> &str { + "List the files in a directory." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("ls") + .optional( + "pattern", + SyntaxShape::GlobPattern, + "the glob pattern to use", + ) + .switch("all", "Show hidden files", Some('a')) + .switch( + "long", + "List all available columns for each entry", + Some('l'), + ) + .switch( + "short-names", + "Only print the file names and not the path", + Some('s'), + ) + .switch("full-paths", "display paths as absolute paths", Some('f')) + .switch( + "du", + "Display the apparent directory size in place of the directory metadata size", + Some('d'), + ) + .category(Category::FileSystem) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let all = call.has_flag("all"); + let long = call.has_flag("long"); + let short_names = call.has_flag("short-names"); + let full_paths = call.has_flag("full-paths"); + let du = call.has_flag("du"); + let ctrl_c = engine_state.ctrlc.clone(); + let call_span = call.head; + let cwd = current_dir(engine_state, stack)?; + let pattern_arg = call.opt::>(engine_state, stack, 0)?; + + let (path, p_tag) = match pattern_arg { + Some(p) => { + let p_tag = p.span; + let mut p = PathBuf::from(p.item); + if p.is_dir() { + if permission_denied(&p) { + #[cfg(unix)] + let error_msg = format!( + "The permissions of {:o} do not allow access for this user", + p.metadata() + .expect( + "this shouldn't be called since we already know there is a dir" + ) + .permissions() + .mode() + & 0o0777 + ); + #[cfg(not(unix))] + let error_msg = String::from("Permission denied"); + return Err(ShellError::SpannedLabeledError( + "Permission denied".to_string(), + error_msg, + p_tag, + )); + } + if is_empty_dir(&p) { + return Ok(Value::nothing(call_span).into_pipeline_data()); + } + p.push("*"); + } + (p, p_tag) + } + None => { + if is_empty_dir(current_dir(engine_state, stack)?) { + return Ok(Value::nothing(call_span).into_pipeline_data()); + } else { + (PathBuf::from("./*"), call_span) + } + } + }; + + let hidden_dir_specified = is_hidden_dir(&path); + + let glob_path = Spanned { + item: path.display().to_string(), + span: p_tag, + }; + let (prefix, paths) = nu_engine::glob_from(&glob_path, &cwd, call_span)?; + + let mut paths_peek = paths.peekable(); + if paths_peek.peek().is_none() { + return Err(ShellError::LabeledError( + format!("No matches found for {}", &path.display().to_string()), + "no matches found".to_string(), + )); + } + + let mut hidden_dirs = vec![]; + + Ok(paths_peek + .into_iter() + .filter_map(move |x| match x { + Ok(path) => { + let metadata = match std::fs::symlink_metadata(&path) { + Ok(metadata) => Some(metadata), + Err(_) => None, + }; + if path_contains_hidden_folder(&path, &hidden_dirs) { + return None; + } + + if !all && !hidden_dir_specified && is_hidden_dir(&path) { + if path.is_dir() { + hidden_dirs.push(path); + } + return None; + } + + let display_name = if short_names { + path.file_name().map(|os| os.to_string_lossy().to_string()) + } else if full_paths { + Some(path.to_string_lossy().to_string()) + } else if let Some(prefix) = &prefix { + if let Ok(remainder) = path.strip_prefix(&prefix) { + let new_prefix = if let Some(pfx) = diff_paths(&prefix, &cwd) { + pfx + } else { + prefix.to_path_buf() + }; + + Some(new_prefix.join(remainder).to_string_lossy().to_string()) + } else { + Some(path.to_string_lossy().to_string()) + } + } else { + Some(path.to_string_lossy().to_string()) + } + .ok_or_else(|| { + ShellError::SpannedLabeledError( + format!("Invalid file name: {:}", path.to_string_lossy()), + "invalid file name".into(), + call_span, + ) + }); + + match display_name { + Ok(name) => { + let entry = dir_entry_dict( + &path, + &name, + metadata.as_ref(), + call_span, + long, + du, + ctrl_c.clone(), + ); + match entry { + Ok(value) => Some(value), + Err(err) => Some(Value::Error { error: err }), + } + } + Err(err) => Some(Value::Error { error: err }), + } + } + _ => Some(Value::Nothing { span: call_span }), + }) + .into_pipeline_data_with_metadata( + PipelineMetadata { + data_source: DataSource::Ls, + }, + engine_state.ctrlc.clone(), + )) + } +} + +fn permission_denied(dir: impl AsRef) -> bool { + match dir.as_ref().read_dir() { + Err(e) => matches!(e.kind(), std::io::ErrorKind::PermissionDenied), + Ok(_) => false, + } +} + +fn is_empty_dir(dir: impl AsRef) -> bool { + match dir.as_ref().read_dir() { + Err(_) => true, + Ok(mut s) => s.next().is_none(), + } +} + +fn is_hidden_dir(dir: impl AsRef) -> bool { + #[cfg(windows)] + { + use std::os::windows::fs::MetadataExt; + + if let Ok(metadata) = dir.as_ref().metadata() { + let attributes = metadata.file_attributes(); + // https://docs.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants + (attributes & 0x2) != 0 + } else { + false + } + } + + #[cfg(not(windows))] + { + dir.as_ref() + .file_name() + .map(|name| name.to_string_lossy().starts_with('.')) + .unwrap_or(false) + } +} + +fn path_contains_hidden_folder(path: &Path, folders: &[PathBuf]) -> bool { + let path_str = path.to_str().expect("failed to read path"); + if folders + .iter() + .any(|p| path_str.starts_with(&p.to_str().expect("failed to read hidden paths"))) + { + return true; + } + false +} + +#[cfg(unix)] +use std::os::unix::fs::FileTypeExt; +use std::path::Path; +use std::sync::atomic::AtomicBool; + +pub fn get_file_type(md: &std::fs::Metadata) -> &str { + let ft = md.file_type(); + let mut file_type = "unknown"; + if ft.is_dir() { + file_type = "dir"; + } else if ft.is_file() { + file_type = "file"; + } else if ft.is_symlink() { + file_type = "symlink"; + } else { + #[cfg(unix)] + { + if ft.is_block_device() { + file_type = "block device"; + } else if ft.is_char_device() { + file_type = "char device"; + } else if ft.is_fifo() { + file_type = "pipe"; + } else if ft.is_socket() { + file_type = "socket"; + } + } + } + file_type +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn dir_entry_dict( + filename: &std::path::Path, // absolute path + display_name: &str, // gile name to be displayed + metadata: Option<&std::fs::Metadata>, + span: Span, + long: bool, + du: bool, + ctrl_c: Option>, +) -> Result { + let mut cols = vec![]; + let mut vals = vec![]; + + cols.push("name".into()); + vals.push(Value::String { + val: display_name.to_string(), + span, + }); + + if let Some(md) = metadata { + cols.push("type".into()); + vals.push(Value::String { + val: get_file_type(md).to_string(), + span, + }); + } else { + cols.push("type".into()); + vals.push(Value::nothing(span)); + } + + if long { + cols.push("target".into()); + if let Some(md) = metadata { + if md.file_type().is_symlink() { + if let Ok(path_to_link) = filename.read_link() { + vals.push(Value::String { + val: path_to_link.to_string_lossy().to_string(), + span, + }); + } else { + vals.push(Value::String { + val: "Could not obtain target file's path".to_string(), + span, + }); + } + } else { + vals.push(Value::nothing(span)); + } + } + } + + if long { + if let Some(md) = metadata { + cols.push("readonly".into()); + vals.push(Value::Bool { + val: md.permissions().readonly(), + span, + }); + + #[cfg(unix)] + { + use std::os::unix::fs::MetadataExt; + let mode = md.permissions().mode(); + cols.push("mode".into()); + vals.push(Value::String { + val: umask::Mode::from(mode).to_string(), + span, + }); + + let nlinks = md.nlink(); + cols.push("num_links".into()); + vals.push(Value::Int { + val: nlinks as i64, + span, + }); + + let inode = md.ino(); + cols.push("inode".into()); + vals.push(Value::Int { + val: inode as i64, + span, + }); + + cols.push("uid".into()); + if let Some(user) = users::get_user_by_uid(md.uid()) { + vals.push(Value::String { + val: user.name().to_string_lossy().into(), + span, + }); + } else { + vals.push(Value::nothing(span)) + } + + cols.push("group".into()); + if let Some(group) = users::get_group_by_gid(md.gid()) { + vals.push(Value::String { + val: group.name().to_string_lossy().into(), + span, + }); + } else { + vals.push(Value::nothing(span)) + } + } + } + } + + cols.push("size".to_string()); + if let Some(md) = metadata { + if md.is_dir() { + if du { + let params = DirBuilder::new(Span { start: 0, end: 2 }, None, false, None, false); + let dir_size = DirInfo::new(filename, ¶ms, None, ctrl_c).get_size(); + + vals.push(Value::Filesize { + val: dir_size as i64, + span, + }); + } else { + let dir_size: u64 = md.len(); + + vals.push(Value::Filesize { + val: dir_size as i64, + span, + }); + }; + } else if md.is_file() { + vals.push(Value::Filesize { + val: md.len() as i64, + span, + }); + } else if md.file_type().is_symlink() { + if let Ok(symlink_md) = filename.symlink_metadata() { + vals.push(Value::Filesize { + val: symlink_md.len() as i64, + span, + }); + } else { + vals.push(Value::nothing(span)); + } + } + } else { + vals.push(Value::nothing(span)); + } + + if let Some(md) = metadata { + if long { + cols.push("created".to_string()); + if let Ok(c) = md.created() { + let utc: DateTime = c.into(); + vals.push(Value::Date { + val: utc.into(), + span, + }); + } else { + vals.push(Value::nothing(span)); + } + + cols.push("accessed".to_string()); + if let Ok(a) = md.accessed() { + let utc: DateTime = a.into(); + vals.push(Value::Date { + val: utc.into(), + span, + }); + } else { + vals.push(Value::nothing(span)); + } + } + + cols.push("modified".to_string()); + if let Ok(m) = md.modified() { + let utc: DateTime = m.into(); + vals.push(Value::Date { + val: utc.into(), + span, + }); + } else { + vals.push(Value::nothing(span)); + } + } else { + if long { + cols.push("created".to_string()); + vals.push(Value::nothing(span)); + + cols.push("accessed".to_string()); + vals.push(Value::nothing(span)); + } + + cols.push("modified".to_string()); + vals.push(Value::nothing(span)); + } + + Ok(Value::Record { cols, vals, span }) +} diff --git a/crates/nu-command/src/filesystem/mkdir.rs b/crates/nu-command/src/filesystem/mkdir.rs new file mode 100644 index 0000000000..fdf05bf1e8 --- /dev/null +++ b/crates/nu-command/src/filesystem/mkdir.rs @@ -0,0 +1,80 @@ +use std::collections::VecDeque; + +use nu_engine::env::current_dir; +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, SyntaxShape, + Value, +}; + +#[derive(Clone)] +pub struct Mkdir; + +impl Command for Mkdir { + fn name(&self) -> &str { + "mkdir" + } + + fn signature(&self) -> Signature { + Signature::build("mkdir") + .rest( + "rest", + SyntaxShape::Filepath, + "the name(s) of the path(s) to create", + ) + .switch("show-created-paths", "show the path(s) created.", Some('s')) + .category(Category::FileSystem) + } + + fn usage(&self) -> &str { + "Make directories, creates intermediary directories as required." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let path = current_dir(engine_state, stack)?; + let mut directories = call + .rest::(engine_state, stack, 0)? + .into_iter() + .map(|dir| path.join(dir)) + .peekable(); + + let show_created_paths = call.has_flag("show-created-paths"); + let mut stream: VecDeque = VecDeque::new(); + + if directories.peek().is_none() { + return Err(ShellError::MissingParameter( + "requires directory paths".to_string(), + call.head, + )); + } + + for (i, dir) in directories.enumerate() { + let span = call.positional[i].span; + let dir_res = std::fs::create_dir_all(&dir); + + if let Err(reason) = dir_res { + return Err(ShellError::CreateNotPossible( + format!("failed to create directory: {}", reason), + call.positional[i].span, + )); + } + + if show_created_paths { + let val = format!("{:}", dir.to_string_lossy()); + stream.push_back(Value::String { val, span }); + } + } + + Ok(stream + .into_iter() + .into_pipeline_data(engine_state.ctrlc.clone())) + } +} diff --git a/crates/nu-command/src/filesystem/mod.rs b/crates/nu-command/src/filesystem/mod.rs new file mode 100644 index 0000000000..dd09ca7bad --- /dev/null +++ b/crates/nu-command/src/filesystem/mod.rs @@ -0,0 +1,20 @@ +mod cd; +mod cp; +mod ls; +mod mkdir; +mod mv; +mod open; +mod rm; +mod save; +mod touch; +mod util; + +pub use cd::Cd; +pub use cp::Cp; +pub use ls::Ls; +pub use mkdir::Mkdir; +pub use mv::Mv; +pub use open::{BufferedReader, Open}; +pub use rm::Rm; +pub use save::Save; +pub use touch::Touch; diff --git a/crates/nu-command/src/filesystem/mv.rs b/crates/nu-command/src/filesystem/mv.rs new file mode 100644 index 0000000000..7371431722 --- /dev/null +++ b/crates/nu-command/src/filesystem/mv.rs @@ -0,0 +1,202 @@ +use std::path::Path; + +use super::util::get_interactive_confirmation; +use nu_engine::env::current_dir; +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, PipelineData, ShellError, Signature, Spanned, SyntaxShape}; + +#[derive(Clone)] +pub struct Mv; + +#[allow(unused_must_use)] +impl Command for Mv { + fn name(&self) -> &str { + "mv" + } + + fn usage(&self) -> &str { + "Move files or directories." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("mv") + .required( + "source", + SyntaxShape::GlobPattern, + "the location to move files/directories from", + ) + .required( + "destination", + SyntaxShape::Filepath, + "the location to move files/directories to", + ) + .switch("interactive", "ask user to confirm action", Some('i')) + .switch("force", "suppress error when no file", Some('f')) + .category(Category::FileSystem) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + // TODO: handle invalid directory or insufficient permissions when moving + let spanned_source: Spanned = call.req(engine_state, stack, 0)?; + let destination: String = call.req(engine_state, stack, 1)?; + let interactive = call.has_flag("interactive"); + let force = call.has_flag("force"); + + let path = current_dir(engine_state, stack)?; + let source = path.join(spanned_source.item.as_str()); + let destination = path.join(destination.as_str()); + + let mut sources = + glob::glob(&source.to_string_lossy()).map_or_else(|_| Vec::new(), Iterator::collect); + + if sources.is_empty() { + return Err(ShellError::FileNotFound(spanned_source.span)); + } + + if interactive && !force { + let mut remove: Vec = vec![]; + for (index, file) in sources.iter().enumerate() { + let prompt = format!( + "Are you shure that you want to move {} to {}?", + file.as_ref() + .map_err(|err| ShellError::SpannedLabeledError( + "Reference error".into(), + err.to_string(), + call.head + ))? + .file_name() + .ok_or_else(|| ShellError::SpannedLabeledError( + "File name error".into(), + "Unable to get file name".into(), + call.head + ))? + .to_str() + .ok_or_else(|| ShellError::SpannedLabeledError( + "Unable to get str error".into(), + "Unable to convert to str file name".into(), + call.head + ))?, + destination + .file_name() + .ok_or_else(|| ShellError::SpannedLabeledError( + "File name error".into(), + "Unable to get file name".into(), + call.head + ))? + .to_str() + .ok_or_else(|| ShellError::SpannedLabeledError( + "Unable to get str error".into(), + "Unable to convert to str file name".into(), + call.head + ))?, + ); + + let input = get_interactive_confirmation(prompt)?; + + if !input { + remove.push(index); + } + } + + remove.reverse(); + + for index in remove { + sources.remove(index); + } + + if sources.is_empty() { + return Err(ShellError::NoFileToBeMoved()); + } + } + + if (destination.exists() && !destination.is_dir() && sources.len() > 1) + || (!destination.exists() && sources.len() > 1) + { + return Err(ShellError::MoveNotPossible { + source_message: "Can't move many files".to_string(), + source_span: call.positional[0].span, + destination_message: "into single file".to_string(), + destination_span: call.positional[1].span, + }); + } + + let some_if_source_is_destination = sources + .iter() + .find(|f| matches!(f, Ok(f) if destination.starts_with(f))); + if destination.exists() && destination.is_dir() && sources.len() == 1 { + if let Some(Ok(_filename)) = some_if_source_is_destination { + return Err(ShellError::MoveNotPossible { + source_message: "Can't move directory".to_string(), + source_span: call.positional[0].span, + destination_message: "into itself".to_string(), + destination_span: call.positional[1].span, + }); + } + } + + if let Some(Ok(_filename)) = some_if_source_is_destination { + sources = sources + .into_iter() + .filter(|f| matches!(f, Ok(f) if !destination.starts_with(f))) + .collect(); + } + + for entry in sources.into_iter().flatten() { + move_file(call, &entry, &destination)? + } + + Ok(PipelineData::new(call.head)) + } +} + +fn move_file(call: &Call, from: &Path, to: &Path) -> Result<(), ShellError> { + if to.exists() && from.is_dir() && to.is_file() { + return Err(ShellError::MoveNotPossible { + source_message: "Can't move a directory".to_string(), + source_span: call.positional[0].span, + destination_message: "to a file".to_string(), + destination_span: call.positional[1].span, + }); + } + + let destination_dir_exists = if to.is_dir() { + true + } else { + to.parent().map(Path::exists).unwrap_or(true) + }; + + if !destination_dir_exists { + return Err(ShellError::DirectoryNotFound(call.positional[1].span)); + } + + let mut to = to.to_path_buf(); + if to.is_dir() { + let from_file_name = match from.file_name() { + Some(name) => name, + None => return Err(ShellError::DirectoryNotFound(call.positional[1].span)), + }; + + to.push(from_file_name); + } + + move_item(call, from, &to) +} + +fn move_item(call: &Call, from: &Path, to: &Path) -> Result<(), ShellError> { + // We first try a rename, which is a quick operation. If that doesn't work, we'll try a copy + // and remove the old file/folder. This is necessary if we're moving across filesystems or devices. + std::fs::rename(&from, &to).map_err(|_| ShellError::MoveNotPossible { + source_message: "failed to move".to_string(), + source_span: call.positional[0].span, + destination_message: "into".to_string(), + destination_span: call.positional[1].span, + }) +} diff --git a/crates/nu-command/src/filesystem/open.rs b/crates/nu-command/src/filesystem/open.rs new file mode 100644 index 0000000000..d3afce0d34 --- /dev/null +++ b/crates/nu-command/src/filesystem/open.rs @@ -0,0 +1,216 @@ +use nu_engine::{get_full_help, CallExt}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoPipelineData, PipelineData, RawStream, ShellError, Signature, Spanned, + SyntaxShape, Value, +}; +use std::io::{BufRead, BufReader, Read}; + +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; +use std::path::Path; + +#[derive(Clone)] +pub struct Open; + +impl Command for Open { + fn name(&self) -> &str { + "open" + } + + fn usage(&self) -> &str { + "Opens a file." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("open") + .optional("filename", SyntaxShape::Filepath, "the filename to use") + .switch("raw", "open file as raw binary", Some('r')) + .category(Category::FileSystem) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let raw = call.has_flag("raw"); + + let call_span = call.head; + let ctrlc = engine_state.ctrlc.clone(); + + let path = call.opt::>(engine_state, stack, 0)?; + + let path = if let Some(path) = path { + path + } else { + // Collect a filename from the input + match input { + PipelineData::Value(Value::Nothing { .. }, ..) => { + return Ok(Value::String { + val: get_full_help( + &Open.signature(), + &Open.examples(), + engine_state, + stack, + ), + span: call.head, + } + .into_pipeline_data()) + } + PipelineData::Value(val, ..) => val.as_spanned_string()?, + _ => { + return Ok(Value::String { + val: get_full_help( + &Open.signature(), + &Open.examples(), + engine_state, + stack, + ), + span: call.head, + } + .into_pipeline_data()) + } + } + }; + let arg_span = path.span; + let path = Path::new(&path.item); + + if permission_denied(&path) { + #[cfg(unix)] + let error_msg = format!( + "The permissions of {:o} do not allow access for this user", + path.metadata() + .expect("this shouldn't be called since we already know there is a dir") + .permissions() + .mode() + & 0o0777 + ); + #[cfg(not(unix))] + let error_msg = String::from("Permission denied"); + Ok(PipelineData::Value( + Value::Error { + error: ShellError::SpannedLabeledError( + "Permission denied".into(), + error_msg, + arg_span, + ), + }, + None, + )) + } else { + let file = match std::fs::File::open(path) { + Ok(file) => file, + Err(err) => { + return Ok(PipelineData::Value( + Value::Error { + error: ShellError::SpannedLabeledError( + "Permission denied".into(), + err.to_string(), + arg_span, + ), + }, + None, + )); + } + }; + + let buf_reader = BufReader::new(file); + + let output = PipelineData::RawStream( + RawStream::new( + Box::new(BufferedReader { input: buf_reader }), + ctrlc, + call_span, + ), + call_span, + None, + ); + + let ext = if raw { + None + } else { + path.extension() + .map(|name| name.to_string_lossy().to_string()) + }; + + if let Some(ext) = ext { + match engine_state.find_decl(format!("from {}", ext).as_bytes()) { + Some(converter_id) => engine_state.get_decl(converter_id).run( + engine_state, + stack, + &Call::new(call_span), + output, + ), + None => Ok(output), + } + } else { + Ok(output) + } + } + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Open a file, with structure (based on file extension)", + example: "open myfile.json", + result: None, + }, + Example { + description: "Open a file, as raw bytes", + example: "open myfile.json --raw", + result: None, + }, + Example { + description: "Open a file, using the input to get filename", + example: "echo 'myfile.txt' | open", + result: None, + }, + ] + } +} + +fn permission_denied(dir: impl AsRef) -> bool { + match dir.as_ref().read_dir() { + Err(e) => matches!(e.kind(), std::io::ErrorKind::PermissionDenied), + Ok(_) => false, + } +} + +pub struct BufferedReader { + input: BufReader, +} + +impl BufferedReader { + pub fn new(input: BufReader) -> Self { + Self { input } + } +} + +impl Iterator for BufferedReader { + type Item = Result, ShellError>; + + fn next(&mut self) -> Option { + let buffer = self.input.fill_buf(); + match buffer { + Ok(s) => { + let result = s.to_vec(); + + let buffer_len = s.len(); + + if buffer_len == 0 { + None + } else { + self.input.consume(buffer_len); + + Some(Ok(result)) + } + } + Err(e) => Some(Err(ShellError::IOError(e.to_string()))), + } + } +} diff --git a/crates/nu-command/src/filesystem/rm.rs b/crates/nu-command/src/filesystem/rm.rs new file mode 100644 index 0000000000..b49ce06298 --- /dev/null +++ b/crates/nu-command/src/filesystem/rm.rs @@ -0,0 +1,294 @@ +#[cfg(unix)] +use std::os::unix::prelude::FileTypeExt; +use std::path::PathBuf; + +use super::util::get_interactive_confirmation; + +use nu_engine::env::current_dir; +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, SyntaxShape, + Value, +}; + +#[derive(Clone)] +pub struct Rm; + +// Where self.0 is the unexpanded target's positional index (i.e. call.positional[self.0].span) +struct Target(usize, PathBuf); + +struct RmArgs { + targets: Vec, + recursive: bool, + trash: bool, + permanent: bool, + force: bool, +} + +impl Command for Rm { + fn name(&self) -> &str { + "rm" + } + + fn usage(&self) -> &str { + "Remove file(s)." + } + + fn signature(&self) -> Signature { + Signature::build("rm") + .switch( + "trash", + "use the platform's recycle bin instead of permanently deleting", + Some('t'), + ) + .switch( + "permanent", + "don't use recycle bin, delete permanently", + Some('p'), + ) + .switch("recursive", "delete subdirectories recursively", Some('r')) + .switch("force", "suppress error when no file", Some('f')) + .switch("interactive", "ask user to confirm action", Some('i')) + .rest( + "rest", + SyntaxShape::GlobPattern, + "the file path(s) to remove", + ) + .category(Category::FileSystem) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + rm(engine_state, stack, call) + } +} + +fn rm( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, +) -> Result { + let trash = call.has_flag("trash"); + let permanent = call.has_flag("permanent"); + let interactive = call.has_flag("interactive"); + + if trash && permanent { + return Err(ShellError::IncompatibleParametersSingle( + "Can't use \"--trash\" with \"--permanent\"".to_string(), + call.head, + )); + } + + let current_path = current_dir(engine_state, stack)?; + let mut paths = call + .rest::(engine_state, stack, 0)? + .into_iter() + .map(|path| current_path.join(path)) + .peekable(); + + if paths.peek().is_none() { + return Err(ShellError::FileNotFound(call.positional[0].span)); + } + + // Expand and flatten files + let resolve_path = |i: usize, path: PathBuf| { + glob::glob(&path.to_string_lossy()).map_or_else( + |_| Vec::new(), + |path_iter| path_iter.flatten().map(|f| Target(i, f)).collect(), + ) + }; + + let mut targets: Vec = vec![]; + for (i, path) in paths.enumerate() { + let mut paths: Vec = resolve_path(i, path); + + if paths.is_empty() { + return Err(ShellError::FileNotFound(call.positional[i].span)); + } + + targets.append(paths.as_mut()); + } + + let recursive = call.has_flag("recursive"); + let force = call.has_flag("force"); + + if interactive && !force { + let mut remove: Vec = vec![]; + for (index, file) in targets.iter().enumerate() { + let prompt: String = format!( + "Are you sure that you what to delete {}?", + file.1 + .file_name() + .ok_or_else(|| ShellError::SpannedLabeledError( + "File name error".into(), + "Unable to get file name".into(), + call.head + ))? + .to_str() + .ok_or_else(|| ShellError::SpannedLabeledError( + "Unable to get str error".into(), + "Unable to convert to str file name".into(), + call.head + ))?, + ); + + let input = get_interactive_confirmation(prompt)?; + + if !input { + remove.push(index); + } + } + + remove.reverse(); + + for index in remove { + targets.remove(index); + } + + if targets.is_empty() { + return Err(ShellError::NoFileToBeRemoved()); + } + } + + let args = RmArgs { + targets, + recursive, + trash, + permanent, + force, + }; + let response = rm_helper(call, args); + + // let temp = rm_helper(call, args).flatten(); + // let temp = input.flatten(call.head, move |_| rm_helper(call, args)); + + Ok(response + .into_iter() + .into_pipeline_data(engine_state.ctrlc.clone())) + // Ok(Value::Nothing { span }) +} + +fn rm_helper(call: &Call, args: RmArgs) -> Vec { + let (targets, recursive, trash, _permanent, force) = ( + args.targets, + args.recursive, + args.trash, + args.permanent, + args.force, + ); + + #[cfg(not(feature = "trash-support"))] + { + if trash { + let error = match call.get_flag_expr("trash").ok_or_else(|| { + ShellError::SpannedLabeledError( + "Flag not found".into(), + "trash flag not found".into(), + call.head, + ) + }) { + Ok(expr) => ShellError::FeatureNotEnabled(expr.span), + Err(err) => err, + }; + + return vec![Value::Error { error }]; + } + } + + if targets.is_empty() && !force { + return vec![Value::Error { + error: ShellError::FileNotFound(call.head), + }]; + } + + targets + .into_iter() + .map(move |target| { + let (i, f) = (target.0, target.1); + + let is_empty = || match f.read_dir() { + Ok(mut p) => p.next().is_none(), + Err(_) => false, + }; + + if let Ok(metadata) = f.symlink_metadata() { + #[cfg(unix)] + let is_socket = metadata.file_type().is_socket(); + #[cfg(unix)] + let is_fifo = metadata.file_type().is_fifo(); + + #[cfg(not(unix))] + let is_socket = false; + #[cfg(not(unix))] + let is_fifo = false; + + if metadata.is_file() + || metadata.file_type().is_symlink() + || recursive + || is_socket + || is_fifo + || is_empty() + { + let result; + #[cfg(feature = "trash-support")] + { + use std::io::Error; + result = if trash { + trash::delete(&f).map_err(|e: trash::Error| { + use std::io::ErrorKind; + Error::new(ErrorKind::Other, format!("{:?}", e)) + }) + } else if metadata.is_file() { + std::fs::remove_file(&f) + } else { + std::fs::remove_dir_all(&f) + }; + } + #[cfg(not(feature = "trash-support"))] + { + result = if metadata.is_file() || is_socket || is_fifo { + std::fs::remove_file(&f) + } else { + std::fs::remove_dir_all(&f) + }; + } + + if let Err(e) = result { + Value::Error { + error: ShellError::RemoveNotPossible( + format!("Could not delete because: {:}\nTry '--trash' flag", e), + call.head, + ), + } + } else { + Value::String { + val: format!("deleted {:}", f.to_string_lossy()), + span: call.positional[i].span, + } + } + } else { + Value::Error { + error: ShellError::RemoveNotPossible( + "Cannot remove. try --recursive".to_string(), + call.positional[i].span, + ), + } + } + } else { + Value::Error { + error: ShellError::RemoveNotPossible( + "no such file or directory".to_string(), + call.positional[i].span, + ), + } + } + }) + .collect() +} diff --git a/crates/nu-command/src/filesystem/save.rs b/crates/nu-command/src/filesystem/save.rs new file mode 100644 index 0000000000..fa660f04c7 --- /dev/null +++ b/crates/nu-command/src/filesystem/save.rs @@ -0,0 +1,118 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Value}; +use std::io::Write; + +use std::path::Path; + +#[derive(Clone)] +pub struct Save; + +impl Command for Save { + fn name(&self) -> &str { + "save" + } + + fn usage(&self) -> &str { + "Save a file." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("save") + .required("filename", SyntaxShape::Filepath, "the filename to use") + .switch("raw", "open file as raw binary", Some('r')) + .category(Category::FileSystem) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let raw = call.has_flag("raw"); + + let span = call.head; + + let config = stack.get_config()?; + + let path = call.req::>(engine_state, stack, 0)?; + let arg_span = path.span; + let path = Path::new(&path.item); + + let mut file = match std::fs::File::create(path) { + Ok(file) => file, + Err(err) => { + return Ok(PipelineData::Value( + Value::Error { + error: ShellError::SpannedLabeledError( + "Permission denied".into(), + err.to_string(), + arg_span, + ), + }, + None, + )); + } + }; + + let ext = if raw { + None + } else { + path.extension() + .map(|name| name.to_string_lossy().to_string()) + }; + + if let Some(ext) = ext { + match engine_state.find_decl(format!("to {}", ext).as_bytes()) { + Some(converter_id) => { + let output = engine_state.get_decl(converter_id).run( + engine_state, + stack, + &Call::new(span), + input, + )?; + + let output = output.into_value(span); + + match output { + Value::String { val, .. } => { + if let Err(err) = file.write_all(val.as_bytes()) { + return Err(ShellError::IOError(err.to_string())); + } + + Ok(PipelineData::new(span)) + } + Value::Binary { val, .. } => { + if let Err(err) = file.write_all(&val) { + return Err(ShellError::IOError(err.to_string())); + } + + Ok(PipelineData::new(span)) + } + v => Err(ShellError::UnsupportedInput(v.get_type().to_string(), span)), + } + } + None => { + let output = input.collect_string("", &config)?; + + if let Err(err) = file.write_all(output.as_bytes()) { + return Err(ShellError::IOError(err.to_string())); + } + + Ok(PipelineData::new(span)) + } + } + } else { + let output = input.collect_string("", &config)?; + + if let Err(err) = file.write_all(output.as_bytes()) { + return Err(ShellError::IOError(err.to_string())); + } + + Ok(PipelineData::new(span)) + } + } +} diff --git a/crates/nu-command/src/filesystem/touch.rs b/crates/nu-command/src/filesystem/touch.rs new file mode 100644 index 0000000000..ece811c976 --- /dev/null +++ b/crates/nu-command/src/filesystem/touch.rs @@ -0,0 +1,55 @@ +use std::fs::OpenOptions; + +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, PipelineData, ShellError, Signature, SyntaxShape}; + +#[derive(Clone)] +pub struct Touch; + +impl Command for Touch { + fn name(&self) -> &str { + "touch" + } + + fn signature(&self) -> Signature { + Signature::build("touch") + .required( + "filename", + SyntaxShape::Filepath, + "the path of the file you want to create", + ) + .rest("rest", SyntaxShape::Filepath, "additional files to create") + .category(Category::FileSystem) + } + + fn usage(&self) -> &str { + "Creates one or more files." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let target: String = call.req(engine_state, stack, 0)?; + let rest: Vec = call.rest(engine_state, stack, 1)?; + + for (index, item) in vec![target].into_iter().chain(rest).enumerate() { + match OpenOptions::new().write(true).create(true).open(&item) { + Ok(_) => continue, + Err(err) => { + return Err(ShellError::CreateNotPossible( + format!("Failed to create file: {}", err), + call.positional[index].span, + )); + } + } + } + + Ok(PipelineData::new(call.head)) + } +} diff --git a/crates/nu-command/src/filesystem/util.rs b/crates/nu-command/src/filesystem/util.rs new file mode 100644 index 0000000000..a15c5a4618 --- /dev/null +++ b/crates/nu-command/src/filesystem/util.rs @@ -0,0 +1,121 @@ +use std::path::{Path, PathBuf}; + +use nu_engine::env::current_dir_str; +use nu_path::canonicalize_with; +use nu_protocol::engine::{EngineState, Stack}; +use nu_protocol::ShellError; + +use dialoguer::Input; +use std::error::Error; + +#[derive(Default)] +pub struct FileStructure { + pub resources: Vec, +} + +#[allow(dead_code)] +impl FileStructure { + pub fn new() -> FileStructure { + FileStructure { resources: vec![] } + } + + pub fn contains_more_than_one_file(&self) -> bool { + self.resources.len() > 1 + } + + pub fn contains_files(&self) -> bool { + !self.resources.is_empty() + } + + pub fn paths_applying_with( + &mut self, + to: F, + ) -> Result, Box> + where + F: Fn((PathBuf, usize)) -> Result<(PathBuf, PathBuf), Box>, + { + self.resources + .iter() + .map(|f| (PathBuf::from(&f.location), f.at)) + .map(to) + .collect() + } + + pub fn walk_decorate( + &mut self, + start_path: &Path, + engine_state: &EngineState, + stack: &Stack, + ) -> Result<(), ShellError> { + self.resources = Vec::::new(); + self.build(start_path, 0, engine_state, stack)?; + self.resources.sort(); + + Ok(()) + } + + fn build( + &mut self, + src: &Path, + lvl: usize, + engine_state: &EngineState, + stack: &Stack, + ) -> Result<(), ShellError> { + let source = canonicalize_with(src, current_dir_str(engine_state, stack)?)?; + + if source.is_dir() { + for entry in std::fs::read_dir(src)? { + let entry = entry?; + let path = entry.path(); + + if path.is_dir() { + self.build(&path, lvl + 1, engine_state, stack)?; + } + + self.resources.push(Resource { + location: path.to_path_buf(), + at: lvl, + }); + } + } else { + self.resources.push(Resource { + location: source, + at: lvl, + }); + } + + Ok(()) + } +} + +#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct Resource { + pub at: usize, + pub location: PathBuf, +} + +impl Resource {} + +pub fn get_interactive_confirmation(prompt: String) -> Result> { + let input = Input::new() + .with_prompt(prompt) + .validate_with(|c_input: &String| -> Result<(), String> { + if c_input.len() == 1 + && (c_input == "y" || c_input == "Y" || c_input == "n" || c_input == "N") + { + Ok(()) + } else if c_input.len() > 1 { + Err("Enter only one letter (Y/N)".to_string()) + } else { + Err("Input not valid".to_string()) + } + }) + .default("Y/N".into()) + .interact_text()?; + + if input == "y" || input == "Y" { + Ok(true) + } else { + Ok(false) + } +} diff --git a/crates/nu-command/src/filters/all.rs b/crates/nu-command/src/filters/all.rs new file mode 100644 index 0000000000..cdc076262b --- /dev/null +++ b/crates/nu-command/src/filters/all.rs @@ -0,0 +1,92 @@ +use nu_engine::{eval_block, CallExt}; +use nu_protocol::{ + ast::Call, + engine::{CaptureBlock, Command, EngineState, Stack}, + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct All; + +impl Command for All { + fn name(&self) -> &str { + "all?" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required( + "predicate", + SyntaxShape::RowCondition, + "the predicate that must match", + ) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Test if every element of the input matches a predicate." + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Find if services are running", + example: "echo [[status]; [UP] [UP]] | all? status == UP", + result: Some(Value::test_bool(true)), + }, + Example { + description: "Check that all values are even", + example: "echo [2 4 6 8] | all? ($it mod 2) == 0", + result: Some(Value::test_bool(true)), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + // let predicate = &call.positional[0]; + let span = call.head; + + let capture_block: CaptureBlock = call.req(engine_state, stack, 0)?; + let block_id = capture_block.block_id; + + let block = engine_state.get_block(block_id); + let var_id = block.signature.get_positional(0).and_then(|arg| arg.var_id); + let mut stack = stack.captures_to_stack(&capture_block.captures); + + let ctrlc = engine_state.ctrlc.clone(); + let engine_state = engine_state.clone(); + + Ok(Value::Bool { + val: input.into_interruptible_iter(ctrlc).all(move |value| { + if let Some(var_id) = var_id { + stack.add_var(var_id, value); + } + + eval_block(&engine_state, &mut stack, block, PipelineData::new(span)) + .map_or(false, |pipeline_data| { + pipeline_data.into_value(span).is_true() + }) + }), + span, + } + .into_pipeline_data()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(All) + } +} diff --git a/crates/nu-command/src/filters/any.rs b/crates/nu-command/src/filters/any.rs new file mode 100644 index 0000000000..b66507f5c5 --- /dev/null +++ b/crates/nu-command/src/filters/any.rs @@ -0,0 +1,91 @@ +use nu_engine::{eval_block, CallExt}; +use nu_protocol::{ + ast::Call, + engine::{CaptureBlock, Command, EngineState, Stack}, + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Any; + +impl Command for Any { + fn name(&self) -> &str { + "any?" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required( + "predicate", + SyntaxShape::RowCondition, + "the predicate that must match", + ) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Tests if any element of the input matches a predicate." + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Find if a service is not running", + example: "echo [[status]; [UP] [DOWN] [UP]] | any? status == DOWN", + result: Some(Value::test_bool(true)), + }, + Example { + description: "Check if any of the values is odd", + example: "echo [2 4 1 6 8] | any? ($it mod 2) == 1", + result: Some(Value::test_bool(true)), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let span = call.head; + + let capture_block: CaptureBlock = call.req(engine_state, stack, 0)?; + let block_id = capture_block.block_id; + + let block = engine_state.get_block(block_id); + let var_id = block.signature.get_positional(0).and_then(|arg| arg.var_id); + let mut stack = stack.captures_to_stack(&capture_block.captures); + + let ctrlc = engine_state.ctrlc.clone(); + let engine_state = engine_state.clone(); + + Ok(Value::Bool { + val: input.into_interruptible_iter(ctrlc).any(move |value| { + if let Some(var_id) = var_id { + stack.add_var(var_id, value); + } + + eval_block(&engine_state, &mut stack, block, PipelineData::new(span)) + .map_or(false, |pipeline_data| { + pipeline_data.into_value(span).is_true() + }) + }), + span, + } + .into_pipeline_data()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Any) + } +} diff --git a/crates/nu-command/src/filters/append.rs b/crates/nu-command/src/filters/append.rs new file mode 100644 index 0000000000..55d6dc67de --- /dev/null +++ b/crates/nu-command/src/filters/append.rs @@ -0,0 +1,121 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span, + SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Append; + +impl Command for Append { + fn name(&self) -> &str { + "append" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("append") + .required("row", SyntaxShape::Any, "the row to append") + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Append a row to the table." + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "[0,1,2,3] | append 4", + description: "Append one Int item", + result: Some(Value::List { + vals: vec![ + Value::test_int(0), + Value::test_int(1), + Value::test_int(2), + Value::test_int(3), + Value::test_int(4), + ], + span: Span::test_data(), + }), + }, + Example { + example: "[0,1] | append [2,3,4]", + description: "Append three Int items", + result: Some(Value::List { + vals: vec![ + Value::test_int(0), + Value::test_int(1), + Value::test_int(2), + Value::test_int(3), + Value::test_int(4), + ], + span: Span::test_data(), + }), + }, + Example { + example: "[0,1] | append [2,nu,4,shell]", + description: "Append Ints and Strings", + result: Some(Value::List { + vals: vec![ + Value::test_int(0), + Value::test_int(1), + Value::test_int(2), + Value::test_string("nu"), + Value::test_int(4), + Value::test_string("shell"), + ], + span: Span::test_data(), + }), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let val: Value = call.req(engine_state, stack, 0)?; + let vec: Vec = process_value(val); + + Ok(input + .into_iter() + .chain(vec) + .into_iter() + .into_pipeline_data(engine_state.ctrlc.clone())) + } +} + +fn process_value(val: Value) -> Vec { + match val { + Value::List { + vals: input_vals, + span: _, + } => { + let mut output = vec![]; + for input_val in input_vals { + output.push(input_val); + } + output + } + _ => { + vec![val] + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Append {}) + } +} diff --git a/crates/nu-command/src/filters/collect.rs b/crates/nu-command/src/filters/collect.rs new file mode 100644 index 0000000000..c9cc64dcb6 --- /dev/null +++ b/crates/nu-command/src/filters/collect.rs @@ -0,0 +1,75 @@ +use nu_engine::{eval_block, CallExt}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{CaptureBlock, Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, Signature, SyntaxShape, Value}; + +#[derive(Clone)] +pub struct Collect; + +impl Command for Collect { + fn name(&self) -> &str { + "collect" + } + + fn signature(&self) -> Signature { + Signature::build("collect") + .required( + "block", + SyntaxShape::Block(Some(vec![SyntaxShape::Any])), + "the block to run once the stream is collected", + ) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Collect the stream and pass it to a block." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let capture_block: CaptureBlock = call.req(engine_state, stack, 0)?; + + let block = engine_state.get_block(capture_block.block_id).clone(); + let mut stack = stack.captures_to_stack(&capture_block.captures); + + let input: Value = input.into_value(call.head); + + if let Some(var) = block.signature.get_positional(0) { + if let Some(var_id) = &var.var_id { + stack.add_var(*var_id, input); + } + } + + eval_block( + engine_state, + &mut stack, + &block, + PipelineData::new(call.head), + ) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Use the second value in the stream", + example: "echo 1 2 3 | collect { |x| echo $x.1 }", + result: Some(Value::test_int(2)), + }] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Collect {}) + } +} diff --git a/crates/nu-command/src/filters/columns.rs b/crates/nu-command/src/filters/columns.rs new file mode 100644 index 0000000000..303251a0f5 --- /dev/null +++ b/crates/nu-command/src/filters/columns.rs @@ -0,0 +1,107 @@ +use nu_engine::column::get_columns; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, ShellError, + Signature, Span, Value, +}; + +#[derive(Clone)] +pub struct Columns; + +impl Command for Columns { + fn name(&self) -> &str { + "columns" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Filters) + } + + fn usage(&self) -> &str { + "Show the columns in the input." + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "[[name,age,grade]; [bill,20,a]] | columns", + description: "Get the columns from the table", + result: None, + }, + Example { + example: "[[name,age,grade]; [bill,20,a]] | columns | first", + description: "Get the first column from the table", + result: None, + }, + Example { + example: "[[name,age,grade]; [bill,20,a]] | columns | nth 1", + description: "Get the second column from the table", + result: None, + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let span = call.head; + getcol(engine_state, span, input) + } +} + +fn getcol( + engine_state: &EngineState, + span: Span, + input: PipelineData, +) -> Result { + match input { + PipelineData::Value( + Value::List { + vals: input_vals, + span, + }, + .., + ) => { + let input_cols = get_columns(&input_vals); + Ok(input_cols + .into_iter() + .map(move |x| Value::String { val: x, span }) + .into_pipeline_data(engine_state.ctrlc.clone())) + } + PipelineData::ListStream(stream, ..) => { + let v: Vec<_> = stream.into_iter().collect(); + let input_cols = get_columns(&v); + + Ok(input_cols + .into_iter() + .map(move |x| Value::String { val: x, span }) + .into_pipeline_data(engine_state.ctrlc.clone())) + } + PipelineData::Value(Value::Record { cols, .. }, ..) => Ok(cols + .into_iter() + .map(move |x| Value::String { val: x, span }) + .into_pipeline_data(engine_state.ctrlc.clone())), + PipelineData::Value(..) | PipelineData::RawStream(..) => { + let cols = vec![]; + let vals = vec![]; + Ok(Value::Record { cols, vals, span }.into_pipeline_data()) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Columns {}) + } +} diff --git a/crates/nu-command/src/filters/compact.rs b/crates/nu-command/src/filters/compact.rs new file mode 100644 index 0000000000..184f90976c --- /dev/null +++ b/crates/nu-command/src/filters/compact.rs @@ -0,0 +1,116 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, engine::Command, engine::EngineState, engine::Stack, Category, Example, + PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Compact; + +impl Command for Compact { + fn name(&self) -> &str { + "compact" + } + + fn signature(&self) -> Signature { + Signature::build("compact") + .rest( + "columns", + SyntaxShape::Any, + "the columns to compact from the table", + ) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Creates a table with non-empty rows." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + compact(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Filter out all records where 'Hello' is null (returns nothing)", + example: r#"echo [["Hello" "World"]; [$nothing 3]]| compact Hello"#, + result: Some(Value::List { + vals: vec![], + span: Span::test_data(), + }), + }, + Example { + description: "Filter out all records where 'World' is null (Returns the table)", + example: r#"echo [["Hello" "World"]; [$nothing 3]]| compact World"#, + result: Some(Value::List { + vals: vec![Value::Record { + cols: vec!["Hello".into(), "World".into()], + vals: vec![Value::nothing(Span::test_data()), Value::test_int(3)], + span: Span::test_data(), + }], + span: Span::test_data(), + }), + }, + Example { + description: "Filter out all instances of nothing from a list (Returns [1,2]", + example: r#"echo [1, $nothing, 2] | compact"#, + result: Some(Value::List { + vals: vec![Value::test_int(1), Value::test_int(2)], + span: Span::test_data(), + }), + }, + ] + } +} + +pub fn compact( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let columns: Vec = call.rest(engine_state, stack, 0)?; + input.filter( + move |item| { + match item { + // Nothing is filtered out + Value::Nothing { .. } => false, + Value::Record { .. } => { + for column in columns.iter() { + match item.get_data_by_key(column) { + None => return false, + Some(x) => { + if let Value::Nothing { .. } = x { + return false; + } + } + } + } + // No defined columns contained Nothing + true + } + // Any non-Nothing, non-record should be kept + _ => true, + } + }, + engine_state.ctrlc.clone(), + ) +} + +#[cfg(test)] +mod tests { + use super::Compact; + + #[test] + fn examples_work_as_expected() { + use crate::test_examples; + test_examples(Compact {}) + } +} diff --git a/crates/nu-command/src/filters/default.rs b/crates/nu-command/src/filters/default.rs new file mode 100644 index 0000000000..1db487cd99 --- /dev/null +++ b/crates/nu-command/src/filters/default.rs @@ -0,0 +1,100 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, Signature, Spanned, SyntaxShape, Value}; + +#[derive(Clone)] +pub struct Default; + +impl Command for Default { + fn name(&self) -> &str { + "default" + } + + fn signature(&self) -> Signature { + Signature::build("default") + .required("column name", SyntaxShape::String, "the name of the column") + .required( + "column value", + SyntaxShape::Any, + "the value of the column to default", + ) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Sets a default row's column if missing." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + default(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Give a default 'target' to all file entries", + example: "ls -la | default target 'nothing'", + result: None, + }] + } +} + +fn default( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let column: Spanned = call.req(engine_state, stack, 0)?; + let value: Value = call.req(engine_state, stack, 1)?; + + let ctrlc = engine_state.ctrlc.clone(); + + input.map( + move |item| match item { + Value::Record { + mut cols, + mut vals, + span, + } => { + let mut idx = 0; + let mut found = false; + + while idx < cols.len() { + if cols[idx] == column.item && matches!(vals[idx], Value::Nothing { .. }) { + vals[idx] = value.clone(); + found = true; + } + idx += 1; + } + + if !found { + cols.push(column.item.clone()); + vals.push(value.clone()); + } + + Value::Record { cols, vals, span } + } + _ => item, + }, + ctrlc, + ) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Default {}) + } +} diff --git a/crates/nu-command/src/filters/drop/column.rs b/crates/nu-command/src/filters/drop/column.rs new file mode 100644 index 0000000000..3b4564a2a4 --- /dev/null +++ b/crates/nu-command/src/filters/drop/column.rs @@ -0,0 +1,163 @@ +use nu_engine::CallExt; +use nu_protocol::ast::{Call, CellPath}; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, FromValue, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, ShellError, + Signature, Span, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct DropColumn; + +impl Command for DropColumn { + fn name(&self) -> &str { + "drop column" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .optional( + "columns", + SyntaxShape::Int, + "starting from the end, the number of columns to remove", + ) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Remove the last number of columns. If you want to remove columns by name, try 'reject'." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + // the number of columns to drop + let columns: Option = call.opt(engine_state, stack, 0)?; + let span = call.head; + + let columns_to_drop = if let Some(quantity) = columns { + quantity + } else { + 1 + }; + + dropcol(engine_state, span, input, columns_to_drop) + } +} + +fn dropcol( + engine_state: &EngineState, + span: Span, + input: PipelineData, + columns: i64, // the number of columns to drop +) -> Result { + let mut keep_columns = vec![]; + + match input { + PipelineData::Value( + Value::List { + vals: input_vals, + span, + }, + .., + ) => { + let mut output = vec![]; + let input_cols = get_input_cols(input_vals.clone()); + let kc = get_keep_columns(input_cols, columns); + keep_columns = get_cellpath_columns(kc, span); + + for input_val in input_vals { + let mut cols = vec![]; + let mut vals = vec![]; + + for path in &keep_columns { + let fetcher = input_val.clone().follow_cell_path(&path.members)?; + cols.push(path.into_string()); + vals.push(fetcher); + } + output.push(Value::Record { cols, vals, span }) + } + + Ok(output + .into_iter() + .into_pipeline_data(engine_state.ctrlc.clone())) + } + PipelineData::ListStream(stream, ..) => { + let mut output = vec![]; + + let v: Vec<_> = stream.into_iter().collect(); + let input_cols = get_input_cols(v.clone()); + let kc = get_keep_columns(input_cols, columns); + keep_columns = get_cellpath_columns(kc, span); + + for input_val in v { + let mut cols = vec![]; + let mut vals = vec![]; + + for path in &keep_columns { + let fetcher = input_val.clone().follow_cell_path(&path.members)?; + cols.push(path.into_string()); + vals.push(fetcher); + } + output.push(Value::Record { cols, vals, span }) + } + + Ok(output + .into_iter() + .into_pipeline_data(engine_state.ctrlc.clone())) + } + PipelineData::Value(v, ..) => { + let mut cols = vec![]; + let mut vals = vec![]; + + for cell_path in &keep_columns { + let result = v.clone().follow_cell_path(&cell_path.members)?; + + cols.push(cell_path.into_string()); + vals.push(result); + } + + Ok(Value::Record { cols, vals, span }.into_pipeline_data()) + } + x => Ok(x), + } +} + +fn get_input_cols(input: Vec) -> Vec { + let rec = input.first(); + match rec { + Some(Value::Record { cols, vals: _, .. }) => cols.to_vec(), + _ => vec!["".to_string()], + } +} + +fn get_cellpath_columns(keep_cols: Vec, span: Span) -> Vec { + let mut output = vec![]; + for keep_col in keep_cols { + let val = Value::String { + val: keep_col, + span, + }; + let cell_path = match CellPath::from_value(&val) { + Ok(v) => v, + Err(_) => return vec![], + }; + output.push(cell_path); + } + output +} + +fn get_keep_columns(input: Vec, mut num_of_columns_to_drop: i64) -> Vec { + let vlen: i64 = input.len() as i64; + + if num_of_columns_to_drop > vlen { + num_of_columns_to_drop = vlen; + } + + let num_of_columns_to_keep = (vlen - num_of_columns_to_drop) as usize; + input[0..num_of_columns_to_keep].to_vec() +} diff --git a/crates/nu-command/src/filters/drop/drop_.rs b/crates/nu-command/src/filters/drop/drop_.rs new file mode 100644 index 0000000000..7f78db62a1 --- /dev/null +++ b/crates/nu-command/src/filters/drop/drop_.rs @@ -0,0 +1,108 @@ +use nu_engine::CallExt; + +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span, + SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Drop; + +impl Command for Drop { + fn name(&self) -> &str { + "drop" + } + + fn signature(&self) -> Signature { + Signature::build("drop") + .optional( + "rows", + SyntaxShape::Int, + "starting from the back, the number of rows to remove", + ) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Remove the last number of rows or columns." + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "[0,1,2,3] | drop", + description: "Remove the last item of a list/table", + result: Some(Value::List { + vals: vec![Value::test_int(0), Value::test_int(1), Value::test_int(2)], + span: Span::test_data(), + }), + }, + Example { + example: "[0,1,2,3] | drop 0", + description: "Remove zero item of a list/table", + result: Some(Value::List { + vals: vec![ + Value::test_int(0), + Value::test_int(1), + Value::test_int(2), + Value::test_int(3), + ], + span: Span::test_data(), + }), + }, + Example { + example: "[0,1,2,3] | drop 2", + description: "Remove the last two items of a list/table", + result: Some(Value::List { + vals: vec![Value::test_int(0), Value::test_int(1)], + span: Span::test_data(), + }), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let rows: Option = call.opt(engine_state, stack, 0)?; + let v: Vec<_> = input.into_iter().collect(); + let vlen: i64 = v.len() as i64; + + let rows_to_drop = if let Some(quantity) = rows { + quantity + } else { + 1 + }; + + if rows_to_drop == 0 { + Ok(v.into_iter().into_pipeline_data(engine_state.ctrlc.clone())) + } else { + let k = if vlen < rows_to_drop { + 0 + } else { + vlen - rows_to_drop + }; + + let iter = v.into_iter().take(k as usize); + Ok(iter.into_pipeline_data(engine_state.ctrlc.clone())) + } + } +} + +#[cfg(test)] +mod test { + use crate::Drop; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Drop {}) + } +} diff --git a/crates/nu-command/src/filters/drop/mod.rs b/crates/nu-command/src/filters/drop/mod.rs new file mode 100644 index 0000000000..6d7162b9ab --- /dev/null +++ b/crates/nu-command/src/filters/drop/mod.rs @@ -0,0 +1,7 @@ +pub mod column; +pub mod drop_; +pub mod nth; + +pub use column::DropColumn; +pub use drop_::Drop; +pub use nth::DropNth; diff --git a/crates/nu-command/src/filters/drop/nth.rs b/crates/nu-command/src/filters/drop/nth.rs new file mode 100644 index 0000000000..e46efa6c3c --- /dev/null +++ b/crates/nu-command/src/filters/drop/nth.rs @@ -0,0 +1,179 @@ +use itertools::Either; +use nu_engine::CallExt; +use nu_protocol::ast::{Call, RangeInclusion}; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, FromValue, IntoInterruptiblePipelineData, PipelineData, PipelineIterator, + Range, ShellError, Signature, Span, Spanned, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct DropNth; + +impl Command for DropNth { + fn name(&self) -> &str { + "drop nth" + } + + fn signature(&self) -> Signature { + Signature::build("drop nth") + .required( + "row number or row range", + // FIXME: we can make this accept either Int or Range when we can compose SyntaxShapes + SyntaxShape::Any, + "the number of the row to drop or a range to drop consecutive rows", + ) + .rest("rest", SyntaxShape::Any, "the number of the row to drop") + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Drop the selected rows." + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "[sam,sarah,2,3,4,5] | drop nth 0 1 2", + description: "Drop the first, second, and third row", + result: Some(Value::List { + vals: vec![Value::test_int(3), Value::test_int(4), Value::test_int(5)], + span: Span::test_data(), + }), + }, + Example { + example: "[0,1,2,3,4,5] | drop nth 0 1 2", + description: "Drop the first, second, and third row", + result: Some(Value::List { + vals: vec![Value::test_int(3), Value::test_int(4), Value::test_int(5)], + span: Span::test_data(), + }), + }, + Example { + example: "[0,1,2,3,4,5] | drop nth 0 2 4", + description: "Drop rows 0 2 4", + result: Some(Value::List { + vals: vec![Value::test_int(1), Value::test_int(3), Value::test_int(5)], + span: Span::test_data(), + }), + }, + Example { + example: "[0,1,2,3,4,5] | drop nth 2 0 4", + description: "Drop rows 2 0 4", + result: Some(Value::List { + vals: vec![Value::test_int(1), Value::test_int(3), Value::test_int(5)], + span: Span::test_data(), + }), + }, + Example { + description: "Drop range rows from second to fourth", + example: "echo [first second third fourth fifth] | drop nth (1..3)", + result: Some(Value::List { + vals: vec![Value::test_string("first"), Value::test_string("fifth")], + span: Span::test_data(), + }), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + // let mut rows: Vec = call.rest(engine_state, stack, 0)?; + // rows.sort_unstable(); + // let pipeline_iter: PipelineIterator = input.into_iter(); + + let number_or_range = extract_int_or_range(engine_state, stack, call)?; + let rows = match number_or_range { + Either::Left(row_number) => { + let and_rows: Vec> = call.rest(engine_state, stack, 1)?; + + let mut rows: Vec<_> = and_rows.into_iter().map(|x| x.item as usize).collect(); + rows.push(row_number as usize); + rows.sort_unstable(); + rows + } + Either::Right(row_range) => { + let from = row_range.from.as_integer()? as usize; + let to = row_range.to.as_integer()? as usize; + + if matches!(row_range.inclusion, RangeInclusion::Inclusive) { + (from..=to).collect() + } else { + (from..to).collect() + } + } + }; + + Ok(DropNthIterator { + input: input.into_iter(), + rows, + current: 0, + } + .into_pipeline_data(engine_state.ctrlc.clone())) + } +} + +fn extract_int_or_range( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, +) -> Result, ShellError> { + let value = call.req::(engine_state, stack, 0)?; + + let int_opt = value.as_integer().map(Either::Left).ok(); + let range_opt: Result = FromValue::from_value(&value); + + let range_opt = range_opt.map(Either::Right).ok(); + + int_opt.or(range_opt).ok_or_else(|| { + ShellError::TypeMismatch( + "int or range".into(), + value.span().unwrap_or_else(|_| Span::new(0, 0)), + ) + }) +} + +struct DropNthIterator { + input: PipelineIterator, + rows: Vec, + current: usize, +} + +impl Iterator for DropNthIterator { + type Item = Value; + + fn next(&mut self) -> Option { + loop { + if let Some(row) = self.rows.get(0) { + if self.current == *row { + self.rows.remove(0); + self.current += 1; + let _ = self.input.next(); + continue; + } else { + self.current += 1; + return self.input.next(); + } + } else { + return self.input.next(); + } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(DropNth {}) + } +} diff --git a/crates/nu-command/src/filters/each.rs b/crates/nu-command/src/filters/each.rs new file mode 100644 index 0000000000..2dbddf66d1 --- /dev/null +++ b/crates/nu-command/src/filters/each.rs @@ -0,0 +1,247 @@ +use nu_engine::{eval_block_with_redirect, CallExt}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{CaptureBlock, Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, Signature, + Span, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Each; + +impl Command for Each { + fn name(&self) -> &str { + "each" + } + + fn usage(&self) -> &str { + "Run a block on each element of input" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("each") + .required( + "block", + SyntaxShape::Block(Some(vec![SyntaxShape::Any])), + "the block to run", + ) + .switch("numbered", "iterate with an index", Some('n')) + .category(Category::Filters) + } + + fn examples(&self) -> Vec { + let stream_test_1 = vec![ + Value::Int { + val: 2, + span: Span::test_data(), + }, + Value::Int { + val: 4, + span: Span::test_data(), + }, + Value::Int { + val: 6, + span: Span::test_data(), + }, + ]; + + vec![Example { + example: "[1 2 3] | each { 2 * $it }", + description: "Multiplies elements in list", + result: Some(Value::List { + vals: stream_test_1, + span: Span::test_data(), + }), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let capture_block: CaptureBlock = call.req(engine_state, stack, 0)?; + + let numbered = call.has_flag("numbered"); + let ctrlc = engine_state.ctrlc.clone(); + let engine_state = engine_state.clone(); + let block = engine_state.get_block(capture_block.block_id).clone(); + let mut stack = stack.captures_to_stack(&capture_block.captures); + let orig_env_vars = stack.env_vars.clone(); + let orig_env_hidden = stack.env_hidden.clone(); + let span = call.head; + + match input { + PipelineData::Value(Value::Range { .. }, ..) + | PipelineData::Value(Value::List { .. }, ..) + | PipelineData::ListStream { .. } => Ok(input + .into_iter() + .enumerate() + .map(move |(idx, x)| { + stack.with_env(&orig_env_vars, &orig_env_hidden); + + if let Some(var) = block.signature.get_positional(0) { + if let Some(var_id) = &var.var_id { + if numbered { + stack.add_var( + *var_id, + Value::Record { + cols: vec!["index".into(), "item".into()], + vals: vec![ + Value::Int { + val: idx as i64, + span, + }, + x, + ], + span, + }, + ); + } else { + stack.add_var(*var_id, x); + } + } + } + + match eval_block_with_redirect( + &engine_state, + &mut stack, + &block, + PipelineData::new(span), + ) { + Ok(v) => v.into_value(span), + Err(error) => Value::Error { error }, + } + }) + .into_pipeline_data(ctrlc)), + PipelineData::RawStream(stream, ..) => Ok(stream + .into_iter() + .enumerate() + .map(move |(idx, x)| { + stack.with_env(&orig_env_vars, &orig_env_hidden); + + let x = match x { + Ok(x) => x, + Err(err) => return Value::Error { error: err }, + }; + + if let Some(var) = block.signature.get_positional(0) { + if let Some(var_id) = &var.var_id { + if numbered { + stack.add_var( + *var_id, + Value::Record { + cols: vec!["index".into(), "item".into()], + vals: vec![ + Value::Int { + val: idx as i64, + span, + }, + x, + ], + span, + }, + ); + } else { + stack.add_var(*var_id, x); + } + } + } + + match eval_block_with_redirect( + &engine_state, + &mut stack, + &block, + PipelineData::new(span), + ) { + Ok(v) => v.into_value(span), + Err(error) => Value::Error { error }, + } + }) + .into_pipeline_data(ctrlc)), + PipelineData::Value(Value::Record { cols, vals, .. }, ..) => { + let mut output_cols = vec![]; + let mut output_vals = vec![]; + + for (col, val) in cols.into_iter().zip(vals.into_iter()) { + //let block = engine_state.get_block(block_id); + + stack.with_env(&orig_env_vars, &orig_env_hidden); + + if let Some(var) = block.signature.get_positional(0) { + if let Some(var_id) = &var.var_id { + stack.add_var( + *var_id, + Value::Record { + cols: vec!["column".into(), "value".into()], + vals: vec![ + Value::String { + val: col.clone(), + span: call.head, + }, + val, + ], + span: call.head, + }, + ); + } + } + + match eval_block_with_redirect( + &engine_state, + &mut stack, + &block, + PipelineData::new(span), + )? { + PipelineData::Value( + Value::Record { + mut cols, mut vals, .. + }, + .., + ) => { + // TODO check that the lengths match when traversing record + output_cols.append(&mut cols); + output_vals.append(&mut vals); + } + x => { + output_cols.push(col); + output_vals.push(x.into_value(span)); + } + } + } + + Ok(Value::Record { + cols: output_cols, + vals: output_vals, + span: call.head, + } + .into_pipeline_data()) + } + PipelineData::Value(x, ..) => { + //let block = engine_state.get_block(block_id); + + if let Some(var) = block.signature.get_positional(0) { + if let Some(var_id) = &var.var_id { + stack.add_var(*var_id, x); + } + } + + eval_block_with_redirect(&engine_state, &mut stack, &block, PipelineData::new(span)) + } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Each {}) + } +} diff --git a/crates/nu-command/src/filters/each_group.rs b/crates/nu-command/src/filters/each_group.rs new file mode 100644 index 0000000000..b3737f001b --- /dev/null +++ b/crates/nu-command/src/filters/each_group.rs @@ -0,0 +1,161 @@ +use nu_engine::{eval_block_with_redirect, CallExt}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{CaptureBlock, Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoInterruptiblePipelineData, PipelineData, Signature, Span, Spanned, + SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct EachGroup; + +impl Command for EachGroup { + fn name(&self) -> &str { + "each group" + } + + fn signature(&self) -> Signature { + Signature::build("each group") + .required("group_size", SyntaxShape::Int, "the size of each group") + .required( + "block", + SyntaxShape::Block(Some(vec![SyntaxShape::Any])), + "the block to run on each group", + ) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Runs a block on groups of `group_size` rows of a table at a time." + } + + fn examples(&self) -> Vec { + let stream_test_1 = vec![ + Value::Int { + val: 3, + span: Span::test_data(), + }, + Value::Int { + val: 7, + span: Span::test_data(), + }, + ]; + + vec![Example { + example: "echo [1 2 3 4] | each group 2 { $it.0 + $it.1 }", + description: "Multiplies elements in list", + result: Some(Value::List { + vals: stream_test_1, + span: Span::test_data(), + }), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let group_size: Spanned = call.req(engine_state, stack, 0)?; + let capture_block: CaptureBlock = call.req(engine_state, stack, 1)?; + let ctrlc = engine_state.ctrlc.clone(); + + //FIXME: add in support for external redirection when engine-q supports it generally + + let each_group_iterator = EachGroupIterator { + block: capture_block, + engine_state: engine_state.clone(), + stack: stack.clone(), + group_size: group_size.item, + input: Box::new(input.into_iter()), + span: call.head, + }; + + Ok(each_group_iterator.into_pipeline_data(ctrlc)) + } +} + +struct EachGroupIterator { + block: CaptureBlock, + engine_state: EngineState, + stack: Stack, + group_size: usize, + input: Box + Send>, + span: Span, +} + +impl Iterator for EachGroupIterator { + type Item = Value; + + fn next(&mut self) -> Option { + let mut group = vec![]; + let mut current_count = 0; + + loop { + let item = self.input.next(); + + match item { + Some(v) => { + group.push(v); + + current_count += 1; + if current_count >= self.group_size { + break; + } + } + None => break, + } + } + + if group.is_empty() { + return None; + } + + Some(run_block_on_vec( + group, + self.block.clone(), + self.engine_state.clone(), + self.stack.clone(), + self.span, + )) + } +} + +pub(crate) fn run_block_on_vec( + input: Vec, + capture_block: CaptureBlock, + engine_state: EngineState, + stack: Stack, + span: Span, +) -> Value { + let value = Value::List { vals: input, span }; + + let mut stack = stack.captures_to_stack(&capture_block.captures); + + let block = engine_state.get_block(capture_block.block_id); + + if let Some(var) = block.signature.get_positional(0) { + if let Some(var_id) = &var.var_id { + stack.add_var(*var_id, value); + } + } + + match eval_block_with_redirect(&engine_state, &mut stack, block, PipelineData::new(span)) { + Ok(pipeline) => pipeline.into_value(span), + Err(error) => Value::Error { error }, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(EachGroup {}) + } +} diff --git a/crates/nu-command/src/filters/each_window.rs b/crates/nu-command/src/filters/each_window.rs new file mode 100644 index 0000000000..e983f8085b --- /dev/null +++ b/crates/nu-command/src/filters/each_window.rs @@ -0,0 +1,229 @@ +use nu_engine::{eval_block_with_redirect, CallExt}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{CaptureBlock, Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoInterruptiblePipelineData, PipelineData, Signature, Span, Spanned, + SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct EachWindow; + +impl Command for EachWindow { + fn name(&self) -> &str { + "each window" + } + + fn signature(&self) -> Signature { + Signature::build("each window") + .required("window_size", SyntaxShape::Int, "the size of each window") + .named( + "stride", + SyntaxShape::Int, + "the number of rows to slide over between windows", + Some('s'), + ) + .required( + "block", + SyntaxShape::Block(Some(vec![SyntaxShape::Any])), + "the block to run on each window", + ) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Runs a block on window groups of `window_size` that slide by n rows." + } + + fn examples(&self) -> Vec { + let stream_test_1 = vec![ + Value::Int { + val: 3, + span: Span::test_data(), + }, + Value::Int { + val: 5, + span: Span::test_data(), + }, + Value::Int { + val: 7, + span: Span::test_data(), + }, + ]; + + let stream_test_2 = vec![ + Value::Int { + val: 3, + span: Span::test_data(), + }, + Value::Int { + val: 9, + span: Span::test_data(), + }, + Value::Int { + val: 15, + span: Span::test_data(), + }, + ]; + + vec![ + Example { + example: "echo [1 2 3 4] | each window 2 { $it.0 + $it.1 }", + description: "A sliding window of two elements", + result: Some(Value::List { + vals: stream_test_1, + span: Span::test_data(), + }), + }, + Example { + example: "[1, 2, 3, 4, 5, 6, 7, 8] | each window 2 --stride 3 { |x| $x.0 + $x.1 }", + description: "A sliding window of two elements, with a stride of 3", + result: Some(Value::List { + vals: stream_test_2, + span: Span::test_data(), + }), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let group_size: Spanned = call.req(engine_state, stack, 0)?; + let capture_block: CaptureBlock = call.req(engine_state, stack, 1)?; + let ctrlc = engine_state.ctrlc.clone(); + let stride: Option = call.get_flag(engine_state, stack, "stride")?; + + let stride = stride.unwrap_or(1); + + //FIXME: add in support for external redirection when engine-q supports it generally + + let each_group_iterator = EachWindowIterator { + block: capture_block, + engine_state: engine_state.clone(), + stack: stack.clone(), + group_size: group_size.item, + input: Box::new(input.into_iter()), + span: call.head, + previous: vec![], + stride, + }; + + Ok(each_group_iterator.into_pipeline_data(ctrlc)) + } +} + +struct EachWindowIterator { + block: CaptureBlock, + engine_state: EngineState, + stack: Stack, + group_size: usize, + input: Box + Send>, + span: Span, + previous: Vec, + stride: usize, +} + +impl Iterator for EachWindowIterator { + type Item = Value; + + fn next(&mut self) -> Option { + let mut group = self.previous.clone(); + let mut current_count = 0; + + if group.is_empty() { + loop { + let item = self.input.next(); + + match item { + Some(v) => { + group.push(v); + + current_count += 1; + if current_count >= self.group_size { + break; + } + } + None => return None, + } + } + } else { + // our historic buffer is already full, so stride instead + + loop { + let item = self.input.next(); + + match item { + Some(v) => { + group.push(v); + + current_count += 1; + if current_count >= self.stride { + break; + } + } + None => return None, + } + } + + for _ in 0..current_count { + let _ = group.remove(0); + } + } + + if group.is_empty() || current_count == 0 { + return None; + } + + self.previous = group.clone(); + + Some(run_block_on_vec( + group, + self.block.clone(), + self.engine_state.clone(), + self.stack.clone(), + self.span, + )) + } +} + +pub(crate) fn run_block_on_vec( + input: Vec, + capture_block: CaptureBlock, + engine_state: EngineState, + stack: Stack, + span: Span, +) -> Value { + let value = Value::List { vals: input, span }; + + let mut stack = stack.captures_to_stack(&capture_block.captures); + + let block = engine_state.get_block(capture_block.block_id); + + if let Some(var) = block.signature.get_positional(0) { + if let Some(var_id) = &var.var_id { + stack.add_var(*var_id, value); + } + } + + match eval_block_with_redirect(&engine_state, &mut stack, block, PipelineData::new(span)) { + Ok(pipeline) => pipeline.into_value(span), + Err(error) => Value::Error { error }, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(EachWindow {}) + } +} diff --git a/crates/nu-command/src/filters/empty.rs b/crates/nu-command/src/filters/empty.rs new file mode 100644 index 0000000000..992eacc8af --- /dev/null +++ b/crates/nu-command/src/filters/empty.rs @@ -0,0 +1,235 @@ +use nu_engine::{eval_block, CallExt}; +use nu_protocol::ast::{Call, CellPath}; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Empty; + +impl Command for Empty { + fn name(&self) -> &str { + "empty?" + } + + fn signature(&self) -> Signature { + Signature::build("empty?") + .rest( + "rest", + SyntaxShape::CellPath, + "the names of the columns to check emptiness", + ) + .named( + "block", + SyntaxShape::Block(Some(vec![SyntaxShape::Any])), + "an optional block to replace if empty", + Some('b'), + ) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Check for empty values." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + empty(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Check if a value is empty", + example: "'' | empty?", + result: Some(Value::Bool { + val: true, + span: Span::test_data(), + }), + }, + Example { + description: "more than one column", + example: "[[meal size]; [arepa small] [taco '']] | empty? meal size", + result: Some( + Value::List { + vals: vec![ + Value::Record{cols: vec!["meal".to_string(), "size".to_string()], vals: vec![ + Value::Bool{val: false, span: Span::test_data()}, + Value::Bool{val: false, span: Span::test_data()} + ], span: Span::test_data()}, + Value::Record{cols: vec!["meal".to_string(), "size".to_string()], vals: vec![ + Value::Bool{val: false, span: Span::test_data()}, + Value::Bool{val: true, span: Span::test_data()} + ], span: Span::test_data()} + ], span: Span::test_data() + }) + }, + Example { + description: "use a block if setting the empty cell contents is wanted", + example: "[[2020/04/16 2020/07/10 2020/11/16]; ['' [27] [37]]] | empty? 2020/04/16 -b { [33 37] }", + result: Some( + Value::List { + vals: vec![ + Value::Record{ + cols: vec!["2020/04/16".to_string(), "2020/07/10".to_string(), "2020/11/16".to_string()], + vals: vec![ + Value::List{vals: vec![ + Value::Int{val: 33, span: Span::test_data()}, + Value::Int{val: 37, span: Span::test_data()} + ], span: Span::test_data()}, + Value::List{vals: vec![ + Value::Int{val: 27, span: Span::test_data()}, + ], span: Span::test_data()}, + Value::List{vals: vec![ + Value::Int{val: 37, span: Span::test_data()}, + ], span: Span::test_data()}, + ], span: Span::test_data()} + ], span: Span::test_data() + } + ) + } + ] + } +} + +fn empty( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let head = call.head; + let columns: Vec = call.rest(engine_state, stack, 0)?; + let has_block = call.has_flag("block"); + + let block = if has_block { + let block_expr = call + .get_flag_expr("block") + .expect("internal error: expected block"); + + let block_id = block_expr + .as_block() + .ok_or_else(|| ShellError::TypeMismatch("expected row condition".to_owned(), head))?; + + let b = engine_state.get_block(block_id); + let evaluated_block = eval_block(engine_state, stack, b, PipelineData::new(head))?; + Some(evaluated_block.into_value(head)) + } else { + None + }; + + input.map( + move |value| { + let columns = columns.clone(); + + process_row(value, block.as_ref(), columns, head) + }, + engine_state.ctrlc.clone(), + ) +} + +fn process_row( + input: Value, + default_block: Option<&Value>, + column_paths: Vec, + head: Span, +) -> Value { + match input { + Value::Record { + cols: _, + ref vals, + span, + } => { + if column_paths.is_empty() { + let is_empty = vals.iter().all(|v| v.clone().is_empty()); + if default_block.is_some() { + if is_empty { + Value::Bool { val: true, span } + } else { + input.clone() + } + } else { + Value::Bool { + val: is_empty, + span, + } + } + } else { + let mut obj = input.clone(); + for column in column_paths { + let path = column.into_string(); + let data = input.get_data_by_key(&path); + let is_empty = match data { + Some(x) => x.is_empty(), + None => true, + }; + + let default = if let Some(x) = default_block { + if is_empty { + x.clone() + } else { + Value::Bool { val: true, span } + } + } else { + Value::Bool { + val: is_empty, + span, + } + }; + let r = obj.update_cell_path(&column.members, Box::new(move |_| default)); + if let Err(error) = r { + return Value::Error { error }; + } + } + obj + } + } + Value::List { vals, .. } if vals.iter().all(|v| v.as_record().is_ok()) => { + { + // we have records + if column_paths.is_empty() { + let is_empty = vals.is_empty() && vals.iter().all(|v| v.clone().is_empty()); + + Value::Bool { + val: is_empty, + span: head, + } + } else { + Value::Bool { + val: true, + span: head, + } + } + } + } + Value::List { vals, .. } => { + let empty = vals.iter().all(|v| v.clone().is_empty()); + Value::Bool { + val: empty, + span: head, + } + } + other => Value::Bool { + val: other.is_empty(), + span: head, + }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Empty {}) + } +} diff --git a/crates/nu-command/src/filters/every.rs b/crates/nu-command/src/filters/every.rs new file mode 100644 index 0000000000..59e15bee2d --- /dev/null +++ b/crates/nu-command/src/filters/every.rs @@ -0,0 +1,96 @@ +use nu_engine::CallExt; + +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span, + SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Every; + +impl Command for Every { + fn name(&self) -> &str { + "every" + } + + fn signature(&self) -> Signature { + Signature::build("every") + .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'), + ) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Show (or skip) every n-th row, starting from the first one." + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "[1 2 3 4 5] | every 2", + description: "Get every second row", + result: Some(Value::List { + vals: vec![Value::test_int(1), Value::test_int(3), Value::test_int(5)], + span: Span::test_data(), + }), + }, + Example { + example: "[1 2 3 4 5] | every 2 --skip", + description: "Skip every second row", + result: Some(Value::List { + vals: vec![Value::test_int(2), Value::test_int(4)], + span: Span::test_data(), + }), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let stride = match call.req::(engine_state, stack, 0)? { + 0 => 1, + stride => stride, + }; + + let skip = call.has_flag("skip"); + + Ok(input + .into_iter() + .enumerate() + .filter_map(move |(i, value)| { + if (i % stride != 0) == skip { + Some(value) + } else { + None + } + }) + .into_pipeline_data(engine_state.ctrlc.clone())) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Every {}) + } +} diff --git a/crates/nu-command/src/filters/find.rs b/crates/nu-command/src/filters/find.rs new file mode 100644 index 0000000000..1a1b9eadd9 --- /dev/null +++ b/crates/nu-command/src/filters/find.rs @@ -0,0 +1,183 @@ +use nu_engine::{eval_block, CallExt}; +use nu_protocol::{ + ast::Call, + engine::{CaptureBlock, Command, EngineState, Stack}, + Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span, + SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Find; + +impl Command for Find { + fn name(&self) -> &str { + "find" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .named( + "predicate", + SyntaxShape::Block(Some(vec![SyntaxShape::Any])), + "the predicate to satisfy", + Some('p'), + ) + .rest("rest", SyntaxShape::Any, "terms to search") + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Searches terms in the input or for elements of the input that satisfies the predicate." + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Search for multiple terms in a command output", + example: r#"ls | find toml md sh"#, + result: None, + }, + Example { + description: "Search for a term in a string", + example: r#"echo Cargo.toml | find toml"#, + result: Some(Value::test_string("Cargo.toml".to_owned())) + }, + Example { + description: "Search a number or a file size in a list of numbers", + example: r#"[1 5 3kb 4 3Mb] | find 5 3kb"#, + result: Some(Value::List { + vals: vec![Value::test_int(5), Value::test_filesize(3000)], + span: Span::test_data() + }), + }, + Example { + description: "Search a char in a list of string", + example: r#"[moe larry curly] | find l"#, + result: Some(Value::List { + vals: vec![Value::test_string("larry"), Value::test_string("curly")], + span: Span::test_data() + }) + }, + Example { + description: "Find the first odd value", + example: "echo [2 4 3 6 5 8] | find --predicate { ($it mod 2) == 1 }", + result: Some(Value::List { + vals: vec![Value::test_int(3), Value::test_int(5)], + span: Span::test_data() + }) + }, + Example { + description: "Find if a service is not running", + example: "echo [[version patch]; [0.1.0 $false] [0.1.1 $true] [0.2.0 $false]] | find -p { $it.patch }", + result: Some(Value::List { + vals: vec![Value::test_record( + vec!["version", "patch"], + vec![Value::test_string("0.1.1"), Value::test_bool(true)] + )], + span: Span::test_data() + }), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let span = call.head; + let ctrlc = engine_state.ctrlc.clone(); + let engine_state = engine_state.clone(); + let metadata = input.metadata(); + + match call.get_flag::(&engine_state, stack, "predicate")? { + Some(predicate) => { + let capture_block = predicate; + let block_id = capture_block.block_id; + + if !call.rest::(&engine_state, stack, 0)?.is_empty() { + return Err(ShellError::IncompatibleParametersSingle( + "expected either a predicate or terms, not both".to_owned(), + span, + )); + } + + let block = engine_state.get_block(block_id).clone(); + let var_id = block.signature.get_positional(0).and_then(|arg| arg.var_id); + + let mut stack = stack.captures_to_stack(&capture_block.captures); + + input.filter( + move |value| { + if let Some(var_id) = var_id { + stack.add_var(var_id, value.clone()); + } + + eval_block( + &engine_state, + &mut stack, + &block, + PipelineData::new_with_metadata(metadata.clone(), span), + ) + .map_or(false, |pipeline_data| { + pipeline_data.into_value(span).is_true() + }) + }, + ctrlc, + ) + } + None => { + let terms = call.rest::(&engine_state, stack, 0)?; + let pipe = input.filter( + move |value| { + terms.iter().any(|term| match value { + Value::Bool { .. } + | Value::Int { .. } + | Value::Filesize { .. } + | Value::Duration { .. } + | Value::Date { .. } + | Value::Range { .. } + | Value::Float { .. } + | Value::Block { .. } + | Value::Nothing { .. } + | Value::Error { .. } => { + value.eq(span, term).map_or(false, |value| value.is_true()) + } + Value::String { .. } + | Value::List { .. } + | Value::CellPath { .. } + | Value::CustomValue { .. } => term + .r#in(span, value) + .map_or(false, |value| value.is_true()), + Value::Record { vals, .. } => vals.iter().any(|val| { + term.r#in(span, val).map_or(false, |value| value.is_true()) + }), + Value::Binary { .. } => false, + }) + }, + ctrlc, + )?; + match metadata { + Some(m) => { + Ok(pipe.into_pipeline_data_with_metadata(m, engine_state.ctrlc.clone())) + } + None => Ok(pipe), + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Find) + } +} diff --git a/crates/nu-command/src/filters/first.rs b/crates/nu-command/src/filters/first.rs new file mode 100644 index 0000000000..7fc2b6ecda --- /dev/null +++ b/crates/nu-command/src/filters/first.rs @@ -0,0 +1,166 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, SyntaxShape, + Type, Value, +}; + +#[derive(Clone)] +pub struct First; + +impl Command for First { + fn name(&self) -> &str { + "first" + } + + fn signature(&self) -> Signature { + Signature::build("first") + .optional( + "rows", + SyntaxShape::Int, + "starting from the front, the number of rows to return", + ) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Show only the first number of rows." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + first_helper(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Return the first item of a list/table", + example: "[1 2 3] | first", + result: Some(Value::test_int(1)), + }, + Example { + description: "Return the first 2 items of a list/table", + example: "[1 2 3] | first 2", + result: Some(Value::List { + vals: vec![Value::test_int(1), Value::test_int(2)], + span: Span::test_data(), + }), + }, + ] + } +} + +fn first_helper( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let head = call.head; + let rows: Option = call.opt(engine_state, stack, 0)?; + let mut rows_desired: usize = match rows { + Some(x) => x as usize, + None => 1, + }; + + let mut input_peek = input.into_iter().peekable(); + if input_peek.peek().is_some() { + match input_peek + .peek() + .ok_or_else(|| { + ShellError::SpannedLabeledError( + "Error in first".into(), + "unable to pick on next value".into(), + call.head, + ) + })? + .get_type() + { + Type::Binary => { + match &mut input_peek.next() { + Some(v) => match &v { + Value::Binary { val, .. } => { + let bytes = val; + if bytes.len() >= rows_desired { + // We only want to see a certain amount of the binary + // so let's grab those parts + let output_bytes = bytes[0..rows_desired].to_vec(); + Ok(Value::Binary { + val: output_bytes, + span: head, + } + .into_pipeline_data()) + } else { + // if we want more rows that the current chunk size (8192) + // we must gradually get bigger chunks while testing + // if it's within the requested rows_desired size + let mut bigger: Vec = vec![]; + bigger.extend(bytes); + while bigger.len() < rows_desired { + match input_peek.next() { + Some(Value::Binary { val, .. }) => bigger.extend(val), + _ => { + // We're at the end of our data so let's break out of this loop + // and set the rows_desired to the size of our data + rows_desired = bigger.len(); + break; + } + } + } + let output_bytes = bigger[0..rows_desired].to_vec(); + Ok(Value::Binary { + val: output_bytes, + span: head, + } + .into_pipeline_data()) + } + } + + _ => todo!(), + }, + None => Ok(Value::List { + vals: input_peek.into_iter().take(rows_desired).collect(), + span: head, + } + .into_pipeline_data()), + } + } + _ => { + if rows_desired == 1 { + match input_peek.next() { + Some(val) => Ok(val.into_pipeline_data()), + None => Err(ShellError::AccessBeyondEndOfStream(head)), + } + } else { + Ok(Value::List { + vals: input_peek.into_iter().take(rows_desired).collect(), + span: head, + } + .into_pipeline_data()) + } + } + } + } else { + Err(ShellError::UnsupportedInput( + String::from("Cannot perform into string on empty input"), + head, + )) + } +} +#[cfg(test)] +mod test { + use super::*; + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(First {}) + } +} diff --git a/crates/nu-command/src/filters/flatten.rs b/crates/nu-command/src/filters/flatten.rs new file mode 100644 index 0000000000..ff76a7c654 --- /dev/null +++ b/crates/nu-command/src/filters/flatten.rs @@ -0,0 +1,309 @@ +use indexmap::IndexMap; +use nu_engine::CallExt; +use nu_protocol::ast::{Call, CellPath, PathMember}; + +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Flatten; + +impl Command for Flatten { + fn name(&self) -> &str { + "flatten" + } + + fn signature(&self) -> Signature { + Signature::build("flatten") + .rest( + "rest", + SyntaxShape::String, + "optionally flatten data by column", + ) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Flatten the table." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + flatten(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "flatten a table", + example: "[[N, u, s, h, e, l, l]] | flatten ", + result: Some(Value::List { + vals: vec![ + Value::test_string("N"), + Value::test_string("u"), + Value::test_string("s"), + Value::test_string("h"), + Value::test_string("e"), + Value::test_string("l"), + Value::test_string("l")], + span: Span::test_data() + }) + }, + Example { + description: "flatten a table, get the first item", + example: "[[N, u, s, h, e, l, l]] | flatten | first", + result: None,//Some(Value::test_string("N")), + }, + Example { + description: "flatten a column having a nested table", + example: "[[origin, people]; [Ecuador, ([[name, meal]; ['Andres', 'arepa']])]] | flatten | get meal", + result: None,//Some(Value::test_string("arepa")), + }, + Example { + description: "restrict the flattening by passing column names", + example: "[[origin, crate, versions]; [World, ([[name]; ['nu-cli']]), ['0.21', '0.22']]] | flatten versions | last | get versions", + result: None, //Some(Value::test_string("0.22")), + }, + Example { + description: "Flatten inner table", + example: "{ a: b, d: [ 1 2 3 4 ], e: [ 4 3 ] } | flatten", + result: Some(Value::List{ + vals: vec![ + Value::Record{ + cols: vec!["a".to_string(), "d".to_string(), "e".to_string()], + vals: vec![Value::test_string("b"), Value::test_int(1), Value::List{vals: vec![Value::test_int(4), Value::test_int(3)], span: Span::test_data()} ], + span: Span::test_data() + }, + Value::Record{ + cols: vec!["a".to_string(), "d".to_string(), "e".to_string()], + vals: vec![Value::test_string("b"), Value::test_int(2), Value::List{vals: vec![Value::test_int(4), Value::test_int(3)], span: Span::test_data()} ], + span: Span::test_data() + }, + Value::Record{ + cols: vec!["a".to_string(), "d".to_string(), "e".to_string()], + vals: vec![Value::test_string("b"), Value::test_int(3), Value::List{vals: vec![Value::test_int(4), Value::test_int(3)], span: Span::test_data()} ], + span: Span::test_data() + }, + Value::Record{ + cols: vec!["a".to_string(), "d".to_string(), "e".to_string()], + vals: vec![Value::test_string("b"), Value::test_int(4), Value::List{vals: vec![Value::test_int(4), Value::test_int(3)], span: Span::test_data()} ], + span: Span::test_data() + } + ], + span: Span::test_data(), + }), + } + ] + } +} + +fn flatten( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let tag = call.head; + let columns: Vec = call.rest(engine_state, stack, 0)?; + + input.flat_map( + move |item| flat_value(&columns, &item, tag), + engine_state.ctrlc.clone(), + ) +} + +enum TableInside<'a> { + Entries(&'a str, &'a Span, Vec<&'a Value>), +} + +fn flat_value(columns: &[CellPath], item: &Value, _name_tag: Span) -> Vec { + let tag = match item.span() { + Ok(x) => x, + Err(e) => return vec![Value::Error { error: e }], + }; + + let res = { + if item.as_record().is_ok() { + let mut out = IndexMap::::new(); + let mut inner_table = None; + let mut tables_explicitly_flattened = 0; + + let records = match item { + Value::Record { + cols, + vals, + span: _, + } => (cols, vals), + x => { + return vec![Value::Error { + error: ShellError::UnsupportedInput( + format!("This should be a record, but instead got {}", x.get_type()), + tag, + ), + }] + } + }; + + let s = match item.span() { + Ok(x) => x, + Err(e) => return vec![Value::Error { error: e }], + }; + + let records_iterator = { + let cols = records.0; + let vals = records.1; + + let mut pairs = vec![]; + for i in 0..cols.len() { + pairs.push((cols[i].as_str(), &vals[i])); + } + pairs + }; + + for (column, value) in records_iterator { + let column_requested = columns.iter().find(|c| c.into_string() == *column); + + match value { + Value::Record { + cols, + vals, + span: _, + } => cols.iter().enumerate().for_each(|(idx, column)| { + out.insert(column.to_string(), vals[idx].clone()); + }), + Value::List { vals, span: _ } if vals.iter().all(|f| f.as_record().is_ok()) => { + let mut cs = vec![]; + let mut vs = vec![]; + + for v in vals { + if let Ok(r) = v.as_record() { + cs.push(r.0); + vs.push(r.1) + } + } + + if column_requested.is_none() && !columns.is_empty() { + if out.contains_key(column) { + out.insert(format!("{}_{}", column, column), value.clone()); + } else { + out.insert(column.to_string(), value.clone()); + } + continue; + } + + let cols = cs.into_iter().flat_map(|f| f.to_vec()); + let vals = vs.into_iter().flat_map(|f| f.to_vec()); + + for (k, v) in cols.into_iter().zip(vals.into_iter()) { + if out.contains_key(&k) { + out.insert(format!("{}_{}", column, k), v.clone()); + } else { + out.insert(k, v.clone()); + } + } + } + Value::List { + vals: values, + span: _, + } => { + if tables_explicitly_flattened >= 1 && column_requested.is_some() { + return vec![Value::Error{ error: ShellError::UnsupportedInput( + "can only flatten one inner table at the same time. tried flattening more than one column with inner tables... but is flattened already".to_string(), + s + )} + ]; + } + + if !columns.is_empty() { + let cell_path = match column_requested { + Some(x) => match x.members.first() { + Some(PathMember::String { val, span: _ }) => Some(val), + Some(PathMember::Int { val: _, span: _ }) => None, + None => None, + }, + None => None, + }; + + if let Some(r) = cell_path { + if !columns.is_empty() { + inner_table = Some(TableInside::Entries( + r, + &s, + values.iter().collect::>(), + )); + + tables_explicitly_flattened += 1; + } + } else { + out.insert(column.to_string(), value.clone()); + } + } else if inner_table.is_none() { + inner_table = Some(TableInside::Entries( + column, + &s, + values.iter().collect::>(), + )); + out.insert(column.to_string(), value.clone()); + } else { + out.insert(column.to_string(), value.clone()); + } + } + _ => { + out.insert(column.to_string(), value.clone()); + } + } + } + + let mut expanded = vec![]; + + if let Some(TableInside::Entries(column, _, entries)) = inner_table { + for entry in entries { + let mut base = out.clone(); + + base.insert(column.to_string(), entry.clone()); + let record = Value::Record { + cols: base.keys().map(|f| f.to_string()).collect::>(), + vals: base.values().cloned().collect(), + span: tag, + }; + expanded.push(record); + } + } else { + let record = Value::Record { + cols: out.keys().map(|f| f.to_string()).collect::>(), + vals: out.values().cloned().collect(), + span: tag, + }; + expanded.push(record); + } + expanded + } else if item.as_list().is_ok() { + if let Value::List { vals, span: _ } = item { + vals.to_vec() + } else { + vec![] + } + } else { + vec![item.clone()] + } + }; + res +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Flatten {}) + } +} diff --git a/crates/nu-command/src/filters/get.rs b/crates/nu-command/src/filters/get.rs new file mode 100644 index 0000000000..8077de9d76 --- /dev/null +++ b/crates/nu-command/src/filters/get.rs @@ -0,0 +1,109 @@ +use nu_engine::CallExt; +use nu_protocol::ast::{Call, CellPath}; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, Signature, + SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Get; + +impl Command for Get { + fn name(&self) -> &str { + "get" + } + + fn usage(&self) -> &str { + "Extract data using a cell path." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("get") + .required( + "cell_path", + SyntaxShape::CellPath, + "the cell path to the data", + ) + .rest("rest", SyntaxShape::CellPath, "additional cell paths") + .switch( + "ignore-errors", + "return nothing if path can't be found", + Some('i'), + ) + .category(Category::Filters) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let span = call.head; + let cell_path: CellPath = call.req(engine_state, stack, 0)?; + let rest: Vec = call.rest(engine_state, stack, 1)?; + let ignore_errors = call.has_flag("ignore-errors"); + let ctrlc = engine_state.ctrlc.clone(); + + if rest.is_empty() { + let output = input + .follow_cell_path(&cell_path.members, call.head) + .map(|x| x.into_pipeline_data()); + + if ignore_errors { + match output { + Ok(output) => Ok(output), + Err(_) => Ok(Value::Nothing { span: call.head }.into_pipeline_data()), + } + } else { + output + } + } else { + let mut output = vec![]; + + let paths = vec![cell_path].into_iter().chain(rest); + + let input = input.into_value(span); + + for path in paths { + let val = input.clone().follow_cell_path(&path.members); + + if ignore_errors { + if let Ok(val) = val { + output.push(val); + } + } else { + output.push(val?); + } + } + + Ok(output.into_iter().into_pipeline_data(ctrlc)) + } + } + fn examples(&self) -> Vec { + vec![ + Example { + description: "Extract the name of files as a list", + example: "ls | get name", + result: None, + }, + Example { + description: "Extract the name of the 3rd entry of a file list", + example: "ls | get name.2", + result: None, + }, + Example { + description: "Extract the name of the 3rd entry of a file list (alternative)", + example: "ls | get 2.name", + result: None, + }, + Example { + description: "Extract the cpu list from the sys information record", + example: "sys | get cpu", + result: None, + }, + ] + } +} diff --git a/crates/nu-command/src/filters/group_by.rs b/crates/nu-command/src/filters/group_by.rs new file mode 100644 index 0000000000..5b36539f70 --- /dev/null +++ b/crates/nu-command/src/filters/group_by.rs @@ -0,0 +1,268 @@ +use nu_engine::{eval_block, CallExt}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{CaptureBlock, Command, EngineState, Stack}; +use nu_protocol::{ + Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, + Value, +}; + +use indexmap::IndexMap; + +#[derive(Clone)] +pub struct GroupBy; + +impl Command for GroupBy { + fn name(&self) -> &str { + "group-by" + } + + fn signature(&self) -> Signature { + Signature::build("group-by").optional( + "grouper", + SyntaxShape::Any, + "the grouper value to use", + ) + } + + fn usage(&self) -> &str { + "Create a new table grouped." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + group_by(engine_state, stack, call, input) + } + + #[allow(clippy::unwrap_used)] + fn examples(&self) -> Vec { + vec![ + Example { + description: "group items by column named \"type\"", + example: r#"ls | group-by type"#, + result: None, + }, + Example { + description: "you can also group by raw values by leaving out the argument", + example: "echo ['1' '3' '1' '3' '2' '1' '1'] | group-by", + result: Some(Value::Record { + cols: vec!["1".to_string(), "3".to_string(), "2".to_string()], + vals: vec![ + Value::List { + vals: vec![ + Value::test_string("1"), + Value::test_string("1"), + Value::test_string("1"), + Value::test_string("1"), + ], + span: Span::test_data(), + }, + Value::List { + vals: vec![Value::test_string("3"), Value::test_string("3")], + span: Span::test_data(), + }, + Value::List { + vals: vec![Value::test_string("2")], + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }, + ] + } +} + +enum Grouper { + ByColumn(Option>), + ByBlock, +} + +pub fn group_by( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let name = call.head; + + let grouper: Option = call.opt(engine_state, stack, 0)?; + let values: Vec = input.into_iter().collect(); + let mut keys: Vec> = vec![]; + let mut group_strategy = Grouper::ByColumn(None); + + let first = values[0].clone(); + + if values.is_empty() { + return Err(ShellError::SpannedLabeledError( + "expected table from pipeline".into(), + "requires a table input".into(), + name, + )); + } + + let value_list = Value::List { + vals: values.clone(), + span: name, + }; + + match grouper { + Some(Value::Block { .. }) => { + let block: Option = call.opt(engine_state, stack, 0)?; + let error_key = "error"; + + for value in values { + if let Some(capture_block) = &block { + let mut stack = stack.captures_to_stack(&capture_block.captures); + let block = engine_state.get_block(capture_block.block_id); + let pipeline = + eval_block(engine_state, &mut stack, block, value.into_pipeline_data()); + + match pipeline { + Ok(s) => { + let collection: Vec = s.into_iter().collect(); + + if collection.len() > 1 { + return Err(ShellError::SpannedLabeledError( + "expected one value from the block".into(), + "requires a table with one value for grouping".into(), + name, + )); + } + + let value = match collection.get(0) { + Some(Value::Error { .. }) | None => Value::String { + val: error_key.to_string(), + span: name, + }, + Some(return_value) => return_value.clone(), + }; + + keys.push(value.as_string()); + } + Err(_) => { + keys.push(Ok(error_key.into())); + } + } + } + } + + group_strategy = Grouper::ByBlock; + } + Some(other) => { + group_strategy = Grouper::ByColumn(Some(Spanned { + item: other.as_string()?, + span: name, + })); + } + _ => {} + } + + let name = if let Ok(span) = first.span() { + span + } else { + name + }; + + let group_value = match group_strategy { + Grouper::ByBlock => { + let map = keys; + + let block = Box::new(move |idx: usize, row: &Value| match map.get(idx) { + Some(Ok(key)) => Ok(key.clone()), + Some(Err(reason)) => Err(reason.clone()), + None => row.as_string(), + }); + + data_group(&value_list, &Some(block), name) + } + Grouper::ByColumn(column_name) => group(&column_name, &value_list, name), + }; + + Ok(PipelineData::Value(group_value?, None)) +} + +#[allow(clippy::type_complexity)] +pub fn data_group( + values: &Value, + grouper: &Option Result + Send>>, + span: Span, +) -> Result { + let mut groups: IndexMap> = IndexMap::new(); + + for (idx, value) in values.clone().into_pipeline_data().into_iter().enumerate() { + let group_key = if let Some(ref grouper) = grouper { + grouper(idx, &value) + } else { + value.as_string() + }; + + let group = groups.entry(group_key?).or_insert(vec![]); + group.push(value); + } + + let mut cols = vec![]; + let mut vals = vec![]; + + for (k, v) in groups { + cols.push(k.to_string()); + vals.push(Value::List { vals: v, span }); + } + + Ok(Value::Record { cols, vals, span }) +} + +pub fn group( + column_name: &Option>, + values: &Value, + span: Span, +) -> Result { + let name = span; + + 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.item) { + Some(group_key) => Ok(group_key.as_string()?), + None => Err(ShellError::CantFindColumn( + column_name.span, + row.span().unwrap_or(column_name.span), + )), + }, + ); + + data_group(values, &Some(block), name) + } + Grouper::ByColumn(None) => { + let block = Box::new(move |_, row: &Value| row.as_string()); + + data_group(values, &Some(block), name) + } + Grouper::ByBlock => Err(ShellError::NushellFailed( + "Block not implemented: This should never happen.".into(), + )), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(GroupBy {}) + } +} diff --git a/crates/nu-command/src/filters/keep/keep_.rs b/crates/nu-command/src/filters/keep/keep_.rs new file mode 100644 index 0000000000..3f3d8a69c9 --- /dev/null +++ b/crates/nu-command/src/filters/keep/keep_.rs @@ -0,0 +1,100 @@ +use std::convert::TryInto; + +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span, + SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Keep; + +impl Command for Keep { + fn name(&self) -> &str { + "keep" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .optional("n", SyntaxShape::Int, "the number of elements to keep") + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Keep the first n elements of the input." + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Keep two elements", + example: "echo [[editions]; [2015] [2018] [2021]] | keep 2", + result: Some(Value::List { + vals: vec![ + Value::Record { + cols: vec!["editions".to_owned()], + vals: vec![Value::test_int(2015)], + span: Span::test_data(), + }, + Value::Record { + cols: vec!["editions".to_owned()], + vals: vec![Value::test_int(2018)], + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }, + Example { + description: "Keep the first value", + example: "echo [2 4 6 8] | keep", + result: Some(Value::List { + vals: vec![Value::test_int(2)], + span: Span::test_data(), + }), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let n: Option = call.opt(engine_state, stack, 0)?; + + let n: usize = match n { + Some(Value::Int { val, span }) => val.try_into().map_err(|err| { + ShellError::UnsupportedInput( + format!("Could not convert {} to unsigned integer: {}", val, err), + span, + ) + })?, + Some(_) => { + let span = call.head; + return Err(ShellError::TypeMismatch("expected integer".into(), span)); + } + None => 1, + }; + + let ctrlc = engine_state.ctrlc.clone(); + + Ok(input.into_iter().take(n).into_pipeline_data(ctrlc)) + } +} + +#[cfg(test)] +mod tests { + use crate::Keep; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Keep {}) + } +} diff --git a/crates/nu-command/src/filters/keep/keep_until.rs b/crates/nu-command/src/filters/keep/keep_until.rs new file mode 100644 index 0000000000..d6238c3df0 --- /dev/null +++ b/crates/nu-command/src/filters/keep/keep_until.rs @@ -0,0 +1,87 @@ +use nu_engine::{eval_block, CallExt}; +use nu_protocol::{ + ast::Call, + engine::{CaptureBlock, Command, EngineState, Stack}, + Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span, + SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct KeepUntil; + +impl Command for KeepUntil { + fn name(&self) -> &str { + "keep until" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required( + "predicate", + SyntaxShape::RowCondition, + "the predicate that kept element must not match", + ) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Keep elements of the input until a predicate is true." + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Keep until the element is positive", + example: "echo [-1 -2 9 1] | keep until $it > 0", + result: Some(Value::List { + vals: vec![Value::test_int(-1), Value::test_int(-2)], + span: Span::test_data(), + }), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let span = call.head; + + let capture_block: CaptureBlock = call.req(engine_state, stack, 0)?; + + let block = engine_state.get_block(capture_block.block_id).clone(); + let var_id = block.signature.get_positional(0).and_then(|arg| arg.var_id); + + let mut stack = stack.captures_to_stack(&capture_block.captures); + + let ctrlc = engine_state.ctrlc.clone(); + let engine_state = engine_state.clone(); + + Ok(input + .into_iter() + .take_while(move |value| { + if let Some(var_id) = var_id { + stack.add_var(var_id, value.clone()); + } + + !eval_block(&engine_state, &mut stack, &block, PipelineData::new(span)) + .map_or(false, |pipeline_data| { + pipeline_data.into_value(span).is_true() + }) + }) + .into_pipeline_data(ctrlc)) + } +} + +#[cfg(test)] +mod tests { + use crate::KeepUntil; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(KeepUntil) + } +} diff --git a/crates/nu-command/src/filters/keep/keep_while.rs b/crates/nu-command/src/filters/keep/keep_while.rs new file mode 100644 index 0000000000..5b5705e962 --- /dev/null +++ b/crates/nu-command/src/filters/keep/keep_while.rs @@ -0,0 +1,87 @@ +use nu_engine::{eval_block, CallExt}; +use nu_protocol::{ + ast::Call, + engine::{CaptureBlock, Command, EngineState, Stack}, + Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span, + SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct KeepWhile; + +impl Command for KeepWhile { + fn name(&self) -> &str { + "keep while" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required( + "predicate", + SyntaxShape::RowCondition, + "the predicate that kept element must not match", + ) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Keep elements of the input while a predicate is true." + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Keep while the element is negative", + example: "echo [-1 -2 9 1] | keep while $it < 0", + result: Some(Value::List { + vals: vec![Value::test_int(-1), Value::test_int(-2)], + span: Span::test_data(), + }), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let span = call.head; + + let capture_block: CaptureBlock = call.req(engine_state, stack, 0)?; + + let block = engine_state.get_block(capture_block.block_id).clone(); + let var_id = block.signature.get_positional(0).and_then(|arg| arg.var_id); + + let mut stack = stack.captures_to_stack(&capture_block.captures); + + let ctrlc = engine_state.ctrlc.clone(); + let engine_state = engine_state.clone(); + + Ok(input + .into_iter() + .take_while(move |value| { + if let Some(var_id) = var_id { + stack.add_var(var_id, value.clone()); + } + + eval_block(&engine_state, &mut stack, &block, PipelineData::new(span)) + .map_or(false, |pipeline_data| { + pipeline_data.into_value(span).is_true() + }) + }) + .into_pipeline_data(ctrlc)) + } +} + +#[cfg(test)] +mod tests { + use crate::KeepWhile; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(KeepWhile) + } +} diff --git a/crates/nu-command/src/filters/keep/mod.rs b/crates/nu-command/src/filters/keep/mod.rs new file mode 100644 index 0000000000..8eaddb54f7 --- /dev/null +++ b/crates/nu-command/src/filters/keep/mod.rs @@ -0,0 +1,7 @@ +mod keep_; +mod keep_until; +mod keep_while; + +pub use keep_::Keep; +pub use keep_until::KeepUntil; +pub use keep_while::KeepWhile; diff --git a/crates/nu-command/src/filters/last.rs b/crates/nu-command/src/filters/last.rs new file mode 100644 index 0000000000..b4485e2367 --- /dev/null +++ b/crates/nu-command/src/filters/last.rs @@ -0,0 +1,85 @@ +use nu_engine::CallExt; + +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span, + SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Last; + +impl Command for Last { + fn name(&self) -> &str { + "last" + } + + fn signature(&self) -> Signature { + Signature::build("last") + .optional( + "rows", + SyntaxShape::Int, + "starting from the back, the number of rows to return", + ) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Show only the last number of rows." + } + + fn examples(&self) -> Vec { + vec![Example { + example: "[1,2,3] | last 2", + description: "Get the last 2 items", + result: Some(Value::List { + vals: vec![Value::test_int(2), Value::test_int(3)], + span: Span::test_data(), + }), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let rows: Option = call.opt(engine_state, stack, 0)?; + let v: Vec<_> = input.into_iter().collect(); + let vlen: i64 = v.len() as i64; + let beginning_rows_to_skip = rows_to_skip(vlen, rows); + + let iter = v.into_iter().skip(beginning_rows_to_skip as usize); + + Ok(iter.into_pipeline_data(engine_state.ctrlc.clone())) + } +} + +fn rows_to_skip(count: i64, rows: Option) -> i64 { + let end_rows_desired = if let Some(quantity) = rows { + quantity + } else { + 1 + }; + + if end_rows_desired < count { + count - end_rows_desired + } else { + 0 + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Last {}) + } +} diff --git a/crates/nu-command/src/filters/length.rs b/crates/nu-command/src/filters/length.rs new file mode 100644 index 0000000000..ef7c5275c2 --- /dev/null +++ b/crates/nu-command/src/filters/length.rs @@ -0,0 +1,105 @@ +use nu_engine::column::get_columns; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, ShellError, Signature, + Span, Value, +}; + +#[derive(Clone)] +pub struct Length; + +impl Command for Length { + fn name(&self) -> &str { + "length" + } + + fn usage(&self) -> &str { + "Count the number of elements in the input." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("length") + .switch("column", "Show the number of columns in a table", Some('c')) + .category(Category::Filters) + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let col = call.has_flag("column"); + if col { + length_col(engine_state, call, input) + } else { + length_row(call, input) + } + } +} + +// this simulates calling input | columns | length +fn length_col( + engine_state: &EngineState, + call: &Call, + input: PipelineData, +) -> Result { + length_row( + call, + getcol(engine_state, call.head, input) + .expect("getcol() should not fail used in column command"), + ) +} + +fn length_row(call: &Call, input: PipelineData) -> Result { + match input { + PipelineData::Value(Value::Nothing { .. }, ..) => Ok(Value::Int { + val: 0, + span: call.head, + } + .into_pipeline_data()), + _ => Ok(Value::Int { + val: input.into_iter().count() as i64, + span: call.head, + } + .into_pipeline_data()), + } +} + +fn getcol( + engine_state: &EngineState, + span: Span, + input: PipelineData, +) -> Result { + match input { + PipelineData::Value( + Value::List { + vals: input_vals, + span, + }, + .., + ) => { + let input_cols = get_columns(&input_vals); + Ok(input_cols + .into_iter() + .map(move |x| Value::String { val: x, span }) + .into_pipeline_data(engine_state.ctrlc.clone())) + } + PipelineData::ListStream(stream, ..) => { + let v: Vec<_> = stream.into_iter().collect(); + let input_cols = get_columns(&v); + + Ok(input_cols + .into_iter() + .map(move |x| Value::String { val: x, span }) + .into_pipeline_data(engine_state.ctrlc.clone())) + } + PipelineData::Value(..) | PipelineData::RawStream(..) => { + let cols = vec![]; + let vals = vec![]; + Ok(Value::Record { cols, vals, span }.into_pipeline_data()) + } + } +} diff --git a/crates/nu-command/src/filters/lines.rs b/crates/nu-command/src/filters/lines.rs new file mode 100644 index 0000000000..c5c13647e2 --- /dev/null +++ b/crates/nu-command/src/filters/lines.rs @@ -0,0 +1,147 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Value, +}; + +#[derive(Clone)] +pub struct Lines; + +impl Command for Lines { + fn name(&self) -> &str { + "lines" + } + + fn usage(&self) -> &str { + "Converts input to lines" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("lines") + .switch("skip-empty", "skip empty lines", Some('s')) + .category(Category::Filters) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let skip_empty = call.has_flag("skip-empty"); + match input { + #[allow(clippy::needless_collect)] + // Collect is needed because the string may not live long enough for + // the Rc structure to continue using it. If split could take ownership + // of the split values, then this wouldn't be needed + PipelineData::Value(Value::String { val, span }, ..) => { + let split_char = if val.contains("\r\n") { "\r\n" } else { "\n" }; + + let mut lines = val + .split(split_char) + .map(|s| s.to_string()) + .collect::>(); + + // if the last one is empty, remove it, as it was just + // a newline at the end of the input we got + if let Some(last) = lines.last() { + if last.is_empty() { + lines.pop(); + } + } + + let iter = lines.into_iter().filter_map(move |s| { + if skip_empty && s.trim().is_empty() { + None + } else { + Some(Value::string(s, span)) + } + }); + + Ok(iter.into_pipeline_data(engine_state.ctrlc.clone())) + } + PipelineData::ListStream(stream, ..) => { + let mut split_char = "\n"; + + let iter = stream + .into_iter() + .filter_map(move |value| { + if let Value::String { val, span } = value { + if split_char != "\r\n" && val.contains("\r\n") { + split_char = "\r\n"; + } + + let mut lines = val + .split(split_char) + .filter_map(|s| { + if skip_empty && s.trim().is_empty() { + None + } else { + Some(s.to_string()) + } + }) + .collect::>(); + + // if the last one is empty, remove it, as it was just + // a newline at the end of the input we got + if let Some(last) = lines.last() { + if last.is_empty() { + lines.pop(); + } + } + + Some( + lines + .into_iter() + .map(move |x| Value::String { val: x, span }), + ) + } else { + None + } + }) + .flatten(); + + Ok(iter.into_pipeline_data(engine_state.ctrlc.clone())) + } + PipelineData::Value(val, ..) => Err(ShellError::UnsupportedInput( + format!("Not supported input: {}", val.as_string()?), + call.head, + )), + PipelineData::RawStream(..) => { + let config = stack.get_config()?; + + //FIXME: Make sure this can fail in the future to let the user + //know to use a different encoding + let s = input.collect_string("", &config)?; + + let split_char = if s.contains("\r\n") { "\r\n" } else { "\n" }; + + #[allow(clippy::needless_collect)] + let mut lines = s + .split(split_char) + .map(|s| s.to_string()) + .collect::>(); + + // if the last one is empty, remove it, as it was just + // a newline at the end of the input we got + if let Some(last) = lines.last() { + if last.is_empty() { + lines.pop(); + } + } + + let iter = lines.into_iter().filter_map(move |s| { + if skip_empty && s.trim().is_empty() { + None + } else { + Some(Value::string(s, head)) + } + }); + + Ok(iter.into_pipeline_data(engine_state.ctrlc.clone())) + } + } + } +} diff --git a/crates/nu-command/src/filters/merge.rs b/crates/nu-command/src/filters/merge.rs new file mode 100644 index 0000000000..5caf5e862d --- /dev/null +++ b/crates/nu-command/src/filters/merge.rs @@ -0,0 +1,192 @@ +use nu_engine::{eval_block, CallExt}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{CaptureBlock, Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, ShellError, + Signature, Span, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Merge; + +impl Command for Merge { + fn name(&self) -> &str { + "merge" + } + + fn usage(&self) -> &str { + "Merge a table into an input table" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("merge") + .required( + "block", + SyntaxShape::Block(Some(vec![])), + "the block to run and merge into the table", + ) + .category(Category::Filters) + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "[a b c] | wrap name | merge { [1 2 3] | wrap index }", + description: "Merge an index column into the input table", + result: Some(Value::List { + vals: vec![ + Value::test_record( + vec!["name", "index"], + vec![Value::test_string("a"), Value::test_int(1)], + ), + Value::test_record( + vec!["name", "index"], + vec![Value::test_string("b"), Value::test_int(2)], + ), + Value::test_record( + vec!["name", "index"], + vec![Value::test_string("c"), Value::test_int(3)], + ), + ], + span: Span::test_data(), + }), + }, + Example { + example: "{a: 1, b: 2} | merge { {c: 3} }", + description: "Merge two records", + result: Some(Value::test_record( + vec!["a", "b", "c"], + vec![Value::test_int(1), Value::test_int(2), Value::test_int(3)], + )), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let block: CaptureBlock = call.req(engine_state, stack, 0)?; + let mut stack = stack.captures_to_stack(&block.captures); + + let metadata = input.metadata(); + let ctrlc = engine_state.ctrlc.clone(); + let block = engine_state.get_block(block.block_id); + let call = call.clone(); + + let result = eval_block( + engine_state, + &mut stack, + block, + PipelineData::new(call.head), + ); + + let table = match result { + Ok(res) => res, + Err(e) => return Err(e), + }; + + match (&input, &table) { + // table (list of records) + ( + PipelineData::Value(Value::List { .. }, ..) | PipelineData::ListStream { .. }, + PipelineData::Value(Value::List { .. }, ..) | PipelineData::ListStream { .. }, + ) => { + let mut table_iter = table.into_iter(); + + let res = + input + .into_iter() + .map(move |inp| match (inp.as_record(), table_iter.next()) { + (Ok((inp_cols, inp_vals)), Some(to_merge)) => { + match to_merge.as_record() { + Ok((to_merge_cols, to_merge_vals)) => { + let cols = [inp_cols, to_merge_cols].concat(); + let vals = [inp_vals, to_merge_vals].concat(); + Value::Record { + cols, + vals, + span: call.head, + } + } + Err(error) => Value::Error { error }, + } + } + (_, None) => inp, + (Err(error), _) => Value::Error { error }, + }); + + if let Some(md) = metadata { + Ok(res.into_pipeline_data_with_metadata(md, ctrlc)) + } else { + Ok(res.into_pipeline_data(ctrlc)) + } + } + // record + ( + PipelineData::Value( + Value::Record { + cols: inp_cols, + vals: inp_vals, + .. + }, + .., + ), + PipelineData::Value( + Value::Record { + cols: to_merge_cols, + vals: to_merge_vals, + .. + }, + .., + ), + ) => { + let mut cols = inp_cols.to_vec(); + cols.extend(to_merge_cols.to_vec()); + + let mut vals = inp_vals.to_vec(); + vals.extend(to_merge_vals.to_vec()); + + Ok(Value::Record { + cols, + vals, + span: call.head, + } + .into_pipeline_data()) + } + (_, PipelineData::Value(val, ..)) | (PipelineData::Value(val, ..), _) => { + let span = if val.span()? == Span::test_data() { + Span::new(call.head.start, call.head.start) + } else { + val.span()? + }; + + Err(ShellError::PipelineMismatch( + "record or table in both the input and the argument block".to_string(), + call.head, + span, + )) + } + _ => Err(ShellError::PipelineMismatch( + "record or table in both the input and the argument block".to_string(), + call.head, + Span::new(call.head.start, call.head.start), + )), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Merge {}) + } +} diff --git a/crates/nu-command/src/filters/mod.rs b/crates/nu-command/src/filters/mod.rs new file mode 100644 index 0000000000..050f979b7c --- /dev/null +++ b/crates/nu-command/src/filters/mod.rs @@ -0,0 +1,93 @@ +mod all; +mod any; +mod append; +mod collect; +mod columns; +mod compact; +mod default; +mod drop; +mod each; +mod each_group; +mod each_window; +mod empty; +mod every; +mod find; +mod first; +mod flatten; +mod get; +mod group_by; +mod keep; +mod last; +mod length; +mod lines; +mod merge; +mod move_; +mod nth; +mod par_each; +mod par_each_group; +mod prepend; +mod range; +mod reduce; +mod reject; +mod rename; +mod reverse; +mod rotate; +mod select; +mod shuffle; +mod skip; +mod sort_by; +mod split_by; +mod transpose; +mod uniq; +mod update; +mod update_cells; +mod where_; +mod wrap; +mod zip_; + +pub use all::All; +pub use any::Any; +pub use append::Append; +pub use collect::Collect; +pub use columns::Columns; +pub use compact::Compact; +pub use default::Default; +pub use drop::*; +pub use each::Each; +pub use each_group::EachGroup; +pub use each_window::EachWindow; +pub use empty::Empty; +pub use every::Every; +pub use find::Find; +pub use first::First; +pub use flatten::Flatten; +pub use get::Get; +pub use group_by::GroupBy; +pub use keep::*; +pub use last::Last; +pub use length::Length; +pub use lines::Lines; +pub use merge::Merge; +pub use move_::Move; +pub use nth::Nth; +pub use par_each::ParEach; +pub use par_each_group::ParEachGroup; +pub use prepend::Prepend; +pub use range::Range; +pub use reduce::Reduce; +pub use reject::Reject; +pub use rename::Rename; +pub use reverse::Reverse; +pub use rotate::Rotate; +pub use select::Select; +pub use shuffle::Shuffle; +pub use skip::*; +pub use sort_by::SortBy; +pub use split_by::SplitBy; +pub use transpose::Transpose; +pub use uniq::*; +pub use update::Update; +pub use update_cells::UpdateCells; +pub use where_::Where; +pub use wrap::Wrap; +pub use zip_::Zip; diff --git a/crates/nu-command/src/filters/move_.rs b/crates/nu-command/src/filters/move_.rs new file mode 100644 index 0000000000..bc5d570081 --- /dev/null +++ b/crates/nu-command/src/filters/move_.rs @@ -0,0 +1,302 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, ShellError, + Signature, Span, Spanned, SyntaxShape, Value, +}; + +#[derive(Clone, Debug)] +enum BeforeOrAfter { + Before(String), + After(String), +} + +#[derive(Clone)] +pub struct Move; + +impl Command for Move { + fn name(&self) -> &str { + "move" + } + + fn usage(&self) -> &str { + "Move columns before or after other columns" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("move") + .rest("columns", SyntaxShape::String, "the columns to move") + .named( + "after", + SyntaxShape::String, + "the column that will precede the columns moved", + None, + ) + .named( + "before", + SyntaxShape::String, + "the column that will be the next after the columns moved", + None, + ) + .category(Category::Filters) + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "[[name value index]; [foo a 1] [bar b 2] [baz c 3]] | move index --before name", + description: "Move a column before the first column", + result: + Some(Value::List { + vals: vec![ + Value::test_record( + vec!["index", "name", "value"], + vec![Value::test_int(1), Value::test_string("foo"), Value::test_string("a")], + ), + Value::test_record( + vec!["index", "name", "value"], + vec![Value::test_int(2), Value::test_string("bar"), Value::test_string("b")], + ), + Value::test_record( + vec!["index", "name", "value"], + vec![Value::test_int(3), Value::test_string("baz"), Value::test_string("c")], + ), + ], + span: Span::test_data(), + }) + }, + Example { + example: "[[name value index]; [foo a 1] [bar b 2] [baz c 3]] | move value name --after index", + description: "Move multiple columns after the last column and reorder them", + result: + Some(Value::List { + vals: vec![ + Value::test_record( + vec!["index", "value", "name"], + vec![Value::test_int(1), Value::test_string("a"), Value::test_string("foo")], + ), + Value::test_record( + vec!["index", "value", "name"], + vec![Value::test_int(2), Value::test_string("b"), Value::test_string("bar")], + ), + Value::test_record( + vec!["index", "value", "name"], + vec![Value::test_int(3), Value::test_string("c"), Value::test_string("baz")], + ), + ], + span: Span::test_data(), + }) + }, + Example { + example: "{ name: foo, value: a, index: 1 } | move name --before index", + description: "Move columns of a record", + result: Some(Value::test_record( + vec!["value", "name", "index"], + vec![Value::test_string("a"), Value::test_string("foo"), Value::test_int(1)], + )) + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let columns: Vec = call.rest(engine_state, stack, 0)?; + let after: Option = call.get_flag(engine_state, stack, "after")?; + let before: Option = call.get_flag(engine_state, stack, "before")?; + + let before_or_after = match (after, before) { + (Some(v), None) => Spanned { + item: BeforeOrAfter::After(v.as_string()?), + span: v.span()?, + }, + (None, Some(v)) => Spanned { + item: BeforeOrAfter::Before(v.as_string()?), + span: v.span()?, + }, + (Some(_), Some(_)) => { + return Err(ShellError::SpannedLabeledError( + "Cannot move columns".to_string(), + "Use either --after, or --before, not both".to_string(), + call.head, + )) + } + (None, None) => { + return Err(ShellError::SpannedLabeledError( + "Cannot move columns".to_string(), + "Missing --after or --before flag".to_string(), + call.head, + )) + } + }; + + let metadata = input.metadata(); + let ctrlc = engine_state.ctrlc.clone(); + let call = call.clone(); + + match input { + PipelineData::Value(Value::List { .. }, ..) | PipelineData::ListStream { .. } => { + let res = input.into_iter().map(move |x| match x.as_record() { + Ok((inp_cols, inp_vals)) => match move_record_columns( + inp_cols, + inp_vals, + &columns, + &before_or_after, + call.head, + ) { + Ok(val) => val, + Err(error) => Value::Error { error }, + }, + Err(error) => Value::Error { error }, + }); + + if let Some(md) = metadata { + Ok(res.into_pipeline_data_with_metadata(md, ctrlc)) + } else { + Ok(res.into_pipeline_data(ctrlc)) + } + } + PipelineData::Value( + Value::Record { + cols: inp_cols, + vals: inp_vals, + .. + }, + .., + ) => Ok(move_record_columns( + &inp_cols, + &inp_vals, + &columns, + &before_or_after, + call.head, + )? + .into_pipeline_data()), + _ => Err(ShellError::PipelineMismatch( + "record or table".to_string(), + call.head, + Span::new(call.head.start, call.head.start), + )), + } + } +} + +// Move columns within a record +fn move_record_columns( + inp_cols: &[String], + inp_vals: &[Value], + columns: &[Value], + before_or_after: &Spanned, + span: Span, +) -> Result { + let mut column_idx: Vec = Vec::with_capacity(columns.len()); + + // Check if before/after column exist + match &before_or_after.item { + BeforeOrAfter::After(after) => { + if !inp_cols.contains(after) { + return Err(ShellError::SpannedLabeledError( + "Cannot move columns".to_string(), + "column does not exist".to_string(), + before_or_after.span, + )); + } + } + BeforeOrAfter::Before(before) => { + if !inp_cols.contains(before) { + return Err(ShellError::SpannedLabeledError( + "Cannot move columns".to_string(), + "column does not exist".to_string(), + before_or_after.span, + )); + } + } + } + + // Find indices of columns to be moved + for column in columns.iter() { + let column_str = column.as_string()?; + + if let Some(idx) = inp_cols.iter().position(|inp_col| &column_str == inp_col) { + column_idx.push(idx); + } else { + return Err(ShellError::SpannedLabeledError( + "Cannot move columns".to_string(), + "column does not exist".to_string(), + column.span()?, + )); + } + } + + if columns.is_empty() {} + + let mut out_cols: Vec = Vec::with_capacity(inp_cols.len()); + let mut out_vals: Vec = Vec::with_capacity(inp_vals.len()); + + for (i, (inp_col, inp_val)) in inp_cols.iter().zip(inp_vals).enumerate() { + match &before_or_after.item { + BeforeOrAfter::After(after) if after == inp_col => { + out_cols.push(inp_col.into()); + out_vals.push(inp_val.clone()); + + for idx in column_idx.iter() { + if let (Some(col), Some(val)) = (inp_cols.get(*idx), inp_vals.get(*idx)) { + out_cols.push(col.into()); + out_vals.push(val.clone()); + } else { + return Err(ShellError::NushellFailedSpanned( + "Error indexing input columns".to_string(), + "originates from here".to_string(), + span, + )); + } + } + } + BeforeOrAfter::Before(before) if before == inp_col => { + for idx in column_idx.iter() { + if let (Some(col), Some(val)) = (inp_cols.get(*idx), inp_vals.get(*idx)) { + out_cols.push(col.into()); + out_vals.push(val.clone()); + } else { + return Err(ShellError::NushellFailedSpanned( + "Error indexing input columns".to_string(), + "originates from here".to_string(), + span, + )); + } + } + + out_cols.push(inp_col.into()); + out_vals.push(inp_val.clone()); + } + _ => { + if !column_idx.contains(&i) { + out_cols.push(inp_col.into()); + out_vals.push(inp_val.clone()); + } + } + } + } + + Ok(Value::Record { + cols: out_cols, + vals: out_vals, + span, + }) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Move {}) + } +} diff --git a/crates/nu-command/src/filters/nth.rs b/crates/nu-command/src/filters/nth.rs new file mode 100644 index 0000000000..d49ffda0d5 --- /dev/null +++ b/crates/nu-command/src/filters/nth.rs @@ -0,0 +1,152 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoInterruptiblePipelineData, PipelineData, PipelineIterator, ShellError, + Signature, Span, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Nth; + +impl Command for Nth { + fn name(&self) -> &str { + "nth" + } + + fn signature(&self) -> Signature { + Signature::build("nth") + .rest("rest", SyntaxShape::Int, "the number of the row to return") + .switch("skip", "Skip the rows instead of selecting them", Some('s')) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Return or skip only the selected rows." + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "[sam,sarah,2,3,4,5] | nth 0 1 2", + description: "Get the first, second, and third row", + result: Some(Value::List { + vals: vec![ + Value::test_string("sam"), + Value::test_string("sarah"), + Value::test_int(2), + ], + span: Span::test_data(), + }), + }, + Example { + example: "[0,1,2,3,4,5] | nth 0 1 2", + description: "Get the first, second, and third row", + result: Some(Value::List { + vals: vec![Value::test_int(0), Value::test_int(1), Value::test_int(2)], + span: Span::test_data(), + }), + }, + Example { + example: "[0,1,2,3,4,5] | nth -s 0 1 2", + description: "Skip the first, second, and third row", + result: Some(Value::List { + vals: vec![Value::test_int(3), Value::test_int(4), Value::test_int(5)], + span: Span::test_data(), + }), + }, + Example { + example: "[0,1,2,3,4,5] | nth 0 2 4", + description: "Get the first, third, and fifth row", + result: Some(Value::List { + vals: vec![Value::test_int(0), Value::test_int(2), Value::test_int(4)], + span: Span::test_data(), + }), + }, + Example { + example: "[0,1,2,3,4,5] | nth 2 0 4", + description: "Get the first, third, and fifth row", + result: Some(Value::List { + vals: vec![Value::test_int(0), Value::test_int(2), Value::test_int(4)], + span: Span::test_data(), + }), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let mut rows: Vec = call.rest(engine_state, stack, 0)?; + rows.sort_unstable(); + let skip = call.has_flag("skip"); + let pipeline_iter: PipelineIterator = input.into_iter(); + + Ok(NthIterator { + input: pipeline_iter, + rows, + skip, + current: 0, + } + .into_pipeline_data(engine_state.ctrlc.clone())) + } +} + +struct NthIterator { + input: PipelineIterator, + rows: Vec, + skip: bool, + current: usize, +} + +impl Iterator for NthIterator { + type Item = Value; + + fn next(&mut self) -> Option { + loop { + if !self.skip { + if let Some(row) = self.rows.get(0) { + if self.current == *row { + self.rows.remove(0); + self.current += 1; + return self.input.next(); + } else { + self.current += 1; + let _ = self.input.next(); + continue; + } + } else { + return None; + } + } else if let Some(row) = self.rows.get(0) { + if self.current == *row { + self.rows.remove(0); + self.current += 1; + let _ = self.input.next(); + continue; + } else { + self.current += 1; + return self.input.next(); + } + } else { + return self.input.next(); + } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Nth {}) + } +} diff --git a/crates/nu-command/src/filters/par_each.rs b/crates/nu-command/src/filters/par_each.rs new file mode 100644 index 0000000000..9b634d3ac1 --- /dev/null +++ b/crates/nu-command/src/filters/par_each.rs @@ -0,0 +1,328 @@ +use nu_engine::{eval_block_with_redirect, CallExt}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{CaptureBlock, Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, Signature, + SyntaxShape, Value, +}; +use rayon::prelude::*; + +#[derive(Clone)] +pub struct ParEach; + +impl Command for ParEach { + fn name(&self) -> &str { + "par-each" + } + + fn usage(&self) -> &str { + "Run a block on each element of input in parallel" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("par-each") + .required( + "block", + SyntaxShape::Block(Some(vec![SyntaxShape::Any])), + "the block to run", + ) + .switch("numbered", "iterate with an index", Some('n')) + .category(Category::Filters) + } + + fn examples(&self) -> Vec { + vec![Example { + example: "[1 2 3] | par-each { 2 * $it }", + description: "Multiplies elements in list", + result: None, + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let capture_block: CaptureBlock = call.req(engine_state, stack, 0)?; + + let numbered = call.has_flag("numbered"); + let ctrlc = engine_state.ctrlc.clone(); + let engine_state = engine_state.clone(); + let block_id = capture_block.block_id; + let mut stack = stack.captures_to_stack(&capture_block.captures); + let span = call.head; + + match input { + PipelineData::Value(Value::Range { val, .. }, ..) => Ok(val + .into_range_iter()? + .enumerate() + .par_bridge() + .map(move |(idx, x)| { + let block = engine_state.get_block(block_id); + + let mut stack = stack.clone(); + + if let Some(var) = block.signature.get_positional(0) { + if let Some(var_id) = &var.var_id { + if numbered { + stack.add_var( + *var_id, + Value::Record { + cols: vec!["index".into(), "item".into()], + vals: vec![ + Value::Int { + val: idx as i64, + span, + }, + x, + ], + span, + }, + ); + } else { + stack.add_var(*var_id, x); + } + } + } + + match eval_block_with_redirect( + &engine_state, + &mut stack, + block, + PipelineData::new(span), + ) { + Ok(v) => v, + Err(error) => Value::Error { error }.into_pipeline_data(), + } + }) + .collect::>() + .into_iter() + .flatten() + .into_pipeline_data(ctrlc)), + PipelineData::Value(Value::List { vals: val, .. }, ..) => Ok(val + .into_iter() + .enumerate() + .par_bridge() + .map(move |(idx, x)| { + let block = engine_state.get_block(block_id); + + let mut stack = stack.clone(); + + if let Some(var) = block.signature.get_positional(0) { + if let Some(var_id) = &var.var_id { + if numbered { + stack.add_var( + *var_id, + Value::Record { + cols: vec!["index".into(), "item".into()], + vals: vec![ + Value::Int { + val: idx as i64, + span, + }, + x, + ], + span, + }, + ); + } else { + stack.add_var(*var_id, x); + } + } + } + + match eval_block_with_redirect( + &engine_state, + &mut stack, + block, + PipelineData::new(span), + ) { + Ok(v) => v, + Err(error) => Value::Error { error }.into_pipeline_data(), + } + }) + .collect::>() + .into_iter() + .flatten() + .into_pipeline_data(ctrlc)), + PipelineData::ListStream(stream, ..) => Ok(stream + .enumerate() + .par_bridge() + .map(move |(idx, x)| { + let block = engine_state.get_block(block_id); + + let mut stack = stack.clone(); + + if let Some(var) = block.signature.get_positional(0) { + if let Some(var_id) = &var.var_id { + if numbered { + stack.add_var( + *var_id, + Value::Record { + cols: vec!["index".into(), "item".into()], + vals: vec![ + Value::Int { + val: idx as i64, + span, + }, + x, + ], + span, + }, + ); + } else { + stack.add_var(*var_id, x); + } + } + } + + match eval_block_with_redirect( + &engine_state, + &mut stack, + block, + PipelineData::new(span), + ) { + Ok(v) => v, + Err(error) => Value::Error { error }.into_pipeline_data(), + } + }) + .collect::>() + .into_iter() + .flatten() + .into_pipeline_data(ctrlc)), + PipelineData::RawStream(stream, ..) => Ok(stream + .enumerate() + .par_bridge() + .map(move |(idx, x)| { + let x = match x { + Ok(x) => x, + Err(err) => return Value::Error { error: err }.into_pipeline_data(), + }; + + let block = engine_state.get_block(block_id); + + let mut stack = stack.clone(); + + if let Some(var) = block.signature.get_positional(0) { + if let Some(var_id) = &var.var_id { + if numbered { + stack.add_var( + *var_id, + Value::Record { + cols: vec!["index".into(), "item".into()], + vals: vec![ + Value::Int { + val: idx as i64, + span, + }, + x, + ], + span, + }, + ); + } else { + stack.add_var(*var_id, x); + } + } + } + + match eval_block_with_redirect( + &engine_state, + &mut stack, + block, + PipelineData::new(span), + ) { + Ok(v) => v, + Err(error) => Value::Error { error }.into_pipeline_data(), + } + }) + .collect::>() + .into_iter() + .flatten() + .into_pipeline_data(ctrlc)), + PipelineData::Value(Value::Record { cols, vals, .. }, ..) => { + let mut output_cols = vec![]; + let mut output_vals = vec![]; + + for (col, val) in cols.into_iter().zip(vals.into_iter()) { + let block = engine_state.get_block(block_id); + + let mut stack = stack.clone(); + + if let Some(var) = block.signature.get_positional(0) { + if let Some(var_id) = &var.var_id { + stack.add_var( + *var_id, + Value::Record { + cols: vec!["column".into(), "value".into()], + vals: vec![ + Value::String { + val: col.clone(), + span: call.head, + }, + val, + ], + span: call.head, + }, + ); + } + } + + match eval_block_with_redirect( + &engine_state, + &mut stack, + block, + PipelineData::new(span), + )? { + PipelineData::Value( + Value::Record { + mut cols, mut vals, .. + }, + .., + ) => { + // TODO check that the lengths match when traversing record + output_cols.append(&mut cols); + output_vals.append(&mut vals); + } + x => { + output_cols.push(col); + output_vals.push(x.into_value(span)); + } + } + } + + Ok(Value::Record { + cols: output_cols, + vals: output_vals, + span: call.head, + } + .into_pipeline_data()) + } + PipelineData::Value(x, ..) => { + let block = engine_state.get_block(block_id); + + if let Some(var) = block.signature.get_positional(0) { + if let Some(var_id) = &var.var_id { + stack.add_var(*var_id, x); + } + } + + eval_block_with_redirect(&engine_state, &mut stack, block, PipelineData::new(span)) + } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(ParEach {}) + } +} diff --git a/crates/nu-command/src/filters/par_each_group.rs b/crates/nu-command/src/filters/par_each_group.rs new file mode 100644 index 0000000000..8bf559147f --- /dev/null +++ b/crates/nu-command/src/filters/par_each_group.rs @@ -0,0 +1,137 @@ +use nu_engine::{eval_block_with_redirect, CallExt}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{CaptureBlock, Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoInterruptiblePipelineData, PipelineData, Signature, Spanned, + SyntaxShape, Value, +}; +use rayon::prelude::*; + +#[derive(Clone)] +pub struct ParEachGroup; + +impl Command for ParEachGroup { + fn name(&self) -> &str { + "par-each group" + } + + fn signature(&self) -> Signature { + Signature::build("par-each group") + .required("group_size", SyntaxShape::Int, "the size of each group") + .required( + "block", + SyntaxShape::Block(Some(vec![SyntaxShape::Any])), + "the block to run on each group", + ) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Runs a block on groups of `group_size` rows of a table at a time." + } + + fn examples(&self) -> Vec { + vec![Example { + example: "echo [1 2 3 4] | par-each group 2 { $it.0 + $it.1 }", + description: "Multiplies elements in list", + result: None, + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let group_size: Spanned = call.req(engine_state, stack, 0)?; + let capture_block: CaptureBlock = call.req(engine_state, stack, 1)?; + let ctrlc = engine_state.ctrlc.clone(); + let span = call.head; + + let stack = stack.captures_to_stack(&capture_block.captures); + + //FIXME: add in support for external redirection when engine-q supports it generally + + let each_group_iterator = EachGroupIterator { + group_size: group_size.item, + input: Box::new(input.into_iter()), + }; + + Ok(each_group_iterator + .par_bridge() + .map(move |x| { + let block = engine_state.get_block(capture_block.block_id); + + let mut stack = stack.clone(); + + if let Some(var) = block.signature.get_positional(0) { + if let Some(var_id) = &var.var_id { + stack.add_var(*var_id, Value::List { vals: x, span }); + } + } + + match eval_block_with_redirect( + engine_state, + &mut stack, + block, + PipelineData::new(span), + ) { + Ok(v) => v.into_value(span), + Err(error) => Value::Error { error }, + } + }) + .collect::>() + .into_iter() + .into_pipeline_data(ctrlc)) + } +} + +struct EachGroupIterator { + group_size: usize, + input: Box + Send>, +} + +impl Iterator for EachGroupIterator { + type Item = Vec; + + fn next(&mut self) -> Option { + let mut group = vec![]; + let mut current_count = 0; + + loop { + let item = self.input.next(); + + match item { + Some(v) => { + group.push(v); + + current_count += 1; + if current_count >= self.group_size { + break; + } + } + None => break, + } + } + + if group.is_empty() { + return None; + } + + Some(group) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(ParEachGroup {}) + } +} diff --git a/crates/nu-command/src/filters/prepend.rs b/crates/nu-command/src/filters/prepend.rs new file mode 100644 index 0000000000..b7685b3877 --- /dev/null +++ b/crates/nu-command/src/filters/prepend.rs @@ -0,0 +1,122 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span, + SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Prepend; + +impl Command for Prepend { + fn name(&self) -> &str { + "prepend" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("prepend") + .required("row", SyntaxShape::Any, "the row to prepend") + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Prepend a row to the table." + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "[1,2,3,4] | prepend 0", + description: "Prepend one Int item", + result: Some(Value::List { + vals: vec![ + Value::test_int(0), + Value::test_int(1), + Value::test_int(2), + Value::test_int(3), + Value::test_int(4), + ], + span: Span::test_data(), + }), + }, + Example { + example: "[2,3,4] | prepend [0,1]", + description: "Prepend two Int items", + result: Some(Value::List { + vals: vec![ + Value::test_int(0), + Value::test_int(1), + Value::test_int(2), + Value::test_int(3), + Value::test_int(4), + ], + span: Span::test_data(), + }), + }, + Example { + example: "[2,nu,4,shell] | prepend [0,1,rocks]", + description: "Prepend Ints and Strings", + result: Some(Value::List { + vals: vec![ + Value::test_int(0), + Value::test_int(1), + Value::test_string("rocks"), + Value::test_int(2), + Value::test_string("nu"), + Value::test_int(4), + Value::test_string("shell"), + ], + span: Span::test_data(), + }), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let val: Value = call.req(engine_state, stack, 0)?; + let vec: Vec = process_value(val); + + Ok(vec + .into_iter() + .chain(input) + .into_iter() + .into_pipeline_data(engine_state.ctrlc.clone())) + } +} + +fn process_value(val: Value) -> Vec { + match val { + Value::List { + vals: input_vals, + span: _, + } => { + let mut output = vec![]; + for input_val in input_vals { + output.push(input_val); + } + output + } + _ => { + vec![val] + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Prepend {}) + } +} diff --git a/crates/nu-command/src/filters/range.rs b/crates/nu-command/src/filters/range.rs new file mode 100644 index 0000000000..9584c494a5 --- /dev/null +++ b/crates/nu-command/src/filters/range.rs @@ -0,0 +1,135 @@ +use nu_engine::CallExt; + +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span, + SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Range; + +impl Command for Range { + fn name(&self) -> &str { + "range" + } + + fn signature(&self) -> Signature { + Signature::build("range") + .optional( + "rows", + SyntaxShape::Range, + "range of rows to return: Eg) 4..7 (=> from 4 to 7)", + ) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Return only the selected rows." + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "[0,1,2,3,4,5] | range 4..5", + description: "Get the last 2 items", + result: Some(Value::List { + vals: vec![Value::test_int(4), Value::test_int(5)], + span: Span::test_data(), + }), + }, + Example { + example: "[0,1,2,3,4,5] | range (-2)..", + description: "Get the last 2 items", + result: Some(Value::List { + vals: vec![Value::test_int(4), Value::test_int(5)], + span: Span::test_data(), + }), + }, + Example { + example: "[0,1,2,3,4,5] | range (-3)..-2", + description: "Get the next to last 2 items", + result: Some(Value::List { + vals: vec![Value::test_int(3), Value::test_int(4)], + span: Span::test_data(), + }), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let rows: nu_protocol::Range = call.req(engine_state, stack, 0)?; + + let rows_from = get_range_val(rows.from); + let rows_to = get_range_val(rows.to); + + // only collect the input if we have any negative indices + if rows_from < 0 || rows_to < 0 { + let v: Vec<_> = input.into_iter().collect(); + let vlen: i64 = v.len() as i64; + + let from = if rows_from < 0 { + (vlen + rows_from) as usize + } else { + rows_from as usize + }; + + let to = if rows_to < 0 { + (vlen + rows_to) as usize + } else if rows_to > v.len() as i64 { + v.len() + } else { + rows_to as usize + }; + + if from > to { + Ok(PipelineData::Value( + Value::Nothing { span: call.head }, + None, + )) + } else { + let iter = v.into_iter().skip(from).take(to - from + 1); + Ok(iter.into_pipeline_data(engine_state.ctrlc.clone())) + } + } else { + let from = rows_from as usize; + let to = rows_to as usize; + + if from > to { + Ok(PipelineData::Value( + Value::Nothing { span: call.head }, + None, + )) + } else { + let iter = input.into_iter().skip(from).take(to - from + 1); + Ok(iter.into_pipeline_data(engine_state.ctrlc.clone())) + } + } + } +} + +fn get_range_val(rows_val: Value) -> i64 { + match rows_val { + Value::Int { val: x, .. } => x, + _ => 0, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Range {}) + } +} diff --git a/crates/nu-command/src/filters/reduce.rs b/crates/nu-command/src/filters/reduce.rs new file mode 100644 index 0000000000..e7a0c70c87 --- /dev/null +++ b/crates/nu-command/src/filters/reduce.rs @@ -0,0 +1,213 @@ +use nu_engine::{eval_block, CallExt}; + +use nu_protocol::ast::Call; +use nu_protocol::engine::{CaptureBlock, Command, EngineState, Stack}; +use nu_protocol::{ + Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Reduce; + +impl Command for Reduce { + fn name(&self) -> &str { + "reduce" + } + + fn signature(&self) -> Signature { + Signature::build("reduce") + .named( + "fold", + SyntaxShape::Any, + "reduce with initial value", + Some('f'), + ) + .required( + "block", + SyntaxShape::Block(Some(vec![SyntaxShape::Any])), + "reducing function", + ) + .switch("numbered", "iterate with an index", Some('n')) + } + + fn usage(&self) -> &str { + "Aggregate a list table to a single value using an accumulator block." + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "[ 1 2 3 4 ] | reduce { $it.acc + $it.item }", + description: "Sum values of a list (same as 'math sum')", + result: Some(Value::Int { + val: 10, + span: Span::test_data(), + }), + }, + Example { + example: "[ 1 2 3 4 ] | reduce -f 10 { $it.acc + $it.item }", + description: "Sum values with a starting value (fold)", + result: Some(Value::Int { + val: 20, + span: Span::test_data(), + }), + }, + Example { + example: r#"[ i o t ] | reduce -f "Arthur, King of the Britons" { $it.acc | str find-replace -a $it.item "X" }"#, + description: "Replace selected characters in a string with 'X'", + result: Some(Value::String { + val: "ArXhur, KXng Xf Xhe BrXXXns".to_string(), + span: Span::test_data(), + }), + }, + Example { + example: r#"[ one longest three bar ] | reduce -n { + if ($it.item | str length) > ($it.acc | str length) { + $it.item + } else { + $it.acc + } + }"#, + description: "Find the longest string and its index", + result: Some(Value::Record { + cols: vec!["index".to_string(), "item".to_string()], + vals: vec![ + Value::Int { + val: 3, + span: Span::test_data(), + }, + Value::String { + val: "longest".to_string(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + // TODO: How to make this interruptible? + // TODO: Change the vars to $acc and $it instead of $it.acc and $it.item + // (requires parser change) + + let span = call.head; + + let fold: Option = call.get_flag(engine_state, stack, "fold")?; + let numbered = call.has_flag("numbered"); + let capture_block: CaptureBlock = call.req(engine_state, stack, 0)?; + let mut stack = stack.captures_to_stack(&capture_block.captures); + let block = engine_state.get_block(capture_block.block_id); + + let orig_env_vars = stack.env_vars.clone(); + let orig_env_hidden = stack.env_hidden.clone(); + + let mut input_iter = input.into_iter(); + + let (off, start_val) = if let Some(val) = fold { + (0, val) + } else if let Some(val) = input_iter.next() { + (1, val) + } else { + return Err(ShellError::SpannedLabeledError( + "Expected input".to_string(), + "needs input".to_string(), + span, + )); + }; + + Ok(input_iter + .enumerate() + .fold(start_val, move |acc, (idx, x)| { + stack.with_env(&orig_env_vars, &orig_env_hidden); + + // if the acc coming from previous iter is indexed, drop the index + let acc = if let Value::Record { cols, vals, .. } = &acc { + if cols.len() == 2 && vals.len() == 2 { + if cols[0].eq("index") && cols[1].eq("item") { + vals[1].clone() + } else { + acc + } + } else { + acc + } + } else { + acc + }; + + if let Some(var) = block.signature.get_positional(0) { + if let Some(var_id) = &var.var_id { + let it = if numbered { + Value::Record { + cols: vec![ + "index".to_string(), + "acc".to_string(), + "item".to_string(), + ], + vals: vec![ + Value::Int { + val: idx as i64 + off, + span, + }, + acc, + x, + ], + span, + } + } else { + Value::Record { + cols: vec!["acc".to_string(), "item".to_string()], + vals: vec![acc, x], + span, + } + }; + + stack.add_var(*var_id, it); + } + } + + let v = match eval_block(engine_state, &mut stack, block, PipelineData::new(span)) { + Ok(v) => v.into_value(span), + Err(error) => Value::Error { error }, + }; + + if numbered { + // make sure the output is indexed + Value::Record { + cols: vec!["index".to_string(), "item".to_string()], + vals: vec![ + Value::Int { + val: idx as i64 + off, + span, + }, + v, + ], + span, + } + } else { + v + } + }) + .into_pipeline_data()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Reduce {}) + } +} diff --git a/crates/nu-command/src/filters/reject.rs b/crates/nu-command/src/filters/reject.rs new file mode 100644 index 0000000000..81fcc3fabd --- /dev/null +++ b/crates/nu-command/src/filters/reject.rs @@ -0,0 +1,157 @@ +use nu_engine::CallExt; +use nu_protocol::ast::{Call, CellPath}; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, FromValue, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, ShellError, + Signature, Span, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Reject; + +impl Command for Reject { + fn name(&self) -> &str { + "reject" + } + + fn signature(&self) -> Signature { + Signature::build("reject") + .rest( + "rest", + SyntaxShape::String, + "the names of columns to remove from the table", + ) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Remove the given columns from the table. If you want to remove rows, try 'drop'." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let columns: Vec = call.rest(engine_state, stack, 0)?; + let span = call.head; + reject(engine_state, span, input, columns) + } +} + +fn reject( + engine_state: &EngineState, + span: Span, + input: PipelineData, + columns: Vec, +) -> Result { + if columns.is_empty() { + return Err(ShellError::CantFindColumn(span, span)); + } + + let mut keep_columns = vec![]; + + match input { + PipelineData::Value( + Value::List { + vals: input_vals, + span, + }, + .., + ) => { + let mut output = vec![]; + let input_cols = get_input_cols(input_vals.clone()); + let kc = get_keep_columns(input_cols, columns); + keep_columns = get_cellpath_columns(kc, span); + + for input_val in input_vals { + let mut cols = vec![]; + let mut vals = vec![]; + + for path in &keep_columns { + let fetcher = input_val.clone().follow_cell_path(&path.members)?; + cols.push(path.into_string()); + vals.push(fetcher); + } + output.push(Value::Record { cols, vals, span }) + } + + Ok(output + .into_iter() + .into_pipeline_data(engine_state.ctrlc.clone())) + } + PipelineData::ListStream(stream, ..) => { + let mut output = vec![]; + + let v: Vec<_> = stream.into_iter().collect(); + let input_cols = get_input_cols(v.clone()); + let kc = get_keep_columns(input_cols, columns); + keep_columns = get_cellpath_columns(kc, span); + + for input_val in v { + let mut cols = vec![]; + let mut vals = vec![]; + + for path in &keep_columns { + let fetcher = input_val.clone().follow_cell_path(&path.members)?; + cols.push(path.into_string()); + vals.push(fetcher); + } + output.push(Value::Record { cols, vals, span }) + } + + Ok(output + .into_iter() + .into_pipeline_data(engine_state.ctrlc.clone())) + } + PipelineData::Value(v, ..) => { + let mut cols = vec![]; + let mut vals = vec![]; + + for cell_path in &keep_columns { + let result = v.clone().follow_cell_path(&cell_path.members)?; + + cols.push(cell_path.into_string()); + vals.push(result); + } + + Ok(Value::Record { cols, vals, span }.into_pipeline_data()) + } + x => Ok(x), + } +} + +fn get_input_cols(input: Vec) -> Vec { + let rec = input.first(); + match rec { + Some(Value::Record { cols, vals: _, .. }) => cols.to_vec(), + _ => vec!["".to_string()], + } +} + +fn get_cellpath_columns(keep_cols: Vec, span: Span) -> Vec { + let mut output = vec![]; + for keep_col in keep_cols { + let val = Value::String { + val: keep_col, + span, + }; + let cell_path = match CellPath::from_value(&val) { + Ok(v) => v, + Err(_) => return vec![], + }; + output.push(cell_path); + } + output +} + +fn get_keep_columns(mut input: Vec, rejects: Vec) -> Vec { + for reject in rejects { + if let Some(index) = input.iter().position(|value| *value == reject) { + input.swap_remove(index); + } + } + input +} diff --git a/crates/nu-command/src/filters/rename.rs b/crates/nu-command/src/filters/rename.rs new file mode 100644 index 0000000000..5abe6c26c9 --- /dev/null +++ b/crates/nu-command/src/filters/rename.rs @@ -0,0 +1,168 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Rename; + +impl Command for Rename { + fn name(&self) -> &str { + "rename" + } + + fn signature(&self) -> Signature { + Signature::build("rename") + .named( + "column", + SyntaxShape::List(Box::new(SyntaxShape::String)), + "column name to be changed", + Some('c'), + ) + .rest("rest", SyntaxShape::String, "the new names for the columns") + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Creates a new table with columns renamed." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + rename(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Rename a column", + example: "[[a, b]; [1, 2]] | rename my_column", + result: Some(Value::List { + vals: vec![Value::Record { + cols: vec!["my_column".to_string(), "b".to_string()], + vals: vec![Value::test_int(1), Value::test_int(2)], + span: Span::test_data(), + }], + span: Span::test_data(), + }), + }, + Example { + description: "Rename many columns", + example: "[[a, b, c]; [1, 2, 3]] | rename eggs ham bacon", + result: Some(Value::List { + vals: vec![Value::Record { + cols: vec!["eggs".to_string(), "ham".to_string(), "bacon".to_string()], + vals: vec![Value::test_int(1), Value::test_int(2), Value::test_int(3)], + span: Span::test_data(), + }], + span: Span::test_data(), + }), + }, + Example { + description: "Rename a specific column", + example: "[[a, b, c]; [1, 2, 3]] | rename -c [a ham]", + result: Some(Value::List { + vals: vec![Value::Record { + cols: vec!["ham".to_string(), "b".to_string(), "c".to_string()], + vals: vec![Value::test_int(1), Value::test_int(2), Value::test_int(3)], + span: Span::test_data(), + }], + span: Span::test_data(), + }), + }, + ] + } +} + +fn rename( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let specified_column: Option> = call.get_flag(engine_state, stack, "column")?; + // get the span for the column's name to be changed and for the given list + let (specified_col_span, list_span) = if let Some(Value::List { + vals: columns, + span: column_span, + }) = call.get_flag(engine_state, stack, "column")? + { + (Some(columns[0].span()?), column_span) + } else { + (None, call.head) + }; + + if let Some(ref cols) = specified_column { + if cols.len() != 2 { + return Err(ShellError::UnsupportedInput( + "The list must contain only two values: the column's name and its replacement value" + .to_string(), + list_span, + )); + } + } + + let columns: Vec = call.rest(engine_state, stack, 0)?; + + input.map( + move |item| match item { + Value::Record { + mut cols, + vals, + span, + } => { + match &specified_column { + Some(c) => { + // check if the specified column to be renamed exists + if !cols.contains(&c[0]) { + return Value::Error { + error: ShellError::UnsupportedInput( + "The specified column does not exist".to_string(), + specified_col_span.unwrap_or(span), + ), + }; + } + for (idx, val) in cols.iter_mut().enumerate() { + if *val == c[0] { + cols[idx] = c[1].to_string(); + break; + } + } + } + None => { + for (idx, val) in columns.iter().enumerate() { + if idx >= cols.len() { + // skip extra new columns names if we already reached the final column + break; + } + cols[idx] = val.clone(); + } + } + } + + Value::Record { cols, vals, span } + } + x => x, + }, + engine_state.ctrlc.clone(), + ) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Rename {}) + } +} diff --git a/crates/nu-command/src/filters/reverse.rs b/crates/nu-command/src/filters/reverse.rs new file mode 100644 index 0000000000..9995a33a56 --- /dev/null +++ b/crates/nu-command/src/filters/reverse.rs @@ -0,0 +1,64 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span, + Value, +}; + +#[derive(Clone)] +pub struct Reverse; + +impl Command for Reverse { + fn name(&self) -> &str { + "reverse" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("reverse").category(Category::Filters) + } + + fn usage(&self) -> &str { + "Reverses the table." + } + + fn examples(&self) -> Vec { + vec![Example { + example: "[0,1,2,3] | reverse", + description: "Reverse the items", + result: Some(Value::List { + vals: vec![ + Value::test_int(3), + Value::test_int(2), + Value::test_int(1), + Value::test_int(0), + ], + span: Span::test_data(), + }), + }] + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + input: PipelineData, + ) -> Result { + #[allow(clippy::needless_collect)] + let v: Vec<_> = input.into_iter().collect(); + let iter = v.into_iter().rev(); + Ok(iter.into_pipeline_data(engine_state.ctrlc.clone())) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Reverse {}) + } +} diff --git a/crates/nu-command/src/filters/rotate.rs b/crates/nu-command/src/filters/rotate.rs new file mode 100644 index 0000000000..ace6e927cd --- /dev/null +++ b/crates/nu-command/src/filters/rotate.rs @@ -0,0 +1,360 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, SyntaxShape, + Value, +}; + +#[derive(Clone)] +pub struct Rotate; + +impl Command for Rotate { + fn name(&self) -> &str { + "rotate" + } + + fn signature(&self) -> Signature { + Signature::build("rotate") + .switch("ccw", "rotate counter clockwise", None) + .rest( + "rest", + SyntaxShape::String, + "the names to give columns once rotated", + ) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Rotates a table clockwise (default) or counter-clockwise (use --ccw flag)." + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Rotate 2x2 table clockwise", + example: "[[a b]; [1 2]] | rotate", + result: Some(Value::List { + vals: vec![ + Value::Record { + cols: vec!["Column0".to_string(), "Column1".to_string()], + vals: vec![Value::test_int(1), Value::test_string("a")], + span: Span::test_data(), + }, + Value::Record { + cols: vec!["Column0".to_string(), "Column1".to_string()], + vals: vec![Value::test_int(2), Value::test_string("b")], + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }, + Example { + description: "Rotate 2x3 table clockwise", + example: "[[a b]; [1 2] [3 4] [5 6]] | rotate", + result: Some(Value::List { + vals: vec![ + Value::Record { + cols: vec![ + "Column0".to_string(), + "Column1".to_string(), + "Column2".to_string(), + "Column3".to_string(), + ], + vals: vec![ + Value::test_int(5), + Value::test_int(3), + Value::test_int(1), + Value::test_string("a"), + ], + span: Span::test_data(), + }, + Value::Record { + cols: vec![ + "Column0".to_string(), + "Column1".to_string(), + "Column2".to_string(), + "Column3".to_string(), + ], + vals: vec![ + Value::test_int(6), + Value::test_int(4), + Value::test_int(2), + Value::test_string("b"), + ], + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }, + Example { + description: "Rotate table clockwise and change columns names", + example: "[[a b]; [1 2]] | rotate col_a col_b", + result: Some(Value::List { + vals: vec![ + Value::Record { + cols: vec!["col_a".to_string(), "col_b".to_string()], + vals: vec![Value::test_int(1), Value::test_string("a")], + span: Span::test_data(), + }, + Value::Record { + cols: vec!["col_a".to_string(), "col_b".to_string()], + vals: vec![Value::test_int(2), Value::test_string("b")], + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }, + Example { + description: "Rotate table counter clockwise", + example: "[[a b]; [1 2]] | rotate --ccw", + result: Some(Value::List { + vals: vec![ + Value::Record { + cols: vec!["Column0".to_string(), "Column1".to_string()], + vals: vec![Value::test_string("b"), Value::test_int(2)], + span: Span::test_data(), + }, + Value::Record { + cols: vec!["Column0".to_string(), "Column1".to_string()], + vals: vec![Value::test_string("a"), Value::test_int(1)], + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }, + Example { + description: "Rotate table counter-clockwise", + example: "[[a b]; [1 2] [3 4] [5 6]] | rotate --ccw", + result: Some(Value::List { + vals: vec![ + Value::Record { + cols: vec![ + "Column0".to_string(), + "Column1".to_string(), + "Column2".to_string(), + "Column3".to_string(), + ], + vals: vec![ + Value::test_string("b"), + Value::test_int(2), + Value::test_int(4), + Value::test_int(6), + ], + span: Span::test_data(), + }, + Value::Record { + cols: vec![ + "Column0".to_string(), + "Column1".to_string(), + "Column2".to_string(), + "Column3".to_string(), + ], + vals: vec![ + Value::test_string("a"), + Value::test_int(1), + Value::test_int(3), + Value::test_int(5), + ], + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }, + Example { + description: "Rotate table counter-clockwise and change columns names", + example: "[[a b]; [1 2]] | rotate --ccw col_a col_b", + result: Some(Value::List { + vals: vec![ + Value::Record { + cols: vec!["col_a".to_string(), "col_b".to_string()], + vals: vec![Value::test_string("b"), Value::test_int(2)], + span: Span::test_data(), + }, + Value::Record { + cols: vec!["col_a".to_string(), "col_b".to_string()], + vals: vec![Value::test_string("a"), Value::test_int(1)], + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + rotate(engine_state, stack, call, input) + } +} + +pub fn rotate( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let col_given_names: Vec = call.rest(engine_state, stack, 0)?; + let mut values = input.into_iter().collect::>(); + let mut old_column_names = vec![]; + let mut new_values = vec![]; + let mut not_a_record = false; + let total_rows = &mut values.len(); + let ccw: bool = call.has_flag("ccw"); + + if !ccw { + values.reverse(); + } + + if !values.is_empty() { + for val in values.into_iter() { + match val { + Value::Record { + cols, + vals, + span: _, + } => { + old_column_names = cols; + for v in vals { + new_values.push(v) + } + } + Value::List { vals, span: _ } => { + not_a_record = true; + for v in vals { + new_values.push(v); + } + } + Value::String { val, span } => { + not_a_record = true; + new_values.push(Value::String { val, span }) + } + x => { + not_a_record = true; + new_values.push(x) + } + } + } + } else { + return Err(ShellError::UnsupportedInput( + "Rotate command requires a Nu value as input".to_string(), + call.head, + )); + } + + let total_columns = &old_column_names.len(); + + // we use this for building columns names, but for non-records we get an extra row so we remove it + if *total_columns == 0 { + *total_rows -= 1; + } + + // holder for the new column names, particularly if none are provided by the user we create names as Column0, Column1, etc. + let mut new_column_names = { + let mut res = vec![]; + for idx in 0..(*total_rows + 1) { + res.push(format!("Column{}", idx)); + } + res.to_vec() + }; + + // we got new names for columns from the input, so we need to swap those we already made + if !col_given_names.is_empty() { + for (idx, val) in col_given_names.into_iter().enumerate() { + if idx > new_column_names.len() - 1 { + break; + } + new_column_names[idx] = val; + } + } + + if not_a_record { + return Ok(Value::List { + vals: vec![Value::Record { + cols: new_column_names, + vals: new_values, + span: call.head, + }], + span: call.head, + } + .into_pipeline_data()); + } + + // holder for the new records + let mut final_values = vec![]; + + // the number of initial columns will be our number of rows, so we iterate through that to get the new number of rows that we need to make + // for counter clockwise, we're iterating from right to left and have a pair of (index, value) + let columns_iter = if ccw { + old_column_names + .iter() + .enumerate() + .rev() + .collect::>() + } else { + // as we're rotating clockwise, we're iterating from left to right and have a pair of (index, value) + old_column_names.iter().enumerate().collect::>() + }; + + for (idx, val) in columns_iter { + // when rotating counter clockwise, the old columns names become the first column's values + let mut res = if ccw { + vec![Value::String { + val: val.to_string(), + span: call.head, + }] + } else { + vec![] + }; + + let new_vals = { + // move through the array with a step, which is every new_values size / total rows, starting from our old column's index + // so if initial data was like this [[a b]; [1 2] [3 4]] - we basically iterate on this [3 4 1 2] array, so we pick 3, then 1, and then when idx increases, we pick 4 and 2 + for i in (idx..new_values.len()).step_by(new_values.len() / *total_rows) { + res.push(new_values[i].clone()); + } + // when rotating clockwise, the old column names become the last column's values + if !ccw { + res.push(Value::String { + val: val.to_string(), + span: call.head, + }); + } + res.to_vec() + }; + final_values.push(Value::Record { + cols: new_column_names.clone(), + vals: new_vals, + span: call.head, + }) + } + + Ok(Value::List { + vals: final_values, + span: call.head, + } + .into_pipeline_data()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Rotate) + } +} diff --git a/crates/nu-command/src/filters/select.rs b/crates/nu-command/src/filters/select.rs new file mode 100644 index 0000000000..da71e48ff3 --- /dev/null +++ b/crates/nu-command/src/filters/select.rs @@ -0,0 +1,135 @@ +use nu_engine::CallExt; +use nu_protocol::ast::{Call, CellPath}; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, ShellError, + Signature, Span, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Select; + +impl Command for Select { + fn name(&self) -> &str { + "select" + } + + fn signature(&self) -> Signature { + Signature::build("select") + .rest( + "rest", + SyntaxShape::CellPath, + "the columns to select from the table", + ) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Down-select table to only these columns." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let columns: Vec = call.rest(engine_state, stack, 0)?; + let span = call.head; + + select(engine_state, span, columns, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Select just the name column", + example: "ls | select name", + result: None, + }, + Example { + description: "Select the name and size columns", + example: "ls | select name size", + result: None, + }, + ] + } +} + +fn select( + engine_state: &EngineState, + span: Span, + columns: Vec, + input: PipelineData, +) -> Result { + if columns.is_empty() { + return Err(ShellError::CantFindColumn(span, span)); //FIXME? + } + + match input { + PipelineData::Value( + Value::List { + vals: input_vals, + span, + }, + .., + ) => { + let mut output = vec![]; + + for input_val in input_vals { + let mut cols = vec![]; + let mut vals = vec![]; + for path in &columns { + //FIXME: improve implementation to not clone + let fetcher = input_val.clone().follow_cell_path(&path.members)?; + + cols.push(path.into_string()); + vals.push(fetcher); + } + + output.push(Value::Record { cols, vals, span }) + } + + Ok(output + .into_iter() + .into_pipeline_data(engine_state.ctrlc.clone())) + } + PipelineData::ListStream(stream, ..) => Ok(stream + .map(move |x| { + let mut cols = vec![]; + let mut vals = vec![]; + for path in &columns { + //FIXME: improve implementation to not clone + match x.clone().follow_cell_path(&path.members) { + Ok(value) => { + cols.push(path.into_string()); + vals.push(value); + } + Err(error) => { + cols.push(path.into_string()); + vals.push(Value::Error { error }); + } + } + } + + Value::Record { cols, vals, span } + }) + .into_pipeline_data(engine_state.ctrlc.clone())), + PipelineData::Value(v, ..) => { + let mut cols = vec![]; + let mut vals = vec![]; + + for cell_path in columns { + // FIXME: remove clone + let result = v.clone().follow_cell_path(&cell_path.members)?; + + cols.push(cell_path.into_string()); + vals.push(result); + } + + Ok(Value::Record { cols, vals, span }.into_pipeline_data()) + } + _ => Ok(PipelineData::new(span)), + } +} diff --git a/crates/nu-command/src/filters/shuffle.rs b/crates/nu-command/src/filters/shuffle.rs new file mode 100644 index 0000000000..4c3f07991c --- /dev/null +++ b/crates/nu-command/src/filters/shuffle.rs @@ -0,0 +1,35 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature}; +use rand::prelude::SliceRandom; +use rand::thread_rng; + +#[derive(Clone)] +pub struct Shuffle; + +impl Command for Shuffle { + fn name(&self) -> &str { + "shuffle" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("shuffle").category(Category::Filters) + } + + fn usage(&self) -> &str { + "Shuffle rows randomly." + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + input: PipelineData, + ) -> Result { + let mut v: Vec<_> = input.into_iter().collect(); + v.shuffle(&mut thread_rng()); + let iter = v.into_iter(); + Ok(iter.into_pipeline_data(engine_state.ctrlc.clone())) + } +} diff --git a/crates/nu-command/src/filters/skip/mod.rs b/crates/nu-command/src/filters/skip/mod.rs new file mode 100644 index 0000000000..9796705bc6 --- /dev/null +++ b/crates/nu-command/src/filters/skip/mod.rs @@ -0,0 +1,7 @@ +mod skip_; +mod skip_until; +mod skip_while; + +pub use skip_::Skip; +pub use skip_until::SkipUntil; +pub use skip_while::SkipWhile; diff --git a/crates/nu-command/src/filters/skip/skip_.rs b/crates/nu-command/src/filters/skip/skip_.rs new file mode 100644 index 0000000000..de1733cab5 --- /dev/null +++ b/crates/nu-command/src/filters/skip/skip_.rs @@ -0,0 +1,91 @@ +use std::convert::TryInto; + +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span, + SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Skip; + +impl Command for Skip { + fn name(&self) -> &str { + "skip" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .optional("n", SyntaxShape::Int, "the number of elements to skip") + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Skip the first n elements of the input." + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Skip two elements", + example: "echo [[editions]; [2015] [2018] [2021]] | skip 2", + result: Some(Value::List { + vals: vec![Value::Record { + cols: vec!["editions".to_owned()], + vals: vec![Value::test_int(2021)], + span: Span::test_data(), + }], + span: Span::test_data(), + }), + }, + Example { + description: "Skip the first value", + example: "echo [2 4 6 8] | skip", + result: Some(Value::List { + vals: vec![Value::test_int(4), Value::test_int(6), Value::test_int(8)], + span: Span::test_data(), + }), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let n: Option = call.opt(engine_state, stack, 0)?; + let span = call.head; + + let n: usize = match n { + Some(Value::Int { val, span }) => val.try_into().map_err(|err| { + ShellError::UnsupportedInput( + format!("Could not convert {} to unsigned integer: {}", val, err), + span, + ) + })?, + Some(_) => return Err(ShellError::TypeMismatch("expected integer".into(), span)), + None => 1, + }; + + let ctrlc = engine_state.ctrlc.clone(); + + Ok(input.into_iter().skip(n).into_pipeline_data(ctrlc)) + } +} + +#[cfg(test)] +mod tests { + use crate::Skip; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Skip {}) + } +} diff --git a/crates/nu-command/src/filters/skip/skip_until.rs b/crates/nu-command/src/filters/skip/skip_until.rs new file mode 100644 index 0000000000..15078ea4a5 --- /dev/null +++ b/crates/nu-command/src/filters/skip/skip_until.rs @@ -0,0 +1,86 @@ +use nu_engine::{eval_block, CallExt}; +use nu_protocol::{ + ast::Call, + engine::{CaptureBlock, Command, EngineState, Stack}, + Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span, + SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct SkipUntil; + +impl Command for SkipUntil { + fn name(&self) -> &str { + "skip until" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required( + "predicate", + SyntaxShape::RowCondition, + "the predicate that skipped element must not match", + ) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Skip elements of the input until a predicate is true." + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Skip until the element is positive", + example: "echo [-2 0 2 -1] | skip until $it > 0", + result: Some(Value::List { + vals: vec![Value::test_int(2), Value::test_int(-1)], + span: Span::test_data(), + }), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let span = call.head; + + let capture_block: CaptureBlock = call.req(engine_state, stack, 0)?; + + let block = engine_state.get_block(capture_block.block_id).clone(); + let var_id = block.signature.get_positional(0).and_then(|arg| arg.var_id); + let mut stack = stack.captures_to_stack(&capture_block.captures); + + let ctrlc = engine_state.ctrlc.clone(); + let engine_state = engine_state.clone(); + + Ok(input + .into_iter() + .skip_while(move |value| { + if let Some(var_id) = var_id { + stack.add_var(var_id, value.clone()); + } + + !eval_block(&engine_state, &mut stack, &block, PipelineData::new(span)) + .map_or(false, |pipeline_data| { + pipeline_data.into_value(span).is_true() + }) + }) + .into_pipeline_data(ctrlc)) + } +} + +#[cfg(test)] +mod tests { + use crate::SkipUntil; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SkipUntil) + } +} diff --git a/crates/nu-command/src/filters/skip/skip_while.rs b/crates/nu-command/src/filters/skip/skip_while.rs new file mode 100644 index 0000000000..8949b334c0 --- /dev/null +++ b/crates/nu-command/src/filters/skip/skip_while.rs @@ -0,0 +1,86 @@ +use nu_engine::{eval_block, CallExt}; +use nu_protocol::{ + ast::Call, + engine::{CaptureBlock, Command, EngineState, Stack}, + Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span, + SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct SkipWhile; + +impl Command for SkipWhile { + fn name(&self) -> &str { + "skip while" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required( + "predicate", + SyntaxShape::RowCondition, + "the predicate that skipped element must match", + ) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Skip elements of the input while a predicate is true." + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Skip while the element is negative", + example: "echo [-2 0 2 -1] | skip while $it < 0", + result: Some(Value::List { + vals: vec![Value::test_int(0), Value::test_int(2), Value::test_int(-1)], + span: Span::test_data(), + }), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let span = call.head; + + let capture_block: CaptureBlock = call.req(engine_state, stack, 0)?; + + let block = engine_state.get_block(capture_block.block_id).clone(); + let var_id = block.signature.get_positional(0).and_then(|arg| arg.var_id); + let mut stack = stack.captures_to_stack(&capture_block.captures); + + let ctrlc = engine_state.ctrlc.clone(); + let engine_state = engine_state.clone(); + + Ok(input + .into_iter() + .skip_while(move |value| { + if let Some(var_id) = var_id { + stack.add_var(var_id, value.clone()); + } + + eval_block(&engine_state, &mut stack, &block, PipelineData::new(span)) + .map_or(false, |pipeline_data| { + pipeline_data.into_value(span).is_true() + }) + }) + .into_pipeline_data(ctrlc)) + } +} + +#[cfg(test)] +mod tests { + use crate::SkipWhile; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SkipWhile) + } +} diff --git a/crates/nu-command/src/filters/sort_by.rs b/crates/nu-command/src/filters/sort_by.rs new file mode 100644 index 0000000000..ddbbfef823 --- /dev/null +++ b/crates/nu-command/src/filters/sort_by.rs @@ -0,0 +1,303 @@ +use chrono::{DateTime, FixedOffset}; +use nu_engine::{column::column_does_not_exist, CallExt}; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Config, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, + Span, SyntaxShape, Value, +}; +use std::cmp::Ordering; + +#[derive(Clone)] +pub struct SortBy; + +impl Command for SortBy { + fn name(&self) -> &str { + "sort-by" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("sort-by") + .rest("columns", SyntaxShape::Any, "the column(s) to sort by") + .switch("reverse", "Sort in reverse order", Some('r')) + .switch( + "insensitive", + "Sort string-based columns case-insensitively", + Some('i'), + ) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Sort by the given columns, in increasing order." + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "[2 0 1] | sort-by", + description: "sort the list by increasing value", + result: Some(Value::List { + vals: vec![Value::test_int(0), Value::test_int(1), Value::test_int(2)], + span: Span::test_data(), + }), + }, + Example { + example: "[2 0 1] | sort-by -r", + description: "sort the list by decreasing value", + result: Some(Value::List { + vals: vec![Value::test_int(2), Value::test_int(1), Value::test_int(0)], + span: Span::test_data(), + }), + }, + Example { + example: "[betty amy sarah] | sort-by", + description: "sort a list of strings", + result: Some(Value::List { + vals: vec![ + Value::test_string("amy"), + Value::test_string("betty"), + Value::test_string("sarah"), + ], + span: Span::test_data(), + }), + }, + Example { + example: "[betty amy sarah] | sort-by -r", + description: "sort a list of strings in reverse", + result: Some(Value::List { + vals: vec![ + Value::test_string("sarah"), + Value::test_string("betty"), + Value::test_string("amy"), + ], + span: Span::test_data(), + }), + }, + Example { + description: "Sort strings (case-insensitive)", + example: "echo [airplane Truck Car] | sort-by -i", + result: Some(Value::List { + vals: vec![ + Value::test_string("airplane"), + Value::test_string("Car"), + Value::test_string("Truck"), + ], + span: Span::test_data(), + }), + }, + Example { + description: "Sort strings (reversed case-insensitive)", + example: "echo [airplane Truck Car] | sort-by -i -r", + result: Some(Value::List { + vals: vec![ + Value::test_string("Truck"), + Value::test_string("Car"), + Value::test_string("airplane"), + ], + span: Span::test_data(), + }), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let columns: Vec = call.rest(engine_state, stack, 0)?; + let reverse = call.has_flag("reverse"); + let insensitive = call.has_flag("insensitive"); + let metadata = &input.metadata(); + let config = stack.get_config()?; + let mut vec: Vec<_> = input.into_iter().collect(); + + sort(&mut vec, columns, call, insensitive, &config)?; + + if reverse { + vec.reverse() + } + + let iter = vec.into_iter(); + match &*metadata { + Some(m) => { + Ok(iter.into_pipeline_data_with_metadata(m.clone(), engine_state.ctrlc.clone())) + } + None => Ok(iter.into_pipeline_data(engine_state.ctrlc.clone())), + } + } +} + +pub fn sort( + vec: &mut [Value], + columns: Vec, + call: &Call, + insensitive: bool, + config: &Config, +) -> Result<(), ShellError> { + let should_sort_case_insensitively = insensitive + && vec + .iter() + .all(|x| matches!(x.get_type(), nu_protocol::Type::String)); + + match &vec[0] { + Value::Record { + cols, + vals: _input_vals, + .. + } => { + if columns.is_empty() { + println!("sort-by requires a column name to sort table data"); + return Err(ShellError::CantFindColumn(call.head, call.head)); + } + + if column_does_not_exist(columns.clone(), cols.to_vec()) { + return Err(ShellError::CantFindColumn(call.head, call.head)); + } + + vec.sort_by(|a, b| { + process( + a, + b, + &columns[0], + call, + should_sort_case_insensitively, + config, + ) + .expect("sort_by Value::Record bug") + .compare() + }); + } + _ => { + vec.sort_by(|a, b| { + if should_sort_case_insensitively { + let lowercase_left = Value::string( + a.into_string("", config).to_ascii_lowercase(), + Span::test_data(), + ); + let lowercase_right = Value::string( + b.into_string("", config).to_ascii_lowercase(), + Span::test_data(), + ); + coerce_compare(&lowercase_left, &lowercase_right) + .expect("sort_by default bug") + .compare() + } else { + coerce_compare(a, b).expect("sort_by default bug").compare() + } + }); + } + } + Ok(()) +} + +pub fn process( + left: &Value, + right: &Value, + column: &str, + call: &Call, + insensitive: bool, + config: &Config, +) -> Result { + let left_value = left.get_data_by_key(column); + + let left_res = match left_value { + Some(left_res) => left_res, + None => Value::Nothing { span: call.head }, + }; + + let right_value = right.get_data_by_key(column); + + let right_res = match right_value { + Some(right_res) => right_res, + None => Value::Nothing { span: call.head }, + }; + + if insensitive { + let lowercase_left = Value::string( + left_res.into_string("", config).to_ascii_lowercase(), + Span::test_data(), + ); + let lowercase_right = Value::string( + right_res.into_string("", config).to_ascii_lowercase(), + Span::test_data(), + ); + coerce_compare(&lowercase_left, &lowercase_right) + } else { + coerce_compare(&left_res, &right_res) + } +} + +#[derive(Debug)] +pub enum CompareValues { + Ints(i64, i64), + Floats(f64, f64), + String(String, String), + Booleans(bool, bool), + Filesize(i64, i64), + Date(DateTime, DateTime), +} + +impl CompareValues { + pub fn compare(&self) -> std::cmp::Ordering { + match self { + CompareValues::Ints(left, right) => left.cmp(right), + CompareValues::Floats(left, right) => process_floats(left, right), + CompareValues::String(left, right) => left.cmp(right), + CompareValues::Booleans(left, right) => left.cmp(right), + CompareValues::Filesize(left, right) => left.cmp(right), + CompareValues::Date(left, right) => left.cmp(right), + } + } +} + +pub fn process_floats(left: &f64, right: &f64) -> std::cmp::Ordering { + let result = left.partial_cmp(right); + match result { + Some(Ordering::Greater) => Ordering::Greater, + Some(Ordering::Less) => Ordering::Less, + _ => Ordering::Equal, + } +} + +pub fn coerce_compare( + left: &Value, + right: &Value, +) -> Result { + match (left, right) { + (Value::Float { val: left, .. }, Value::Float { val: right, .. }) => { + Ok(CompareValues::Floats(*left, *right)) + } + (Value::Filesize { val: left, .. }, Value::Filesize { val: right, .. }) => { + Ok(CompareValues::Filesize(*left, *right)) + } + (Value::Date { val: left, .. }, Value::Date { val: right, .. }) => { + Ok(CompareValues::Date(*left, *right)) + } + (Value::Int { val: left, .. }, Value::Int { val: right, .. }) => { + Ok(CompareValues::Ints(*left, *right)) + } + (Value::String { val: left, .. }, Value::String { val: right, .. }) => { + Ok(CompareValues::String(left.clone(), right.clone())) + } + (Value::Bool { val: left, .. }, Value::Bool { val: right, .. }) => { + Ok(CompareValues::Booleans(*left, *right)) + } + _ => Err(("coerce_compare_left", "coerce_compare_right")), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SortBy {}) + } +} diff --git a/crates/nu-command/src/filters/split_by.rs b/crates/nu-command/src/filters/split_by.rs new file mode 100644 index 0000000000..e559165e2e --- /dev/null +++ b/crates/nu-command/src/filters/split_by.rs @@ -0,0 +1,270 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct SplitBy; + +impl Command for SplitBy { + fn name(&self) -> &str { + "split-by" + } + + fn signature(&self) -> Signature { + Signature::build("split-by").optional( + "splitter", + SyntaxShape::Any, + "the splitter value to use", + ) + } + + fn usage(&self) -> &str { + "Create a new table splitted." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + split_by(engine_state, stack, call, input) + } + + #[allow(clippy::unwrap_used)] + fn examples(&self) -> Vec { + vec![Example { + description: "split items by column named \"lang\"", + example: r#" + { + '2019': [ + { name: 'andres', lang: 'rb', year: '2019' }, + { name: 'jt', lang: 'rs', year: '2019' } + ], + '2021': [ + { name: 'storm', lang: 'rs', 'year': '2021' } + ] + } | split-by lang + "#, + result: Some(Value::Record { + cols: vec!["rb".to_string(), "rs".to_string()], + vals: vec![ + Value::Record { + cols: vec!["2019".to_string()], + vals: vec![Value::List { + vals: vec![Value::Record { + cols: vec![ + "name".to_string(), + "lang".to_string(), + "year".to_string(), + ], + vals: vec![ + Value::test_string("andres"), + Value::test_string("rb"), + Value::test_string("2019"), + ], + span: Span::test_data(), + }], + span: Span::test_data(), + }], + span: Span::test_data(), + }, + Value::Record { + cols: vec!["2019".to_string(), "2021".to_string()], + vals: vec![ + Value::List { + vals: vec![Value::Record { + cols: vec![ + "name".to_string(), + "lang".to_string(), + "year".to_string(), + ], + vals: vec![ + Value::test_string("jt"), + Value::test_string("rs"), + Value::test_string("2019"), + ], + span: Span::test_data(), + }], + span: Span::test_data(), + }, + Value::List { + vals: vec![Value::Record { + cols: vec![ + "name".to_string(), + "lang".to_string(), + "year".to_string(), + ], + vals: vec![ + Value::test_string("storm"), + Value::test_string("rs"), + Value::test_string("2021"), + ], + span: Span::test_data(), + }], + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }] + } +} + +enum Grouper { + ByColumn(Option>), +} + +pub fn split_by( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let name = call.head; + + let splitter: Option = call.opt(engine_state, stack, 0)?; + + match splitter { + Some(v) => { + let splitter = Some(Spanned { + item: v.as_string()?, + span: name, + }); + Ok(split(&splitter, input, name)?) + } + None => Err(ShellError::SpannedLabeledError( + "expected name".into(), + "requires a column name for splitting".into(), + name, + )), + } +} + +pub fn split( + column_name: &Option>, + values: PipelineData, + span: Span, +) -> Result { + 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.item) { + Some(group_key) => Ok(group_key.as_string()?), + None => Err(ShellError::CantFindColumn( + column_name.span, + row.span().unwrap_or(column_name.span), + )), + }, + ); + + data_split(values, &Some(block), span) + } + Grouper::ByColumn(None) => { + let block = Box::new(move |_, row: &Value| row.as_string()); + + data_split(values, &Some(block), span) + } + } +} + +#[allow(clippy::type_complexity)] +pub fn data_split( + value: PipelineData, + splitter: &Option Result + Send>>, + span: Span, +) -> Result { + let mut splits = indexmap::IndexMap::new(); + + let mut cols = vec![]; + let mut vals = vec![]; + + match value { + PipelineData::Value( + Value::Record { + cols, + vals: grouped_rows, + span, + }, + _, + ) => { + for (idx, list) in grouped_rows.iter().enumerate() { + match super::group_by::data_group(list, splitter, span) { + Ok(grouped) => { + if let Value::Record { + vals: li, + cols: sub_cols, + .. + } = grouped + { + for (inner_idx, subset) in li.iter().enumerate() { + let s = splits + .entry(sub_cols[inner_idx].clone()) + .or_insert(indexmap::IndexMap::new()); + + s.insert(cols[idx].clone(), subset.clone()); + } + } + } + Err(reason) => return Err(reason), + } + } + } + _ => { + return Err(ShellError::SpannedLabeledError( + "unsupported input".into(), + "requires a table with one row for splitting".into(), + span, + )) + } + } + + for (k, rows) in splits { + cols.push(k.to_string()); + + let mut sub_cols = vec![]; + let mut sub_vals = vec![]; + + for (k, v) in rows { + sub_cols.push(k); + sub_vals.push(v); + } + + vals.push(Value::Record { + cols: sub_cols, + vals: sub_vals, + span, + }); + } + + Ok(PipelineData::Value( + Value::Record { cols, vals, span }, + None, + )) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SplitBy {}) + } +} diff --git a/crates/nu-command/src/filters/transpose.rs b/crates/nu-command/src/filters/transpose.rs new file mode 100644 index 0000000000..ac7922ef44 --- /dev/null +++ b/crates/nu-command/src/filters/transpose.rs @@ -0,0 +1,177 @@ +use nu_engine::column::get_columns; +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Transpose; + +pub struct TransposeArgs { + rest: Vec>, + header_row: bool, + ignore_titles: bool, +} + +impl Command for Transpose { + fn name(&self) -> &str { + "transpose" + } + + fn signature(&self) -> Signature { + Signature::build("transpose") + .switch( + "header-row", + "treat the first row as column names", + Some('r'), + ) + .switch( + "ignore-titles", + "don't transpose the column names into values", + Some('i'), + ) + .rest( + "rest", + SyntaxShape::String, + "the names to give columns once transposed", + ) + } + + fn usage(&self) -> &str { + "Transposes the table contents so rows become columns and columns become rows." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + transpose(engine_state, stack, call, input) + } +} + +pub fn transpose( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let name = call.head; + let transpose_args = TransposeArgs { + header_row: call.has_flag("header-row"), + ignore_titles: call.has_flag("ignore-titles"), + rest: call.rest(engine_state, stack, 0)?, + }; + + let ctrlc = engine_state.ctrlc.clone(); + let input: Vec<_> = input.into_iter().collect(); + let args = transpose_args; + + let descs = get_columns(&input); + + let mut headers: Vec = vec![]; + + if !args.rest.is_empty() && args.header_row { + return Err(ShellError::SpannedLabeledError( + "Can not provide header names and use header row".into(), + "using header row".into(), + name, + )); + } + + if args.header_row { + for i in input.clone() { + if let Some(desc) = descs.get(0) { + match &i.get_data_by_key(desc) { + Some(x) => { + if let Ok(s) = x.as_string() { + headers.push(s.to_string()); + } else { + return Err(ShellError::SpannedLabeledError( + "Header row needs string headers".into(), + "used non-string headers".into(), + name, + )); + } + } + _ => { + return Err(ShellError::SpannedLabeledError( + "Header row is incomplete and can't be used".into(), + "using incomplete header row".into(), + name, + )); + } + } + } else { + return Err(ShellError::SpannedLabeledError( + "Header row is incomplete and can't be used".into(), + "using incomplete header row".into(), + name, + )); + } + } + } else { + for i in 0..=input.len() { + if let Some(name) = args.rest.get(i) { + headers.push(name.item.clone()) + } else { + headers.push(format!("Column{}", i)); + } + } + } + + let descs: Vec<_> = if args.header_row { + descs.into_iter().skip(1).collect() + } else { + descs + }; + + Ok((descs.into_iter().map(move |desc| { + let mut column_num: usize = 0; + let mut cols = vec![]; + let mut vals = vec![]; + + if !args.ignore_titles && !args.header_row { + cols.push(headers[column_num].clone()); + vals.push(Value::string(desc.clone(), name)); + column_num += 1 + } + + for i in input.clone() { + match &i.get_data_by_key(&desc) { + Some(x) => { + cols.push(headers[column_num].clone()); + vals.push(x.clone()); + } + _ => { + cols.push(headers[column_num].clone()); + vals.push(Value::nothing(name)); + } + } + column_num += 1; + } + + Value::Record { + cols, + vals, + span: name, + } + })) + .into_pipeline_data(ctrlc)) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Transpose {}) + } +} diff --git a/crates/nu-command/src/filters/uniq.rs b/crates/nu-command/src/filters/uniq.rs new file mode 100644 index 0000000000..b6b935fda6 --- /dev/null +++ b/crates/nu-command/src/filters/uniq.rs @@ -0,0 +1,206 @@ +use std::collections::VecDeque; + +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Value, +}; + +#[derive(Clone)] +pub struct Uniq; + +impl Command for Uniq { + fn name(&self) -> &str { + "uniq" + } + + fn signature(&self) -> Signature { + Signature::build("uniq") + .switch("count", "Count the unique rows", Some('c')) + .switch( + "repeated", + "Count the rows that has more than one value", + Some('d'), + ) + .switch( + "ignore-case", + "Ignore differences in case when comparing", + Some('i'), + ) + .switch("unique", "Only return unique values", Some('u')) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Return the unique rows." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + uniq(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Remove duplicate rows of a list/table", + example: "[2 3 3 4] | uniq", + result: Some(Value::List { + vals: vec![Value::test_int(2), Value::test_int(3), Value::test_int(4)], + span: Span::test_data(), + }), + }, + Example { + description: "Only print duplicate lines, one for each group", + example: "[1 2 2] | uniq -d", + result: Some(Value::test_int(2)), + }, + Example { + description: "Only print unique lines lines", + example: "[1 2 2] | uniq -u", + result: Some(Value::test_int(1)), + }, + Example { + description: "Ignore differences in case when comparing", + example: "['hello' 'goodbye' 'Hello'] | uniq -i", + result: Some(Value::List { + vals: vec![Value::test_string("hello"), Value::test_string("goodbye")], + span: Span::test_data(), + }), + }, + Example { + description: "Remove duplicate rows and show counts of a list/table", + example: "[1 2 2] | uniq -c", + result: Some(Value::List { + vals: vec![ + Value::Record { + cols: vec!["value".to_string(), "count".to_string()], + vals: vec![Value::test_int(1), Value::test_int(1)], + span: Span::test_data(), + }, + Value::Record { + cols: vec!["value".to_string(), "count".to_string()], + vals: vec![Value::test_int(2), Value::test_int(2)], + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }, + ] + } +} + +fn to_lowercase(value: nu_protocol::Value) -> nu_protocol::Value { + match value { + Value::String { val: s, span } => Value::String { + val: s.to_lowercase(), + span, + }, + other => other, + } +} + +fn uniq( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let head = call.head; + let should_show_count = call.has_flag("count"); + let show_repeated = call.has_flag("repeated"); + let ignore_case = call.has_flag("ignore-case"); + let only_uniques = call.has_flag("unique"); + + let uniq_values = { + let counter = &mut Vec::new(); + for line in input.into_iter() { + let item = if ignore_case { + to_lowercase(line) + } else { + line + }; + + if counter.is_empty() { + counter.push((item, 1)); + } else { + // check if the value item already exists in our collection. if it does, increase counter, otherwise add it to the collection + match counter.iter_mut().find(|x| x.0 == item) { + Some(x) => x.1 += 1, + None => counter.push((item, 1)), + } + } + } + counter.to_vec() + }; + + let uv = uniq_values.to_vec(); + let mut values = if show_repeated { + uv.into_iter().filter(|i| i.1 > 1).collect() + } else { + uv + }; + + if only_uniques { + values = values.into_iter().filter(|i| i.1 == 1).collect::<_>() + } + + let mut values_vec_deque = VecDeque::new(); + + if should_show_count { + for item in values { + values_vec_deque.push_back({ + let cols = vec!["value".to_string(), "count".to_string()]; + let vals = vec![ + item.0, + Value::Int { + val: item.1, + span: head, + }, + ]; + Value::Record { + cols, + vals, + span: head, + } + }); + } + } else { + for item in values { + values_vec_deque.push_back(item.0); + } + } + + // keeps the original Nushell semantics + if values_vec_deque.len() == 1 { + if let Some(x) = values_vec_deque.pop_front() { + Ok(x.into_pipeline_data()) + } else { + Err(ShellError::NushellFailed("No input given...".to_string())) + } + } else { + Ok(Value::List { + vals: values_vec_deque.into_iter().collect(), + span: head, + } + .into_pipeline_data()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Uniq {}) + } +} diff --git a/crates/nu-command/src/filters/update.rs b/crates/nu-command/src/filters/update.rs new file mode 100644 index 0000000000..9b01be148a --- /dev/null +++ b/crates/nu-command/src/filters/update.rs @@ -0,0 +1,127 @@ +use nu_engine::{eval_block, CallExt}; +use nu_protocol::ast::{Call, CellPath}; +use nu_protocol::engine::{CaptureBlock, Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, FromValue, IntoPipelineData, PipelineData, ShellError, Signature, Span, + SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Update; + +impl Command for Update { + fn name(&self) -> &str { + "update" + } + + fn signature(&self) -> Signature { + Signature::build("update") + .required( + "field", + SyntaxShape::CellPath, + "the name of the column to update", + ) + .required( + "replacement value", + SyntaxShape::Any, + "the new value to give the cell(s)", + ) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Update an existing column to have a new value." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + update(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Update a column value", + example: "echo {'name': 'nu', 'stars': 5} | update name 'Nushell'", + result: Some(Value::Record { cols: vec!["name".into(), "stars".into()], vals: vec![Value::test_string("Nushell"), Value::test_int(5)], span: Span::test_data()}), + }, Example { + description: "Use in block form for more involved updating logic", + example: "echo [[project, authors]; ['nu', ['Andrés', 'JT', 'Yehuda']]] | update authors { get authors | str collect ',' }", + result: Some(Value::List { vals: vec![Value::Record { cols: vec!["project".into(), "authors".into()], vals: vec![Value::test_string("nu"), Value::test_string("Andrés,JT,Yehuda")], span: Span::test_data()}], span: Span::test_data()}), + }] + } +} + +fn update( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let span = call.head; + + let cell_path: CellPath = call.req(engine_state, stack, 0)?; + let replacement: Value = call.req(engine_state, stack, 1)?; + let engine_state = engine_state.clone(); + let ctrlc = engine_state.ctrlc.clone(); + + // Replace is a block, so set it up and run it instead of using it as the replacement + if replacement.as_block().is_ok() { + let capture_block: CaptureBlock = FromValue::from_value(&replacement)?; + let block = engine_state.get_block(capture_block.block_id).clone(); + + let mut stack = stack.captures_to_stack(&capture_block.captures); + let orig_env_vars = stack.env_vars.clone(); + let orig_env_hidden = stack.env_hidden.clone(); + + input.map( + move |mut input| { + stack.with_env(&orig_env_vars, &orig_env_hidden); + + if let Some(var) = block.signature.get_positional(0) { + if let Some(var_id) = &var.var_id { + stack.add_var(*var_id, input.clone()) + } + } + + let output = eval_block( + &engine_state, + &mut stack, + &block, + input.clone().into_pipeline_data(), + ); + + match output { + Ok(pd) => { + if let Err(e) = + input.replace_data_at_cell_path(&cell_path.members, pd.into_value(span)) + { + return Value::Error { error: e }; + } + + input + } + Err(e) => Value::Error { error: e }, + } + }, + ctrlc, + ) + } else { + input.map( + move |mut input| { + let replacement = replacement.clone(); + + if let Err(e) = input.replace_data_at_cell_path(&cell_path.members, replacement) { + return Value::Error { error: e }; + } + + input + }, + ctrlc, + ) + } +} diff --git a/crates/nu-command/src/filters/update_cells.rs b/crates/nu-command/src/filters/update_cells.rs new file mode 100644 index 0000000000..6dbe50c0c6 --- /dev/null +++ b/crates/nu-command/src/filters/update_cells.rs @@ -0,0 +1,247 @@ +use nu_engine::{eval_block, CallExt}; +use nu_protocol::ast::{Block, Call}; +use nu_protocol::engine::{CaptureBlock, Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, + PipelineIterator, ShellError, Signature, Span, SyntaxShape, Value, +}; +use std::collections::HashSet; +use std::iter::FromIterator; + +#[derive(Clone)] +pub struct UpdateCells; + +impl Command for UpdateCells { + fn name(&self) -> &str { + "update cells" + } + + fn signature(&self) -> Signature { + Signature::build("update cells") + .required( + "block", + SyntaxShape::Block(Some(vec![SyntaxShape::Any])), + "the block to run an update for each cell", + ) + .named( + "columns", + SyntaxShape::Table, + "list of columns to update", + Some('c'), + ) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Update the table cells." + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Update the zero value cells to empty strings.", + example: r#"[ + [2021-04-16, 2021-06-10, 2021-09-18, 2021-10-15, 2021-11-16, 2021-11-17, 2021-11-18]; + [ 37, 0, 0, 0, 37, 0, 0] +] | update cells {|value| + if $value == 0 { + "" + } else { + $value + } +}"#, + result: Some(Value::List { + vals: vec![Value::Record { + cols: vec![ + "2021-04-16".into(), + "2021-06-10".into(), + "2021-09-18".into(), + "2021-10-15".into(), + "2021-11-16".into(), + "2021-11-17".into(), + "2021-11-18".into(), + ], + vals: vec![ + Value::test_int(37), + Value::test_string(""), + Value::test_string(""), + Value::test_string(""), + Value::test_int(37), + Value::test_string(""), + Value::test_string(""), + ], + span: Span::test_data(), + }], + span: Span::test_data(), + }), + }, + Example { + description: "Update the zero value cells to empty strings in 2 last columns.", + example: r#"[ + [2021-04-16, 2021-06-10, 2021-09-18, 2021-10-15, 2021-11-16, 2021-11-17, 2021-11-18]; + [ 37, 0, 0, 0, 37, 0, 0] +] | update cells -c ["2021-11-18", "2021-11-17"] {|value| + if $value == 0 { + "" + } else { + $value + } +}"#, + result: Some(Value::List { + vals: vec![Value::Record { + cols: vec![ + "2021-04-16".into(), + "2021-06-10".into(), + "2021-09-18".into(), + "2021-10-15".into(), + "2021-11-16".into(), + "2021-11-17".into(), + "2021-11-18".into(), + ], + vals: vec![ + Value::test_int(37), + Value::test_int(0), + Value::test_int(0), + Value::test_int(0), + Value::test_int(37), + Value::test_string(""), + Value::test_string(""), + ], + span: Span::test_data(), + }], + span: Span::test_data(), + }), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + // the block to run on each cell + let engine_state = engine_state.clone(); + let block: CaptureBlock = call.req(&engine_state, stack, 0)?; + let mut stack = stack.captures_to_stack(&block.captures); + let orig_env_vars = stack.env_vars.clone(); + let orig_env_hidden = stack.env_hidden.clone(); + + let ctrlc = engine_state.ctrlc.clone(); + let block: Block = engine_state.get_block(block.block_id).clone(); + + let span = call.head; + + stack.with_env(&orig_env_vars, &orig_env_hidden); + + // the columns to update + let columns: Option = call.get_flag(&engine_state, &mut stack, "columns")?; + let columns: Option> = match columns { + Some(val) => { + let cols = val + .as_list()? + .iter() + .map(|val| val.as_string()) + .collect::, ShellError>>()?; + Some(HashSet::from_iter(cols.into_iter())) + } + None => None, + }; + + Ok(UpdateCellIterator { + input: input.into_iter(), + engine_state, + stack, + block, + columns, + span, + } + .into_pipeline_data(ctrlc)) + } +} + +struct UpdateCellIterator { + input: PipelineIterator, + columns: Option>, + engine_state: EngineState, + stack: Stack, + block: Block, + span: Span, +} + +impl Iterator for UpdateCellIterator { + type Item = Value; + + fn next(&mut self) -> Option { + match self.input.next() { + Some(val) => { + if let Some(ref cols) = self.columns { + if !val.columns().iter().any(|c| cols.contains(c)) { + return Some(val); + } + } + + match val { + Value::Record { vals, cols, span } => Some(Value::Record { + vals: cols + .iter() + .zip(vals.into_iter()) + .map(|(col, val)| match &self.columns { + Some(cols) if !cols.contains(col) => val, + _ => process_cell( + val, + &self.engine_state, + &mut self.stack, + &self.block, + span, + ), + }) + .collect(), + cols, + span, + }), + val => Some(process_cell( + val, + &self.engine_state, + &mut self.stack, + &self.block, + self.span, + )), + } + } + None => None, + } + } +} + +fn process_cell( + val: Value, + engine_state: &EngineState, + stack: &mut Stack, + block: &Block, + span: Span, +) -> Value { + if let Some(var) = block.signature.get_positional(0) { + if let Some(var_id) = &var.var_id { + stack.add_var(*var_id, val.clone()); + } + } + match eval_block(engine_state, stack, block, val.into_pipeline_data()) { + Ok(pd) => pd.into_value(span), + Err(e) => Value::Error { error: e }, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(UpdateCells {}) + } +} diff --git a/crates/nu-command/src/filters/where_.rs b/crates/nu-command/src/filters/where_.rs new file mode 100644 index 0000000000..99d587f124 --- /dev/null +++ b/crates/nu-command/src/filters/where_.rs @@ -0,0 +1,66 @@ +use nu_engine::{eval_block_with_redirect, CallExt}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{CaptureBlock, Command, EngineState, Stack}; +use nu_protocol::{Category, PipelineData, Signature, SyntaxShape}; + +#[derive(Clone)] +pub struct Where; + +impl Command for Where { + fn name(&self) -> &str { + "where" + } + + fn usage(&self) -> &str { + "Filter values based on a condition." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("where") + .required("cond", SyntaxShape::RowCondition, "condition") + .category(Category::Filters) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let span = call.head; + + let metadata = input.metadata(); + + let block: CaptureBlock = call.req(engine_state, stack, 0)?; + let mut stack = stack.captures_to_stack(&block.captures); + let block = engine_state.get_block(block.block_id).clone(); + + let ctrlc = engine_state.ctrlc.clone(); + let engine_state = engine_state.clone(); + + input + .filter( + move |value| { + if let Some(var) = block.signature.get_positional(0) { + if let Some(var_id) = &var.var_id { + stack.add_var(*var_id, value.clone()); + } + } + let result = eval_block_with_redirect( + &engine_state, + &mut stack, + &block, + PipelineData::new(span), + ); + + match result { + Ok(result) => result.into_value(span).is_true(), + _ => false, + } + }, + ctrlc, + ) + .map(|x| x.set_metadata(metadata)) + } +} diff --git a/crates/nu-command/src/filters/wrap.rs b/crates/nu-command/src/filters/wrap.rs new file mode 100644 index 0000000000..6b3667e98b --- /dev/null +++ b/crates/nu-command/src/filters/wrap.rs @@ -0,0 +1,67 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, Signature, + SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Wrap; + +impl Command for Wrap { + fn name(&self) -> &str { + "wrap" + } + + fn usage(&self) -> &str { + "Wrap the value into a column." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("wrap") + .required("name", SyntaxShape::String, "the name of the column") + .category(Category::Filters) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let span = call.head; + let name: String = call.req(engine_state, stack, 0)?; + + match input { + PipelineData::Value(Value::List { vals, .. }, ..) => Ok(vals + .into_iter() + .map(move |x| Value::Record { + cols: vec![name.clone()], + vals: vec![x], + span, + }) + .into_pipeline_data(engine_state.ctrlc.clone())), + PipelineData::ListStream(stream, ..) => Ok(stream + .map(move |x| Value::Record { + cols: vec![name.clone()], + vals: vec![x], + span, + }) + .into_pipeline_data(engine_state.ctrlc.clone())), + PipelineData::RawStream(..) => Ok(Value::Record { + cols: vec![name], + vals: vec![input.into_value(call.head)], + span, + } + .into_pipeline_data()), + PipelineData::Value(input, ..) => Ok(Value::Record { + cols: vec![name], + vals: vec![input], + span, + } + .into_pipeline_data()), + } + } +} diff --git a/crates/nu-command/src/filters/zip_.rs b/crates/nu-command/src/filters/zip_.rs new file mode 100644 index 0000000000..6f36288ca4 --- /dev/null +++ b/crates/nu-command/src/filters/zip_.rs @@ -0,0 +1,112 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, Signature, + Span, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Zip; + +impl Command for Zip { + fn name(&self) -> &str { + "zip" + } + + fn usage(&self) -> &str { + "Combine a stream with the input" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("zip") + .required("other", SyntaxShape::Any, "the other input") + .category(Category::Filters) + } + + fn examples(&self) -> Vec { + let test_row_1 = Value::List { + vals: vec![ + Value::Int { + val: 1, + span: Span::test_data(), + }, + Value::Int { + val: 4, + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }; + + let test_row_2 = Value::List { + vals: vec![ + Value::Int { + val: 2, + span: Span::test_data(), + }, + Value::Int { + val: 5, + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }; + + let test_row_3 = Value::List { + vals: vec![ + Value::Int { + val: 3, + span: Span::test_data(), + }, + Value::Int { + val: 6, + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }; + + vec![Example { + example: "1..3 | zip 4..6", + description: "Zip multiple streams and get one of the results", + result: Some(Value::List { + vals: vec![test_row_1, test_row_2, test_row_3], + span: Span::test_data(), + }), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let other: Value = call.req(engine_state, stack, 0)?; + let head = call.head; + let ctrlc = engine_state.ctrlc.clone(); + + Ok(input + .into_iter() + .zip(other.into_pipeline_data().into_iter()) + .map(move |(x, y)| Value::List { + vals: vec![x, y], + span: head, + }) + .into_pipeline_data(ctrlc)) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Zip {}) + } +} diff --git a/crates/nu-command/src/formats/from/command.rs b/crates/nu-command/src/formats/from/command.rs new file mode 100644 index 0000000000..719face4cf --- /dev/null +++ b/crates/nu-command/src/formats/from/command.rs @@ -0,0 +1,30 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, PipelineData, ShellError, Signature}; + +#[derive(Clone)] +pub struct From; + +impl Command for From { + fn name(&self) -> &str { + "from" + } + + fn usage(&self) -> &str { + "Parse a string or binary data into structured data" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("from").category(Category::Formats) + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::new(call.head)) + } +} diff --git a/crates/nu-command/src/formats/from/csv.rs b/crates/nu-command/src/formats/from/csv.rs new file mode 100644 index 0000000000..cf60873b1b --- /dev/null +++ b/crates/nu-command/src/formats/from/csv.rs @@ -0,0 +1,115 @@ +use super::delimited::from_delimited_data; + +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Value}; + +#[derive(Clone)] +pub struct FromCsv; + +impl Command for FromCsv { + fn name(&self) -> &str { + "from csv" + } + + fn signature(&self) -> Signature { + Signature::build("from csv") + .named( + "separator", + SyntaxShape::String, + "a character to separate columns, defaults to ','", + Some('s'), + ) + .switch( + "noheaders", + "don't treat the first row as column names", + Some('n'), + ) + .category(Category::Formats) + } + + fn usage(&self) -> &str { + "Parse text as .csv and create table." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + from_csv(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Convert comma-separated data to a table", + example: "open data.txt | from csv", + result: None, + }, + Example { + description: "Convert comma-separated data to a table, ignoring headers", + example: "open data.txt | from csv --noheaders", + result: None, + }, + Example { + description: "Convert comma-separated data to a table, ignoring headers", + example: "open data.txt | from csv -n", + result: None, + }, + Example { + description: "Convert semicolon-separated data to a table", + example: "open data.txt | from csv --separator ';'", + result: None, + }, + ] + } +} + +fn from_csv( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let name = call.head; + + let noheaders = call.has_flag("noheaders"); + let separator: Option = call.get_flag(engine_state, stack, "separator")?; + let config = stack.get_config().unwrap_or_default(); + + let sep = match separator { + Some(Value::String { val: s, span }) => { + if s == r"\t" { + '\t' + } else { + let vec_s: Vec = s.chars().collect(); + if vec_s.len() != 1 { + return Err(ShellError::MissingParameter( + "single character separator".into(), + span, + )); + }; + vec_s[0] + } + } + _ => ',', + }; + + from_delimited_data(noheaders, sep, input, name, &config) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(FromCsv {}) + } +} diff --git a/crates/nu-command/src/formats/from/delimited.rs b/crates/nu-command/src/formats/from/delimited.rs new file mode 100644 index 0000000000..b8c0191548 --- /dev/null +++ b/crates/nu-command/src/formats/from/delimited.rs @@ -0,0 +1,62 @@ +use csv::ReaderBuilder; +use nu_protocol::{Config, IntoPipelineData, PipelineData, ShellError, Span, Value}; + +fn from_delimited_string_to_value( + s: String, + noheaders: bool, + separator: char, + span: Span, +) -> Result { + let mut reader = ReaderBuilder::new() + .has_headers(!noheaders) + .delimiter(separator as u8) + .from_reader(s.as_bytes()); + + let headers = if noheaders { + (1..=reader.headers()?.len()) + .map(|i| format!("Column{}", i)) + .collect::>() + } else { + reader.headers()?.iter().map(String::from).collect() + }; + + let mut rows = vec![]; + for row in reader.records() { + let mut output_row = vec![]; + for value in row?.iter() { + if let Ok(i) = value.parse::() { + output_row.push(Value::Int { val: i, span }); + } else if let Ok(f) = value.parse::() { + output_row.push(Value::Float { val: f, span }); + } else { + output_row.push(Value::String { + val: value.into(), + span, + }); + } + } + rows.push(Value::Record { + cols: headers.clone(), + vals: output_row, + span, + }); + } + + Ok(Value::List { vals: rows, span }) +} + +pub fn from_delimited_data( + noheaders: bool, + sep: char, + input: PipelineData, + name: Span, + config: &Config, +) -> Result { + let concat_string = input.collect_string("", config)?; + + Ok( + from_delimited_string_to_value(concat_string, noheaders, sep, name) + .map_err(|x| ShellError::DelimiterError(x.to_string(), name))? + .into_pipeline_data(), + ) +} diff --git a/crates/nu-command/src/formats/from/eml.rs b/crates/nu-command/src/formats/from/eml.rs new file mode 100644 index 0000000000..36a1a94a03 --- /dev/null +++ b/crates/nu-command/src/formats/from/eml.rs @@ -0,0 +1,252 @@ +use ::eml_parser::eml::*; +use ::eml_parser::EmlParser; +use indexmap::map::IndexMap; +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::Category; +use nu_protocol::Config; +use nu_protocol::{ + Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct FromEml; + +const DEFAULT_BODY_PREVIEW: usize = 50; + +impl Command for FromEml { + fn name(&self) -> &str { + "from eml" + } + + fn signature(&self) -> Signature { + Signature::build("from eml") + .named( + "preview-body", + SyntaxShape::Int, + "How many bytes of the body to preview", + Some('b'), + ) + .category(Category::Formats) + } + + fn usage(&self) -> &str { + "Parse text as .eml and create table." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let preview_body: Option> = + call.get_flag(engine_state, stack, "preview-body")?; + let config = stack.get_config().unwrap_or_default(); + from_eml(input, preview_body, head, &config) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Convert eml structured data into table", + example: "'From: test@email.com +Subject: Welcome +To: someone@somewhere.com + +Test' | from eml", + result: Some(Value::Record { + cols: vec![ + "Subject".to_string(), + "From".to_string(), + "To".to_string(), + "Body".to_string(), + ], + vals: vec![ + Value::test_string("Welcome"), + Value::Record { + cols: vec!["Name".to_string(), "Address".to_string()], + vals: vec![ + Value::nothing(Span::test_data()), + Value::test_string("test@email.com"), + ], + span: Span::test_data(), + }, + Value::Record { + cols: vec!["Name".to_string(), "Address".to_string()], + vals: vec![ + Value::nothing(Span::test_data()), + Value::test_string("someone@somewhere.com"), + ], + span: Span::test_data(), + }, + Value::test_string("Test"), + ], + span: Span::test_data(), + }), + }, + Example { + description: "Convert eml structured data into table", + example: "'From: test@email.com +Subject: Welcome +To: someone@somewhere.com + +Test' | from eml -b 1", + result: Some(Value::Record { + cols: vec![ + "Subject".to_string(), + "From".to_string(), + "To".to_string(), + "Body".to_string(), + ], + vals: vec![ + Value::test_string("Welcome"), + Value::Record { + cols: vec!["Name".to_string(), "Address".to_string()], + vals: vec![ + Value::nothing(Span::test_data()), + Value::test_string("test@email.com"), + ], + span: Span::test_data(), + }, + Value::Record { + cols: vec!["Name".to_string(), "Address".to_string()], + vals: vec![ + Value::nothing(Span::test_data()), + Value::test_string("someone@somewhere.com"), + ], + span: Span::test_data(), + }, + Value::test_string("T"), + ], + span: Span::test_data(), + }), + }, + ] + } +} + +fn emailaddress_to_value(span: Span, email_address: &EmailAddress) -> Value { + let (n, a) = match email_address { + EmailAddress::AddressOnly { address } => ( + Value::nothing(span), + Value::String { + val: address.to_string(), + span, + }, + ), + EmailAddress::NameAndEmailAddress { name, address } => ( + Value::String { + val: name.to_string(), + span, + }, + Value::String { + val: address.to_string(), + span, + }, + ), + }; + + Value::Record { + cols: vec!["Name".to_string(), "Address".to_string()], + vals: vec![n, a], + span, + } +} + +fn headerfieldvalue_to_value(head: Span, value: &HeaderFieldValue) -> Value { + use HeaderFieldValue::*; + + match value { + SingleEmailAddress(address) => emailaddress_to_value(head, address), + MultipleEmailAddresses(addresses) => Value::List { + vals: addresses + .iter() + .map(|a| emailaddress_to_value(head, a)) + .collect(), + span: head, + }, + Unstructured(s) => Value::String { + val: s.to_string(), + span: head, + }, + Empty => Value::nothing(head), + } +} + +fn from_eml( + input: PipelineData, + preview_body: Option>, + head: Span, + config: &Config, +) -> Result { + let value = input.collect_string("", config)?; + + let body_preview = preview_body + .map(|b| b.item as usize) + .unwrap_or(DEFAULT_BODY_PREVIEW); + + let eml = EmlParser::from_string(value) + .with_body_preview(body_preview) + .parse() + .map_err(|_| { + ShellError::CantConvert("structured data from eml".into(), "string".into(), head) + })?; + + let mut collected = IndexMap::new(); + + if let Some(subj) = eml.subject { + collected.insert( + "Subject".to_string(), + Value::String { + val: subj, + span: head, + }, + ); + } + + if let Some(from) = eml.from { + collected.insert("From".to_string(), headerfieldvalue_to_value(head, &from)); + } + + if let Some(to) = eml.to { + collected.insert("To".to_string(), headerfieldvalue_to_value(head, &to)); + } + + for HeaderField { name, value } in &eml.headers { + collected.insert(name.to_string(), headerfieldvalue_to_value(head, value)); + } + + if let Some(body) = eml.body { + collected.insert( + "Body".to_string(), + Value::String { + val: body, + span: head, + }, + ); + } + + Ok(PipelineData::Value( + Value::from(Spanned { + item: collected, + span: head, + }), + None, + )) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(FromEml {}) + } +} diff --git a/crates/nu-command/src/formats/from/ics.rs b/crates/nu-command/src/formats/from/ics.rs new file mode 100644 index 0000000000..c320de7bd5 --- /dev/null +++ b/crates/nu-command/src/formats/from/ics.rs @@ -0,0 +1,334 @@ +extern crate ical; +use ical::parser::ical::component::*; +use ical::property::Property; +use indexmap::map::IndexMap; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Config, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, + Spanned, Value, +}; +use std::io::BufReader; + +#[derive(Clone)] +pub struct FromIcs; + +impl Command for FromIcs { + fn name(&self) -> &str { + "from ics" + } + + fn signature(&self) -> Signature { + Signature::build("from ics").category(Category::Formats) + } + + fn usage(&self) -> &str { + "Parse text as .ics and create table." + } + + fn run( + &self, + _engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let config = stack.get_config().unwrap_or_default(); + from_ics(input, head, &config) + } + + fn examples(&self) -> Vec { + vec![Example { + example: "'BEGIN:VCALENDAR +END:VCALENDAR' | from ics", + description: "Converts ics formatted string to table", + result: Some(Value::List { + vals: vec![Value::Record { + cols: vec![ + "properties".to_string(), + "events".to_string(), + "alarms".to_string(), + "to-Dos".to_string(), + "journals".to_string(), + "free-busys".to_string(), + "timezones".to_string(), + ], + vals: vec![ + Value::List { + vals: vec![], + span: Span::test_data(), + }, + Value::List { + vals: vec![], + span: Span::test_data(), + }, + Value::List { + vals: vec![], + span: Span::test_data(), + }, + Value::List { + vals: vec![], + span: Span::test_data(), + }, + Value::List { + vals: vec![], + span: Span::test_data(), + }, + Value::List { + vals: vec![], + span: Span::test_data(), + }, + Value::List { + vals: vec![], + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }], + span: Span::test_data(), + }), + }] + } +} + +fn from_ics(input: PipelineData, head: Span, config: &Config) -> Result { + let input_string = input.collect_string("", config)?; + + let input_string = input_string + .lines() + .map(|x| x.trim().to_string()) + .collect::>() + .join("\n"); + + let input_bytes = input_string.as_bytes(); + let buf_reader = BufReader::new(input_bytes); + let parser = ical::IcalParser::new(buf_reader); + + let mut output = vec![]; + + for calendar in parser { + match calendar { + Ok(c) => output.push(calendar_to_value(c, head)), + Err(e) => output.push(Value::Error { + error: ShellError::UnsupportedInput( + format!("input cannot be parsed as .ics ({})", e), + head, + ), + }), + } + } + Ok(Value::List { + vals: output, + span: head, + } + .into_pipeline_data()) +} + +fn calendar_to_value(calendar: IcalCalendar, span: Span) -> Value { + let mut row = IndexMap::new(); + + row.insert( + "properties".to_string(), + properties_to_value(calendar.properties, span), + ); + row.insert("events".to_string(), events_to_value(calendar.events, span)); + row.insert("alarms".to_string(), alarms_to_value(calendar.alarms, span)); + row.insert("to-Dos".to_string(), todos_to_value(calendar.todos, span)); + row.insert( + "journals".to_string(), + journals_to_value(calendar.journals, span), + ); + row.insert( + "free-busys".to_string(), + free_busys_to_value(calendar.free_busys, span), + ); + row.insert( + "timezones".to_string(), + timezones_to_value(calendar.timezones, span), + ); + + Value::from(Spanned { item: row, span }) +} + +fn events_to_value(events: Vec, span: Span) -> Value { + Value::List { + vals: events + .into_iter() + .map(|event| { + let mut row = IndexMap::new(); + row.insert( + "properties".to_string(), + properties_to_value(event.properties, span), + ); + row.insert("alarms".to_string(), alarms_to_value(event.alarms, span)); + Value::from(Spanned { item: row, span }) + }) + .collect::>(), + span, + } +} + +fn alarms_to_value(alarms: Vec, span: Span) -> Value { + Value::List { + vals: alarms + .into_iter() + .map(|alarm| { + let mut row = IndexMap::new(); + row.insert( + "properties".to_string(), + properties_to_value(alarm.properties, span), + ); + Value::from(Spanned { item: row, span }) + }) + .collect::>(), + span, + } +} + +fn todos_to_value(todos: Vec, span: Span) -> Value { + Value::List { + vals: todos + .into_iter() + .map(|todo| { + let mut row = IndexMap::new(); + row.insert( + "properties".to_string(), + properties_to_value(todo.properties, span), + ); + row.insert("alarms".to_string(), alarms_to_value(todo.alarms, span)); + Value::from(Spanned { item: row, span }) + }) + .collect::>(), + span, + } +} + +fn journals_to_value(journals: Vec, span: Span) -> Value { + Value::List { + vals: journals + .into_iter() + .map(|journal| { + let mut row = IndexMap::new(); + row.insert( + "properties".to_string(), + properties_to_value(journal.properties, span), + ); + Value::from(Spanned { item: row, span }) + }) + .collect::>(), + span, + } +} + +fn free_busys_to_value(free_busys: Vec, span: Span) -> Value { + Value::List { + vals: free_busys + .into_iter() + .map(|free_busy| { + let mut row = IndexMap::new(); + row.insert( + "properties".to_string(), + properties_to_value(free_busy.properties, span), + ); + Value::from(Spanned { item: row, span }) + }) + .collect::>(), + span, + } +} + +fn timezones_to_value(timezones: Vec, span: Span) -> Value { + Value::List { + vals: timezones + .into_iter() + .map(|timezone| { + let mut row = IndexMap::new(); + row.insert( + "properties".to_string(), + properties_to_value(timezone.properties, span), + ); + row.insert( + "transitions".to_string(), + timezone_transitions_to_value(timezone.transitions, span), + ); + Value::from(Spanned { item: row, span }) + }) + .collect::>(), + span, + } +} + +fn timezone_transitions_to_value(transitions: Vec, span: Span) -> Value { + Value::List { + vals: transitions + .into_iter() + .map(|transition| { + let mut row = IndexMap::new(); + row.insert( + "properties".to_string(), + properties_to_value(transition.properties, span), + ); + Value::from(Spanned { item: row, span }) + }) + .collect::>(), + span, + } +} + +fn properties_to_value(properties: Vec, span: Span) -> Value { + Value::List { + vals: properties + .into_iter() + .map(|prop| { + let mut row = IndexMap::new(); + + let name = Value::String { + val: prop.name, + span, + }; + let value = match prop.value { + Some(val) => Value::String { val, span }, + None => Value::nothing(span), + }; + let params = match prop.params { + Some(param_list) => params_to_value(param_list, span), + None => Value::nothing(span), + }; + + row.insert("name".to_string(), name); + row.insert("value".to_string(), value); + row.insert("params".to_string(), params); + Value::from(Spanned { item: row, span }) + }) + .collect::>(), + span, + } +} + +fn params_to_value(params: Vec<(String, Vec)>, span: Span) -> Value { + let mut row = IndexMap::new(); + + for (param_name, param_values) in params { + let values: Vec = param_values + .into_iter() + .map(|val| Value::string(val, span)) + .collect(); + let values = Value::List { vals: values, span }; + row.insert(param_name, values); + } + + Value::from(Spanned { item: row, span }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(FromIcs {}) + } +} diff --git a/crates/nu-command/src/formats/from/ini.rs b/crates/nu-command/src/formats/from/ini.rs new file mode 100644 index 0000000000..f7b83ec5cd --- /dev/null +++ b/crates/nu-command/src/formats/from/ini.rs @@ -0,0 +1,109 @@ +use indexmap::map::IndexMap; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Config, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Value, +}; + +#[derive(Clone)] +pub struct FromIni; + +impl Command for FromIni { + fn name(&self) -> &str { + "from ini" + } + + fn signature(&self) -> Signature { + Signature::build("from ini").category(Category::Formats) + } + + fn usage(&self) -> &str { + "Parse text as .ini and create table" + } + + fn examples(&self) -> Vec { + vec![Example { + example: "'[foo] +a=1 +b=2' | from ini", + description: "Converts ini formatted string to table", + result: Some(Value::Record { + cols: vec!["foo".to_string()], + vals: vec![Value::Record { + cols: vec!["a".to_string(), "b".to_string()], + vals: vec![ + Value::String { + val: "1".to_string(), + span: Span::test_data(), + }, + Value::String { + val: "2".to_string(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }], + span: Span::test_data(), + }), + }] + } + + fn run( + &self, + _engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let config = stack.get_config().unwrap_or_default(); + from_ini(input, head, &config) + } +} + +pub fn from_ini_string_to_value(s: String, span: Span) -> Result { + let v: Result>, serde_ini::de::Error> = + serde_ini::from_str(&s); + match v { + Ok(index_map) => { + let (cols, vals) = index_map + .into_iter() + .fold((vec![], vec![]), |mut acc, (k, v)| { + let (cols, vals) = v.into_iter().fold((vec![], vec![]), |mut acc, (k, v)| { + acc.0.push(k); + acc.1.push(Value::String { val: v, span }); + acc + }); + acc.0.push(k); + acc.1.push(Value::Record { cols, vals, span }); + acc + }); + Ok(Value::Record { cols, vals, span }) + } + Err(err) => Err(ShellError::UnsupportedInput( + format!("Could not load ini: {}", err), + span, + )), + } +} + +fn from_ini(input: PipelineData, head: Span, config: &Config) -> Result { + let concat_string = input.collect_string("", config)?; + + match from_ini_string_to_value(concat_string, head) { + Ok(x) => Ok(x.into_pipeline_data()), + Err(other) => Err(other), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(FromIni {}) + } +} diff --git a/crates/nu-command/src/formats/from/json.rs b/crates/nu-command/src/formats/from/json.rs new file mode 100644 index 0000000000..4afb0f5b09 --- /dev/null +++ b/crates/nu-command/src/formats/from/json.rs @@ -0,0 +1,173 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, ShellError, + Signature, Span, Value, +}; + +#[derive(Clone)] +pub struct FromJson; + +impl Command for FromJson { + fn name(&self) -> &str { + "from json" + } + + fn usage(&self) -> &str { + "Convert from json to structured data" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("from json") + .switch("objects", "treat each line as a separate value", Some('o')) + .category(Category::Formats) + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "'{ a:1 }' | from json", + description: "Converts json formatted string to table", + result: Some(Value::Record { + cols: vec!["a".to_string()], + vals: vec![Value::Int { + val: 1, + span: Span::test_data(), + }], + span: Span::test_data(), + }), + }, + Example { + example: "'{ a:1, b: [1, 2] }' | from json", + description: "Converts json formatted string to table", + result: Some(Value::Record { + cols: vec!["a".to_string(), "b".to_string()], + vals: vec![ + Value::Int { + val: 1, + span: Span::test_data(), + }, + Value::List { + vals: vec![ + Value::Int { + val: 1, + span: Span::test_data(), + }, + Value::Int { + val: 2, + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let span = call.head; + let config = stack.get_config().unwrap_or_default(); + let mut string_input = input.collect_string("", &config)?; + string_input.push('\n'); + + // TODO: turn this into a structured underline of the nu_json error + if call.has_flag("objects") { + #[allow(clippy::needless_collect)] + let lines: Vec = string_input.lines().map(|x| x.to_string()).collect(); + Ok(lines + .into_iter() + .map(move |mut x| { + x.push('\n'); + match convert_string_to_value(x, span) { + Ok(v) => v, + Err(error) => Value::Error { error }, + } + }) + .into_pipeline_data(engine_state.ctrlc.clone())) + } else { + Ok(convert_string_to_value(string_input, span)?.into_pipeline_data()) + } + } +} + +fn convert_nujson_to_value(value: &nu_json::Value, span: Span) -> Value { + match value { + nu_json::Value::Array(array) => { + let v: Vec = array + .iter() + .map(|x| convert_nujson_to_value(x, span)) + .collect(); + + Value::List { vals: v, span } + } + nu_json::Value::Bool(b) => Value::Bool { val: *b, span }, + nu_json::Value::F64(f) => Value::Float { val: *f, span }, + nu_json::Value::I64(i) => Value::Int { val: *i, span }, + nu_json::Value::Null => Value::Nothing { span }, + nu_json::Value::Object(k) => { + let mut cols = vec![]; + let mut vals = vec![]; + + for item in k { + cols.push(item.0.clone()); + vals.push(convert_nujson_to_value(item.1, span)); + } + + Value::Record { cols, vals, span } + } + nu_json::Value::U64(u) => { + if *u > i64::MAX as u64 { + Value::Error { + error: ShellError::CantConvert( + "i64 sized integer".into(), + "larger than i64".into(), + span, + ), + } + } else { + Value::Int { + val: *u as i64, + span, + } + } + } + nu_json::Value::String(s) => Value::String { + val: s.clone(), + span, + }, + } +} + +fn convert_string_to_value(string_input: String, span: Span) -> Result { + let result: Result = nu_json::from_str(&string_input); + match result { + Ok(value) => Ok(convert_nujson_to_value(&value, span)), + + Err(_x) => Err(ShellError::CantConvert( + "structured data from json".into(), + "string".into(), + span, + )), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(FromJson {}) + } +} diff --git a/crates/nu-command/src/formats/from/mod.rs b/crates/nu-command/src/formats/from/mod.rs new file mode 100644 index 0000000000..7f650d1641 --- /dev/null +++ b/crates/nu-command/src/formats/from/mod.rs @@ -0,0 +1,33 @@ +mod command; +mod csv; +mod delimited; +mod eml; +mod ics; +mod ini; +mod json; +mod ods; +mod ssv; +mod toml; +mod tsv; +mod url; +mod vcf; +mod xlsx; +mod xml; +mod yaml; + +pub use self::csv::FromCsv; +pub use self::toml::FromToml; +pub use self::url::FromUrl; +pub use command::From; +pub use eml::FromEml; +pub use ics::FromIcs; +pub use ini::FromIni; +pub use json::FromJson; +pub use ods::FromOds; +pub use ssv::FromSsv; +pub use tsv::FromTsv; +pub use vcf::FromVcf; +pub use xlsx::FromXlsx; +pub use xml::FromXml; +pub use yaml::FromYaml; +pub use yaml::FromYml; diff --git a/crates/nu-command/src/formats/from/ods.rs b/crates/nu-command/src/formats/from/ods.rs new file mode 100644 index 0000000000..146bffd8a1 --- /dev/null +++ b/crates/nu-command/src/formats/from/ods.rs @@ -0,0 +1,210 @@ +use calamine::*; +use indexmap::map::IndexMap; +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; +use std::io::Cursor; + +#[derive(Clone)] +pub struct FromOds; + +impl Command for FromOds { + fn name(&self) -> &str { + "from ods" + } + + fn signature(&self) -> Signature { + Signature::build("from ods") + .named( + "sheets", + SyntaxShape::List(Box::new(SyntaxShape::String)), + "Only convert specified sheets", + Some('s'), + ) + .category(Category::Formats) + } + + fn usage(&self) -> &str { + "Parse OpenDocument Spreadsheet(.ods) data and create table." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + + let sel_sheets = if let Some(Value::List { vals: columns, .. }) = + call.get_flag(engine_state, stack, "sheets")? + { + convert_columns(columns.as_slice(), call.head)? + } else { + vec![] + }; + + from_ods(input, head, sel_sheets) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Convert binary .ods data to a table", + example: "open test.txt | from ods", + result: None, + }, + Example { + description: "Convert binary .ods data to a table, specifying the tables", + example: "open test.txt | from ods -s [Spreadsheet1]", + result: None, + }, + ] + } +} + +fn convert_columns(columns: &[Value], span: Span) -> Result, ShellError> { + let res = columns + .iter() + .map(|value| match &value { + Value::String { val: s, .. } => Ok(s.clone()), + _ => Err(ShellError::IncompatibleParametersSingle( + "Incorrect column format, Only string as column name".to_string(), + value.span().unwrap_or(span), + )), + }) + .collect::, _>>()?; + + Ok(res) +} + +fn collect_binary(input: PipelineData, span: Span) -> Result, ShellError> { + let mut bytes = vec![]; + let mut values = input.into_iter(); + + loop { + match values.next() { + Some(Value::Binary { val: b, .. }) => { + bytes.extend_from_slice(&b); + } + Some(x) => { + return Err(ShellError::UnsupportedInput( + "Expected binary from pipeline".to_string(), + x.span().unwrap_or(span), + )) + } + None => break, + } + } + + Ok(bytes) +} + +fn from_ods( + input: PipelineData, + head: Span, + sel_sheets: Vec, +) -> Result { + let bytes = collect_binary(input, head)?; + let buf: Cursor> = Cursor::new(bytes); + let mut ods = Ods::<_>::new(buf) + .map_err(|_| ShellError::UnsupportedInput("Could not load ods file".to_string(), head))?; + + let mut dict = IndexMap::new(); + + let mut sheet_names = ods.sheet_names().to_owned(); + if !sel_sheets.is_empty() { + sheet_names.retain(|e| sel_sheets.contains(e)); + } + + for sheet_name in &sheet_names { + let mut sheet_output = vec![]; + + if let Some(Ok(current_sheet)) = ods.worksheet_range(sheet_name) { + for row in current_sheet.rows() { + let mut row_output = IndexMap::new(); + for (i, cell) in row.iter().enumerate() { + let value = match cell { + DataType::Empty => Value::nothing(head), + DataType::String(s) => Value::string(s, head), + DataType::Float(f) => Value::Float { + val: *f, + span: head, + }, + DataType::Int(i) => Value::Int { + val: *i, + span: head, + }, + DataType::Bool(b) => Value::Bool { + val: *b, + span: head, + }, + _ => Value::nothing(head), + }; + + row_output.insert(format!("Column{}", i), value); + } + + let (cols, vals) = + row_output + .into_iter() + .fold((vec![], vec![]), |mut acc, (k, v)| { + acc.0.push(k); + acc.1.push(v); + acc + }); + + let record = Value::Record { + cols, + vals, + span: head, + }; + + sheet_output.push(record); + } + + dict.insert( + sheet_name, + Value::List { + vals: sheet_output, + span: head, + }, + ); + } else { + return Err(ShellError::UnsupportedInput( + "Could not load sheet".to_string(), + head, + )); + } + } + + let (cols, vals) = dict.into_iter().fold((vec![], vec![]), |mut acc, (k, v)| { + acc.0.push(k.clone()); + acc.1.push(v); + acc + }); + + let record = Value::Record { + cols, + vals, + span: head, + }; + + Ok(PipelineData::Value(record, None)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(FromOds {}) + } +} diff --git a/crates/nu-command/src/formats/from/ssv.rs b/crates/nu-command/src/formats/from/ssv.rs new file mode 100644 index 0000000000..d2cadf1b66 --- /dev/null +++ b/crates/nu-command/src/formats/from/ssv.rs @@ -0,0 +1,500 @@ +use indexmap::map::IndexMap; +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Spanned, + SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct FromSsv; + +const DEFAULT_MINIMUM_SPACES: usize = 2; + +impl Command for FromSsv { + fn name(&self) -> &str { + "from ssv" + } + + fn signature(&self) -> Signature { + Signature::build("from ssv") + .switch( + "noheaders", + "don't treat the first row as column names", + Some('n'), + ) + .switch("aligned-columns", "assume columns are aligned", Some('a')) + .named( + "minimum-spaces", + SyntaxShape::Int, + "the minimum spaces to separate columns", + Some('m'), + ) + .category(Category::Formats) + } + + fn usage(&self) -> &str { + "Parse text as space-separated values and create a table. The default minimum number of spaces counted as a separator is 2." + } + + fn examples(&self) -> Vec { + vec![Example { + example: r#"'FOO BAR +1 2' | from ssv"#, + description: "Converts ssv formatted string to table", + result: Some(Value::List { vals: vec![Value::Record { cols: vec!["FOO".to_string(), "BAR".to_string()], vals: vec![Value::String { val: "1".to_string(), span: Span::test_data() }, Value::String { val: "2".to_string(), span: Span::test_data() }], span: Span::test_data() }], span: Span::test_data() }), + }, Example { + example: r#"'FOO BAR +1 2' | from ssv -n"#, + description: "Converts ssv formatted string to table but not treating the first row as column names", + result: Some( + Value::List { vals: vec![Value::Record { cols: vec!["Column1".to_string(), "Column2".to_string()], vals: vec![Value::String { val: "FOO".to_string(), span: Span::test_data() }, Value::String { val: "BAR".to_string(), span: Span::test_data() }], span: Span::test_data() }, Value::Record { cols: vec!["Column1".to_string(), "Column2".to_string()], vals: vec![Value::String { val: "1".to_string(), span: Span::test_data() }, Value::String { val: "2".to_string(), span: Span::test_data() }], span: Span::test_data() }], span: Span::test_data() }), + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + from_ssv(engine_state, stack, call, input) + } +} + +enum HeaderOptions<'a> { + WithHeaders(&'a str), + WithoutHeaders, +} + +fn parse_aligned_columns<'a>( + lines: impl Iterator, + headers: HeaderOptions, + separator: &str, +) -> Vec> { + fn construct<'a>( + lines: impl Iterator, + headers: Vec<(String, usize)>, + ) -> Vec> { + lines + .map(|l| { + headers + .iter() + .enumerate() + .map(|(i, (header_name, start_position))| { + let val = match headers.get(i + 1) { + Some((_, end)) => { + if *end < l.len() { + l.get(*start_position..*end) + } else { + l.get(*start_position..) + } + } + None => l.get(*start_position..), + } + .unwrap_or("") + .trim() + .into(); + (header_name.clone(), val) + }) + .collect() + }) + .collect() + } + + let find_indices = |line: &str| { + let values = line + .split(&separator) + .map(str::trim) + .filter(|s| !s.is_empty()); + values + .fold( + (0, vec![]), + |(current_pos, mut indices), value| match line[current_pos..].find(value) { + None => (current_pos, indices), + Some(index) => { + let absolute_index = current_pos + index; + indices.push(absolute_index); + (absolute_index + value.len(), indices) + } + }, + ) + .1 + }; + + let parse_with_headers = |lines, headers_raw: &str| { + let indices = find_indices(headers_raw); + let headers = headers_raw + .split(&separator) + .map(str::trim) + .filter(|s| !s.is_empty()) + .map(String::from) + .zip(indices); + + let columns = headers.collect::>(); + + construct(lines, columns) + }; + + let parse_without_headers = |ls: Vec<&str>| { + let mut indices = ls + .iter() + .flat_map(|s| find_indices(*s)) + .collect::>(); + + indices.sort_unstable(); + indices.dedup(); + + let headers: Vec<(String, usize)> = indices + .iter() + .enumerate() + .map(|(i, position)| (format!("Column{}", i + 1), *position)) + .collect(); + + construct(ls.iter().map(|s| s.to_owned()), headers) + }; + + match headers { + HeaderOptions::WithHeaders(headers_raw) => parse_with_headers(lines, headers_raw), + HeaderOptions::WithoutHeaders => parse_without_headers(lines.collect()), + } +} + +fn parse_separated_columns<'a>( + lines: impl Iterator, + headers: HeaderOptions, + separator: &str, +) -> Vec> { + fn collect<'a>( + headers: Vec, + rows: impl Iterator, + separator: &str, + ) -> Vec> { + rows.map(|r| { + headers + .iter() + .zip(r.split(separator).map(str::trim).filter(|s| !s.is_empty())) + .map(|(a, b)| (a.to_owned(), b.to_owned())) + .collect() + }) + .collect() + } + + let parse_with_headers = |lines, headers_raw: &str| { + let headers = headers_raw + .split(&separator) + .map(str::trim) + .map(str::to_owned) + .filter(|s| !s.is_empty()) + .collect(); + collect(headers, lines, separator) + }; + + let parse_without_headers = |ls: Vec<&str>| { + let num_columns = ls.iter().map(|r| r.len()).max().unwrap_or(0); + + let headers = (1..=num_columns) + .map(|i| format!("Column{}", i)) + .collect::>(); + collect(headers, ls.into_iter(), separator) + }; + + match headers { + HeaderOptions::WithHeaders(headers_raw) => parse_with_headers(lines, headers_raw), + HeaderOptions::WithoutHeaders => parse_without_headers(lines.collect()), + } +} + +fn string_to_table( + s: &str, + noheaders: bool, + aligned_columns: bool, + split_at: usize, +) -> Vec> { + let mut lines = s.lines().filter(|l| !l.trim().is_empty()); + let separator = " ".repeat(std::cmp::max(split_at, 1)); + + let (ls, header_options) = if noheaders { + (lines, HeaderOptions::WithoutHeaders) + } else { + match lines.next() { + Some(header) => (lines, HeaderOptions::WithHeaders(header)), + None => return vec![], + } + }; + + let f = if aligned_columns { + parse_aligned_columns + } else { + parse_separated_columns + }; + + f(ls, header_options, &separator) +} + +fn from_ssv_string_to_value( + s: &str, + noheaders: bool, + aligned_columns: bool, + split_at: usize, + span: Span, +) -> Value { + let rows = string_to_table(s, noheaders, aligned_columns, split_at) + .iter() + .map(|row| { + let mut dict = IndexMap::new(); + for (col, entry) in row { + dict.insert( + col.to_string(), + Value::String { + val: entry.to_string(), + span, + }, + ); + } + Value::from(Spanned { item: dict, span }) + }) + .collect(); + + Value::List { vals: rows, span } +} + +fn from_ssv( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let config = stack.get_config().unwrap_or_default(); + let name = call.head; + + let noheaders = call.has_flag("noheaders"); + let aligned_columns = call.has_flag("aligned-columns"); + let minimum_spaces: Option> = + call.get_flag(engine_state, stack, "minimum-spaces")?; + + let concat_string = input.collect_string("", &config)?; + let split_at = match minimum_spaces { + Some(number) => number.item, + None => DEFAULT_MINIMUM_SPACES, + }; + + Ok( + from_ssv_string_to_value(&concat_string, noheaders, aligned_columns, split_at, name) + .into_pipeline_data(), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn owned(x: &str, y: &str) -> (String, String) { + (String::from(x), String::from(y)) + } + + #[test] + fn it_trims_empty_and_whitespace_only_lines() { + let input = r#" + + a b + + 1 2 + + 3 4 + "#; + let result = string_to_table(input, false, true, 1); + assert_eq!( + result, + vec![ + vec![owned("a", "1"), owned("b", "2")], + vec![owned("a", "3"), owned("b", "4")] + ] + ); + } + + #[test] + fn it_deals_with_single_column_input() { + let input = r#" + a + 1 + 2 + "#; + let result = string_to_table(input, false, true, 1); + assert_eq!(result, vec![vec![owned("a", "1")], vec![owned("a", "2")]]); + } + + #[test] + fn it_uses_first_row_as_data_when_noheaders() { + let input = r#" + a b + 1 2 + 3 4 + "#; + let result = string_to_table(input, true, true, 1); + assert_eq!( + result, + vec![ + vec![owned("Column1", "a"), owned("Column2", "b")], + vec![owned("Column1", "1"), owned("Column2", "2")], + vec![owned("Column1", "3"), owned("Column2", "4")] + ] + ); + } + + #[test] + fn it_allows_a_predefined_number_of_spaces() { + let input = r#" + column a column b + entry 1 entry number 2 + 3 four + "#; + + let result = string_to_table(input, false, true, 3); + assert_eq!( + result, + vec![ + vec![ + owned("column a", "entry 1"), + owned("column b", "entry number 2") + ], + vec![owned("column a", "3"), owned("column b", "four")] + ] + ); + } + + #[test] + fn it_trims_remaining_separator_space() { + let input = r#" + colA colB colC + val1 val2 val3 + "#; + + let trimmed = |s: &str| s.trim() == s; + + let result = string_to_table(input, false, true, 2); + assert!(result + .iter() + .all(|row| row.iter().all(|(a, b)| trimmed(a) && trimmed(b)))); + } + + #[test] + fn it_keeps_empty_columns() { + let input = r#" + colA col B col C + val2 val3 + val4 val 5 val 6 + val7 val8 + "#; + + let result = string_to_table(input, false, true, 2); + assert_eq!( + result, + vec![ + vec![ + owned("colA", ""), + owned("col B", "val2"), + owned("col C", "val3") + ], + vec![ + owned("colA", "val4"), + owned("col B", "val 5"), + owned("col C", "val 6") + ], + vec![ + owned("colA", "val7"), + owned("col B", ""), + owned("col C", "val8") + ], + ] + ); + } + + #[test] + fn it_can_produce_an_empty_stream_for_header_only_input() { + let input = "colA col B"; + + let result = string_to_table(input, false, true, 2); + let expected: Vec> = vec![]; + assert_eq!(expected, result); + } + + #[test] + fn it_uses_the_full_final_column() { + let input = r#" + colA col B + val1 val2 trailing value that should be included + "#; + + let result = string_to_table(input, false, true, 2); + assert_eq!( + result, + vec![vec![ + owned("colA", "val1"), + owned("col B", "val2 trailing value that should be included"), + ]] + ); + } + + #[test] + fn it_handles_empty_values_when_noheaders_and_aligned_columns() { + let input = r#" + a multi-word value b d + 1 3-3 4 + last + "#; + + let result = string_to_table(input, true, true, 2); + assert_eq!( + result, + vec![ + vec![ + owned("Column1", "a multi-word value"), + owned("Column2", "b"), + owned("Column3", ""), + owned("Column4", "d"), + owned("Column5", "") + ], + vec![ + owned("Column1", "1"), + owned("Column2", ""), + owned("Column3", "3-3"), + owned("Column4", "4"), + owned("Column5", "") + ], + vec![ + owned("Column1", ""), + owned("Column2", ""), + owned("Column3", ""), + owned("Column4", ""), + owned("Column5", "last") + ], + ] + ); + } + + #[test] + fn input_is_parsed_correctly_if_either_option_works() { + let input = r#" + docker-registry docker-registry=default docker-registry=default 172.30.78.158 5000/TCP + kubernetes component=apiserver,provider=kubernetes 172.30.0.2 443/TCP + kubernetes-ro component=apiserver,provider=kubernetes 172.30.0.1 80/TCP + "#; + + let aligned_columns_noheaders = string_to_table(input, true, true, 2); + let separator_noheaders = string_to_table(input, true, false, 2); + let aligned_columns_with_headers = string_to_table(input, false, true, 2); + let separator_with_headers = string_to_table(input, false, false, 2); + assert_eq!(aligned_columns_noheaders, separator_noheaders); + assert_eq!(aligned_columns_with_headers, separator_with_headers); + } + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(FromSsv {}) + } +} diff --git a/crates/nu-command/src/formats/from/toml.rs b/crates/nu-command/src/formats/from/toml.rs new file mode 100644 index 0000000000..477b2c3ec1 --- /dev/null +++ b/crates/nu-command/src/formats/from/toml.rs @@ -0,0 +1,141 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Value, +}; + +#[derive(Clone)] +pub struct FromToml; + +impl Command for FromToml { + fn name(&self) -> &str { + "from toml" + } + + fn signature(&self) -> Signature { + Signature::build("from toml").category(Category::Formats) + } + + fn usage(&self) -> &str { + "Parse text as .toml and create table." + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "'a = 1' | from toml", + description: "Converts toml formatted string to table", + result: Some(Value::Record { + cols: vec!["a".to_string()], + vals: vec![Value::Int { + val: 1, + span: Span::test_data(), + }], + span: Span::test_data(), + }), + }, + Example { + example: "'a = 1 +b = [1, 2]' | from toml", + description: "Converts toml formatted string to table", + result: Some(Value::Record { + cols: vec!["a".to_string(), "b".to_string()], + vals: vec![ + Value::Int { + val: 1, + span: Span::test_data(), + }, + Value::List { + vals: vec![ + Value::Int { + val: 1, + span: Span::test_data(), + }, + Value::Int { + val: 2, + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }, + ] + } + + fn run( + &self, + _engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let span = call.head; + let config = stack.get_config().unwrap_or_default(); + let mut string_input = input.collect_string("", &config)?; + string_input.push('\n'); + Ok(convert_string_to_value(string_input, span)?.into_pipeline_data()) + } +} + +fn convert_toml_to_value(value: &toml::Value, span: Span) -> Value { + match value { + toml::Value::Array(array) => { + let v: Vec = array + .iter() + .map(|x| convert_toml_to_value(x, span)) + .collect(); + + Value::List { vals: v, span } + } + toml::Value::Boolean(b) => Value::Bool { val: *b, span }, + toml::Value::Float(f) => Value::Float { val: *f, span }, + toml::Value::Integer(i) => Value::Int { val: *i, span }, + toml::Value::Table(k) => { + let mut cols = vec![]; + let mut vals = vec![]; + + for item in k { + cols.push(item.0.clone()); + vals.push(convert_toml_to_value(item.1, span)); + } + + Value::Record { cols, vals, span } + } + toml::Value::String(s) => Value::String { + val: s.clone(), + span, + }, + toml::Value::Datetime(d) => Value::String { + val: d.to_string(), + span, + }, + } +} + +pub fn convert_string_to_value(string_input: String, span: Span) -> Result { + let result: Result = toml::from_str(&string_input); + match result { + Ok(value) => Ok(convert_toml_to_value(&value, span)), + + Err(_x) => Err(ShellError::CantConvert( + "structured data from toml".into(), + "string".into(), + span, + )), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(FromToml {}) + } +} diff --git a/crates/nu-command/src/formats/from/tsv.rs b/crates/nu-command/src/formats/from/tsv.rs new file mode 100644 index 0000000000..991c58a312 --- /dev/null +++ b/crates/nu-command/src/formats/from/tsv.rs @@ -0,0 +1,59 @@ +use super::delimited::from_delimited_data; + +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Config, PipelineData, ShellError, Signature}; + +#[derive(Clone)] +pub struct FromTsv; + +impl Command for FromTsv { + fn name(&self) -> &str { + "from tsv" + } + + fn signature(&self) -> Signature { + Signature::build("from tsv") + .switch( + "noheaders", + "don't treat the first row as column names", + Some('n'), + ) + .category(Category::Formats) + } + + fn usage(&self) -> &str { + "Parse text as .tsv and create table." + } + + fn run( + &self, + _engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let config = stack.get_config().unwrap_or_default(); + from_tsv(call, input, &config) + } +} + +fn from_tsv(call: &Call, input: PipelineData, config: &Config) -> Result { + let name = call.head; + + let noheaders = call.has_flag("noheaders"); + + from_delimited_data(noheaders, '\t', input, name, config) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(FromTsv {}) + } +} diff --git a/crates/nu-command/src/formats/from/url.rs b/crates/nu-command/src/formats/from/url.rs new file mode 100644 index 0000000000..1ee91d4296 --- /dev/null +++ b/crates/nu-command/src/formats/from/url.rs @@ -0,0 +1,96 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Config, Example, PipelineData, ShellError, Signature, Span, Value}; + +#[derive(Clone)] +pub struct FromUrl; + +impl Command for FromUrl { + fn name(&self) -> &str { + "from url" + } + + fn signature(&self) -> Signature { + Signature::build("from url").category(Category::Formats) + } + + fn usage(&self) -> &str { + "Parse url-encoded string as a table." + } + + fn run( + &self, + _engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let config = stack.get_config().unwrap_or_default(); + from_url(input, head, &config) + } + + fn examples(&self) -> Vec { + vec![Example { + example: "'bread=baguette&cheese=comt%C3%A9&meat=ham&fat=butter' | from url", + description: "Convert url encoded string into a table", + result: Some(Value::Record { + cols: vec![ + "bread".to_string(), + "cheese".to_string(), + "meat".to_string(), + "fat".to_string(), + ], + vals: vec![ + Value::test_string("baguette"), + Value::test_string("comté"), + Value::test_string("ham"), + Value::test_string("butter"), + ], + span: Span::test_data(), + }), + }] + } +} + +fn from_url(input: PipelineData, head: Span, config: &Config) -> Result { + let concat_string = input.collect_string("", config)?; + + let result = serde_urlencoded::from_str::>(&concat_string); + + match result { + Ok(result) => { + let mut cols = vec![]; + let mut vals = vec![]; + for (k, v) in result { + cols.push(k); + vals.push(Value::String { val: v, span: head }) + } + + Ok(PipelineData::Value( + Value::Record { + cols, + vals, + span: head, + }, + None, + )) + } + _ => Err(ShellError::UnsupportedInput( + "String not compatible with url-encoding".to_string(), + head, + )), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(FromUrl {}) + } +} diff --git a/crates/nu-command/src/formats/from/vcf.rs b/crates/nu-command/src/formats/from/vcf.rs new file mode 100644 index 0000000000..4b98535474 --- /dev/null +++ b/crates/nu-command/src/formats/from/vcf.rs @@ -0,0 +1,221 @@ +use ical::parser::vcard::component::*; +use ical::property::Property; +use indexmap::map::IndexMap; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Config, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, + Spanned, Value, +}; + +#[derive(Clone)] +pub struct FromVcf; + +impl Command for FromVcf { + fn name(&self) -> &str { + "from vcf" + } + + fn signature(&self) -> Signature { + Signature::build("from vcf").category(Category::Formats) + } + + fn usage(&self) -> &str { + "Parse text as .vcf and create table." + } + + fn run( + &self, + _engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let config = stack.get_config().unwrap_or_default(); + from_vcf(input, head, &config) + } + + fn examples(&self) -> Vec { + vec![Example { + example: "'BEGIN:VCARD +N:Foo +FN:Bar +EMAIL:foo@bar.com +END:VCARD' | from vcf", + description: "Converts ics formatted string to table", + result: Some(Value::List { + vals: vec![Value::Record { + cols: vec!["properties".to_string()], + vals: vec![Value::List { + vals: vec![ + Value::Record { + cols: vec![ + "name".to_string(), + "value".to_string(), + "params".to_string(), + ], + vals: vec![ + Value::String { + val: "N".to_string(), + span: Span::test_data(), + }, + Value::String { + val: "Foo".to_string(), + span: Span::test_data(), + }, + Value::Nothing { + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }, + Value::Record { + cols: vec![ + "name".to_string(), + "value".to_string(), + "params".to_string(), + ], + vals: vec![ + Value::String { + val: "FN".to_string(), + span: Span::test_data(), + }, + Value::String { + val: "Bar".to_string(), + span: Span::test_data(), + }, + Value::Nothing { + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }, + Value::Record { + cols: vec![ + "name".to_string(), + "value".to_string(), + "params".to_string(), + ], + vals: vec![ + Value::String { + val: "EMAIL".to_string(), + span: Span::test_data(), + }, + Value::String { + val: "foo@bar.com".to_string(), + span: Span::test_data(), + }, + Value::Nothing { + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }], + span: Span::test_data(), + }], + span: Span::test_data(), + }), + }] + } +} + +fn from_vcf(input: PipelineData, head: Span, config: &Config) -> Result { + let input_string = input.collect_string("", config)?; + + let input_string = input_string + .lines() + .map(|x| x.trim().to_string()) + .collect::>() + .join("\n"); + + let input_bytes = input_string.as_bytes(); + let cursor = std::io::Cursor::new(input_bytes); + let parser = ical::VcardParser::new(cursor); + + let iter = parser.map(move |contact| match contact { + Ok(c) => contact_to_value(c, head), + Err(e) => Value::Error { + error: ShellError::UnsupportedInput( + format!("input cannot be parsed as .vcf ({})", e), + head, + ), + }, + }); + + let collected: Vec<_> = iter.collect(); + Ok(Value::List { + vals: collected, + span: head, + } + .into_pipeline_data()) +} + +fn contact_to_value(contact: VcardContact, span: Span) -> Value { + let mut row = IndexMap::new(); + row.insert( + "properties".to_string(), + properties_to_value(contact.properties, span), + ); + Value::from(Spanned { item: row, span }) +} + +fn properties_to_value(properties: Vec, span: Span) -> Value { + Value::List { + vals: properties + .into_iter() + .map(|prop| { + let mut row = IndexMap::new(); + + let name = Value::String { + val: prop.name, + span, + }; + let value = match prop.value { + Some(val) => Value::String { val, span }, + None => Value::Nothing { span }, + }; + let params = match prop.params { + Some(param_list) => params_to_value(param_list, span), + None => Value::Nothing { span }, + }; + + row.insert("name".to_string(), name); + row.insert("value".to_string(), value); + row.insert("params".to_string(), params); + Value::from(Spanned { item: row, span }) + }) + .collect::>(), + span, + } +} + +fn params_to_value(params: Vec<(String, Vec)>, span: Span) -> Value { + let mut row = IndexMap::new(); + + for (param_name, param_values) in params { + let values: Vec = param_values + .into_iter() + .map(|val| Value::string(val, span)) + .collect(); + let values = Value::List { vals: values, span }; + row.insert(param_name, values); + } + + Value::from(Spanned { item: row, span }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(FromVcf {}) + } +} diff --git a/crates/nu-command/src/formats/from/xlsx.rs b/crates/nu-command/src/formats/from/xlsx.rs new file mode 100644 index 0000000000..3aa98655c2 --- /dev/null +++ b/crates/nu-command/src/formats/from/xlsx.rs @@ -0,0 +1,210 @@ +use calamine::*; +use indexmap::map::IndexMap; +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; +use std::io::Cursor; + +#[derive(Clone)] +pub struct FromXlsx; + +impl Command for FromXlsx { + fn name(&self) -> &str { + "from xlsx" + } + + fn signature(&self) -> Signature { + Signature::build("from xlsx") + .named( + "sheets", + SyntaxShape::List(Box::new(SyntaxShape::String)), + "Only convert specified sheets", + Some('s'), + ) + .category(Category::Formats) + } + + fn usage(&self) -> &str { + "Parse binary Excel(.xlsx) data and create table." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + + let sel_sheets = if let Some(Value::List { vals: columns, .. }) = + call.get_flag(engine_state, stack, "sheets")? + { + convert_columns(columns.as_slice(), call.head)? + } else { + vec![] + }; + + from_xlsx(input, head, sel_sheets) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Convert binary .xlsx data to a table", + example: "open test.txt | from xlsx", + result: None, + }, + Example { + description: "Convert binary .xlsx data to a table, specifying the tables", + example: "open test.txt | from xlsx -s [Spreadsheet1]", + result: None, + }, + ] + } +} + +fn convert_columns(columns: &[Value], span: Span) -> Result, ShellError> { + let res = columns + .iter() + .map(|value| match &value { + Value::String { val: s, .. } => Ok(s.clone()), + _ => Err(ShellError::IncompatibleParametersSingle( + "Incorrect column format, Only string as column name".to_string(), + value.span().unwrap_or(span), + )), + }) + .collect::, _>>()?; + + Ok(res) +} + +fn collect_binary(input: PipelineData, span: Span) -> Result, ShellError> { + let mut bytes = vec![]; + let mut values = input.into_iter(); + + loop { + match values.next() { + Some(Value::Binary { val: b, .. }) => { + bytes.extend_from_slice(&b); + } + Some(x) => { + return Err(ShellError::UnsupportedInput( + "Expected binary from pipeline".to_string(), + x.span().unwrap_or(span), + )) + } + None => break, + } + } + + Ok(bytes) +} + +fn from_xlsx( + input: PipelineData, + head: Span, + sel_sheets: Vec, +) -> Result { + let bytes = collect_binary(input, head)?; + let buf: Cursor> = Cursor::new(bytes); + let mut xlsx = Xlsx::<_>::new(buf) + .map_err(|_| ShellError::UnsupportedInput("Could not load xlsx file".to_string(), head))?; + + let mut dict = IndexMap::new(); + + let mut sheet_names = xlsx.sheet_names().to_owned(); + if !sel_sheets.is_empty() { + sheet_names.retain(|e| sel_sheets.contains(e)); + } + + for sheet_name in &sheet_names { + let mut sheet_output = vec![]; + + if let Some(Ok(current_sheet)) = xlsx.worksheet_range(sheet_name) { + for row in current_sheet.rows() { + let mut row_output = IndexMap::new(); + for (i, cell) in row.iter().enumerate() { + let value = match cell { + DataType::Empty => Value::nothing(head), + DataType::String(s) => Value::string(s, head), + DataType::Float(f) => Value::Float { + val: *f, + span: head, + }, + DataType::Int(i) => Value::Int { + val: *i, + span: head, + }, + DataType::Bool(b) => Value::Bool { + val: *b, + span: head, + }, + _ => Value::nothing(head), + }; + + row_output.insert(format!("Column{}", i), value); + } + + let (cols, vals) = + row_output + .into_iter() + .fold((vec![], vec![]), |mut acc, (k, v)| { + acc.0.push(k); + acc.1.push(v); + acc + }); + + let record = Value::Record { + cols, + vals, + span: head, + }; + + sheet_output.push(record); + } + + dict.insert( + sheet_name, + Value::List { + vals: sheet_output, + span: head, + }, + ); + } else { + return Err(ShellError::UnsupportedInput( + "Could not load sheet".to_string(), + head, + )); + } + } + + let (cols, vals) = dict.into_iter().fold((vec![], vec![]), |mut acc, (k, v)| { + acc.0.push(k.clone()); + acc.1.push(v); + acc + }); + + let record = Value::Record { + cols, + vals, + span: head, + }; + + Ok(PipelineData::Value(record, None)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(FromXlsx {}) + } +} diff --git a/crates/nu-command/src/formats/from/xml.rs b/crates/nu-command/src/formats/from/xml.rs new file mode 100644 index 0000000000..bf6756cc82 --- /dev/null +++ b/crates/nu-command/src/formats/from/xml.rs @@ -0,0 +1,379 @@ +use indexmap::map::IndexMap; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Config, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, + Spanned, Value, +}; + +#[derive(Clone)] +pub struct FromXml; + +impl Command for FromXml { + fn name(&self) -> &str { + "from xml" + } + + fn signature(&self) -> Signature { + Signature::build("from xml").category(Category::Formats) + } + + fn usage(&self) -> &str { + "Parse text as .xml and create table." + } + + fn run( + &self, + _engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let config = stack.get_config().unwrap_or_default(); + from_xml(input, head, &config) + } + + fn examples(&self) -> Vec { + vec![Example { + example: r#"' + + Event +' | from xml"#, + description: "Converts xml formatted string to table", + result: Some(Value::Record { + cols: vec!["note".to_string()], + vals: vec![Value::Record { + cols: vec!["children".to_string(), "attributes".to_string()], + vals: vec![ + Value::List { + vals: vec![Value::Record { + cols: vec!["remember".to_string()], + vals: vec![Value::Record { + cols: vec!["children".to_string(), "attributes".to_string()], + vals: vec![ + Value::List { + vals: vec![Value::String { + val: "Event".to_string(), + span: Span::test_data(), + }], + span: Span::test_data(), + }, + Value::Record { + cols: vec![], + vals: vec![], + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }], + span: Span::test_data(), + }], + span: Span::test_data(), + }, + Value::Record { + cols: vec![], + vals: vec![], + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }], + span: Span::test_data(), + }), + }] + } +} + +fn from_attributes_to_value(attributes: &[roxmltree::Attribute], span: Span) -> Value { + let mut collected = IndexMap::new(); + for a in attributes { + collected.insert(String::from(a.name()), Value::string(a.value(), span)); + } + + let (cols, vals) = collected + .into_iter() + .fold((vec![], vec![]), |mut acc, (k, v)| { + acc.0.push(k); + acc.1.push(v); + acc + }); + + Value::Record { cols, vals, span } +} + +fn from_node_to_value(n: &roxmltree::Node, span: Span) -> Value { + if n.is_element() { + let name = n.tag_name().name().trim().to_string(); + + let mut children_values = vec![]; + for c in n.children() { + children_values.push(from_node_to_value(&c, span)); + } + + let children_values: Vec = children_values + .into_iter() + .filter(|x| match x { + Value::String { val: f, .. } => { + !f.trim().is_empty() // non-whitespace characters? + } + _ => true, + }) + .collect(); + + let mut collected = IndexMap::new(); + + let attribute_value: Value = from_attributes_to_value(n.attributes(), span); + + let mut row = IndexMap::new(); + row.insert( + String::from("children"), + Value::List { + vals: children_values, + span, + }, + ); + row.insert(String::from("attributes"), attribute_value); + collected.insert(name, Value::from(Spanned { item: row, span })); + + Value::from(Spanned { + item: collected, + span, + }) + } else if n.is_comment() { + Value::String { + val: "".to_string(), + span, + } + } else if n.is_pi() { + Value::String { + val: "".to_string(), + span, + } + } else if n.is_text() { + match n.text() { + Some(text) => Value::String { + val: text.to_string(), + span, + }, + None => Value::String { + val: "".to_string(), + span, + }, + } + } else { + Value::String { + val: "".to_string(), + span, + } + } +} + +fn from_document_to_value(d: &roxmltree::Document, span: Span) -> Value { + from_node_to_value(&d.root_element(), span) +} + +pub fn from_xml_string_to_value(s: String, span: Span) -> Result { + let parsed = roxmltree::Document::parse(&s)?; + Ok(from_document_to_value(&parsed, span)) +} + +fn from_xml(input: PipelineData, head: Span, config: &Config) -> Result { + let concat_string = input.collect_string("", config)?; + + match from_xml_string_to_value(concat_string, head) { + Ok(x) => Ok(x.into_pipeline_data()), + _ => Err(ShellError::UnsupportedInput( + "Could not parse string as xml".to_string(), + head, + )), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use indexmap::indexmap; + use indexmap::IndexMap; + use nu_protocol::{Spanned, Value}; + + fn string(input: impl Into) -> Value { + Value::String { + val: input.into(), + span: Span::test_data(), + } + } + + fn row(entries: IndexMap) -> Value { + Value::from(Spanned { + item: entries, + span: Span::test_data(), + }) + } + + fn table(list: &[Value]) -> Value { + Value::List { + vals: list.to_vec(), + span: Span::test_data(), + } + } + + fn parse(xml: &str) -> Result { + from_xml_string_to_value(xml.to_string(), Span::test_data()) + } + + #[test] + fn parses_empty_element() -> Result<(), roxmltree::Error> { + let source = ""; + + assert_eq!( + parse(source)?, + row(indexmap! { + "nu".into() => row(indexmap! { + "children".into() => table(&[]), + "attributes".into() => row(indexmap! {}) + }) + }) + ); + + Ok(()) + } + + #[test] + fn parses_element_with_text() -> Result<(), roxmltree::Error> { + let source = "La era de los tres caballeros"; + + assert_eq!( + parse(source)?, + row(indexmap! { + "nu".into() => row(indexmap! { + "children".into() => table(&[string("La era de los tres caballeros")]), + "attributes".into() => row(indexmap! {}) + }) + }) + ); + + Ok(()) + } + + #[test] + fn parses_element_with_elements() -> Result<(), roxmltree::Error> { + let source = "\ + + Andrés + Jonathan + Yehuda +"; + + assert_eq!( + parse(source)?, + row(indexmap! { + "nu".into() => row(indexmap! { + "children".into() => table(&[ + row(indexmap! { + "dev".into() => row(indexmap! { + "children".into() => table(&[string("Andrés")]), + "attributes".into() => row(indexmap! {}) + }) + }), + row(indexmap! { + "dev".into() => row(indexmap! { + "children".into() => table(&[string("Jonathan")]), + "attributes".into() => row(indexmap! {}) + }) + }), + row(indexmap! { + "dev".into() => row(indexmap! { + "children".into() => table(&[string("Yehuda")]), + "attributes".into() => row(indexmap! {}) + }) + }) + ]), + "attributes".into() => row(indexmap! {}) + }) + }) + ); + + Ok(()) + } + + #[test] + fn parses_element_with_attribute() -> Result<(), roxmltree::Error> { + let source = "\ + +"; + + assert_eq!( + parse(source)?, + row(indexmap! { + "nu".into() => row(indexmap! { + "children".into() => table(&[]), + "attributes".into() => row(indexmap! { + "version".into() => string("2.0") + }) + }) + }) + ); + + Ok(()) + } + + #[test] + fn parses_element_with_attribute_and_element() -> Result<(), roxmltree::Error> { + let source = "\ + + 2.0 +"; + + assert_eq!( + parse(source)?, + row(indexmap! { + "nu".into() => row(indexmap! { + "children".into() => table(&[ + row(indexmap! { + "version".into() => row(indexmap! { + "children".into() => table(&[string("2.0")]), + "attributes".into() => row(indexmap! {}) + }) + }) + ]), + "attributes".into() => row(indexmap! { + "version".into() => string("2.0") + }) + }) + }) + ); + + Ok(()) + } + + #[test] + fn parses_element_with_multiple_attributes() -> Result<(), roxmltree::Error> { + let source = "\ + +"; + + assert_eq!( + parse(source)?, + row(indexmap! { + "nu".into() => row(indexmap! { + "children".into() => table(&[]), + "attributes".into() => row(indexmap! { + "version".into() => string("2.0"), + "age".into() => string("25") + }) + }) + }) + ); + + Ok(()) + } + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(FromXml {}) + } +} diff --git a/crates/nu-command/src/formats/from/yaml.rs b/crates/nu-command/src/formats/from/yaml.rs new file mode 100644 index 0000000000..64a6d8deda --- /dev/null +++ b/crates/nu-command/src/formats/from/yaml.rs @@ -0,0 +1,279 @@ +use itertools::Itertools; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Config, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, + Spanned, Value, +}; +use serde::de::Deserialize; +use std::collections::HashMap; + +#[derive(Clone)] +pub struct FromYaml; + +impl Command for FromYaml { + fn name(&self) -> &str { + "from yaml" + } + + fn signature(&self) -> Signature { + Signature::build("from yaml").category(Category::Formats) + } + + fn usage(&self) -> &str { + "Parse text as .yaml/.yml and create table." + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "'a: 1' | from yaml", + description: "Converts yaml formatted string to table", + result: Some(Value::Record { + cols: vec!["a".to_string()], + vals: vec![Value::Int { + val: 1, + span: Span::test_data(), + }], + span: Span::test_data(), + }), + }, + Example { + example: "'[ a: 1, b: [1, 2] ]' | from yaml", + description: "Converts yaml formatted string to table", + result: Some(Value::List { + vals: vec![ + Value::Record { + cols: vec!["a".to_string()], + vals: vec![Value::test_int(1)], + span: Span::test_data(), + }, + Value::Record { + cols: vec!["b".to_string()], + vals: vec![Value::List { + vals: vec![Value::test_int(1), Value::test_int(2)], + span: Span::test_data(), + }], + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }, + ] + } + + fn run( + &self, + _engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let config = stack.get_config().unwrap_or_default(); + from_yaml(input, head, &config) + } +} + +#[derive(Clone)] +pub struct FromYml; + +impl Command for FromYml { + fn name(&self) -> &str { + "from yml" + } + + fn signature(&self) -> Signature { + Signature::build("from yml").category(Category::Formats) + } + + fn usage(&self) -> &str { + "Parse text as .yaml/.yml and create table." + } + + fn run( + &self, + _engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let config = stack.get_config().unwrap_or_default(); + from_yaml(input, head, &config) + } +} + +fn convert_yaml_value_to_nu_value(v: &serde_yaml::Value, span: Span) -> Result { + let err_not_compatible_number = + ShellError::UnsupportedInput("Expected a compatible number".to_string(), span); + Ok(match v { + serde_yaml::Value::Bool(b) => Value::Bool { val: *b, span }, + serde_yaml::Value::Number(n) if n.is_i64() => Value::Int { + val: n.as_i64().ok_or(err_not_compatible_number)?, + span, + }, + serde_yaml::Value::Number(n) if n.is_f64() => Value::Float { + val: n.as_f64().ok_or(err_not_compatible_number)?, + span, + }, + serde_yaml::Value::String(s) => Value::String { + val: s.to_string(), + span, + }, + serde_yaml::Value::Sequence(a) => { + let result: Result, ShellError> = a + .iter() + .map(|x| convert_yaml_value_to_nu_value(x, span)) + .collect(); + Value::List { + vals: result?, + span, + } + } + serde_yaml::Value::Mapping(t) => { + let mut collected = Spanned { + item: HashMap::new(), + span, + }; + + for (k, v) in t { + // A ShellError that we re-use multiple times in the Mapping scenario + let err_unexpected_map = ShellError::UnsupportedInput( + format!("Unexpected YAML:\nKey: {:?}\nValue: {:?}", k, v), + span, + ); + match (k, v) { + (serde_yaml::Value::String(k), _) => { + collected + .item + .insert(k.clone(), convert_yaml_value_to_nu_value(v, span)?); + } + // 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(Value::String { + val: "{{ ".to_owned() + s + " }}", + span, + }) + } + _ => None, + }) + .ok_or(err_unexpected_map); + } + (_, _) => { + return Err(err_unexpected_map); + } + } + } + + Value::from(collected) + } + serde_yaml::Value::Null => Value::nothing(span), + x => unimplemented!("Unsupported yaml case: {:?}", x), + }) +} + +pub fn from_yaml_string_to_value(s: String, span: Span) -> Result { + let mut documents = vec![]; + + for document in serde_yaml::Deserializer::from_str(&s) { + let v: serde_yaml::Value = serde_yaml::Value::deserialize(document).map_err(|x| { + ShellError::UnsupportedInput(format!("Could not load yaml: {}", x), span) + })?; + documents.push(convert_yaml_value_to_nu_value(&v, span)?); + } + + match documents.len() { + 0 => Ok(Value::nothing(span)), + 1 => Ok(documents.remove(0)), + _ => Ok(Value::List { + vals: documents, + span, + }), + } +} + +fn from_yaml(input: PipelineData, head: Span, config: &Config) -> Result { + let concat_string = input.collect_string("", config)?; + + match from_yaml_string_to_value(concat_string, head) { + Ok(x) => Ok(x.into_pipeline_data()), + Err(other) => Err(other), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[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(Value::Record { + cols: vec!["value".to_string()], + vals: vec![Value::String { + val: "{{ something }}".to_string(), + span: Span::test_data(), + }], + span: Span::test_data(), + }), + }, + TestCase { + description: "Double Curly Braces Without Quotes", + input: r#"value: {{ something }}"#, + expected: Ok(Value::Record { + cols: vec!["value".to_string()], + vals: vec![Value::String { + val: "{{ something }}".to_string(), + span: Span::test_data(), + }], + span: Span::test_data(), + }), + }, + ]; + let config = Config::default(); + for tc in tt { + let actual = from_yaml_string_to_value(tc.input.to_owned(), Span::test_data()); + if actual.is_err() { + assert!( + tc.expected.is_err(), + "actual is Err for test:\nTest Description {}\nErr: {:?}", + tc.description, + actual + ); + } else { + assert_eq!( + actual.unwrap().into_string("", &config), + tc.expected.unwrap().into_string("", &config) + ); + } + } + } + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(FromYaml {}) + } +} diff --git a/crates/nu-command/src/commands/formats/mod.rs b/crates/nu-command/src/formats/mod.rs similarity index 100% rename from crates/nu-command/src/commands/formats/mod.rs rename to crates/nu-command/src/formats/mod.rs diff --git a/crates/nu-command/src/formats/to/command.rs b/crates/nu-command/src/formats/to/command.rs new file mode 100644 index 0000000000..068d875a55 --- /dev/null +++ b/crates/nu-command/src/formats/to/command.rs @@ -0,0 +1,30 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, PipelineData, ShellError, Signature}; + +#[derive(Clone)] +pub struct To; + +impl Command for To { + fn name(&self) -> &str { + "to" + } + + fn usage(&self) -> &str { + "Translate structured data to a format" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("to").category(Category::Formats) + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::new(call.head)) + } +} diff --git a/crates/nu-command/src/formats/to/csv.rs b/crates/nu-command/src/formats/to/csv.rs new file mode 100644 index 0000000000..5a33ba1b74 --- /dev/null +++ b/crates/nu-command/src/formats/to/csv.rs @@ -0,0 +1,106 @@ +use crate::formats::to::delimited::to_delimited_data; +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Config, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, + Value, +}; + +#[derive(Clone)] +pub struct ToCsv; + +impl Command for ToCsv { + fn name(&self) -> &str { + "to csv" + } + + fn signature(&self) -> Signature { + Signature::build("to csv") + .named( + "separator", + SyntaxShape::String, + "a character to separate columns, defaults to ','", + Some('s'), + ) + .switch( + "noheaders", + "do not output the columns names as the first row", + Some('n'), + ) + .category(Category::Formats) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Outputs an CSV string representing the contents of this table", + example: "[[foo bar]; [1 2]] | to csv", + result: Some(Value::test_string("foo,bar\n1,2\n")), + }, + Example { + description: "Outputs an CSV string representing the contents of this table", + example: "[[foo bar]; [1 2]] | to csv -s ';' ", + result: Some(Value::test_string("foo;bar\n1;2\n")), + }, + ] + } + + fn usage(&self) -> &str { + "Convert table into .csv text " + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let noheaders = call.has_flag("noheaders"); + let separator: Option> = call.get_flag(engine_state, stack, "separator")?; + let config = stack.get_config().unwrap_or_default(); + to_csv(input, noheaders, separator, head, config) + } +} + +fn to_csv( + input: PipelineData, + noheaders: bool, + separator: Option>, + head: Span, + config: Config, +) -> Result { + let sep = match separator { + Some(Spanned { item: s, span, .. }) => { + if s == r"\t" { + '\t' + } else { + let vec_s: Vec = s.chars().collect(); + if vec_s.len() != 1 { + return Err(ShellError::UnsupportedInput( + "Expected a single separator char from --separator".to_string(), + span, + )); + }; + vec_s[0] + } + } + _ => ',', + }; + + to_delimited_data(noheaders, sep, "CSV", input, head, config) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(ToCsv {}) + } +} diff --git a/crates/nu-command/src/formats/to/delimited.rs b/crates/nu-command/src/formats/to/delimited.rs new file mode 100644 index 0000000000..ab54c3a2c1 --- /dev/null +++ b/crates/nu-command/src/formats/to/delimited.rs @@ -0,0 +1,148 @@ +use csv::WriterBuilder; +use indexmap::{indexset, IndexSet}; +use nu_protocol::{Config, IntoPipelineData, PipelineData, ShellError, Span, Value}; +use std::collections::VecDeque; + +fn from_value_to_delimited_string( + value: &Value, + separator: char, + config: &Config, + head: Span, +) -> Result { + match value { + Value::Record { cols, vals, span } => { + let mut wtr = WriterBuilder::new() + .delimiter(separator as u8) + .from_writer(vec![]); + let mut fields: VecDeque = VecDeque::new(); + let mut values: VecDeque = VecDeque::new(); + + for (k, v) in cols.iter().zip(vals.iter()) { + fields.push_back(k.clone()); + + values.push_back(to_string_tagged_value(v, config, *span)?); + } + + wtr.write_record(fields).expect("can not write."); + wtr.write_record(values).expect("can not write."); + + let v = String::from_utf8(wtr.into_inner().map_err(|_| { + ShellError::UnsupportedInput("Could not convert record".to_string(), *span) + })?) + .map_err(|_| { + ShellError::UnsupportedInput("Could not convert record".to_string(), *span) + })?; + Ok(v) + } + Value::List { vals, span } => { + let mut wtr = WriterBuilder::new() + .delimiter(separator as u8) + .from_writer(vec![]); + + let merged_descriptors = merge_descriptors(vals); + + if merged_descriptors.is_empty() { + wtr.write_record( + vals.iter() + .map(|ele| { + to_string_tagged_value(ele, config, *span) + .unwrap_or_else(|_| String::new()) + }) + .collect::>(), + ) + .expect("can not write"); + } else { + wtr.write_record(merged_descriptors.iter().map(|item| &item[..])) + .expect("can not write."); + + for l in vals { + let mut row = vec![]; + for desc in &merged_descriptors { + row.push(match l.to_owned().get_data_by_key(desc) { + Some(s) => to_string_tagged_value(&s, config, *span)?, + None => String::new(), + }); + } + wtr.write_record(&row).expect("can not write"); + } + } + let v = String::from_utf8(wtr.into_inner().map_err(|_| { + ShellError::UnsupportedInput("Could not convert record".to_string(), *span) + })?) + .map_err(|_| { + ShellError::UnsupportedInput("Could not convert record".to_string(), *span) + })?; + Ok(v) + } + _ => to_string_tagged_value(value, config, head), + } +} + +fn to_string_tagged_value(v: &Value, config: &Config, span: Span) -> Result { + match &v { + Value::String { .. } + | Value::Bool { .. } + | Value::Int { .. } + | Value::Duration { .. } + | Value::Binary { .. } + | Value::CustomValue { .. } + | Value::Error { .. } + | Value::Filesize { .. } + | Value::CellPath { .. } + | Value::List { .. } + | Value::Record { .. } + | Value::Float { .. } => Ok(v.clone().into_abbreviated_string(config)), + Value::Date { val, .. } => Ok(val.to_string()), + Value::Nothing { .. } => Ok(String::new()), + _ => Err(ShellError::UnsupportedInput( + "Unexpected value".to_string(), + v.span().unwrap_or(span), + )), + } +} + +pub fn merge_descriptors(values: &[Value]) -> Vec { + let mut ret: Vec = vec![]; + let mut seen: IndexSet = indexset! {}; + for value in values { + let data_descriptors = match value { + Value::Record { cols, .. } => cols.to_owned(), + _ => vec!["".to_string()], + }; + for desc in data_descriptors { + if !seen.contains(&desc) { + seen.insert(desc.to_string()); + ret.push(desc.to_string()); + } + } + } + ret +} + +pub fn to_delimited_data( + noheaders: bool, + sep: char, + format_name: &'static str, + input: PipelineData, + span: Span, + config: Config, +) -> Result { + let value = input.into_value(span); + let output = match from_value_to_delimited_string(&value, sep, &config, span) { + Ok(mut x) => { + if noheaders { + if let Some(second_line) = x.find('\n') { + let start = second_line + 1; + x.replace_range(0..start, ""); + } + } + Ok(x) + } + Err(_) => Err(ShellError::CantConvert( + format_name.into(), + value.get_type().to_string(), + value.span().unwrap_or(span), + )), + }?; + Ok(Value::string(output, span).into_pipeline_data()) +} diff --git a/crates/nu-command/src/formats/to/html.rs b/crates/nu-command/src/formats/to/html.rs new file mode 100644 index 0000000000..60fc130e2d --- /dev/null +++ b/crates/nu-command/src/formats/to/html.rs @@ -0,0 +1,723 @@ +use crate::formats::to::delimited::merge_descriptors; +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Config, Example, IntoPipelineData, PipelineData, ShellError, Signature, Spanned, + SyntaxShape, Value, +}; +use regex::Regex; +use rust_embed::RustEmbed; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::error::Error; +use std::fmt::Write; + +#[derive(Serialize, Deserialize, Debug)] +pub struct HtmlThemes { + themes: Vec, +} + +#[allow(non_snake_case)] +#[derive(Serialize, Deserialize, Debug)] +pub struct HtmlTheme { + name: String, + black: String, + red: String, + green: String, + yellow: String, + blue: String, + purple: String, + cyan: String, + white: String, + brightBlack: String, + brightRed: String, + brightGreen: String, + brightYellow: String, + brightBlue: String, + brightPurple: String, + brightCyan: String, + brightWhite: String, + background: String, + foreground: String, +} + +impl Default for HtmlThemes { + fn default() -> Self { + HtmlThemes { + themes: vec![HtmlTheme::default()], + } + } +} + +impl Default for HtmlTheme { + fn default() -> Self { + HtmlTheme { + name: "nu_default".to_string(), + black: "black".to_string(), + red: "red".to_string(), + green: "green".to_string(), + yellow: "#717100".to_string(), + blue: "blue".to_string(), + purple: "#c800c8".to_string(), + cyan: "#037979".to_string(), + white: "white".to_string(), + brightBlack: "black".to_string(), + brightRed: "red".to_string(), + brightGreen: "green".to_string(), + brightYellow: "#717100".to_string(), + brightBlue: "blue".to_string(), + brightPurple: "#c800c8".to_string(), + brightCyan: "#037979".to_string(), + brightWhite: "white".to_string(), + background: "white".to_string(), + foreground: "black".to_string(), + } + } +} + +#[derive(RustEmbed)] +#[folder = "assets/"] +struct Assets; + +#[derive(Clone)] +pub struct ToHtml; + +impl Command for ToHtml { + fn name(&self) -> &str { + "to html" + } + + fn signature(&self) -> Signature { + Signature::build("to html") + .switch("html_color", "change ansi colors to html colors", Some('c')) + .switch("no_color", "remove all ansi colors in output", Some('n')) + .switch( + "dark", + "indicate your background color is a darker color", + Some('d'), + ) + .switch( + "partial", + "only output the html for the content itself", + Some('p'), + ) + .named( + "theme", + SyntaxShape::String, + "the name of the theme to use (github, blulocolight, ...)", + Some('t'), + ) + .switch("list", "list the names of all available themes", Some('l')) + .category(Category::Formats) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Outputs an HTML string representing the contents of this table", + example: "[[foo bar]; [1 2]] | to html", + result: Some(Value::test_string( + r#"
foobar
12
"#, + )), + }, + Example { + description: "Optionally, only output the html for the content itself", + example: "[[foo bar]; [1 2]] | to html --partial", + result: Some(Value::test_string( + r#"
foobar
12
"#, + )), + }, + Example { + description: "Optionally, output the string with a dark background", + example: "[[foo bar]; [1 2]] | to html --dark", + result: Some(Value::test_string( + r#"
foobar
12
"#, + )), + }, + ] + } + + fn usage(&self) -> &str { + "Convert table into simple HTML" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + to_html(input, call, engine_state, stack) + } +} + +fn get_theme_from_asset_file( + is_dark: bool, + theme: &Option>, +) -> Result, ShellError> { + let theme_name = match theme { + Some(s) => s.item.clone(), + None => "default".to_string(), // There is no theme named "default" so this will be HtmlTheme::default(), which is "nu_default". + }; + + // 228 themes come from + // https://github.com/mbadolato/iTerm2-Color-Schemes/tree/master/windowsterminal + // we should find a hit on any name in there + let asset = get_asset_by_name_as_html_themes("228_themes.zip", "228_themes.json"); + + // If asset doesn't work, make sure to return the default theme + let asset = match asset { + Ok(a) => a, + _ => HtmlThemes::default(), + }; + + // Find the theme by theme name + let th = asset + .themes + .iter() + .find(|&n| n.name.to_lowercase() == theme_name.to_lowercase()); // case insensitive search + + // If no theme is found by the name provided, ensure we return the default theme + let default_theme = HtmlTheme::default(); + let th = match th { + Some(t) => t, + None => &default_theme, + }; + + // this just means no theme was passed in + if th.name.to_lowercase().eq(&"nu_default".to_string()) + // this means there was a theme passed in + && theme.is_some() + { + return Err(ShellError::NotFound( + theme.as_ref().expect("this should never trigger").span, + )); + } + + Ok(convert_html_theme_to_hash_map(is_dark, th)) +} + +#[allow(unused_variables)] +fn get_asset_by_name_as_html_themes( + zip_name: &str, + json_name: &str, +) -> Result> { + match Assets::get(zip_name) { + Some(content) => { + let asset: Vec = content.data.into(); + let reader = std::io::Cursor::new(asset); + #[cfg(feature = "zip")] + { + use std::io::Read; + let mut archive = zip::ZipArchive::new(reader)?; + let mut zip_file = archive.by_name(json_name)?; + let mut contents = String::new(); + zip_file.read_to_string(&mut contents)?; + Ok(nu_json::from_str(&contents)?) + } + #[cfg(not(feature = "zip"))] + { + let th = HtmlThemes::default(); + Ok(th) + } + } + None => { + let th = HtmlThemes::default(); + Ok(th) + } + } +} + +fn convert_html_theme_to_hash_map( + is_dark: bool, + theme: &HtmlTheme, +) -> HashMap<&'static str, String> { + let mut hm: HashMap<&str, String> = HashMap::new(); + + hm.insert("bold_black", theme.brightBlack[..].to_string()); + hm.insert("bold_red", theme.brightRed[..].to_string()); + hm.insert("bold_green", theme.brightGreen[..].to_string()); + hm.insert("bold_yellow", theme.brightYellow[..].to_string()); + hm.insert("bold_blue", theme.brightBlue[..].to_string()); + hm.insert("bold_magenta", theme.brightPurple[..].to_string()); + hm.insert("bold_cyan", theme.brightCyan[..].to_string()); + hm.insert("bold_white", theme.brightWhite[..].to_string()); + + hm.insert("black", theme.black[..].to_string()); + hm.insert("red", theme.red[..].to_string()); + hm.insert("green", theme.green[..].to_string()); + hm.insert("yellow", theme.yellow[..].to_string()); + hm.insert("blue", theme.blue[..].to_string()); + hm.insert("magenta", theme.purple[..].to_string()); + hm.insert("cyan", theme.cyan[..].to_string()); + hm.insert("white", theme.white[..].to_string()); + + // Try to make theme work with light or dark but + // flipping the foreground and background but leave + // the other colors the same. + if is_dark { + hm.insert("background", theme.black[..].to_string()); + hm.insert("foreground", theme.white[..].to_string()); + } else { + hm.insert("background", theme.white[..].to_string()); + hm.insert("foreground", theme.black[..].to_string()); + } + + hm +} + +fn get_list_of_theme_names() -> Vec { + let asset = get_asset_by_name_as_html_themes("228_themes.zip", "228_themes.json"); + + // If asset doesn't work, make sure to return the default theme + let html_themes = match asset { + Ok(a) => a, + _ => HtmlThemes::default(), + }; + + let theme_names: Vec = html_themes.themes.iter().map(|n| n.name.clone()).collect(); + + theme_names +} + +fn to_html( + input: PipelineData, + call: &Call, + engine_state: &EngineState, + stack: &mut Stack, +) -> Result { + let head = call.head; + let html_color = call.has_flag("html_color"); + let no_color = call.has_flag("no_color"); + let dark = call.has_flag("dark"); + let partial = call.has_flag("partial"); + let list = call.has_flag("list"); + let theme: Option> = call.get_flag(engine_state, stack, "theme")?; + let config = stack.get_config().unwrap_or_default(); + + let vec_of_values = input.into_iter().collect::>(); + let headers = merge_descriptors(&vec_of_values); + let headers = Some(headers) + .filter(|headers| !headers.is_empty() && (headers.len() > 1 || !headers[0].is_empty())); + let mut output_string = String::new(); + let mut regex_hm: HashMap = HashMap::new(); + + if list { + // Get the list of theme names + let theme_names = get_list_of_theme_names(); + + // Put that list into the output string + for s in &theme_names { + writeln!(&mut output_string, "{}", s).unwrap(); + } + + output_string.push_str("\nScreenshots of themes can be found here:\n"); + output_string.push_str("https://github.com/mbadolato/iTerm2-Color-Schemes\n"); + } else { + let theme_span = match &theme { + Some(v) => v.span, + None => head, + }; + + let color_hm = get_theme_from_asset_file(dark, &theme); + let color_hm = match color_hm { + Ok(c) => c, + _ => { + return Err(ShellError::SpannedLabeledError( + "Error finding theme name".to_string(), + "Error finding theme name".to_string(), + theme_span, + )) + } + }; + + // change the color of the page + if !partial { + write!( + &mut output_string, + r"", + color_hm + .get("background") + .expect("Error getting background color"), + color_hm + .get("foreground") + .expect("Error getting foreground color") + ) + .unwrap(); + } else { + write!( + &mut output_string, + "
", + color_hm + .get("background") + .expect("Error getting background color"), + color_hm + .get("foreground") + .expect("Error getting foreground color") + ) + .unwrap(); + } + + let inner_value = match vec_of_values.len() { + 0 => String::default(), + 1 => match headers { + Some(headers) => html_table(vec_of_values, headers, &config), + None => { + let value = &vec_of_values[0]; + html_value(value.clone(), &config) + } + }, + _ => match headers { + Some(headers) => html_table(vec_of_values, headers, &config), + None => html_list(vec_of_values, &config), + }, + }; + + output_string.push_str(&inner_value); + + if !partial { + output_string.push_str(""); + } else { + output_string.push_str("
") + } + + // Check to see if we want to remove all color or change ansi to html colors + if html_color { + setup_html_color_regexes(&mut regex_hm, &color_hm); + output_string = run_regexes(®ex_hm, &output_string); + } else if no_color { + setup_no_color_regexes(&mut regex_hm); + output_string = run_regexes(®ex_hm, &output_string); + } + } + Ok(Value::string(output_string, head).into_pipeline_data()) +} + +fn html_list(list: Vec, config: &Config) -> String { + let mut output_string = String::new(); + output_string.push_str("
    "); + for value in list { + output_string.push_str("
  1. "); + output_string.push_str(&html_value(value, config)); + output_string.push_str("
  2. "); + } + output_string.push_str("
"); + output_string +} + +fn html_table(table: Vec, headers: Vec, config: &Config) -> String { + let mut output_string = String::new(); + + output_string.push_str(""); + + output_string.push_str(""); + for header in &headers { + output_string.push_str(""); + } + output_string.push_str(""); + + for row in table { + if let Value::Record { span, .. } = row { + output_string.push_str(""); + for header in &headers { + let data = row.get_data_by_key(header); + output_string.push_str(""); + } + output_string.push_str(""); + } + } + output_string.push_str("
"); + output_string.push_str(&htmlescape::encode_minimal(header)); + output_string.push_str("
"); + output_string.push_str(&html_value( + data.unwrap_or_else(|| Value::nothing(span)), + config, + )); + output_string.push_str("
"); + + output_string +} + +fn html_value(value: Value, config: &Config) -> String { + let mut output_string = String::new(); + match value { + Value::Binary { val, .. } => { + let output = nu_pretty_hex::pretty_hex(&val); + output_string.push_str("
");
+            output_string.push_str(&output);
+            output_string.push_str("
"); + } + other => output_string.push_str( + &htmlescape::encode_minimal(&other.into_abbreviated_string(config)) + .replace("\n", "
"), + ), + } + output_string +} + +fn setup_html_color_regexes( + hash: &mut HashMap, + color_hm: &HashMap<&str, String>, +) { + // All the bold colors + hash.insert( + 0, + ( + r"(?P\[0m)(?P[[:alnum:][:space:][:punct:]]*)", + // Reset the text color, normal weight font + format!( + r"$word", + color_hm + .get("foreground") + .expect("Error getting reset text color") + ), + ), + ); + hash.insert( + 1, + ( + // Bold Black + r"(?P\[1;30m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm + .get("foreground") + .expect("Error getting bold black text color") + ), + ), + ); + hash.insert( + 2, + ( + // Bold Red + r"(?P
\[1;31m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm + .get("bold_red") + .expect("Error getting bold red text color"), + ), + ), + ); + hash.insert( + 3, + ( + // Bold Green + r"(?P\[1;32m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm + .get("bold_green") + .expect("Error getting bold green text color"), + ), + ), + ); + hash.insert( + 4, + ( + // Bold Yellow + r"(?P\[1;33m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm + .get("bold_yellow") + .expect("Error getting bold yellow text color"), + ), + ), + ); + hash.insert( + 5, + ( + // Bold Blue + r"(?P\[1;34m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm + .get("bold_blue") + .expect("Error getting bold blue text color"), + ), + ), + ); + hash.insert( + 6, + ( + // Bold Magenta + r"(?P\[1;35m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm + .get("bold_magenta") + .expect("Error getting bold magenta text color"), + ), + ), + ); + hash.insert( + 7, + ( + // Bold Cyan + r"(?P\[1;36m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm + .get("bold_cyan") + .expect("Error getting bold cyan text color"), + ), + ), + ); + hash.insert( + 8, + ( + // Bold White + // Let's change this to black since the html background + // is white. White on white = no bueno. + r"(?P\[1;37m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm + .get("foreground") + .expect("Error getting bold bold white text color"), + ), + ), + ); + // All the normal colors + hash.insert( + 9, + ( + // Black + r"(?P\[30m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm + .get("foreground") + .expect("Error getting black text color"), + ), + ), + ); + hash.insert( + 10, + ( + // Red + r"(?P\[31m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm.get("red").expect("Error getting red text color"), + ), + ), + ); + hash.insert( + 11, + ( + // Green + r"(?P\[32m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm + .get("green") + .expect("Error getting green text color"), + ), + ), + ); + hash.insert( + 12, + ( + // Yellow + r"(?P\[33m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm + .get("yellow") + .expect("Error getting yellow text color"), + ), + ), + ); + hash.insert( + 13, + ( + // Blue + r"(?P\[34m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm.get("blue").expect("Error getting blue text color"), + ), + ), + ); + hash.insert( + 14, + ( + // Magenta + r"(?P\[35m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm + .get("magenta") + .expect("Error getting magenta text color"), + ), + ), + ); + hash.insert( + 15, + ( + // Cyan + r"(?P\[36m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm.get("cyan").expect("Error getting cyan text color"), + ), + ), + ); + hash.insert( + 16, + ( + // White + // Let's change this to black since the html background + // is white. White on white = no bueno. + r"(?P\[37m)(?P[[:alnum:][:space:][:punct:]]*)", + format!( + r"$word", + color_hm + .get("foreground") + .expect("Error getting white text color"), + ), + ), + ); +} + +fn setup_no_color_regexes(hash: &mut HashMap) { + // We can just use one regex here because we're just removing ansi sequences + // and not replacing them with html colors. + // attribution: https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python + hash.insert( + 0, + ( + r"(?:\x1B[@-Z\\-_]|[\x80-\x9A\x9C-\x9F]|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~])", + r"$name_group_doesnt_exist".to_string(), + ), + ); +} + +fn run_regexes(hash: &HashMap, contents: &str) -> String { + let mut working_string = contents.to_owned(); + let hash_count: u32 = hash.len() as u32; + for n in 0..hash_count { + let value = hash.get(&n).expect("error getting hash at index"); + //println!("{},{}", value.0, value.1); + let re = Regex::new(value.0).expect("problem with color regex"); + let after = re.replace_all(&working_string, &value.1[..]).to_string(); + working_string = after.clone(); + } + working_string +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(ToHtml {}) + } +} diff --git a/crates/nu-command/src/formats/to/json.rs b/crates/nu-command/src/formats/to/json.rs new file mode 100644 index 0000000000..fcf94ef214 --- /dev/null +++ b/crates/nu-command/src/formats/to/json.rs @@ -0,0 +1,145 @@ +use nu_protocol::ast::{Call, PathMember}; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Value, +}; + +#[derive(Clone)] +pub struct ToJson; + +impl Command for ToJson { + fn name(&self) -> &str { + "to json" + } + + fn signature(&self) -> Signature { + Signature::build("to json") + .switch("raw", "remove all of the whitespace", Some('r')) + .category(Category::Formats) + } + + fn usage(&self) -> &str { + "Converts table data into JSON text." + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let raw = call.has_flag("raw"); + if raw { + to_json_raw(call, input) + } else { + to_json(call, input) + } + } + + fn examples(&self) -> Vec { + vec![Example { + description: + "Outputs an unformatted JSON string representing the contents of this table", + example: "[1 2 3] | to json", + result: Some(Value::test_string("[\n 1,\n 2,\n 3\n]")), + }] + } +} + +pub fn value_to_json_value(v: &Value) -> Result { + Ok(match v { + Value::Bool { val, .. } => nu_json::Value::Bool(*val), + Value::Filesize { val, .. } => nu_json::Value::I64(*val), + Value::Duration { val, .. } => nu_json::Value::I64(*val), + Value::Date { val, .. } => nu_json::Value::String(val.to_string()), + Value::Float { val, .. } => nu_json::Value::F64(*val), + Value::Int { val, .. } => nu_json::Value::I64(*val), + Value::Nothing { .. } => nu_json::Value::Null, + Value::String { val, .. } => nu_json::Value::String(val.to_string()), + Value::CellPath { val, .. } => nu_json::Value::Array( + val.members + .iter() + .map(|x| match &x { + PathMember::String { val, .. } => Ok(nu_json::Value::String(val.clone())), + PathMember::Int { val, .. } => Ok(nu_json::Value::U64(*val as u64)), + }) + .collect::, ShellError>>()?, + ), + + Value::List { vals, .. } => nu_json::Value::Array(json_list(vals)?), + Value::Error { error } => return Err(error.clone()), + Value::Block { .. } | Value::Range { .. } => nu_json::Value::Null, + Value::Binary { val, .. } => { + nu_json::Value::Array(val.iter().map(|x| nu_json::Value::U64(*x as u64)).collect()) + } + Value::Record { cols, vals, .. } => { + let mut m = nu_json::Map::new(); + for (k, v) in cols.iter().zip(vals) { + m.insert(k.clone(), value_to_json_value(v)?); + } + nu_json::Value::Object(m) + } + Value::CustomValue { val, .. } => val.to_json(), + }) +} + +fn json_list(input: &[Value]) -> Result, ShellError> { + let mut out = vec![]; + + for value in input { + out.push(value_to_json_value(value)?); + } + + Ok(out) +} + +fn to_json(call: &Call, input: PipelineData) -> Result { + let span = call.head; + + let value = input.into_value(span); + + let json_value = value_to_json_value(&value)?; + match nu_json::to_string(&json_value) { + Ok(serde_json_string) => Ok(Value::String { + val: serde_json_string, + span, + } + .into_pipeline_data()), + _ => Ok(Value::Error { + error: ShellError::CantConvert("JSON".into(), value.get_type().to_string(), span), + } + .into_pipeline_data()), + } +} + +fn to_json_raw(call: &Call, input: PipelineData) -> Result { + let span = call.head; + + let value = input.into_value(span); + + let json_value = value_to_json_value(&value)?; + match nu_json::to_string_raw(&json_value) { + Ok(serde_json_string) => Ok(Value::String { + val: serde_json_string, + span, + } + .into_pipeline_data()), + _ => Ok(Value::Error { + error: ShellError::CantConvert("JSON".into(), value.get_type().to_string(), span), + } + .into_pipeline_data()), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(ToJson {}) + } +} diff --git a/crates/nu-command/src/formats/to/md.rs b/crates/nu-command/src/formats/to/md.rs new file mode 100644 index 0000000000..c12ae90563 --- /dev/null +++ b/crates/nu-command/src/formats/to/md.rs @@ -0,0 +1,443 @@ +use crate::formats::to::delimited::merge_descriptors; +use indexmap::map::IndexMap; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Config, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Value, +}; + +#[derive(Clone)] +pub struct ToMd; + +impl Command for ToMd { + fn name(&self) -> &str { + "to md" + } + + fn signature(&self) -> Signature { + Signature::build("to md") + .switch( + "pretty", + "Formats the Markdown table to vertically align items", + Some('p'), + ) + .switch( + "per-element", + "treat each row as markdown syntax element", + Some('e'), + ) + .category(Category::Formats) + } + + fn usage(&self) -> &str { + "Convert table into simple Markdown" + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Outputs an MD string representing the contents of this table", + example: "[[foo bar]; [1 2]] | to md", + result: Some(Value::test_string("|foo|bar|\n|-|-|\n|1|2|\n")), + }, + Example { + description: "Optionally, output a formatted markdown string", + example: "[[foo bar]; [1 2]] | to md --pretty", + result: Some(Value::test_string( + "| foo | bar |\n| --- | --- |\n| 1 | 2 |\n", + )), + }, + Example { + description: "Treat each row as a markdown element", + example: r#"[{"H1": "Welcome to Nushell" } [[foo bar]; [1 2]]] | to md --per-element --pretty"#, + result: Some(Value::test_string( + "# Welcome to Nushell\n| foo | bar |\n| --- | --- |\n| 1 | 2 |", + )), + }, + ] + } + + fn run( + &self, + _engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let pretty = call.has_flag("pretty"); + let per_element = call.has_flag("per-element"); + let config = stack.get_config().unwrap_or_default(); + to_md(input, pretty, per_element, config, head) + } +} + +fn to_md( + input: PipelineData, + pretty: bool, + per_element: bool, + config: Config, + head: Span, +) -> Result { + let (grouped_input, single_list) = group_by(input, head, &config); + if per_element || single_list { + return Ok(Value::string( + grouped_input + .into_iter() + .map(move |val| match val { + Value::List { .. } => table(val.into_pipeline_data(), pretty, &config), + other => fragment(other, pretty, &config), + }) + .collect::>() + .join(""), + head, + ) + .into_pipeline_data()); + } + Ok(Value::string(table(grouped_input, pretty, &config), head).into_pipeline_data()) +} + +fn fragment(input: Value, pretty: bool, config: &Config) -> String { + let headers = match input { + Value::Record { ref cols, .. } => cols.to_owned(), + _ => vec![], + }; + let mut out = String::new(); + + if headers.len() == 1 { + let markup = match (&headers[0]).to_ascii_lowercase().as_ref() { + "h1" => "# ".to_string(), + "h2" => "## ".to_string(), + "h3" => "### ".to_string(), + "blockquote" => "> ".to_string(), + + _ => return table(input.into_pipeline_data(), pretty, config), + }; + + out.push_str(&markup); + let data = match input.get_data_by_key(&headers[0]) { + Some(v) => v, + None => input, + }; + out.push_str(&data.into_string("|", config)); + } else if let Value::Record { .. } = input { + out = table(input.into_pipeline_data(), pretty, config) + } else { + out = input.into_string("|", config) + } + + out.push('\n'); + out +} + +fn collect_headers(headers: &[String]) -> (Vec, Vec) { + let mut escaped_headers: Vec = Vec::new(); + let mut column_widths: Vec = Vec::new(); + + if !headers.is_empty() && (headers.len() > 1 || !headers[0].is_empty()) { + for header in headers { + let escaped_header_string = htmlescape::encode_minimal(header); + column_widths.push(escaped_header_string.len()); + escaped_headers.push(escaped_header_string); + } + } else { + column_widths = vec![0; headers.len()] + } + + (escaped_headers, column_widths) +} + +fn table(input: PipelineData, pretty: bool, config: &Config) -> String { + let vec_of_values = input.into_iter().collect::>(); + let headers = merge_descriptors(&vec_of_values); + + let (escaped_headers, mut column_widths) = collect_headers(&headers); + + let mut escaped_rows: Vec> = Vec::new(); + + for row in vec_of_values { + let mut escaped_row: Vec = Vec::new(); + + match row.to_owned() { + Value::Record { span, .. } => { + for i in 0..headers.len() { + let data = row.get_data_by_key(&headers[i]); + let value_string = data + .unwrap_or_else(|| Value::nothing(span)) + .into_string("|", config); + let new_column_width = value_string.len(); + + escaped_row.push(value_string); + + if column_widths[i] < new_column_width { + column_widths[i] = new_column_width; + } + } + } + p => { + let value_string = htmlescape::encode_minimal(&p.into_abbreviated_string(config)); + escaped_row.push(value_string); + } + } + + escaped_rows.push(escaped_row); + } + + let output_string = if (column_widths.is_empty() || column_widths.iter().all(|x| *x == 0)) + && escaped_rows.is_empty() + { + String::from("") + } else { + get_output_string(&escaped_headers, &escaped_rows, &column_widths, pretty) + .trim() + .to_string() + }; + + output_string +} + +pub fn group_by(values: PipelineData, head: Span, config: &Config) -> (PipelineData, bool) { + let mut lists = IndexMap::new(); + let mut single_list = false; + for val in values { + if let Value::Record { ref cols, .. } = val { + lists + .entry(cols.concat()) + .and_modify(|v: &mut Vec| v.push(val.clone())) + .or_insert_with(|| vec![val.clone()]); + } else { + lists + .entry(val.into_string(",", config)) + .and_modify(|v: &mut Vec| v.push(val.clone())) + .or_insert_with(|| vec![val.clone()]); + } + } + let mut output = vec![]; + for (_, mut value) in lists { + if value.len() == 1 { + output.push(value.pop().unwrap_or_else(|| Value::nothing(head))) + } else { + output.push(Value::List { + vals: value.to_vec(), + span: head, + }) + } + } + if output.len() == 1 { + single_list = true; + } + ( + Value::List { + vals: output, + span: head, + } + .into_pipeline_data(), + single_list, + ) +} + +fn get_output_string( + headers: &[String], + rows: &[Vec], + column_widths: &[usize], + pretty: bool, +) -> String { + let mut output_string = String::new(); + + if !headers.is_empty() { + output_string.push('|'); + + for i in 0..headers.len() { + if pretty { + output_string.push(' '); + output_string.push_str(&get_padded_string( + headers[i].clone(), + column_widths[i], + ' ', + )); + output_string.push(' '); + } else { + output_string.push_str(&headers[i]); + } + + output_string.push('|'); + } + + output_string.push_str("\n|"); + + #[allow(clippy::needless_range_loop)] + for i in 0..headers.len() { + if pretty { + output_string.push(' '); + output_string.push_str(&get_padded_string( + String::from("-"), + column_widths[i], + '-', + )); + output_string.push(' '); + } else { + output_string.push('-'); + } + + output_string.push('|'); + } + + output_string.push('\n'); + } + + for row in rows { + if !headers.is_empty() { + output_string.push('|'); + } + + for i in 0..row.len() { + if pretty { + output_string.push(' '); + output_string.push_str(&get_padded_string(row[i].clone(), column_widths[i], ' ')); + output_string.push(' '); + } else { + output_string.push_str(&row[i]); + } + + if !headers.is_empty() { + output_string.push('|'); + } + } + + output_string.push('\n'); + } + + output_string +} + +fn get_padded_string(text: String, desired_length: usize, padding_character: char) -> String { + let repeat_length = if text.len() > desired_length { + 0 + } else { + desired_length - text.len() + }; + + format!( + "{}{}", + text, + padding_character.to_string().repeat(repeat_length) + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use nu_protocol::{Config, IntoPipelineData, Span, Value}; + + fn one(string: &str) -> String { + string + .lines() + .skip(1) + .map(|line| line.trim()) + .collect::>() + .join("\n") + .trim_end() + .to_string() + } + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(ToMd {}) + } + + #[test] + fn render_h1() { + let value = Value::Record { + cols: vec!["H1".to_string()], + vals: vec![Value::test_string("Ecuador")], + span: Span::test_data(), + }; + + assert_eq!(fragment(value, false, &Config::default()), "# Ecuador\n"); + } + + #[test] + fn render_h2() { + let value = Value::Record { + cols: vec!["H2".to_string()], + vals: vec![Value::test_string("Ecuador")], + span: Span::test_data(), + }; + + assert_eq!(fragment(value, false, &Config::default()), "## Ecuador\n"); + } + + #[test] + fn render_h3() { + let value = Value::Record { + cols: vec!["H3".to_string()], + vals: vec![Value::test_string("Ecuador")], + span: Span::test_data(), + }; + + assert_eq!(fragment(value, false, &Config::default()), "### Ecuador\n"); + } + + #[test] + fn render_blockquote() { + let value = Value::Record { + cols: vec!["BLOCKQUOTE".to_string()], + vals: vec![Value::test_string("Ecuador")], + span: Span::test_data(), + }; + + assert_eq!(fragment(value, false, &Config::default()), "> Ecuador\n"); + } + + #[test] + fn render_table() { + let value = Value::List { + vals: vec![ + Value::Record { + cols: vec!["country".to_string()], + vals: vec![Value::test_string("Ecuador")], + span: Span::test_data(), + }, + Value::Record { + cols: vec!["country".to_string()], + vals: vec![Value::test_string("New Zealand")], + span: Span::test_data(), + }, + Value::Record { + cols: vec!["country".to_string()], + vals: vec![Value::test_string("USA")], + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }; + + assert_eq!( + table( + value.clone().into_pipeline_data(), + false, + &Config::default() + ), + one(r#" + |country| + |-| + |Ecuador| + |New Zealand| + |USA| + "#) + ); + + assert_eq!( + table(value.into_pipeline_data(), true, &Config::default()), + one(r#" + | country | + | ----------- | + | Ecuador | + | New Zealand | + | USA | + "#) + ); + } +} diff --git a/crates/nu-command/src/formats/to/mod.rs b/crates/nu-command/src/formats/to/mod.rs new file mode 100644 index 0000000000..6dab222368 --- /dev/null +++ b/crates/nu-command/src/formats/to/mod.rs @@ -0,0 +1,22 @@ +mod command; +mod csv; +mod delimited; +mod html; +mod json; +mod md; +mod toml; +mod tsv; +mod url; +mod xml; +mod yaml; + +pub use self::csv::ToCsv; +pub use self::toml::ToToml; +pub use self::url::ToUrl; +pub use command::To; +pub use html::ToHtml; +pub use json::ToJson; +pub use md::ToMd; +pub use tsv::ToTsv; +pub use xml::ToXml; +pub use yaml::ToYaml; diff --git a/crates/nu-command/src/formats/to/toml.rs b/crates/nu-command/src/formats/to/toml.rs new file mode 100644 index 0000000000..6c1315f453 --- /dev/null +++ b/crates/nu-command/src/formats/to/toml.rs @@ -0,0 +1,256 @@ +use nu_protocol::ast::{Call, PathMember}; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Type, Value, +}; + +#[derive(Clone)] +pub struct ToToml; + +impl Command for ToToml { + fn name(&self) -> &str { + "to toml" + } + + fn signature(&self) -> Signature { + Signature::build("to toml").category(Category::Formats) + } + + fn usage(&self) -> &str { + "Convert table into .toml text" + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Outputs an TOML string representing the contents of this table", + example: r#"[[foo bar]; ["1" "2"]] | to toml"#, + result: Some(Value::test_string("bar = \"2\"\nfoo = \"1\"\n")), + }] + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + to_toml(engine_state, input, head) + } +} + +// Helper method to recursively convert nu_protocol::Value -> toml::Value +// This shouldn't be called at the top-level +fn helper(engine_state: &EngineState, v: &Value) -> Result { + Ok(match &v { + Value::Bool { val, .. } => toml::Value::Boolean(*val), + Value::Int { val, .. } => toml::Value::Integer(*val), + Value::Filesize { val, .. } => toml::Value::Integer(*val), + Value::Duration { val, .. } => toml::Value::String(val.to_string()), + Value::Date { val, .. } => toml::Value::String(val.to_string()), + Value::Range { .. } => toml::Value::String("".to_string()), + Value::Float { val, .. } => toml::Value::Float(*val), + Value::String { val, .. } => toml::Value::String(val.clone()), + Value::Record { cols, vals, .. } => { + let mut m = toml::map::Map::new(); + for (k, v) in cols.iter().zip(vals.iter()) { + m.insert(k.clone(), helper(engine_state, v)?); + } + toml::Value::Table(m) + } + Value::List { vals, .. } => toml::Value::Array(toml_list(engine_state, vals)?), + Value::Block { span, .. } => { + let code = engine_state.get_span_contents(span); + let code = String::from_utf8_lossy(code).to_string(); + toml::Value::String(code) + } + Value::Nothing { .. } => toml::Value::String("".to_string()), + Value::Error { error } => return Err(error.clone()), + Value::Binary { val, .. } => toml::Value::Array( + val.iter() + .map(|x| toml::Value::Integer(*x as i64)) + .collect(), + ), + Value::CellPath { val, .. } => toml::Value::Array( + val.members + .iter() + .map(|x| match &x { + PathMember::String { val, .. } => Ok(toml::Value::String(val.clone())), + PathMember::Int { val, .. } => Ok(toml::Value::Integer(*val as i64)), + }) + .collect::, ShellError>>()?, + ), + Value::CustomValue { .. } => toml::Value::String("".to_string()), + }) +} + +fn toml_list(engine_state: &EngineState, input: &[Value]) -> Result, ShellError> { + let mut out = vec![]; + + for value in input { + out.push(helper(engine_state, value)?); + } + + Ok(out) +} + +fn toml_into_pipeline_data( + toml_value: &toml::Value, + value_type: Type, + span: Span, +) -> Result { + match toml::to_string(&toml_value) { + Ok(serde_toml_string) => Ok(Value::String { + val: serde_toml_string, + span, + } + .into_pipeline_data()), + _ => Ok(Value::Error { + error: ShellError::CantConvert("TOML".into(), value_type.to_string(), span), + } + .into_pipeline_data()), + } +} + +fn value_to_toml_value( + engine_state: &EngineState, + v: &Value, + head: Span, +) -> Result { + match v { + Value::Record { .. } => helper(engine_state, v), + Value::List { ref vals, span } => match &vals[..] { + [Value::Record { .. }, _end @ ..] => helper(engine_state, v), + _ => Err(ShellError::UnsupportedInput( + "Expected a table with TOML-compatible structure from pipeline".to_string(), + *span, + )), + }, + Value::String { val, span } => { + // Attempt to de-serialize the String + toml::de::from_str(val).map_err(|_| { + ShellError::UnsupportedInput( + format!("{:?} unable to de-serialize string to TOML", val), + *span, + ) + }) + } + _ => Err(ShellError::UnsupportedInput( + format!("{:?} is not a valid top-level TOML", v.get_type()), + v.span().unwrap_or(head), + )), + } +} + +fn to_toml( + engine_state: &EngineState, + input: PipelineData, + span: Span, +) -> Result { + let value = input.into_value(span); + + let toml_value = value_to_toml_value(engine_state, &value, span)?; + match toml_value { + toml::Value::Array(ref vec) => match vec[..] { + [toml::Value::Table(_)] => toml_into_pipeline_data( + vec.iter().next().expect("this should never trigger"), + value.get_type(), + span, + ), + _ => toml_into_pipeline_data(&toml_value, value.get_type(), span), + }, + _ => toml_into_pipeline_data(&toml_value, value.get_type(), span), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use nu_protocol::Spanned; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(ToToml {}) + } + + #[test] + fn test_value_to_toml_value() { + // + // Positive Tests + // + + let engine_state = EngineState::new(); + + let mut m = indexmap::IndexMap::new(); + m.insert("rust".to_owned(), Value::test_string("editor")); + m.insert("is".to_owned(), Value::nothing(Span::test_data())); + m.insert( + "features".to_owned(), + Value::List { + vals: vec![Value::test_string("hello"), Value::test_string("array")], + span: Span::test_data(), + }, + ); + let tv = value_to_toml_value( + &engine_state, + &Value::from(Spanned { + item: m, + span: Span::test_data(), + }), + Span::test_data(), + ) + .expect("Expected Ok from valid TOML dictionary"); + assert_eq!( + tv.get("features"), + Some(&toml::Value::Array(vec![ + toml::Value::String("hello".to_owned()), + toml::Value::String("array".to_owned()) + ])) + ); + // TOML string + let tv = value_to_toml_value( + &engine_state, + &Value::test_string( + r#" + title = "TOML Example" + + [owner] + name = "Tom Preston-Werner" + dob = 1979-05-27T07:32:00-08:00 # First class dates + + [dependencies] + rustyline = "4.1.0" + sysinfo = "0.8.4" + chrono = { version = "0.4.6", features = ["serde"] } + "#, + ), + Span::test_data(), + ) + .expect("Expected Ok from valid TOML string"); + assert_eq!( + tv.get("title").unwrap(), + &toml::Value::String("TOML Example".to_owned()) + ); + // + // Negative Tests + // + value_to_toml_value( + &engine_state, + &Value::test_string("not_valid"), + Span::test_data(), + ) + .expect_err("Expected non-valid toml (String) to cause error!"); + value_to_toml_value( + &engine_state, + &Value::List { + vals: vec![Value::test_string("1")], + span: Span::test_data(), + }, + Span::test_data(), + ) + .expect_err("Expected non-valid toml (Table) to cause error!"); + } +} diff --git a/crates/nu-command/src/formats/to/tsv.rs b/crates/nu-command/src/formats/to/tsv.rs new file mode 100644 index 0000000000..957fa15383 --- /dev/null +++ b/crates/nu-command/src/formats/to/tsv.rs @@ -0,0 +1,69 @@ +use crate::formats::to::delimited::to_delimited_data; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Config, Example, PipelineData, ShellError, Signature, Span, Value}; + +#[derive(Clone)] +pub struct ToTsv; + +impl Command for ToTsv { + fn name(&self) -> &str { + "to tsv" + } + + fn signature(&self) -> Signature { + Signature::build("to tsv") + .switch( + "noheaders", + "do not output the column names as the first row", + Some('n'), + ) + .category(Category::Formats) + } + + fn usage(&self) -> &str { + "Convert table into .tsv text" + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Outputs an TSV string representing the contents of this table", + example: "[[foo bar]; [1 2]] | to tsv", + result: Some(Value::test_string("foo\tbar\n1\t2\n")), + }] + } + + fn run( + &self, + _engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let noheaders = call.has_flag("noheaders"); + let config = stack.get_config().unwrap_or_default(); + to_tsv(input, noheaders, head, config) + } +} + +fn to_tsv( + input: PipelineData, + noheaders: bool, + head: Span, + config: Config, +) -> Result { + to_delimited_data(noheaders, '\t', "TSV", input, head, config) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(ToTsv {}) + } +} diff --git a/crates/nu-command/src/formats/to/url.rs b/crates/nu-command/src/formats/to/url.rs new file mode 100644 index 0000000000..ec157f8976 --- /dev/null +++ b/crates/nu-command/src/formats/to/url.rs @@ -0,0 +1,94 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Value, +}; + +#[derive(Clone)] +pub struct ToUrl; + +impl Command for ToUrl { + fn name(&self) -> &str { + "to url" + } + + fn signature(&self) -> Signature { + Signature::build("to url").category(Category::Formats) + } + + fn usage(&self) -> &str { + "Convert table into url-encoded text" + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Outputs an URL string representing the contents of this table", + example: r#"[[foo bar]; ["1" "2"]] | to url"#, + result: Some(Value::test_string("foo=1&bar=2")), + }] + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + to_url(input, head) + } +} + +fn to_url(input: PipelineData, head: Span) -> Result { + let output: Result = input + .into_iter() + .map(move |value| match value { + Value::Record { + ref cols, ref vals, .. + } => { + let mut row_vec = vec![]; + for (k, v) in cols.iter().zip(vals.iter()) { + match v.as_string() { + Ok(s) => { + row_vec.push((k.clone(), s.to_string())); + } + _ => { + return Err(ShellError::UnsupportedInput( + "Expected table with string values".to_string(), + head, + )); + } + } + } + + match serde_urlencoded::to_string(row_vec) { + Ok(s) => Ok(s), + _ => Err(ShellError::CantConvert( + "URL".into(), + value.get_type().to_string(), + head, + )), + } + } + other => Err(ShellError::UnsupportedInput( + "Expected a table from pipeline".to_string(), + other.span().unwrap_or(head), + )), + }) + .collect(); + + Ok(Value::string(output?, head).into_pipeline_data()) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(ToUrl {}) + } +} diff --git a/crates/nu-command/src/formats/to/xml.rs b/crates/nu-command/src/formats/to/xml.rs new file mode 100644 index 0000000000..6bccfbfd76 --- /dev/null +++ b/crates/nu-command/src/formats/to/xml.rs @@ -0,0 +1,202 @@ +use indexmap::IndexMap; +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Config, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, + Spanned, SyntaxShape, Value, +}; +use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; +use std::collections::HashSet; +use std::io::Cursor; +use std::io::Write; + +#[derive(Clone)] +pub struct ToXml; + +impl Command for ToXml { + fn name(&self) -> &str { + "to xml" + } + + fn signature(&self) -> Signature { + Signature::build("to xml") + .named( + "pretty", + SyntaxShape::Int, + "Formats the XML text with the provided indentation setting", + Some('p'), + ) + .category(Category::Formats) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Outputs an XML string representing the contents of this table", + example: r#"{ "note": { "children": [{ "remember": {"attributes" : {}, "children": [Event]}}], "attributes": {} } } | to xml"#, + result: Some(Value::test_string( + "Event", + )), + }, + Example { + description: "Optionally, formats the text with a custom indentation setting", + example: r#"{ "note": { "children": [{ "remember": {"attributes" : {}, "children": [Event]}}], "attributes": {} } } | to xml -p 3"#, + result: Some(Value::test_string( + "\n Event\n", + )), + }, + ] + } + + fn usage(&self) -> &str { + "Convert table into .xml text" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let config = stack.get_config().unwrap_or_default(); + let pretty: Option> = call.get_flag(engine_state, stack, "pretty")?; + to_xml(input, head, pretty, &config) + } +} + +pub fn add_attributes<'a>( + element: &mut quick_xml::events::BytesStart<'a>, + attributes: &'a IndexMap, +) { + for (k, v) in attributes { + element.push_attribute((k.as_str(), v.as_str())); + } +} + +pub fn get_attributes(row: &Value, config: &Config) -> Option> { + if let Value::Record { .. } = row { + if let Some(Value::Record { cols, vals, .. }) = row.get_data_by_key("attributes") { + let mut h = IndexMap::new(); + for (k, v) in cols.iter().zip(vals.iter()) { + h.insert(k.clone(), v.clone().into_abbreviated_string(config)); + } + return Some(h); + } + } + None +} + +pub fn get_children(row: &Value) -> Option> { + if let Value::Record { .. } = row { + if let Some(Value::List { vals, .. }) = row.get_data_by_key("children") { + return Some(vals); + } + } + None +} + +pub fn is_xml_row(row: &Value) -> bool { + if let Value::Record { cols, .. } = &row { + let keys: HashSet<&String> = cols.iter().collect(); + let children: String = "children".to_string(); + let attributes: String = "attributes".to_string(); + return keys.contains(&children) && keys.contains(&attributes) && keys.len() == 2; + } + false +} + +pub fn write_xml_events( + current: Value, + writer: &mut quick_xml::Writer, + config: &Config, +) -> Result<(), ShellError> { + match current { + Value::Record { cols, vals, span } => { + for (k, v) in cols.iter().zip(vals.iter()) { + let mut e = BytesStart::owned(k.as_bytes(), k.len()); + if !is_xml_row(v) { + return Err(ShellError::SpannedLabeledError( + "Expected a row with 'children' and 'attributes' columns".to_string(), + "missing 'children' and 'attributes' columns ".to_string(), + span, + )); + } + let a = get_attributes(v, config); + if let Some(ref a) = a { + add_attributes(&mut e, a); + } + writer + .write_event(Event::Start(e)) + .expect("Couldn't open XML node"); + let c = get_children(v); + if let Some(c) = c { + for v in c { + write_xml_events(v, writer, config)?; + } + } + writer + .write_event(Event::End(BytesEnd::borrowed(k.as_bytes()))) + .expect("Couldn't close XML node"); + } + } + Value::List { vals, .. } => { + for v in vals { + write_xml_events(v, writer, config)?; + } + } + _ => { + let s = current.into_abbreviated_string(config); + writer + .write_event(Event::Text(BytesText::from_plain_str(s.as_str()))) + .expect("Couldn't write XML text"); + } + } + Ok(()) +} + +fn to_xml( + input: PipelineData, + head: Span, + pretty: Option>, + config: &Config, +) -> Result { + let mut w = pretty.as_ref().map_or_else( + || quick_xml::Writer::new(Cursor::new(Vec::new())), + |p| quick_xml::Writer::new_with_indent(Cursor::new(Vec::new()), b' ', p.item as usize), + ); + + let value = input.into_value(head); + let value_type = value.get_type(); + + match write_xml_events(value, &mut w, config) { + Ok(_) => { + let b = w.into_inner().into_inner(); + let s = if let Ok(s) = String::from_utf8(b) { + s + } else { + return Err(ShellError::NonUtf8(head)); + }; + Ok(Value::string(s, head).into_pipeline_data()) + } + Err(_) => Err(ShellError::CantConvert( + "XML".into(), + value_type.to_string(), + head, + )), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(ToXml {}) + } +} diff --git a/crates/nu-command/src/formats/to/yaml.rs b/crates/nu-command/src/formats/to/yaml.rs new file mode 100644 index 0000000000..8525a8a84c --- /dev/null +++ b/crates/nu-command/src/formats/to/yaml.rs @@ -0,0 +1,122 @@ +use nu_protocol::ast::{Call, PathMember}; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Value, +}; + +#[derive(Clone)] +pub struct ToYaml; + +impl Command for ToYaml { + fn name(&self) -> &str { + "to yaml" + } + + fn signature(&self) -> Signature { + Signature::build("to yaml").category(Category::Formats) + } + + fn usage(&self) -> &str { + "Convert table into .yaml/.yml text" + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Outputs an YAML string representing the contents of this table", + example: r#"[[foo bar]; ["1" "2"]] | to yaml"#, + result: Some(Value::test_string("---\n- foo: \"1\"\n bar: \"2\"\n")), + }] + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + to_yaml(input, head) + } +} + +pub fn value_to_yaml_value(v: &Value) -> Result { + Ok(match &v { + Value::Bool { val, .. } => serde_yaml::Value::Bool(*val), + Value::Int { val, .. } => serde_yaml::Value::Number(serde_yaml::Number::from(*val)), + Value::Filesize { val, .. } => serde_yaml::Value::Number(serde_yaml::Number::from(*val)), + Value::Duration { val, .. } => serde_yaml::Value::String(val.to_string()), + Value::Date { val, .. } => serde_yaml::Value::String(val.to_string()), + Value::Range { .. } => serde_yaml::Value::Null, + Value::Float { val, .. } => serde_yaml::Value::Number(serde_yaml::Number::from(*val)), + Value::String { val, .. } => serde_yaml::Value::String(val.clone()), + Value::Record { cols, vals, .. } => { + let mut m = serde_yaml::Mapping::new(); + for (k, v) in cols.iter().zip(vals.iter()) { + m.insert( + serde_yaml::Value::String(k.clone()), + value_to_yaml_value(v)?, + ); + } + serde_yaml::Value::Mapping(m) + } + Value::List { vals, .. } => { + let mut out = vec![]; + + for value in vals { + out.push(value_to_yaml_value(value)?); + } + + serde_yaml::Value::Sequence(out) + } + Value::Block { .. } => serde_yaml::Value::Null, + Value::Nothing { .. } => serde_yaml::Value::Null, + Value::Error { error } => return Err(error.clone()), + Value::Binary { val, .. } => serde_yaml::Value::Sequence( + val.iter() + .map(|x| serde_yaml::Value::Number(serde_yaml::Number::from(*x))) + .collect(), + ), + Value::CellPath { val, .. } => serde_yaml::Value::Sequence( + val.members + .iter() + .map(|x| match &x { + PathMember::String { val, .. } => Ok(serde_yaml::Value::String(val.clone())), + PathMember::Int { val, .. } => { + Ok(serde_yaml::Value::Number(serde_yaml::Number::from(*val))) + } + }) + .collect::, ShellError>>()?, + ), + Value::CustomValue { .. } => serde_yaml::Value::Null, + }) +} + +fn to_yaml(input: PipelineData, head: Span) -> Result { + let value = input.into_value(head); + + let yaml_value = value_to_yaml_value(&value)?; + match serde_yaml::to_string(&yaml_value) { + Ok(serde_yaml_string) => Ok(Value::String { + val: serde_yaml_string, + span: head, + } + .into_pipeline_data()), + _ => Ok(Value::Error { + error: ShellError::CantConvert("YAML".into(), value.get_type().to_string(), head), + } + .into_pipeline_data()), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(ToYaml {}) + } +} diff --git a/crates/nu-command/src/generators/cal.rs b/crates/nu-command/src/generators/cal.rs new file mode 100644 index 0000000000..dad74beb14 --- /dev/null +++ b/crates/nu-command/src/generators/cal.rs @@ -0,0 +1,395 @@ +use chrono::{Datelike, Local, NaiveDate}; +use indexmap::IndexMap; +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Spanned, + SyntaxShape, Value, +}; +use std::collections::VecDeque; + +#[derive(Clone)] +pub struct Cal; + +struct Arguments { + year: bool, + quarter: bool, + month: bool, + month_names: bool, + full_year: Option>, + week_start: Option>, +} + +impl Command for Cal { + fn name(&self) -> &str { + "cal" + } + + fn signature(&self) -> Signature { + Signature::build("cal") + .switch("year", "Display the year column", Some('y')) + .switch("quarter", "Display the quarter column", Some('q')) + .switch("month", "Display the month column", Some('m')) + .named( + "full-year", + SyntaxShape::Int, + "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", + None, + ) + .category(Category::Generators) + } + + fn usage(&self) -> &str { + "Display a calendar." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + cal(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "This month's calendar", + example: "cal", + result: None, + }, + Example { + description: "The calendar for all of 2012", + 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, + }, + ] + } +} + +pub fn cal( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, +) -> Result { + let mut calendar_vec_deque = VecDeque::new(); + let tag = call.head; + + let (current_year, current_month, current_day) = get_current_date(); + + let arguments = Arguments { + year: call.has_flag("year"), + month: call.has_flag("month"), + month_names: call.has_flag("month-names"), + quarter: call.has_flag("quarter"), + full_year: call.get_flag(engine_state, stack, "full-year")?, + week_start: call.get_flag(engine_state, stack, "week-start")?, + }; + + let mut selected_year: i32 = current_year; + let mut current_day_option: Option = Some(current_day); + + let full_year_value = &arguments.full_year; + let month_range = if let Some(full_year_value) = full_year_value { + selected_year = full_year_value.item as i32; + + if selected_year != current_year { + current_day_option = None + } + (1, 12) + } else { + (current_month, current_month) + }; + + add_months_of_year_to_table( + &arguments, + &mut calendar_vec_deque, + tag, + selected_year, + month_range, + current_month, + current_day_option, + )?; + + Ok(Value::List { + vals: calendar_vec_deque.into_iter().collect(), + span: tag, + } + .into_pipeline_data()) +} + +fn get_invalid_year_shell_error(head: Span) -> ShellError { + ShellError::UnsupportedInput("The year is invalid".to_string(), head) +} + +struct MonthHelper { + 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 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, + 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(), + }) + } + + 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 + if selected_month == 12 { + selected_year += 1; + selected_month = 1; + } else { + selected_month += 1; + }; + + let next_month_naive_date = + NaiveDate::from_ymd_opt(selected_year, selected_month, 1).ok_or(())?; + + Ok(next_month_naive_date.pred().day()) + } +} + +fn get_current_date() -> (i32, u32, u32) { + let local_now_date = Local::now().date(); + + let current_year: i32 = local_now_date.year(); + let current_month: u32 = local_now_date.month(); + let current_day: u32 = local_now_date.day(); + + (current_year, current_month, current_day) +} + +fn add_months_of_year_to_table( + arguments: &Arguments, + calendar_vec_deque: &mut VecDeque, + tag: Span, + selected_year: i32, + (start_month, end_month): (u32, u32), + current_month: u32, + current_day_option: Option, +) -> Result<(), ShellError> { + for month_number in start_month..=end_month { + let mut new_current_day_option: Option = None; + + if let Some(current_day) = current_day_option { + if month_number == current_month { + new_current_day_option = Some(current_day) + } + } + + let add_month_to_table_result = add_month_to_table( + arguments, + calendar_vec_deque, + tag, + selected_year, + month_number, + new_current_day_option, + ); + + add_month_to_table_result? + } + + Ok(()) +} + +fn add_month_to_table( + arguments: &Arguments, + calendar_vec_deque: &mut VecDeque, + tag: Span, + selected_year: i32, + current_month: u32, + current_day_option: Option, +) -> Result<(), ShellError> { + let month_helper_result = MonthHelper::new(selected_year, current_month); + + let full_year_value: &Option> = &arguments.full_year; + + let month_helper = match month_helper_result { + Ok(month_helper) => month_helper, + Err(()) => match full_year_value { + Some(x) => return Err(get_invalid_year_shell_error(x.span)), + None => { + return Err(ShellError::UnknownOperator( + "Issue parsing command, invalid command".to_string(), + tag, + )) + } + }, + }; + + let mut days_of_the_week = [ + "sunday", + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + ]; + + let mut week_start_day = days_of_the_week[0].to_string(); + if let Some(day) = &arguments.week_start { + let s = &day.item; + if days_of_the_week.contains(&s.as_str()) { + week_start_day = s.to_string(); + } else { + return Err(ShellError::UnsupportedInput( + "The specified week start day is invalid".to_string(), + day.span, + )); + } + } + + 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 = arguments.year; + let should_show_quarter_column = arguments.quarter; + let should_show_month_column = arguments.month; + let should_show_month_names = arguments.month_names; + + while day_number <= day_limit { + let mut indexmap = IndexMap::new(); + + if should_show_year_column { + indexmap.insert( + "year".to_string(), + Value::Int { + val: month_helper.selected_year as i64, + span: tag, + }, + ); + } + + if should_show_quarter_column { + indexmap.insert( + "quarter".to_string(), + Value::Int { + val: month_helper.quarter_number as i64, + span: tag, + }, + ); + } + + if should_show_month_column || should_show_month_names { + let month_value = if should_show_month_names { + Value::String { + val: month_helper.month_name.clone(), + span: tag, + } + } else { + Value::Int { + val: month_helper.selected_month as i64, + span: tag, + } + }; + + indexmap.insert("month".to_string(), month_value); + } + + for day in &days_of_the_week { + let should_add_day_number_to_table = + (day_number > total_start_offset) && (day_number <= day_limit); + + let mut value = Value::Nothing { span: tag }; + + if should_add_day_number_to_table { + let adjusted_day_number = day_number - total_start_offset; + + value = Value::Int { + val: adjusted_day_number as i64, + span: tag, + }; + + if let Some(current_day) = current_day_option { + if current_day == adjusted_day_number { + // TODO: Update the value here with a color when color support is added + // This colors the current day + } + } + } + + indexmap.insert((*day).to_string(), value); + + day_number += 1; + } + + let cols: Vec = indexmap.keys().map(|f| f.to_string()).collect(); + let mut vals: Vec = Vec::new(); + for c in &cols { + if let Some(x) = indexmap.get(c) { + vals.push(x.to_owned()) + } + } + calendar_vec_deque.push_back(Value::Record { + cols, + vals, + span: tag, + }) + } + + Ok(()) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Cal {}) + } +} diff --git a/crates/nu-command/src/generators/mod.rs b/crates/nu-command/src/generators/mod.rs new file mode 100644 index 0000000000..c150009e50 --- /dev/null +++ b/crates/nu-command/src/generators/mod.rs @@ -0,0 +1,7 @@ +mod cal; +mod seq; +mod seq_date; + +pub use cal::Cal; +pub use seq::Seq; +pub use seq_date::SeqDate; diff --git a/crates/nu-command/src/generators/seq.rs b/crates/nu-command/src/generators/seq.rs new file mode 100644 index 0000000000..88ffe16309 --- /dev/null +++ b/crates/nu-command/src/generators/seq.rs @@ -0,0 +1,369 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Spanned, + SyntaxShape, Value, +}; +use std::cmp; + +#[derive(Clone)] +pub struct Seq; + +impl Command for Seq { + fn name(&self) -> &str { + "seq" + } + + fn signature(&self) -> Signature { + Signature::build("seq") + .rest("rest", SyntaxShape::Number, "sequence values") + .named( + "separator", + SyntaxShape::String, + "separator character (defaults to \\n)", + Some('s'), + ) + .named( + "terminator", + SyntaxShape::String, + "terminator character (defaults to \\n)", + Some('t'), + ) + .switch( + "widths", + "equalize widths of all numbers by padding with zeros", + Some('w'), + ) + .category(Category::Generators) + } + + fn usage(&self) -> &str { + "Print sequences of numbers." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + seq(engine_state, stack, call) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "sequence 1 to 10 with newline separator", + example: "seq 1 10", + result: Some(Value::List { + vals: vec![ + Value::test_int(1), + Value::test_int(2), + Value::test_int(3), + Value::test_int(4), + Value::test_int(5), + Value::test_int(6), + Value::test_int(7), + Value::test_int(8), + Value::test_int(9), + Value::test_int(10), + ], + span: Span::test_data(), + }), + }, + Example { + description: "sequence 1.0 to 2.0 by 0.1s with newline separator", + example: "seq 1.0 0.1 2.0", + result: Some(Value::List { + vals: vec![ + Value::test_float(1.0000), + Value::test_float(1.1000), + Value::test_float(1.2000), + Value::test_float(1.3000), + Value::test_float(1.4000), + Value::test_float(1.5000), + Value::test_float(1.6000), + Value::test_float(1.7000), + Value::test_float(1.8000), + Value::test_float(1.9000), + Value::test_float(2.0000), + ], + span: Span::test_data(), + }), + }, + Example { + description: "sequence 1 to 10 with pipe separator", + example: "seq -s '|' 1 10", + result: Some(Value::test_string("1|2|3|4|5|6|7|8|9|10")), + }, + Example { + description: "sequence 1 to 10 with pipe separator padded with 0", + example: "seq -s '|' -w 1 10", + result: Some(Value::test_string("01|02|03|04|05|06|07|08|09|10")), + }, + Example { + description: "sequence 1 to 10 with pipe separator padded by 2s", + example: "seq -s ' | ' -w 1 2 10", + result: Some(Value::test_string("01 | 03 | 05 | 07 | 09")), + }, + ] + } +} + +fn seq( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, +) -> Result { + let span = call.head; + let rest_nums: Vec> = call.rest(engine_state, stack, 0)?; + let separator: Option> = call.get_flag(engine_state, stack, "separator")?; + let terminator: Option> = call.get_flag(engine_state, stack, "terminator")?; + let widths = call.has_flag("widths"); + + if rest_nums.is_empty() { + return Err(ShellError::SpannedLabeledError( + "seq requires some parameters".into(), + "needs parameter".into(), + call.head, + )); + } + + let sep: String = match separator { + Some(s) => { + if s.item == r"\t" { + '\t'.to_string() + } else if s.item == r"\n" { + '\n'.to_string() + } else if s.item == r"\r" { + '\r'.to_string() + } else { + let vec_s: Vec = s.item.chars().collect(); + if vec_s.is_empty() { + return Err(ShellError::SpannedLabeledError( + "Expected a single separator char from --separator".into(), + "requires a single character string input".into(), + s.span, + )); + }; + vec_s.iter().collect() + } + } + _ => '\n'.to_string(), + }; + + let term: String = match terminator { + Some(t) => { + if t.item == r"\t" { + '\t'.to_string() + } else if t.item == r"\n" { + '\n'.to_string() + } else if t.item == r"\r" { + '\r'.to_string() + } else { + let vec_t: Vec = t.item.chars().collect(); + if vec_t.is_empty() { + return Err(ShellError::SpannedLabeledError( + "Expected a single terminator char from --terminator".into(), + "requires a single character string input".into(), + t.span, + )); + }; + vec_t.iter().collect() + } + } + _ => '\n'.to_string(), + }; + + let rest_nums: Vec = rest_nums.iter().map(|n| n.item.to_string()).collect(); + + run_seq(sep, Some(term), widths, rest_nums, span) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Seq {}) + } +} + +fn parse_float(mut s: &str) -> Result { + if s.starts_with('+') { + s = &s[1..]; + } + match s.parse() { + Ok(n) => Ok(n), + Err(e) => Err(format!( + "seq: invalid floating point argument `{}`: {}", + s, e + )), + } +} + +fn escape_sequences(s: &str) -> String { + s.replace("\\n", "\n").replace("\\t", "\t") +} + +pub fn run_seq( + sep: String, + termy: Option, + widths: bool, + free: Vec, + span: Span, +) -> Result { + let mut largest_dec = 0; + let mut padding = 0; + let first = if free.len() > 1 { + let slice = &free[0][..]; + let len = slice.len(); + let dec = slice.find('.').unwrap_or(len); + largest_dec = len - dec; + padding = dec; + match parse_float(slice) { + Ok(n) => n, + Err(s) => return Err(ShellError::LabeledError(s, "error parsing float".into())), + } + } else { + 1.0 + }; + let step = if free.len() > 2 { + let slice = &free[1][..]; + let len = slice.len(); + let dec = slice.find('.').unwrap_or(len); + largest_dec = cmp::max(largest_dec, len - dec); + padding = cmp::max(padding, dec); + match parse_float(slice) { + Ok(n) => n, + Err(s) => return Err(ShellError::LabeledError(s, "error parsing float".into())), + } + } else { + 1.0 + }; + let last = { + let slice = &free[free.len() - 1][..]; + padding = cmp::max(padding, slice.find('.').unwrap_or_else(|| slice.len())); + match parse_float(slice) { + Ok(n) => n, + Err(s) => { + return Err(ShellError::LabeledError(s, "error parsing float".into())); + } + } + }; + if largest_dec > 0 { + largest_dec -= 1; + } + let separator = escape_sequences(&sep[..]); + let terminator = match termy { + Some(term) => escape_sequences(&term[..]), + None => separator.clone(), + }; + Ok(print_seq( + first, + step, + last, + largest_dec, + separator, + terminator, + widths, + padding, + span, + )) +} + +fn done_printing(next: f64, step: f64, last: f64) -> bool { + if step >= 0f64 { + next > last + } else { + next < last + } +} + +#[allow(clippy::too_many_arguments)] +fn print_seq( + first: f64, + step: f64, + last: f64, + largest_dec: usize, + separator: String, + terminator: String, + pad: bool, + padding: usize, + span: Span, +) -> PipelineData { + let mut i = 0isize; + let mut value = first + i as f64 * step; + // for string output + let mut ret_str = "".to_owned(); + // for number output + let mut ret_num = vec![]; + // If the separator and terminator are line endings we can convert to numbers + let use_num = + (separator == "\n" || separator == "\r") && (terminator == "\n" || terminator == "\r"); + + while !done_printing(value, step, last) { + if use_num { + ret_num.push(value); + } else { + // formatting for string output with potential padding + let istr = format!("{:.*}", largest_dec, value); + let ilen = istr.len(); + let before_dec = istr.find('.').unwrap_or(ilen); + if pad && before_dec < padding { + for _ in 0..(padding - before_dec) { + ret_str.push('0'); + } + } + ret_str.push_str(&istr); + } + i += 1; + value = first + i as f64 * step; + if !done_printing(value, step, last) { + ret_str.push_str(&separator); + } + } + + if !use_num && ((first >= last && step < 0f64) || (first <= last && step > 0f64)) { + ret_str.push_str(&terminator); + } + + if use_num { + // we'd like to keep the datatype the same for the output, so check + // and see if any of the output is really decimals, and if it is + // we'll make the entire output decimals + let contains_decimals = vec_contains_decimals(&ret_num); + let rows: Vec = ret_num + .iter() + .map(|v| { + if contains_decimals { + Value::float(*v, span) + } else { + Value::int(*v as i64, span) + } + }) + .collect(); + + Value::List { vals: rows, span }.into_pipeline_data() + } else { + let rows: String = ret_str.lines().collect(); + Value::string(rows, span).into_pipeline_data() + } +} + +fn vec_contains_decimals(array: &[f64]) -> bool { + let mut found_decimal = false; + for x in array { + if x.fract() != 0.0 { + found_decimal = true; + break; + } + } + + found_decimal +} diff --git a/crates/nu-command/src/generators/seq_date.rs b/crates/nu-command/src/generators/seq_date.rs new file mode 100644 index 0000000000..0837f6c1df --- /dev/null +++ b/crates/nu-command/src/generators/seq_date.rs @@ -0,0 +1,370 @@ +use chrono::naive::NaiveDate; +use chrono::{Duration, Local}; +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Spanned, + SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct SeqDate; + +impl Command for SeqDate { + fn name(&self) -> &str { + "seq date" + } + + fn usage(&self) -> &str { + "print sequences of dates" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("seq date") + .named( + "separator", + SyntaxShape::String, + "separator character (defaults to \\n)", + Some('s'), + ) + .named( + "output-format", + SyntaxShape::String, + "prints dates in this format (defaults to %Y-%m-%d)", + Some('o'), + ) + .named( + "input-format", + SyntaxShape::String, + "give argument dates in this format (defaults to %Y-%m-%d)", + Some('i'), + ) + .named( + "begin-date", + SyntaxShape::String, + "beginning date range", + Some('b'), + ) + .named("end-date", SyntaxShape::String, "ending date", Some('e')) + .named( + "increment", + SyntaxShape::Int, + "increment dates by this number", + Some('n'), + ) + .named( + "days", + SyntaxShape::Int, + "number of days to print", + Some('d'), + ) + .switch("reverse", "print dates in reverse", Some('r')) + .category(Category::Generators) + } + + fn examples(&self) -> Vec { + let span = Span::test_data(); + + vec![ + Example { + description: "print the next 10 days in YYYY-MM-DD format with newline separator", + example: "seq date --days 10", + result: None, + }, + Example { + description: "print the previous 10 days in YYYY-MM-DD format with newline separator", + example: "seq date --days 10 -r", + result: None, + }, + Example { + description: "print the previous 10 days starting today in MM/DD/YYYY format with newline separator", + example: "seq date --days 10 -o '%m/%d/%Y' -r", + result: None, + }, + Example { + description: "print the first 10 days in January, 2020", + example: "seq date -b '2020-01-01' -e '2020-01-10'", + result: Some(Value::List { + vals: vec![ + Value::String { val: "2020-01-01".into(), span, }, + Value::String { val: "2020-01-02".into(), span, }, + Value::String { val: "2020-01-03".into(), span, }, + Value::String { val: "2020-01-04".into(), span, }, + Value::String { val: "2020-01-05".into(), span, }, + Value::String { val: "2020-01-06".into(), span, }, + Value::String { val: "2020-01-07".into(), span, }, + Value::String { val: "2020-01-08".into(), span, }, + Value::String { val: "2020-01-09".into(), span, }, + Value::String { val: "2020-01-10".into(), span, }, + ], + span, + }), + }, + Example { + description: "print every fifth day between January 1st 2020 and January 31st 2020", + example: "seq date -b '2020-01-01' -e '2020-01-31' -n 5", + result: Some(Value::List { + vals: vec![ + Value::String { val: "2020-01-01".into(), span, }, + Value::String { val: "2020-01-06".into(), span, }, + Value::String { val: "2020-01-11".into(), span, }, + Value::String { val: "2020-01-16".into(), span, }, + Value::String { val: "2020-01-21".into(), span, }, + Value::String { val: "2020-01-26".into(), span, }, + Value::String { val: "2020-01-31".into(), span, }, + ], + span, + }), + }, + Example { + description: "starting on May 5th, 2020, print the next 10 days in your locale's date format, colon separated", + example: "seq date -o %x -s ':' -d 10 -b '2020-05-01'", + result: None, + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let separator: Option> = call.get_flag(engine_state, stack, "separator")?; + let output_format: Option> = + call.get_flag(engine_state, stack, "output-format")?; + let input_format: Option> = + call.get_flag(engine_state, stack, "input-format")?; + let begin_date: Option> = + call.get_flag(engine_state, stack, "begin-date")?; + let end_date: Option> = call.get_flag(engine_state, stack, "end-date")?; + let increment: Option> = call.get_flag(engine_state, stack, "increment")?; + let days: Option> = call.get_flag(engine_state, stack, "days")?; + let reverse = call.has_flag("reverse"); + + let sep: String = match separator { + Some(s) => { + if s.item == r"\t" { + '\t'.to_string() + } else if s.item == r"\n" { + '\n'.to_string() + } else if s.item == r"\r" { + '\r'.to_string() + } else { + let vec_s: Vec = s.item.chars().collect(); + if vec_s.is_empty() { + return Err(ShellError::SpannedLabeledError( + "Expected a single separator char from --separator".to_string(), + "requires a single character string input".to_string(), + s.span, + )); + }; + vec_s.iter().collect() + } + } + _ => '\n'.to_string(), + }; + + let outformat = match output_format { + Some(s) => Some(Value::string(s.item, s.span)), + _ => None, + }; + + let informat = match input_format { + Some(s) => Some(Value::string(s.item, s.span)), + _ => None, + }; + + let begin = match begin_date { + Some(s) => Some(s.item), + _ => None, + }; + + let end = match end_date { + Some(s) => Some(s.item), + _ => None, + }; + + let inc = match increment { + Some(i) => Value::int(i.item, i.span), + _ => Value::int(1_i64, Span::test_data()), + }; + + let day_count = days.map(|i| Value::int(i.item, i.span)); + + let mut rev = false; + if reverse { + rev = reverse; + } + + Ok( + run_seq_dates(sep, outformat, informat, begin, end, inc, day_count, rev)? + .into_pipeline_data(), + ) + } +} + +pub fn parse_date_string(s: &str, format: &str) -> Result { + let d = match NaiveDate::parse_from_str(s, format) { + Ok(d) => d, + Err(_) => return Err("Failed to parse date."), + }; + Ok(d) +} + +#[allow(clippy::too_many_arguments)] +pub fn run_seq_dates( + separator: String, + output_format: Option, + input_format: Option, + beginning_date: Option, + ending_date: Option, + increment: Value, + day_count: Option, + reverse: bool, +) -> Result { + let today = Local::today().naive_local(); + let mut step_size: i64 = increment + .as_i64() + .expect("unable to change increment to i64"); + + if step_size == 0 { + return Err(ShellError::SpannedLabeledError( + "increment cannot be 0".to_string(), + "increment cannot be 0".to_string(), + increment.span()?, + )); + } + + let in_format = match input_format { + Some(i) => match i.as_string() { + Ok(v) => v, + Err(e) => { + return Err(ShellError::LabeledError( + e.to_string(), + "error with input_format as_string".to_string(), + )); + } + }, + _ => "%Y-%m-%d".to_string(), + }; + + let out_format = match output_format { + Some(i) => match i.as_string() { + Ok(v) => v, + Err(e) => { + return Err(ShellError::LabeledError( + e.to_string(), + "error with output_format as_string".to_string(), + )); + } + }, + _ => "%Y-%m-%d".to_string(), + }; + + let start_date = match beginning_date { + Some(d) => match parse_date_string(&d, &in_format) { + Ok(nd) => nd, + Err(e) => { + return Err(ShellError::SpannedLabeledError( + e.to_string(), + "Failed to parse date".to_string(), + Span::test_data(), + )) + } + }, + _ => today, + }; + + let mut end_date = match ending_date { + Some(d) => match parse_date_string(&d, &in_format) { + Ok(nd) => nd, + Err(e) => { + return Err(ShellError::SpannedLabeledError( + e.to_string(), + "Failed to parse date".to_string(), + Span::test_data(), + )) + } + }, + _ => today, + }; + + let mut days_to_output = match day_count { + Some(d) => d.as_i64()?, + None => 0i64, + }; + + // Make the signs opposite if we're created dates in reverse direction + if reverse { + step_size *= -1; + days_to_output *= -1; + } + + if days_to_output != 0 { + end_date = match start_date.checked_add_signed(Duration::days(days_to_output)) { + Some(date) => date, + None => { + return Err(ShellError::SpannedLabeledError( + "integer value too large".to_string(), + "integer value too large".to_string(), + Span::test_data(), + )); + } + } + } + + // conceptually counting down with a positive step or counting up with a negative step + // makes no sense, attempt to do what one means by inverting the signs in those cases. + if (start_date > end_date) && (step_size > 0) || (start_date < end_date) && step_size < 0 { + step_size = -step_size; + } + + let is_out_of_range = + |next| (step_size > 0 && next > end_date) || (step_size < 0 && next < end_date); + + let mut next = start_date; + if is_out_of_range(next) { + return Err(ShellError::SpannedLabeledError( + "date is out of range".to_string(), + "date is out of range".to_string(), + Span::test_data(), + )); + } + + let mut ret_str = String::from(""); + loop { + ret_str.push_str(&next.format(&out_format).to_string()); + next += Duration::days(step_size); + + if is_out_of_range(next) { + break; + } + + ret_str.push_str(&separator); + } + + let rows: Vec = ret_str + .lines() + .map(|v| Value::string(v, Span::test_data())) + .collect(); + + Ok(Value::List { + vals: rows, + span: Span::test_data(), + }) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SeqDate {}) + } +} diff --git a/crates/nu-command/src/hash/base64.rs b/crates/nu-command/src/hash/base64.rs new file mode 100644 index 0000000000..0a5ccc424e --- /dev/null +++ b/crates/nu-command/src/hash/base64.rs @@ -0,0 +1,299 @@ +use base64::{decode_config, encode_config}; +use nu_engine::CallExt; +use nu_protocol::ast::{Call, CellPath}; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Base64Config { + pub character_set: String, + pub action_type: ActionType, +} + +#[derive(Clone, Copy, PartialEq)] +pub enum ActionType { + Encode, + Decode, +} + +#[derive(Clone)] +pub struct Base64; + +impl Command for Base64 { + fn name(&self) -> &str { + "hash base64" + } + + fn signature(&self) -> Signature { + Signature::build("hash base64") + .named( + "character_set", + SyntaxShape::String, + "specify the character rules for encoding the input.\n\ + \tValid values are 'standard', 'standard-no-padding', 'url-safe', 'url-safe-no-padding',\ + 'binhex', 'bcrypt', 'crypt'", + Some('c'), + ) + .switch( + "encode", + "encode the input as base64. This is the default behavior if not specified.", + Some('e') + ) + .switch( + "decode", + "decode the input from base64", + Some('d')) + .rest( + "rest", + SyntaxShape::CellPath, + "optionally base64 encode / decode data by column paths", + ) + .category(Category::Hash) + } + + fn usage(&self) -> &str { + "base64 encode or decode a value" + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Base64 encode a string with default settings", + example: "echo 'username:password' | hash base64", + result: Some(Value::string("dXNlcm5hbWU6cGFzc3dvcmQ=", Span::test_data())), + }, + Example { + description: "Base64 encode a string with the binhex character set", + example: "echo 'username:password' | hash base64 --character_set binhex --encode", + result: Some(Value::string("F@0NEPjJD97kE'&bEhFZEP3", Span::test_data())), + }, + Example { + description: "Base64 decode a value", + example: "echo 'dXNlcm5hbWU6cGFzc3dvcmQ=' | hash base64 --decode", + result: Some(Value::string("username:password", Span::test_data())), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input) + } +} + +fn operate( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let head = call.head; + let encode = call.has_flag("encode"); + let decode = call.has_flag("decode"); + let character_set: Option> = + call.get_flag(engine_state, stack, "character_set")?; + let column_paths: Vec = call.rest(engine_state, stack, 0)?; + + if encode && decode { + return Err(ShellError::SpannedLabeledError( + "only one of --decode and --encode flags can be used".to_string(), + "conflicting flags".to_string(), + head, + )); + } + + // Default the action to be encoding if no flags are specified. + let action_type = if decode { + ActionType::Decode + } else { + ActionType::Encode + }; + + // Default the character set to standard if the argument is not specified. + let character_set = match character_set { + Some(inner_tag) => inner_tag.item, + None => "standard".to_string(), + }; + + let encoding_config = Base64Config { + character_set, + action_type, + }; + + input.map( + move |v| { + if column_paths.is_empty() { + match action(&v, &encoding_config, &head) { + Ok(v) => v, + Err(e) => Value::Error { error: e }, + } + } else { + let mut ret = v; + + for path in &column_paths { + let config = encoding_config.clone(); + + let r = ret.update_cell_path( + &path.members, + Box::new(move |old| match action(old, &config, &head) { + Ok(v) => v, + Err(e) => Value::Error { error: e }, + }), + ); + if let Err(error) = r { + return Value::Error { error }; + } + } + + ret + } + }, + engine_state.ctrlc.clone(), + ) +} + +fn action( + input: &Value, + base64_config: &Base64Config, + command_span: &Span, +) -> Result { + match input { + Value::String { val, span } => { + let base64_config_enum: base64::Config = if &base64_config.character_set == "standard" { + base64::STANDARD + } else if &base64_config.character_set == "standard-no-padding" { + base64::STANDARD_NO_PAD + } else if &base64_config.character_set == "url-safe" { + base64::URL_SAFE + } else if &base64_config.character_set == "url-safe-no-padding" { + base64::URL_SAFE_NO_PAD + } else if &base64_config.character_set == "binhex" { + base64::BINHEX + } else if &base64_config.character_set == "bcrypt" { + base64::BCRYPT + } else if &base64_config.character_set == "crypt" { + base64::CRYPT + } else { + return Err(ShellError::SpannedLabeledError( + "value is not an accepted character set".to_string(), + format!( + "{} is not a valid character-set.\nPlease use `help hash base64` to see a list of valid character sets.", + &base64_config.character_set + ), + *span, + )); + }; + + match base64_config.action_type { + ActionType::Encode => Ok(Value::string( + encode_config(&val, base64_config_enum), + *command_span, + )), + + ActionType::Decode => { + let decode_result = decode_config(&val, base64_config_enum); + + match decode_result { + Ok(decoded_value) => Ok(Value::string( + std::string::String::from_utf8_lossy(&decoded_value), + *command_span, + )), + Err(_) => Err(ShellError::SpannedLabeledError( + "value could not be base64 decoded".to_string(), + format!( + "invalid base64 input for character set {}", + &base64_config.character_set + ), + *command_span, + )), + } + } + } + } + other => Err(ShellError::TypeMismatch( + format!("value is {}, not string", other.get_type()), + other.span()?, + )), + } +} + +#[cfg(test)] +mod tests { + use super::{action, ActionType, Base64Config}; + use nu_protocol::{Span, Value}; + + #[test] + fn base64_encode_standard() { + let word = Value::string("username:password", Span::test_data()); + let expected = Value::string("dXNlcm5hbWU6cGFzc3dvcmQ=", Span::test_data()); + + let actual = action( + &word, + &Base64Config { + character_set: "standard".to_string(), + action_type: ActionType::Encode, + }, + &Span::test_data(), + ) + .unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn base64_encode_standard_no_padding() { + let word = Value::string("username:password", Span::test_data()); + let expected = Value::string("dXNlcm5hbWU6cGFzc3dvcmQ", Span::test_data()); + + let actual = action( + &word, + &Base64Config { + character_set: "standard-no-padding".to_string(), + action_type: ActionType::Encode, + }, + &Span::test_data(), + ) + .unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn base64_encode_url_safe() { + let word = Value::string("this is for url", Span::test_data()); + let expected = Value::string("dGhpcyBpcyBmb3IgdXJs", Span::test_data()); + + let actual = action( + &word, + &Base64Config { + character_set: "url-safe".to_string(), + action_type: ActionType::Encode, + }, + &Span::test_data(), + ) + .unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn base64_decode_binhex() { + let word = Value::string("A5\"KC9jRB@IIF'8bF!", Span::test_data()); + let expected = Value::string("a binhex test", Span::test_data()); + + let actual = action( + &word, + &Base64Config { + character_set: "binhex".to_string(), + action_type: ActionType::Decode, + }, + &Span::test_data(), + ) + .unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-command/src/hash/generic_digest.rs b/crates/nu-command/src/hash/generic_digest.rs new file mode 100644 index 0000000000..30a5f6b472 --- /dev/null +++ b/crates/nu-command/src/hash/generic_digest.rs @@ -0,0 +1,113 @@ +use nu_engine::CallExt; +use nu_protocol::ast::{Call, CellPath}; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Example, PipelineData, ShellError, Signature, SyntaxShape, Value}; +use std::marker::PhantomData; + +pub trait HashDigest: digest::Digest + Clone { + fn name() -> &'static str; + fn examples() -> Vec; +} + +#[derive(Clone)] +pub struct GenericDigest { + name: String, + usage: String, + phantom: PhantomData, +} + +impl Default for GenericDigest { + fn default() -> Self { + Self { + name: format!("hash {}", D::name()), + usage: format!("hash a value using the {} hash algorithm", D::name()), + phantom: PhantomData, + } + } +} + +impl Command for GenericDigest +where + D: HashDigest + Send + Sync + 'static, + digest::Output: core::fmt::LowerHex, +{ + fn name(&self) -> &str { + &self.name + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).rest( + "rest", + SyntaxShape::CellPath, + format!("optionally {} hash data by cell path", D::name()), + ) + } + + fn usage(&self) -> &str { + &self.usage + } + + fn examples(&self) -> Vec { + D::examples() + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let cell_paths: Vec = call.rest(engine_state, stack, 0)?; + + input.map( + move |v| { + if cell_paths.is_empty() { + action::(&v) + } else { + let mut v = v; + for path in &cell_paths { + let ret = v + .update_cell_path(&path.members, Box::new(move |old| action::(old))); + if let Err(error) = ret { + return Value::Error { error }; + } + } + v + } + }, + engine_state.ctrlc.clone(), + ) + } +} + +pub fn action(input: &Value) -> Value +where + D: HashDigest, + digest::Output: core::fmt::LowerHex, +{ + let (bytes, span) = match input { + Value::String { val, span } => (val.as_bytes(), *span), + Value::Binary { val, span } => (val.as_slice(), *span), + other => { + let span = match input.span() { + Ok(span) => span, + Err(error) => return Value::Error { error }, + }; + + return Value::Error { + error: ShellError::UnsupportedInput( + format!( + "Type `{}` is not supported for {} hashing input", + other.get_type(), + D::name() + ), + span, + ), + }; + } + }; + + let val = format!("{:x}", D::digest(bytes)); + Value::String { val, span } +} diff --git a/crates/nu-command/src/hash/hash_.rs b/crates/nu-command/src/hash/hash_.rs new file mode 100644 index 0000000000..8a3b1d1938 --- /dev/null +++ b/crates/nu-command/src/hash/hash_.rs @@ -0,0 +1,35 @@ +use nu_engine::get_full_help; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, IntoPipelineData, PipelineData, ShellError, Signature, Value}; + +#[derive(Clone)] +pub struct Hash; + +impl Command for Hash { + fn name(&self) -> &str { + "hash" + } + + fn signature(&self) -> Signature { + Signature::build("hash").category(Category::Hash) + } + + fn usage(&self) -> &str { + "Apply hash function." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(Value::String { + val: get_full_help(&Self.signature(), &Self.examples(), engine_state, stack), + span: call.head, + } + .into_pipeline_data()) + } +} diff --git a/crates/nu-command/src/hash/md5.rs b/crates/nu-command/src/hash/md5.rs new file mode 100644 index 0000000000..5d1af8fc06 --- /dev/null +++ b/crates/nu-command/src/hash/md5.rs @@ -0,0 +1,68 @@ +use super::generic_digest::{GenericDigest, HashDigest}; +use ::md5::Md5; +use nu_protocol::{Example, Span, Value}; + +pub type HashMd5 = GenericDigest; + +impl HashDigest for Md5 { + fn name() -> &'static str { + "md5" + } + + fn examples() -> Vec { + vec![ + Example { + description: "md5 encode a string", + example: "echo 'abcdefghijklmnopqrstuvwxyz' | hash md5", + result: Some(Value::String { + val: "c3fcd3d76192e4007dfb496cca67e13b".to_owned(), + span: Span::test_data(), + }), + }, + Example { + description: "md5 encode a file", + example: "open ./nu_0_24_1_windows.zip | hash md5", + result: None, + }, + ] + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::hash::generic_digest; + + #[test] + fn test_examples() { + crate::test_examples(HashMd5::default()) + } + + #[test] + fn hash_string() { + let binary = Value::String { + val: "abcdefghijklmnopqrstuvwxyz".to_owned(), + span: Span::test_data(), + }; + let expected = Value::String { + val: "c3fcd3d76192e4007dfb496cca67e13b".to_owned(), + span: Span::test_data(), + }; + let actual = generic_digest::action::(&binary); + assert_eq!(actual, expected); + } + + #[test] + fn hash_bytes() { + let binary = Value::Binary { + val: vec![0xC0, 0xFF, 0xEE], + span: Span::test_data(), + }; + let expected = Value::String { + val: "5f80e231382769b0102b1164cf722d83".to_owned(), + span: Span::test_data(), + }; + let actual = generic_digest::action::(&binary); + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-command/src/hash/mod.rs b/crates/nu-command/src/hash/mod.rs new file mode 100644 index 0000000000..013ff57f05 --- /dev/null +++ b/crates/nu-command/src/hash/mod.rs @@ -0,0 +1,10 @@ +mod base64; +mod generic_digest; +mod hash_; +mod md5; +mod sha256; + +pub use self::base64::Base64; +pub use self::hash_::Hash; +pub use self::md5::HashMd5; +pub use self::sha256::HashSha256; diff --git a/crates/nu-command/src/hash/sha256.rs b/crates/nu-command/src/hash/sha256.rs new file mode 100644 index 0000000000..5c1485a35e --- /dev/null +++ b/crates/nu-command/src/hash/sha256.rs @@ -0,0 +1,69 @@ +use super::generic_digest::{GenericDigest, HashDigest}; +use ::sha2::Sha256; +use nu_protocol::{Example, Span, Value}; + +pub type HashSha256 = GenericDigest; + +impl HashDigest for Sha256 { + fn name() -> &'static str { + "sha256" + } + + fn examples() -> Vec { + vec![ + Example { + description: "sha256 encode a string", + example: "echo 'abcdefghijklmnopqrstuvwxyz' | hash sha256", + result: Some(Value::String { + val: "71c480df93d6ae2f1efad1447c66c9525e316218cf51fc8d9ed832f2daf18b73" + .to_owned(), + span: Span::test_data(), + }), + }, + Example { + description: "sha256 encode a file", + example: "open ./nu_0_24_1_windows.zip | hash sha256", + result: None, + }, + ] + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::hash::generic_digest; + + #[test] + fn test_examples() { + crate::test_examples(HashSha256::default()) + } + + #[test] + fn hash_string() { + let binary = Value::String { + val: "abcdefghijklmnopqrstuvwxyz".to_owned(), + span: Span::test_data(), + }; + let expected = Value::String { + val: "71c480df93d6ae2f1efad1447c66c9525e316218cf51fc8d9ed832f2daf18b73".to_owned(), + span: Span::test_data(), + }; + let actual = generic_digest::action::(&binary); + assert_eq!(actual, expected); + } + + #[test] + fn hash_bytes() { + let binary = Value::Binary { + val: vec![0xC0, 0xFF, 0xEE], + span: Span::test_data(), + }; + let expected = Value::String { + val: "c47a10dc272b1221f0380a2ae0f7d7fa830b3e378f2f5309bbf13f61ad211913".to_owned(), + span: Span::test_data(), + }; + let actual = generic_digest::action::(&binary); + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-command/src/lib.rs b/crates/nu-command/src/lib.rs index ecb91f5c2c..3caca19722 100644 --- a/crates/nu-command/src/lib.rs +++ b/crates/nu-command/src/lib.rs @@ -1,27 +1,50 @@ -#![recursion_limit = "2048"] - -#[cfg(test)] -#[macro_use] -extern crate indexmap; - -#[macro_use] -mod prelude; -mod classified; -pub mod commands; +mod conversions; +mod core_commands; +mod date; mod default_context; -pub mod utils; +mod env; +mod example_test; +mod experimental; +mod filesystem; +mod filters; +mod formats; +mod generators; +mod hash; +mod math; +mod network; +mod path; +mod platform; +mod random; +mod shells; +mod strings; +mod system; +mod viewers; +pub use conversions::*; +pub use core_commands::*; +pub use date::*; +pub use default_context::*; +pub use env::*; #[cfg(test)] -mod examples; +pub use example_test::test_examples; +pub use experimental::*; +pub use filesystem::*; +pub use filters::*; +pub use formats::*; +pub use generators::*; +pub use hash::*; +pub use math::*; +pub use network::*; +pub use path::*; +pub use platform::*; +pub use random::*; +pub use shells::*; +pub use strings::*; +pub use system::*; +pub use viewers::*; -pub use crate::default_context::create_default_context; -pub use nu_data::config; -pub use nu_data::dict::TaggedListBuilder; -pub use nu_data::primitive; -pub use nu_data::value; -pub use nu_stream::{ActionStream, InputStream, InterruptibleStream}; -pub use nu_value_ext::ValueExt; -pub use num_traits::cast::ToPrimitive; +#[cfg(feature = "dataframe")] +mod dataframe; -// TODO: Temporary redirect -pub use nu_protocol::{did_you_mean, TaggedDictBuilder}; +#[cfg(feature = "dataframe")] +pub use dataframe::*; diff --git a/crates/nu-command/src/math/abs.rs b/crates/nu-command/src/math/abs.rs new file mode 100644 index 0000000000..fa3ece0a2b --- /dev/null +++ b/crates/nu-command/src/math/abs.rs @@ -0,0 +1,84 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Span, Value}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "math abs" + } + + fn signature(&self) -> Signature { + Signature::build("math abs").category(Category::Math) + } + + fn usage(&self) -> &str { + "Returns absolute values of a list of numbers" + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + input.map( + move |value| abs_helper(value, head), + engine_state.ctrlc.clone(), + ) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Get absolute of each value in a list of numbers", + example: "[-50 -100.0 25] | math abs", + result: Some(Value::List { + vals: vec![ + Value::test_int(50), + Value::Float { + val: 100.0, + span: Span::test_data(), + }, + Value::test_int(25), + ], + span: Span::test_data(), + }), + }] + } +} + +fn abs_helper(val: Value, head: Span) -> Value { + match val { + Value::Int { val, span } => Value::int(val.abs(), span), + Value::Float { val, span } => Value::Float { + val: val.abs(), + span, + }, + Value::Duration { val, span } => Value::Duration { + val: val.abs(), + span, + }, + _ => Value::Error { + error: ShellError::UnsupportedInput( + String::from("Only numerical values are supported"), + head, + ), + }, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/math/avg.rs b/crates/nu-command/src/math/avg.rs new file mode 100644 index 0000000000..f8d0c11187 --- /dev/null +++ b/crates/nu-command/src/math/avg.rs @@ -0,0 +1,84 @@ +use crate::math::reducers::{reducer_for, Reduce}; +use crate::math::utils::run_with_function; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Span, Value}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "math avg" + } + + fn signature(&self) -> Signature { + Signature::build("math avg").category(Category::Math) + } + + fn usage(&self) -> &str { + "Finds the average of a list of numbers or tables" + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + run_with_function(call, input, average) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Get the average of a list of numbers", + example: "[-50 100.0 25] | math avg", + result: Some(Value::Float { + val: 25.0, + span: Span::test_data(), + }), + }] + } +} + +pub fn average(values: &[Value], head: &Span) -> Result { + let sum = reducer_for(Reduce::Summation); + let total = &sum( + Value::Int { + val: 0, + span: *head, + }, + values.to_vec(), + *head, + )?; + match total { + Value::Filesize { val, span } => Ok(Value::Filesize { + val: val / values.len() as i64, + span: *span, + }), + Value::Duration { val, span } => Ok(Value::Duration { + val: val / values.len() as i64, + span: *span, + }), + _ => total.div( + *head, + &Value::Int { + val: values.len() as i64, + span: *head, + }, + ), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/math/ceil.rs b/crates/nu-command/src/math/ceil.rs new file mode 100644 index 0000000000..ffc2a3b069 --- /dev/null +++ b/crates/nu-command/src/math/ceil.rs @@ -0,0 +1,73 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Span, Value}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "math ceil" + } + + fn signature(&self) -> Signature { + Signature::build("math ceil").category(Category::Math) + } + + fn usage(&self) -> &str { + "Applies the ceil function to a list of numbers" + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + input.map( + move |value| operate(value, head), + engine_state.ctrlc.clone(), + ) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Apply the ceil function to a list of numbers", + example: "[1.5 2.3 -3.1] | math ceil", + result: Some(Value::List { + vals: vec![Value::test_int(2), Value::test_int(3), Value::test_int(-3)], + span: Span::test_data(), + }), + }] + } +} + +fn operate(value: Value, head: Span) -> Value { + match value { + Value::Int { .. } => value, + Value::Float { val, span } => Value::Float { + val: val.ceil(), + span, + }, + other => Value::Error { + error: ShellError::UnsupportedInput( + String::from("Only numerical values are supported"), + other.span().unwrap_or(head), + ), + }, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/math/eval.rs b/crates/nu-command/src/math/eval.rs new file mode 100644 index 0000000000..02bc56f3d2 --- /dev/null +++ b/crates/nu-command/src/math/eval.rs @@ -0,0 +1,120 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "math eval" + } + + fn usage(&self) -> &str { + "Evaluate a math expression into a number" + } + + fn signature(&self) -> Signature { + Signature::build("math eval") + .optional( + "math expression", + SyntaxShape::String, + "the math expression to evaluate", + ) + .category(Category::Math) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let spanned_expr: Option> = call.opt(engine_state, stack, 0)?; + eval(spanned_expr, input, engine_state, call.head) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Evalulate math in the pipeline", + example: "'10 / 4' | math eval", + result: Some(Value::Float { + val: 2.5, + span: Span::test_data(), + }), + }] + } +} + +pub fn eval( + spanned_expr: Option>, + input: PipelineData, + engine_state: &EngineState, + head: Span, +) -> Result { + if let Some(expr) = spanned_expr { + match parse(&expr.item, &expr.span) { + Ok(value) => Ok(PipelineData::Value(value, None)), + Err(err) => Err(ShellError::UnsupportedInput( + format!("Math evaluation error: {}", err), + expr.span, + )), + } + } else { + if let PipelineData::Value(Value::Nothing { .. }, ..) = input { + return Ok(input); + } + input.map( + move |val| { + if let Ok(string) = val.as_string() { + match parse(&string, &val.span().unwrap_or(head)) { + Ok(value) => value, + Err(err) => Value::Error { + error: ShellError::UnsupportedInput( + format!("Math evaluation error: {}", err), + val.span().unwrap_or(head), + ), + }, + } + } else { + Value::Error { + error: ShellError::UnsupportedInput( + "Expected a string from pipeline".to_string(), + val.span().unwrap_or(head), + ), + } + } + }, + engine_state.ctrlc.clone(), + ) + } +} + +pub fn parse(math_expression: &str, span: &Span) -> Result { + let mut ctx = meval::Context::new(); + ctx.var("tau", std::f64::consts::TAU); + match meval::eval_str_with_context(math_expression, &ctx) { + Ok(num) if num.is_infinite() || num.is_nan() => Err("cannot represent result".to_string()), + Ok(num) => Ok(Value::Float { + val: num, + span: *span, + }), + Err(error) => Err(error.to_string().to_lowercase()), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/math/floor.rs b/crates/nu-command/src/math/floor.rs new file mode 100644 index 0000000000..5442eaf2e1 --- /dev/null +++ b/crates/nu-command/src/math/floor.rs @@ -0,0 +1,73 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Span, Value}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "math floor" + } + + fn signature(&self) -> Signature { + Signature::build("math floor").category(Category::Math) + } + + fn usage(&self) -> &str { + "Applies the floor function to a list of numbers" + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + input.map( + move |value| operate(value, head), + engine_state.ctrlc.clone(), + ) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Apply the floor function to a list of numbers", + example: "[1.5 2.3 -3.1] | math floor", + result: Some(Value::List { + vals: vec![Value::test_int(1), Value::test_int(2), Value::test_int(-4)], + span: Span::test_data(), + }), + }] + } +} + +fn operate(value: Value, head: Span) -> Value { + match value { + Value::Int { .. } => value, + Value::Float { val, span } => Value::Float { + val: val.floor(), + span, + }, + other => Value::Error { + error: ShellError::UnsupportedInput( + String::from("Only numerical values are supported"), + other.span().unwrap_or(head), + ), + }, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/math/math_.rs b/crates/nu-command/src/math/math_.rs new file mode 100644 index 0000000000..ae2e13c186 --- /dev/null +++ b/crates/nu-command/src/math/math_.rs @@ -0,0 +1,42 @@ +use nu_engine::get_full_help; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, IntoPipelineData, PipelineData, Signature, Value, +}; + +#[derive(Clone)] +pub struct MathCommand; + +impl Command for MathCommand { + fn name(&self) -> &str { + "math" + } + + fn signature(&self) -> Signature { + Signature::build("math").category(Category::Math) + } + + fn usage(&self) -> &str { + "Use mathematical functions as aggregate functions on a list of numbers or tables." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(Value::String { + val: get_full_help( + &MathCommand.signature(), + &MathCommand.examples(), + engine_state, + stack, + ), + span: call.head, + } + .into_pipeline_data()) + } +} diff --git a/crates/nu-command/src/math/max.rs b/crates/nu-command/src/math/max.rs new file mode 100644 index 0000000000..2635192c57 --- /dev/null +++ b/crates/nu-command/src/math/max.rs @@ -0,0 +1,57 @@ +use crate::math::reducers::{reducer_for, Reduce}; +use crate::math::utils::run_with_function; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Span, Value}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "math max" + } + + fn signature(&self) -> Signature { + Signature::build("math max").category(Category::Math) + } + + fn usage(&self) -> &str { + "Finds the maximum within a list of numbers or tables" + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + run_with_function(call, input, maximum) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Find the maximum of list of numbers", + example: "[-50 100 25] | math max", + result: Some(Value::test_int(100)), + }] + } +} + +pub fn maximum(values: &[Value], head: &Span) -> Result { + let max_func = reducer_for(Reduce::Maximum); + max_func(Value::nothing(*head), values.to_vec(), *head) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/math/median.rs b/crates/nu-command/src/math/median.rs new file mode 100644 index 0000000000..cae1499a8f --- /dev/null +++ b/crates/nu-command/src/math/median.rs @@ -0,0 +1,123 @@ +use std::cmp::Ordering; + +use crate::math::avg::average; +use crate::math::utils::run_with_function; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Span, Value}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "math median" + } + + fn signature(&self) -> Signature { + Signature::build("math median").category(Category::Math) + } + + fn usage(&self) -> &str { + "Gets the median of a list of numbers" + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + run_with_function(call, input, median) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Get the median of a list of numbers", + example: "[3 8 9 12 12 15] | math median", + result: Some(Value::Float { + val: 10.5, + span: Span::test_data(), + }), + }] + } +} + +enum Pick { + MedianAverage, + Median, +} + +pub fn median(values: &[Value], head: &Span) -> 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()); + } + + if let Some(Err(values)) = values + .windows(2) + .map(|elem| { + if elem[0].partial_cmp(&elem[1]).is_none() { + return Err(ShellError::OperatorMismatch { + op_span: *head, + lhs_ty: elem[0].get_type(), + lhs_span: elem[0].span()?, + rhs_ty: elem[1].get_type(), + rhs_span: elem[1].span()?, + }); + } + Ok(elem[0].partial_cmp(&elem[1]).unwrap_or(Ordering::Equal)) + }) + .find(|elem| elem.is_err()) + { + return Err(values); + } + + sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); + + match take { + Pick::Median => { + let idx = (values.len() as f64 / 2.0).floor() as usize; + let out = sorted + .get(idx) + .ok_or_else(|| ShellError::UnsupportedInput("Empty input".to_string(), *head))?; + 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::UnsupportedInput("Empty input".to_string(), *head))? + .clone(); + + let right = sorted + .get(idx_end) + .ok_or_else(|| ShellError::UnsupportedInput("Empty input".to_string(), *head))? + .clone(); + + average(&[left, right], head) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/math/min.rs b/crates/nu-command/src/math/min.rs new file mode 100644 index 0000000000..c897f4394b --- /dev/null +++ b/crates/nu-command/src/math/min.rs @@ -0,0 +1,57 @@ +use crate::math::reducers::{reducer_for, Reduce}; +use crate::math::utils::run_with_function; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Span, Value}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "math min" + } + + fn signature(&self) -> Signature { + Signature::build("math min").category(Category::Math) + } + + fn usage(&self) -> &str { + "Finds the minimum within a list of numbers or tables" + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + run_with_function(call, input, minimum) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Get the minimum of a list of numbers", + example: "[-50 100 25] | math min", + result: Some(Value::test_int(-50)), + }] + } +} + +pub fn minimum(values: &[Value], head: &Span) -> Result { + let min_func = reducer_for(Reduce::Minimum); + min_func(Value::nothing(*head), values.to_vec(), *head) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/math/mod.rs b/crates/nu-command/src/math/mod.rs new file mode 100644 index 0000000000..b201ec56c7 --- /dev/null +++ b/crates/nu-command/src/math/mod.rs @@ -0,0 +1,35 @@ +mod abs; +mod avg; +mod ceil; +mod eval; +mod floor; +pub mod math_; +mod max; +mod median; +mod min; +mod mode; +mod product; +mod reducers; +mod round; +mod sqrt; +mod stddev; +mod sum; +mod utils; +mod variance; + +pub use abs::SubCommand as MathAbs; +pub use avg::SubCommand as MathAvg; +pub use ceil::SubCommand as MathCeil; +pub use eval::SubCommand as MathEval; +pub use floor::SubCommand as MathFloor; +pub use math_::MathCommand as Math; +pub use max::SubCommand as MathMax; +pub use median::SubCommand as MathMedian; +pub use min::SubCommand as MathMin; +pub use mode::SubCommand as MathMode; +pub use product::SubCommand as MathProduct; +pub use round::SubCommand as MathRound; +pub use sqrt::SubCommand as MathSqrt; +pub use stddev::SubCommand as MathStddev; +pub use sum::SubCommand as MathSum; +pub use variance::SubCommand as MathVariance; diff --git a/crates/nu-command/src/math/mode.rs b/crates/nu-command/src/math/mode.rs new file mode 100644 index 0000000000..4115a4f07b --- /dev/null +++ b/crates/nu-command/src/math/mode.rs @@ -0,0 +1,171 @@ +use crate::math::utils::run_with_function; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Span, Value}; +use std::cmp::Ordering; + +#[derive(Clone)] +pub struct SubCommand; + +#[derive(Hash, Eq, PartialEq, Debug)] +enum NumberTypes { + Float, + Int, + Duration, + Filesize, +} + +#[derive(Hash, Eq, PartialEq, Debug)] +struct HashableType { + bytes: [u8; 8], + original_type: NumberTypes, +} + +impl HashableType { + fn new(bytes: [u8; 8], original_type: NumberTypes) -> HashableType { + HashableType { + bytes, + original_type, + } + } +} + +impl Command for SubCommand { + fn name(&self) -> &str { + "math mode" + } + + fn signature(&self) -> Signature { + Signature::build("math mode").category(Category::Math) + } + + fn usage(&self) -> &str { + "Gets the most frequent element(s) from a list of numbers or tables" + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + run_with_function(call, input, mode) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Get the mode(s) of a list of numbers", + example: "[3 3 9 12 12 15] | math mode", + result: Some(Value::List { + vals: vec![Value::test_int(3), Value::test_int(12)], + span: Span::test_data(), + }), + }] + } +} + +pub fn mode(values: &[Value], head: &Span) -> Result { + if let Some(Err(values)) = values + .windows(2) + .map(|elem| { + if elem[0].partial_cmp(&elem[1]).is_none() { + return Err(ShellError::OperatorMismatch { + op_span: *head, + lhs_ty: elem[0].get_type(), + lhs_span: elem[0].span()?, + rhs_ty: elem[1].get_type(), + rhs_span: elem[1].span()?, + }); + } + Ok(elem[0].partial_cmp(&elem[1]).unwrap_or(Ordering::Equal)) + }) + .find(|elem| elem.is_err()) + { + return Err(values); + } + //In e-q, Value doesn't implement Hash or Eq, so we have to get the values inside + // But f64 doesn't implement Hash, so we get the binary representation to use as + // key in the HashMap + let hashable_values = values + .iter() + .map(|val| match val { + Value::Int { val, .. } => Ok(HashableType::new(val.to_ne_bytes(), NumberTypes::Int)), + Value::Duration { val, .. } => { + Ok(HashableType::new(val.to_ne_bytes(), NumberTypes::Duration)) + } + Value::Float { val, .. } => { + Ok(HashableType::new(val.to_ne_bytes(), NumberTypes::Float)) + } + Value::Filesize { val, .. } => { + Ok(HashableType::new(val.to_ne_bytes(), NumberTypes::Filesize)) + } + other => Err(ShellError::UnsupportedInput( + "Unable to give a result with this input".to_string(), + other.span()?, + )), + }) + .collect::, ShellError>>()?; + + let mut frequency_map = std::collections::HashMap::new(); + for v in hashable_values { + let counter = frequency_map.entry(v).or_insert(0); + *counter += 1; + } + + let mut max_freq = -1; + let mut modes = Vec::::new(); + for (value, frequency) in &frequency_map { + match max_freq.cmp(frequency) { + Ordering::Less => { + max_freq = *frequency; + modes.clear(); + modes.push(recreate_value(value, *head)); + } + Ordering::Equal => { + modes.push(recreate_value(value, *head)); + } + Ordering::Greater => (), + } + } + + modes.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); + Ok(Value::List { + vals: modes, + span: *head, + }) +} + +fn recreate_value(hashable_value: &HashableType, head: Span) -> Value { + let bytes = hashable_value.bytes; + match &hashable_value.original_type { + NumberTypes::Int => Value::Int { + val: i64::from_ne_bytes(bytes), + span: head, + }, + NumberTypes::Float => Value::Float { + val: f64::from_ne_bytes(bytes), + span: head, + }, + NumberTypes::Duration => Value::Duration { + val: i64::from_ne_bytes(bytes), + span: head, + }, + NumberTypes::Filesize => Value::Filesize { + val: i64::from_ne_bytes(bytes), + span: head, + }, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/math/product.rs b/crates/nu-command/src/math/product.rs new file mode 100644 index 0000000000..563d991371 --- /dev/null +++ b/crates/nu-command/src/math/product.rs @@ -0,0 +1,58 @@ +use crate::math::reducers::{reducer_for, Reduce}; +use crate::math::utils::run_with_function; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Span, Value}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "math product" + } + + fn signature(&self) -> Signature { + Signature::build("math product").category(Category::Math) + } + + fn usage(&self) -> &str { + "Finds the product of a list of numbers or tables" + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + run_with_function(call, input, product) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Get the product of a list of numbers", + example: "[2 3 3 4] | math product", + result: Some(Value::test_int(72)), + }] + } +} + +/// Calculate product of given values +pub fn product(values: &[Value], head: &Span) -> Result { + let product_func = reducer_for(Reduce::Product); + product_func(Value::nothing(*head), values.to_vec(), *head) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/math/reducers.rs b/crates/nu-command/src/math/reducers.rs new file mode 100644 index 0000000000..8f8eca2a74 --- /dev/null +++ b/crates/nu-command/src/math/reducers.rs @@ -0,0 +1,144 @@ +use nu_protocol::{ShellError, Span, Value}; +use std::cmp::Ordering; + +#[allow(dead_code)] +pub enum Reduce { + Summation, + Product, + Minimum, + Maximum, +} + +pub type ReducerFunction = + Box, Span) -> Result + Send + Sync + 'static>; + +pub fn reducer_for(command: Reduce) -> ReducerFunction { + match command { + Reduce::Summation => Box::new(|_, values, head| sum(values, head)), + Reduce::Product => Box::new(|_, values, head| product(values, head)), + Reduce::Minimum => Box::new(|_, values, head| min(values, head)), + Reduce::Maximum => Box::new(|_, values, head| max(values, head)), + } +} + +pub fn max(data: Vec, head: Span) -> Result { + let mut biggest = data + .first() + .ok_or_else(|| ShellError::UnsupportedInput("Empty input".to_string(), head))? + .clone(); + + for value in &data { + if let Some(result) = value.partial_cmp(&biggest) { + if result == Ordering::Greater { + biggest = value.clone(); + } + } else { + return Err(ShellError::OperatorMismatch { + op_span: head, + lhs_ty: biggest.get_type(), + lhs_span: biggest.span()?, + rhs_ty: value.get_type(), + rhs_span: value.span()?, + }); + } + } + Ok(biggest) +} + +pub fn min(data: Vec, head: Span) -> Result { + let mut smallest = data + .first() + .ok_or_else(|| ShellError::UnsupportedInput("Empty input".to_string(), head))? + .clone(); + + for value in &data { + if let Some(result) = value.partial_cmp(&smallest) { + if result == Ordering::Less { + smallest = value.clone(); + } + } else { + return Err(ShellError::OperatorMismatch { + op_span: head, + lhs_ty: smallest.get_type(), + lhs_span: smallest.span()?, + rhs_ty: value.get_type(), + rhs_span: value.span()?, + }); + } + } + Ok(smallest) +} + +pub fn sum(data: Vec, head: Span) -> Result { + let initial_value = data.get(0); + + let mut acc = match initial_value { + Some(Value::Filesize { span, .. }) => Ok(Value::Filesize { + val: 0, + span: *span, + }), + Some(Value::Duration { span, .. }) => Ok(Value::Duration { + val: 0, + span: *span, + }), + Some(Value::Int { span, .. }) | Some(Value::Float { span, .. }) => Ok(Value::Int { + val: 0, + span: *span, + }), + None => Err(ShellError::UnsupportedInput( + "Empty input".to_string(), + head, + )), + _ => Ok(Value::nothing(head)), + }?; + + for value in &data { + match value { + Value::Int { .. } + | Value::Float { .. } + | Value::Filesize { .. } + | Value::Duration { .. } => { + acc = acc.add(head, value)?; + } + other => { + return Err(ShellError::UnsupportedInput( + "Attempted to compute the sum of a value that cannot be summed".to_string(), + other.span().unwrap_or(head), + )); + } + } + } + Ok(acc) +} + +pub fn product(data: Vec, head: Span) -> Result { + let initial_value = data.get(0); + + let mut acc = match initial_value { + Some(Value::Int { span, .. }) | Some(Value::Float { span, .. }) => Ok(Value::Int { + val: 1, + span: *span, + }), + None => Err(ShellError::UnsupportedInput( + "Empty input".to_string(), + head, + )), + _ => Ok(Value::nothing(head)), + }?; + + for value in &data { + match value { + Value::Int { .. } | Value::Float { .. } => { + acc = acc.mul(head, value)?; + } + other => { + return Err(ShellError::UnsupportedInput( + "Attempted to compute the product of a value that cannot be multiplied" + .to_string(), + other.span().unwrap_or(head), + )); + } + } + } + Ok(acc) +} diff --git a/crates/nu-command/src/math/round.rs b/crates/nu-command/src/math/round.rs new file mode 100644 index 0000000000..06b0b601ea --- /dev/null +++ b/crates/nu-command/src/math/round.rs @@ -0,0 +1,114 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "math round" + } + + fn signature(&self) -> Signature { + Signature::build("math round") + .named( + "precision", + SyntaxShape::Number, + "digits of precision", + Some('p'), + ) + .category(Category::Math) + } + + fn usage(&self) -> &str { + "Applies the round function to a list of numbers" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let precision_param: Option = call.get_flag(engine_state, stack, "precision")?; + let head = call.head; + input.map( + move |value| operate(value, head, precision_param), + engine_state.ctrlc.clone(), + ) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Apply the round function to a list of numbers", + example: "[1.5 2.3 -3.1] | math round", + result: Some(Value::List { + vals: vec![Value::test_int(2), Value::test_int(2), Value::test_int(-3)], + span: Span::test_data(), + }), + }, + Example { + description: "Apply the round function with precision specified", + example: "[1.555 2.333 -3.111] | math round -p 2", + result: Some(Value::List { + vals: vec![ + Value::Float { + val: 1.56, + span: Span::test_data(), + }, + Value::Float { + val: 2.33, + span: Span::test_data(), + }, + Value::Float { + val: -3.11, + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }, + ] + } +} + +fn operate(value: Value, head: Span, precision: Option) -> Value { + match value { + Value::Float { val, span } => match precision { + Some(precision_number) => Value::Float { + val: ((val * ((10_f64).powf(precision_number as f64))).round() + / (10_f64).powf(precision_number as f64)), + span, + }, + None => Value::Int { + val: val.round() as i64, + span, + }, + }, + Value::Int { .. } => value, + other => Value::Error { + error: ShellError::UnsupportedInput( + String::from("Only numerical values are supported"), + other.span().unwrap_or(head), + ), + }, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/math/sqrt.rs b/crates/nu-command/src/math/sqrt.rs new file mode 100644 index 0000000000..dcf50ac32e --- /dev/null +++ b/crates/nu-command/src/math/sqrt.rs @@ -0,0 +1,91 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Span, Value}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "math sqrt" + } + + fn signature(&self) -> Signature { + Signature::build("math sqrt").category(Category::Math) + } + + fn usage(&self) -> &str { + "Applies the square root function to a list of numbers" + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + input.map( + move |value| operate(value, head), + engine_state.ctrlc.clone(), + ) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Apply the square root function to a list of numbers", + example: "[9 16] | math sqrt", + result: Some(Value::List { + vals: vec![Value::test_int(3), Value::test_int(4)], + span: Span::test_data(), + }), + }] + } +} + +fn operate(value: Value, head: Span) -> Value { + match value { + Value::Int { val, span } => { + let squared = (val as f64).sqrt(); + if squared.is_nan() { + return error_negative_sqrt(span); + } + Value::Float { val: squared, span } + } + Value::Float { val, span } => { + let squared = val.sqrt(); + if squared.is_nan() { + return error_negative_sqrt(span); + } + Value::Float { val: squared, span } + } + other => Value::Error { + error: ShellError::UnsupportedInput( + String::from("Only numerical values are supported"), + other.span().unwrap_or(head), + ), + }, + } +} + +fn error_negative_sqrt(span: Span) -> Value { + Value::Error { + error: ShellError::UnsupportedInput( + String::from("Can't square root a negative number"), + span, + ), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/math/stddev.rs b/crates/nu-command/src/math/stddev.rs new file mode 100644 index 0000000000..d7d370dd8a --- /dev/null +++ b/crates/nu-command/src/math/stddev.rs @@ -0,0 +1,83 @@ +use super::variance::compute_variance as variance; +use crate::math::utils::run_with_function; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Span, Value}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "math stddev" + } + + fn signature(&self) -> Signature { + Signature::build("math stddev") + .switch("sample", "calculate sample standard deviation", Some('s')) + .category(Category::Math) + } + + fn usage(&self) -> &str { + "Finds the stddev of a list of numbers or tables" + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let sample = call.has_flag("sample"); + run_with_function(call, input, compute_stddev(sample)) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Get the stddev of a list of numbers", + example: "[1 2 3 4 5] | math stddev", + result: Some(Value::Float { + val: std::f64::consts::SQRT_2, + span: Span::test_data(), + }), + }, + Example { + description: "Get the sample stddev of a list of numbers", + example: "[1 2 3 4 5] | math stddev -s", + result: Some(Value::Float { + val: 1.5811388300841898, + span: Span::test_data(), + }), + }, + ] + } +} + +pub fn compute_stddev(sample: bool) -> impl Fn(&[Value], &Span) -> Result { + move |values: &[Value], span: &Span| { + let variance = variance(sample)(values, span); + match variance { + Ok(Value::Float { val, span }) => Ok(Value::Float { val: val.sqrt(), span }), + Ok(Value::Int { val, span }) => Ok(Value::Float { val: (val as f64).sqrt(), span }), + Err(ShellError::UnsupportedInput(_, err_span)) => Err(ShellError::UnsupportedInput( + "Attempted to compute the standard deviation with an item that cannot be used for that.".to_string(), + err_span, + )), + other => other + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/math/sum.rs b/crates/nu-command/src/math/sum.rs new file mode 100644 index 0000000000..814856dec2 --- /dev/null +++ b/crates/nu-command/src/math/sum.rs @@ -0,0 +1,64 @@ +use crate::math::reducers::{reducer_for, Reduce}; +use crate::math::utils::run_with_function; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Span, Value}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "math sum" + } + + fn signature(&self) -> Signature { + Signature::build("math sum").category(Category::Math) + } + + fn usage(&self) -> &str { + "Finds the sum of a list of numbers or tables" + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + run_with_function(call, input, summation) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Sum a list of numbers", + example: "[1 2 3] | math sum", + result: Some(Value::test_int(6)), + }, + Example { + description: "Get the disk usage for the current directory", + example: "ls | get size | math sum", + result: None, + }, + ] + } +} + +pub fn summation(values: &[Value], head: &Span) -> Result { + let sum_func = reducer_for(Reduce::Summation); + sum_func(Value::nothing(*head), values.to_vec(), *head) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/math/utils.rs b/crates/nu-command/src/math/utils.rs new file mode 100644 index 0000000000..27dcbcf82d --- /dev/null +++ b/crates/nu-command/src/math/utils.rs @@ -0,0 +1,96 @@ +use indexmap::map::IndexMap; +use nu_protocol::ast::Call; +use nu_protocol::{IntoPipelineData, PipelineData, ShellError, Span, Spanned, Value}; + +pub fn run_with_function( + call: &Call, + input: PipelineData, + mf: impl Fn(&[Value], &Span) -> Result, +) -> Result { + let name = call.head; + let res = calculate(input, name, mf); + match res { + Ok(v) => Ok(v.into_pipeline_data()), + Err(e) => Err(e), + } +} + +fn helper_for_tables( + values: &[Value], + name: Span, + mf: impl Fn(&[Value], &Span) -> Result, +) -> Result { + // If we are not dealing with Primitives, then perhaps we are dealing with a table + // Create a key for each column name + let mut column_values = IndexMap::new(); + for val in values { + if let Value::Record { cols, vals, .. } = val { + for (key, value) in cols.iter().zip(vals.iter()) { + column_values + .entry(key.clone()) + .and_modify(|v: &mut Vec| v.push(value.clone())) + .or_insert_with(|| vec![value.clone()]); + } + } else { + //Turns out we are not dealing with a table + return mf(values, &name); + } + } + // The mathematical function operates over the columns of the table + let mut column_totals = IndexMap::new(); + for (col_name, col_vals) in column_values { + if let Ok(out) = mf(&col_vals, &name) { + column_totals.insert(col_name, out); + } + } + if column_totals.keys().len() == 0 { + return Err(ShellError::UnsupportedInput( + "Unable to give a result with this input".to_string(), + name, + )); + } + + Ok(Value::from(Spanned { + item: column_totals, + span: name, + })) +} + +pub fn calculate( + values: PipelineData, + name: Span, + mf: impl Fn(&[Value], &Span) -> Result, +) -> Result { + match values { + PipelineData::ListStream(s, ..) => helper_for_tables(&s.collect::>(), name, mf), + PipelineData::Value(Value::List { ref vals, .. }, ..) => match &vals[..] { + [Value::Record { .. }, _end @ ..] => helper_for_tables(vals, name, mf), + _ => mf(vals, &name), + }, + PipelineData::Value(Value::Record { vals, cols, span }, ..) => { + let new_vals: Result, ShellError> = + vals.into_iter().map(|val| mf(&[val], &name)).collect(); + match new_vals { + Ok(vec) => Ok(Value::Record { + cols, + vals: vec, + span, + }), + Err(err) => Err(err), + } + } + PipelineData::Value(Value::Range { val, .. }, ..) => { + let new_vals: Result, ShellError> = val + .into_range_iter()? + .map(|val| mf(&[val], &name)) + .collect(); + + mf(&new_vals?, &name) + } + PipelineData::Value(val, ..) => mf(&[val], &name), + _ => Err(ShellError::UnsupportedInput( + "Input data is not supported by this command.".to_string(), + name, + )), + } +} diff --git a/crates/nu-command/src/math/variance.rs b/crates/nu-command/src/math/variance.rs new file mode 100644 index 0000000000..50ca64c20f --- /dev/null +++ b/crates/nu-command/src/math/variance.rs @@ -0,0 +1,128 @@ +use crate::math::utils::run_with_function; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Span, Value}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "math variance" + } + + fn signature(&self) -> Signature { + Signature::build("math variance") + .switch("sample", "calculate sample variance", Some('s')) + .category(Category::Math) + } + + fn usage(&self) -> &str { + "Finds the variance of a list of numbers or tables" + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let sample = call.has_flag("sample"); + run_with_function(call, input, compute_variance(sample)) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Get the variance of a list of numbers", + example: "echo [1 2 3 4 5] | math variance", + result: Some(Value::Float { + val: 2.0, + span: Span::test_data(), + }), + }, + Example { + description: "Get the sample variance of a list of numbers", + example: "[1 2 3 4 5] | math variance -s", + result: Some(Value::Float { + val: 2.5, + span: Span::test_data(), + }), + }, + ] + } +} + +fn sum_of_squares(values: &[Value], span: &Span) -> Result { + let n = Value::Int { + val: values.len() as i64, + span: *span, + }; + let mut sum_x = Value::Int { + val: 0, + span: *span, + }; + let mut sum_x2 = Value::Int { + val: 0, + span: *span, + }; + for value in values { + let v = match &value { + Value::Int { .. } + | Value::Float { .. } => { + Ok(value) + }, + _ => Err(ShellError::UnsupportedInput( + "Attempted to compute the sum of squared values of a value that cannot be summed or squared.".to_string(), + value.span().unwrap_or(*span), + )) + }?; + let v_squared = &v.mul(*span, v)?; + sum_x2 = sum_x2.add(*span, v_squared)?; + sum_x = sum_x.add(*span, v)?; + } + + let sum_x_squared = sum_x.mul(*span, &sum_x)?; + let sum_x_squared_div_n = sum_x_squared.div(*span, &n)?; + + let ss = sum_x2.sub(*span, &sum_x_squared_div_n)?; + + Ok(ss) +} + +pub fn compute_variance(sample: bool) -> impl Fn(&[Value], &Span) -> Result { + move |values: &[Value], span: &Span| { + let n = if sample { + values.len() - 1 + } else { + values.len() + }; + let sum_of_squares = sum_of_squares(values, span); + let ss = match sum_of_squares { + Err(ShellError::UnsupportedInput(_, err_span)) => Err(ShellError::UnsupportedInput( + "Attempted to compute the variance with an item that cannot be used for that." + .to_string(), + err_span, + )), + other => other, + }?; + let n = Value::Int { + val: n as i64, + span: *span, + }; + ss.div(*span, &n) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/network/fetch.rs b/crates/nu-command/src/network/fetch.rs new file mode 100644 index 0000000000..84a72614a9 --- /dev/null +++ b/crates/nu-command/src/network/fetch.rs @@ -0,0 +1,380 @@ +use base64::encode; +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::RawStream; + +use nu_protocol::{ + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; +use reqwest::blocking::Response; + +use std::collections::HashMap; +use std::io::{BufRead, BufReader, Read}; + +use reqwest::StatusCode; +use std::path::PathBuf; +use std::str::FromStr; +use std::time::Duration; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "fetch" + } + + fn signature(&self) -> Signature { + Signature::build("fetch") + .desc("Load from a URL into a cell, convert to table if possible (avoid by appending '--raw').") + .required( + "URL", + SyntaxShape::String, + "the URL to fetch the contents from", + ) + .named( + "user", + SyntaxShape::Any, + "the username when authenticating", + Some('u'), + ) + .named( + "password", + SyntaxShape::Any, + "the password when authenticating", + Some('p'), + ) + .named("timeout", SyntaxShape::Int, "timeout period in seconds", Some('t')) + .named("headers",SyntaxShape::Any, "custom headers you want to add ", Some('H')) + .switch("raw", "fetch contents as text rather than a table", Some('r')) + .filter() + .category(Category::Network) + } + + fn usage(&self) -> &str { + "Fetch the contents from a URL (HTTP GET operation)." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + run_fetch(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Fetch content from url.com", + example: "fetch url.com", + result: None, + }, + Example { + description: "Fetch content from url.com, with username and password", + example: "fetch -u myuser -p mypass url.com", + result: None, + }, + Example { + description: "Fetch content from url.com, with custom header", + example: "fetch -H [my-header-key my-header-value] url.com", + result: None, + }, + ] + } +} + +struct Arguments { + url: Option, + raw: bool, + user: Option, + password: Option, + timeout: Option, + headers: Option, +} + +fn run_fetch( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, +) -> Result { + let args = Arguments { + url: Some(call.req(engine_state, stack, 0)?), + raw: call.has_flag("raw"), + user: call.get_flag(engine_state, stack, "user")?, + password: call.get_flag(engine_state, stack, "password")?, + timeout: call.get_flag(engine_state, stack, "timeout")?, + headers: call.get_flag(engine_state, stack, "headers")?, + }; + helper(engine_state, stack, call, args) +} + +// Helper function that actually goes to retrieve the resource from the url given +// The Option return a possible file extension which can be used in AutoConvert commands +fn helper( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + args: Arguments, +) -> std::result::Result { + let url_value = if let Some(val) = args.url { + val + } else { + return Err(ShellError::UnsupportedInput( + "Expecting a url as a string but got nothing".to_string(), + call.head, + )); + }; + + let span = url_value.span()?; + let requested_url = url_value.as_string()?; + let url = match url::Url::parse(&requested_url) { + Ok(u) => u, + Err(_e) => { + return Err(ShellError::UnsupportedInput( + "Incomplete or incorrect url. Expected a full url, e.g., https://www.example.com" + .to_string(), + span, + )); + } + }; + let user = args.user.clone(); + let password = args.password; + let timeout = args.timeout; + let headers = args.headers; + let raw = args.raw; + let login = match (user, password) { + (Some(user), Some(password)) => Some(encode(&format!("{}:{}", user, password))), + (Some(user), _) => Some(encode(&format!("{}:", user))), + _ => None, + }; + + let client = http_client(); + let mut request = client.get(url); + + if let Some(timeout) = timeout { + let val = timeout.as_i64()?; + if val.is_negative() || val < 1 { + return Err(ShellError::UnsupportedInput( + "Timeout value must be an integer and larger than 0".to_string(), + timeout.span().unwrap_or_else(|_| Span::new(0, 0)), + )); + } + + request = request.timeout(Duration::from_secs(val as u64)); + } + + if let Some(login) = login { + request = request.header("Authorization", format!("Basic {}", login)); + } + + if let Some(headers) = headers { + let mut custom_headers: HashMap = HashMap::new(); + + match &headers { + Value::List { vals: table, .. } => { + if table.len() == 1 { + // single row([key1 key2]; [val1 val2]) + match &table[0] { + Value::Record { cols, vals, .. } => { + for (k, v) in cols.iter().zip(vals.iter()) { + custom_headers.insert(k.to_string(), v.clone()); + } + } + + x => { + return Err(ShellError::CantConvert( + "string list or single row".into(), + x.get_type().to_string(), + headers.span().unwrap_or_else(|_| Span::new(0, 0)), + )); + } + } + } else { + // primitive values ([key1 val1 key2 val2]) + for row in table.chunks(2) { + if row.len() == 2 { + custom_headers.insert(row[0].as_string()?, (&row[1]).clone()); + } + } + } + } + + x => { + return Err(ShellError::CantConvert( + "string list or single row".into(), + x.get_type().to_string(), + headers.span().unwrap_or_else(|_| Span::new(0, 0)), + )); + } + }; + + for (k, v) in &custom_headers { + if let Ok(s) = v.as_string() { + request = request.header(k, s); + } + } + } + + match request.send() { + Ok(resp) => match resp.headers().get("content-type") { + Some(content_type) => { + let content_type = content_type.to_str().map_err(|e| { + ShellError::LabeledError(e.to_string(), "MIME type were invalid".to_string()) + })?; + let content_type = mime::Mime::from_str(content_type).map_err(|_| { + ShellError::LabeledError( + format!("MIME type unknown: {}", content_type), + "given unknown MIME type".to_string(), + ) + })?; + let ext = match (content_type.type_(), content_type.subtype()) { + (mime::TEXT, mime::PLAIN) => { + let path_extension = url::Url::parse(&requested_url) + .map_err(|_| { + ShellError::LabeledError( + format!("Cannot parse URL: {}", requested_url), + "cannot parse".to_string(), + ) + })? + .path_segments() + .and_then(|segments| segments.last()) + .and_then(|name| if name.is_empty() { None } else { Some(name) }) + .and_then(|name| { + PathBuf::from(name) + .extension() + .map(|name| name.to_string_lossy().to_string()) + }); + path_extension + } + _ => Some(content_type.subtype().to_string()), + }; + + let output = response_to_buffer(resp, engine_state, span); + + if raw { + return Ok(output); + } + + if let Some(ext) = ext { + match engine_state.find_decl(format!("from {}", ext).as_bytes()) { + Some(converter_id) => engine_state.get_decl(converter_id).run( + engine_state, + stack, + &Call::new(span), + output, + ), + None => Ok(output), + } + } else { + Ok(output) + } + } + None => Ok(response_to_buffer(resp, engine_state, span)), + }, + Err(e) if e.is_timeout() => Err(ShellError::NetworkFailure( + format!("Request to {} has timed out", requested_url), + span, + )), + Err(e) if e.is_status() => match e.status() { + Some(err_code) if err_code == StatusCode::NOT_FOUND => Err(ShellError::NetworkFailure( + format!("Requested file not found (404): {:?}", requested_url), + span, + )), + Some(err_code) if err_code == StatusCode::MOVED_PERMANENTLY => { + Err(ShellError::NetworkFailure( + format!("Resource moved permanently (301): {:?}", requested_url), + span, + )) + } + Some(err_code) if err_code == StatusCode::BAD_REQUEST => { + Err(ShellError::NetworkFailure( + format!("Bad request (400) to {:?}", requested_url), + span, + )) + } + Some(err_code) if err_code == StatusCode::FORBIDDEN => Err(ShellError::NetworkFailure( + format!("Access forbidden (403) to {:?}", requested_url), + span, + )), + _ => Err(ShellError::NetworkFailure( + format!( + "Cannot make request to {:?}. Error is {:?}", + requested_url, + e.to_string() + ), + span, + )), + }, + Err(e) => Err(ShellError::NetworkFailure( + format!( + "Cannot make request to {:?}. Error is {:?}", + requested_url, + e.to_string() + ), + span, + )), + } +} + +pub struct BufferedReader { + input: BufReader, +} + +impl Iterator for BufferedReader { + type Item = Result, ShellError>; + + fn next(&mut self) -> Option { + let buffer = self.input.fill_buf(); + match buffer { + Ok(s) => { + let result = s.to_vec(); + + let buffer_len = s.len(); + + if buffer_len == 0 { + None + } else { + self.input.consume(buffer_len); + + Some(Ok(result)) + } + } + Err(e) => Some(Err(ShellError::IOError(e.to_string()))), + } + } +} + +fn response_to_buffer( + response: Response, + engine_state: &EngineState, + span: Span, +) -> nu_protocol::PipelineData { + let buffered_input = BufReader::new(response); + + PipelineData::RawStream( + RawStream::new( + Box::new(BufferedReader { + input: buffered_input, + }), + engine_state.ctrlc.clone(), + span, + ), + span, + None, + ) +} + +// Only panics if the user agent is invalid but we define it statically so either +// it always or never fails +#[allow(clippy::unwrap_used)] +fn http_client() -> reqwest::blocking::Client { + reqwest::blocking::Client::builder() + .user_agent("nushell") + .build() + .unwrap() +} diff --git a/crates/nu-command/src/network/mod.rs b/crates/nu-command/src/network/mod.rs new file mode 100644 index 0000000000..6f5eb9cf17 --- /dev/null +++ b/crates/nu-command/src/network/mod.rs @@ -0,0 +1,5 @@ +mod fetch; +mod url; + +pub use self::url::*; +pub use fetch::SubCommand as Fetch; diff --git a/crates/nu-command/src/network/url/host.rs b/crates/nu-command/src/network/url/host.rs new file mode 100644 index 0000000000..18332bc5b5 --- /dev/null +++ b/crates/nu-command/src/network/url/host.rs @@ -0,0 +1,65 @@ +use super::{operator, url}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, Signature, Span, SyntaxShape, Value}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "url host" + } + + fn signature(&self) -> Signature { + Signature::build("url host") + .rest( + "rest", + SyntaxShape::CellPath, + "optionally operate by cell path", + ) + .category(Category::Network) + } + + fn usage(&self) -> &str { + "gets the host of a url" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operator(engine_state, stack, call, input, &host) + } + + fn examples(&self) -> Vec { + let span = Span::test_data(); + vec![Example { + description: "Get host of a url", + example: "echo 'http://www.example.com/foo/bar' | url host", + result: Some(Value::String { + val: "www.example.com".to_string(), + span, + }), + }] + } +} + +fn host(url: &url::Url) -> &str { + url.host_str().unwrap_or("") +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/network/url/mod.rs b/crates/nu-command/src/network/url/mod.rs new file mode 100644 index 0000000000..871ea4860f --- /dev/null +++ b/crates/nu-command/src/network/url/mod.rs @@ -0,0 +1,92 @@ +mod host; +mod path; +mod query; +mod scheme; +mod url_; + +use nu_engine::CallExt; +use nu_protocol::{ + ast::{Call, CellPath}, + engine::{EngineState, Stack}, + PipelineData, ShellError, Span, Value, +}; +use url::{self}; + +pub use self::host::SubCommand as UrlHost; +pub use self::path::SubCommand as UrlPath; +pub use self::query::SubCommand as UrlQuery; +pub use self::scheme::SubCommand as UrlScheme; +pub use url_::Url; + +fn handle_value(action: &F, v: &Value, span: Span) -> Value +where + F: Fn(&url::Url) -> &str + Send + 'static, +{ + let a = |url| Value::String { + val: action(url).to_string(), + span, + }; + + match v { + Value::String { val: s, .. } => { + let s = s.trim(); + + match url::Url::parse(s) { + Ok(url) => a(&url), + Err(_) => Value::String { + val: "".to_string(), + span, + }, + } + } + other => { + let span = other.span(); + match span { + Ok(s) => { + let got = format!("Expected a string, got {} instead", other.get_type()); + Value::Error { + error: ShellError::UnsupportedInput(got, s), + } + } + Err(e) => Value::Error { error: e }, + } + } + } +} + +fn operator( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + action: &'static F, +) -> Result +where + F: Fn(&url::Url) -> &str + Send + Sync + 'static, +{ + let span = call.head; + let column_paths: Vec = call.rest(engine_state, stack, 0)?; + + input.map( + move |v| { + if column_paths.is_empty() { + handle_value(&action, &v, span) + } else { + let mut ret = v; + + for path in &column_paths { + let r = ret.update_cell_path( + &path.members, + Box::new(move |old| handle_value(&action, old, span)), + ); + if let Err(error) = r { + return Value::Error { error }; + } + } + + ret + } + }, + engine_state.ctrlc.clone(), + ) +} diff --git a/crates/nu-command/src/network/url/path.rs b/crates/nu-command/src/network/url/path.rs new file mode 100644 index 0000000000..4096a0e14b --- /dev/null +++ b/crates/nu-command/src/network/url/path.rs @@ -0,0 +1,71 @@ +use super::{operator, url}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, Signature, Span, SyntaxShape, Value}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "url path" + } + + fn signature(&self) -> Signature { + Signature::build("url path") + .rest( + "rest", + SyntaxShape::CellPath, + "optionally operate by cell path", + ) + .category(Category::Network) + } + + fn usage(&self) -> &str { + "gets the path of a url" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operator(engine_state, stack, call, input, &url::Url::path) + } + + fn examples(&self) -> Vec { + let span = Span::test_data(); + vec![ + Example { + description: "Get path of a url", + example: "echo 'http://www.example.com/foo/bar' | url path", + result: Some(Value::String { + val: "/foo/bar".to_string(), + span, + }), + }, + Example { + description: "A trailing slash will be reflected in the path", + example: "echo 'http://www.example.com' | url path", + result: Some(Value::String { + val: "/".to_string(), + span, + }), + }, + ] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/network/url/query.rs b/crates/nu-command/src/network/url/query.rs new file mode 100644 index 0000000000..5c9ff7700e --- /dev/null +++ b/crates/nu-command/src/network/url/query.rs @@ -0,0 +1,75 @@ +use super::{operator, url}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, Signature, Span, SyntaxShape, Value}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "url query" + } + + fn signature(&self) -> Signature { + Signature::build("url query") + .rest( + "rest", + SyntaxShape::CellPath, + "optionally operate by cell path", + ) + .category(Category::Network) + } + + fn usage(&self) -> &str { + "gets the query of a url" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operator(engine_state, stack, call, input, &query) + } + + fn examples(&self) -> Vec { + let span = Span::test_data(); + vec![ + Example { + description: "Get query of a url", + example: "echo 'http://www.example.com/?foo=bar&baz=quux' | url query", + result: Some(Value::String { + val: "foo=bar&baz=quux".to_string(), + span, + }), + }, + Example { + description: "No query gives the empty string", + example: "echo 'http://www.example.com/' | url query", + result: Some(Value::String { + val: "".to_string(), + span, + }), + }, + ] + } +} + +fn query(url: &url::Url) -> &str { + url.query().unwrap_or("") +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/network/url/scheme.rs b/crates/nu-command/src/network/url/scheme.rs new file mode 100644 index 0000000000..b95d3e2eff --- /dev/null +++ b/crates/nu-command/src/network/url/scheme.rs @@ -0,0 +1,71 @@ +use super::{operator, url}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, Signature, Span, SyntaxShape, Value}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "url scheme" + } + + fn signature(&self) -> Signature { + Signature::build("url scheme") + .rest( + "rest", + SyntaxShape::CellPath, + "optionally operate by cell path", + ) + .category(Category::Network) + } + + fn usage(&self) -> &str { + "gets the scheme (eg http, file) of a url" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operator(engine_state, stack, call, input, &url::Url::scheme) + } + + fn examples(&self) -> Vec { + let span = Span::test_data(); + vec![ + Example { + description: "Get scheme of a url", + example: "echo 'http://www.example.com' | url scheme", + result: Some(Value::String { + val: "http".to_string(), + span, + }), + }, + Example { + description: "You get an empty string if there is no scheme", + example: "echo 'test' | url scheme", + result: Some(Value::String { + val: "".to_string(), + span, + }), + }, + ] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/network/url/url_.rs b/crates/nu-command/src/network/url/url_.rs new file mode 100644 index 0000000000..52ba716079 --- /dev/null +++ b/crates/nu-command/src/network/url/url_.rs @@ -0,0 +1,37 @@ +use nu_engine::get_full_help; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, IntoPipelineData, PipelineData, Signature, Value, +}; + +#[derive(Clone)] +pub struct Url; + +impl Command for Url { + fn name(&self) -> &str { + "url" + } + + fn signature(&self) -> Signature { + Signature::build("url").category(Category::Network) + } + + fn usage(&self) -> &str { + "Apply url function." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(Value::String { + val: get_full_help(&Url.signature(), &Url.examples(), engine_state, stack), + span: call.head, + } + .into_pipeline_data()) + } +} diff --git a/crates/nu-command/src/path/basename.rs b/crates/nu-command/src/path/basename.rs new file mode 100644 index 0000000000..223a86d5c1 --- /dev/null +++ b/crates/nu-command/src/path/basename.rs @@ -0,0 +1,151 @@ +use std::path::Path; + +use nu_engine::CallExt; +use nu_protocol::{engine::Command, Example, Signature, Span, Spanned, SyntaxShape, Value}; + +use super::PathSubcommandArguments; + +struct Arguments { + columns: Option>, + replace: Option>, +} + +impl PathSubcommandArguments for Arguments { + fn get_columns(&self) -> Option> { + self.columns.clone() + } +} + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "path basename" + } + + fn signature(&self) -> Signature { + Signature::build("path basename") + .named( + "columns", + SyntaxShape::Table, + "Optionally operate by column path", + Some('c'), + ) + .named( + "replace", + SyntaxShape::String, + "Return original path with basename replaced by this string", + Some('r'), + ) + } + + fn usage(&self) -> &str { + "Get the final component of a path" + } + + fn run( + &self, + engine_state: &nu_protocol::engine::EngineState, + stack: &mut nu_protocol::engine::Stack, + call: &nu_protocol::ast::Call, + input: nu_protocol::PipelineData, + ) -> Result { + let head = call.head; + let args = Arguments { + columns: call.get_flag(engine_state, stack, "columns")?, + replace: call.get_flag(engine_state, stack, "replace")?, + }; + + input.map( + move |value| super::operate(&get_basename, &args, value, head), + engine_state.ctrlc.clone(), + ) + } + + #[cfg(windows)] + fn examples(&self) -> Vec { + vec![ + Example { + description: "Get basename of a path", + example: "'C:\\Users\\joe\\test.txt' | path basename", + result: Some(Value::test_string("test.txt")), + }, + Example { + description: "Get basename of a path in a column", + example: "ls .. | path basename -c [ name ]", + result: None, + }, + Example { + description: "Get basename of a path in a column", + example: "[[name];[C:\\Users\\Joe]] | path basename -c [ name ]", + result: Some(Value::List { + vals: vec![Value::Record { + cols: vec!["name".to_string()], + vals: vec![Value::test_string("Joe")], + span: Span::test_data(), + }], + span: Span::test_data(), + }), + }, + Example { + description: "Replace basename of a path", + example: "'C:\\Users\\joe\\test.txt' | path basename -r 'spam.png'", + result: Some(Value::test_string("C:\\Users\\joe\\spam.png")), + }, + ] + } + + #[cfg(not(windows))] + fn examples(&self) -> Vec { + vec![ + Example { + description: "Get basename of a path", + example: "'/home/joe/test.txt' | path basename", + result: Some(Value::test_string("test.txt")), + }, + Example { + description: "Get basename of a path by column", + example: "[[name];[/home/joe]] | path basename -c [ name ]", + result: Some(Value::List { + vals: vec![Value::Record { + cols: vec!["name".to_string()], + vals: vec![Value::test_string("joe")], + span: Span::test_data(), + }], + span: Span::test_data(), + }), + }, + Example { + description: "Replace basename of a path", + example: "'/home/joe/test.txt' | path basename -r 'spam.png'", + result: Some(Value::test_string("/home/joe/spam.png")), + }, + ] + } +} + +fn get_basename(path: &Path, span: Span, args: &Arguments) -> Value { + match &args.replace { + Some(r) => Value::string(path.with_file_name(r.item.clone()).to_string_lossy(), span), + None => Value::string( + match path.file_name() { + Some(n) => n.to_string_lossy(), + None => "".into(), + }, + span, + ), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/path/dirname.rs b/crates/nu-command/src/path/dirname.rs new file mode 100644 index 0000000000..061fa0eb06 --- /dev/null +++ b/crates/nu-command/src/path/dirname.rs @@ -0,0 +1,168 @@ +use std::path::Path; + +use nu_engine::CallExt; +use nu_protocol::{engine::Command, Example, Signature, Span, Spanned, SyntaxShape, Value}; + +use super::PathSubcommandArguments; + +struct Arguments { + columns: Option>, + replace: Option>, + num_levels: Option, +} + +impl PathSubcommandArguments for Arguments { + fn get_columns(&self) -> Option> { + self.columns.clone() + } +} + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "path dirname" + } + + fn signature(&self) -> Signature { + Signature::build("path dirname") + .named( + "columns", + SyntaxShape::Table, + "Optionally operate by column path", + Some('c'), + ) + .named( + "replace", + SyntaxShape::String, + "Return original path with dirname replaced by this string", + Some('r'), + ) + .named( + "num-levels", + SyntaxShape::Int, + "Number of directories to walk up", + Some('n'), + ) + } + + fn usage(&self) -> &str { + "Get the parent directory of a path" + } + + fn run( + &self, + engine_state: &nu_protocol::engine::EngineState, + stack: &mut nu_protocol::engine::Stack, + call: &nu_protocol::ast::Call, + input: nu_protocol::PipelineData, + ) -> Result { + let head = call.head; + let args = Arguments { + columns: call.get_flag(engine_state, stack, "columns")?, + replace: call.get_flag(engine_state, stack, "replace")?, + num_levels: call.get_flag(engine_state, stack, "num-levels")?, + }; + + input.map( + move |value| super::operate(&get_dirname, &args, value, head), + engine_state.ctrlc.clone(), + ) + } + + #[cfg(windows)] + fn examples(&self) -> Vec { + vec![ + Example { + description: "Get dirname of a path", + example: "'C:\\Users\\joe\\code\\test.txt' | path dirname", + result: Some(Value::test_string("C:\\Users\\joe\\code")), + }, + Example { + description: "Get dirname of a path in a column", + example: "ls ('.' | path expand) | path dirname -c [ name ]", + result: None, + }, + Example { + description: "Walk up two levels", + example: "'C:\\Users\\joe\\code\\test.txt' | path dirname -n 2", + result: Some(Value::test_string("C:\\Users\\joe")), + }, + Example { + description: "Replace the part that would be returned with a custom path", + example: + "'C:\\Users\\joe\\code\\test.txt' | path dirname -n 2 -r C:\\Users\\viking", + result: Some(Value::test_string("C:\\Users\\viking\\code\\test.txt")), + }, + ] + } + + #[cfg(not(windows))] + fn examples(&self) -> Vec { + vec![ + Example { + description: "Get dirname of a path", + example: "'/home/joe/code/test.txt' | path dirname", + result: Some(Value::test_string("/home/joe/code")), + }, + Example { + description: "Get dirname of a path in a column", + example: "ls ('.' | path expand) | path dirname -c [ name ]", + result: None, + }, + Example { + description: "Walk up two levels", + example: "'/home/joe/code/test.txt' | path dirname -n 2", + result: Some(Value::test_string("/home/joe")), + }, + Example { + description: "Replace the part that would be returned with a custom path", + example: "'/home/joe/code/test.txt' | path dirname -n 2 -r /home/viking", + result: Some(Value::test_string("/home/viking/code/test.txt")), + }, + ] + } +} + +fn get_dirname(path: &Path, span: Span, args: &Arguments) -> Value { + let num_levels = args.num_levels.as_ref().map_or(1, |val| *val); + + let mut dirname = path; + let mut reached_top = false; + for _ in 0..num_levels { + dirname = dirname.parent().unwrap_or_else(|| { + reached_top = true; + dirname + }); + if reached_top { + break; + } + } + + let path = match args.replace { + Some(ref newdir) => { + let remainder = path.strip_prefix(dirname).unwrap_or(dirname); + if !remainder.as_os_str().is_empty() { + Path::new(&newdir.item).join(remainder) + } else { + Path::new(&newdir.item).to_path_buf() + } + } + None => dirname.to_path_buf(), + }; + + Value::string(path.to_string_lossy(), span) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/path/exists.rs b/crates/nu-command/src/path/exists.rs new file mode 100644 index 0000000000..343c38e91c --- /dev/null +++ b/crates/nu-command/src/path/exists.rs @@ -0,0 +1,113 @@ +use std::path::Path; + +use nu_engine::CallExt; +use nu_protocol::{engine::Command, Example, Signature, Span, SyntaxShape, Value}; + +use super::PathSubcommandArguments; + +struct Arguments { + columns: Option>, +} + +impl PathSubcommandArguments for Arguments { + fn get_columns(&self) -> Option> { + self.columns.clone() + } +} + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "path exists" + } + + fn signature(&self) -> Signature { + Signature::build("path exists").named( + "columns", + SyntaxShape::Table, + "Optionally operate by column path", + Some('c'), + ) + } + + fn usage(&self) -> &str { + "Check whether a path exists" + } + + fn run( + &self, + engine_state: &nu_protocol::engine::EngineState, + stack: &mut nu_protocol::engine::Stack, + call: &nu_protocol::ast::Call, + input: nu_protocol::PipelineData, + ) -> Result { + let head = call.head; + let args = Arguments { + columns: call.get_flag(engine_state, stack, "columns")?, + }; + + input.map( + move |value| super::operate(&exists, &args, value, head), + engine_state.ctrlc.clone(), + ) + } + + #[cfg(windows)] + fn examples(&self) -> Vec { + vec![ + Example { + description: "Check if a file exists", + example: "'C:\\Users\\joe\\todo.txt' | path exists", + result: Some(Value::Bool { + val: false, + span: Span::test_data(), + }), + }, + Example { + description: "Check if a file exists in a column", + example: "ls | path exists -c [ name ]", + result: None, + }, + ] + } + + #[cfg(not(windows))] + fn examples(&self) -> Vec { + vec![ + Example { + description: "Check if a file exists", + example: "'/home/joe/todo.txt' | path exists", + result: Some(Value::Bool { + val: false, + span: Span::test_data(), + }), + }, + Example { + description: "Check if a file exists in a column", + example: "ls | path exists -c [ name ]", + result: None, + }, + ] + } +} + +fn exists(path: &Path, span: Span, _args: &Arguments) -> Value { + Value::Bool { + val: path.exists(), + span, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/path/expand.rs b/crates/nu-command/src/path/expand.rs new file mode 100644 index 0000000000..a4ae4c602b --- /dev/null +++ b/crates/nu-command/src/path/expand.rs @@ -0,0 +1,140 @@ +use std::path::Path; + +use nu_engine::env::current_dir_str; +use nu_engine::CallExt; +use nu_path::{canonicalize_with, expand_path_with}; +use nu_protocol::{engine::Command, Example, ShellError, Signature, Span, SyntaxShape, Value}; + +use super::PathSubcommandArguments; + +struct Arguments { + strict: bool, + columns: Option>, + cwd: String, +} + +impl PathSubcommandArguments for Arguments { + fn get_columns(&self) -> Option> { + self.columns.clone() + } +} + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "path expand" + } + + fn signature(&self) -> Signature { + Signature::build("path expand") + .switch( + "strict", + "Throw an error if the path could not be expanded", + Some('s'), + ) + .named( + "columns", + SyntaxShape::Table, + "Optionally operate by column path", + Some('c'), + ) + } + + fn usage(&self) -> &str { + "Try to expand a path to its absolute form" + } + + fn run( + &self, + engine_state: &nu_protocol::engine::EngineState, + stack: &mut nu_protocol::engine::Stack, + call: &nu_protocol::ast::Call, + input: nu_protocol::PipelineData, + ) -> Result { + let head = call.head; + let args = Arguments { + strict: call.has_flag("strict"), + columns: call.get_flag(engine_state, stack, "columns")?, + cwd: current_dir_str(engine_state, stack)?, + }; + + input.map( + move |value| super::operate(&expand, &args, value, head), + engine_state.ctrlc.clone(), + ) + } + + #[cfg(windows)] + fn examples(&self) -> Vec { + vec![ + Example { + description: "Expand an absolute path", + example: r"'C:\Users\joe\foo\..\bar' | path expand", + result: Some(Value::test_string(r"C:\Users\joe\bar")), + }, + Example { + description: "Expand a path in a column", + example: "ls | path expand -c [ name ]", + result: None, + }, + Example { + description: "Expand a relative path", + example: r"'foo\..\bar' | path expand", + result: None, + }, + ] + } + + #[cfg(not(windows))] + fn examples(&self) -> Vec { + vec![ + Example { + description: "Expand an absolute path", + example: "'/home/joe/foo/../bar' | path expand", + result: Some(Value::test_string("/home/joe/bar")), + }, + Example { + description: "Expand a path in a column", + example: "ls | path expand -c [ name ]", + result: None, + }, + Example { + description: "Expand a relative path", + example: "'foo/../bar' | path expand", + result: None, + }, + ] + } +} + +fn expand(path: &Path, span: Span, args: &Arguments) -> Value { + if let Ok(p) = canonicalize_with(path, &args.cwd) { + Value::string(p.to_string_lossy(), span) + } else if args.strict { + Value::Error { + error: ShellError::SpannedLabeledError( + "Could not expand path".into(), + "could not be expanded (path might not exist, non-final \ + component is not a directory, or other cause)" + .into(), + span, + ), + } + } else { + Value::string(expand_path_with(path, &args.cwd).to_string_lossy(), span) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/path/join.rs b/crates/nu-command/src/path/join.rs new file mode 100644 index 0000000000..ed9dbca963 --- /dev/null +++ b/crates/nu-command/src/path/join.rs @@ -0,0 +1,270 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; + +use nu_engine::CallExt; +use nu_protocol::{ + engine::Command, Example, ListStream, PipelineData, ShellError, Signature, Span, Spanned, + SyntaxShape, Value, +}; + +use super::PathSubcommandArguments; + +struct Arguments { + columns: Option>, + append: Option>, +} + +impl PathSubcommandArguments for Arguments { + fn get_columns(&self) -> Option> { + self.columns.clone() + } +} + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "path join" + } + + fn signature(&self) -> Signature { + Signature::build("path join") + .named( + "columns", + SyntaxShape::Table, + "Optionally operate by column path", + Some('c'), + ) + .optional("append", SyntaxShape::String, "Path to append to the input") + } + + fn usage(&self) -> &str { + "Join a structured path or a list of path parts." + } + + fn extra_usage(&self) -> &str { + r#"Optionally, append an additional path to the result. It is designed to accept +the output of 'path parse' and 'path split' subcommands."# + } + + fn run( + &self, + engine_state: &nu_protocol::engine::EngineState, + stack: &mut nu_protocol::engine::Stack, + call: &nu_protocol::ast::Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let args = Arguments { + columns: call.get_flag(engine_state, stack, "columns")?, + append: call.opt(engine_state, stack, 0)?, + }; + + match input { + PipelineData::Value(val, md) => { + Ok(PipelineData::Value(handle_value(val, &args, head), md)) + } + PipelineData::ListStream(stream, md) => Ok(PipelineData::ListStream( + ListStream::from_stream( + stream.map(move |val| handle_value(val, &args, head)), + engine_state.ctrlc.clone(), + ), + md, + )), + _ => Err(ShellError::UnsupportedInput( + "Input data is not supported by this command.".to_string(), + head, + )), + } + } + + #[cfg(windows)] + fn examples(&self) -> Vec { + vec![ + Example { + description: "Append a filename to a path", + example: r"'C:\Users\viking' | path join spam.txt", + result: Some(Value::test_string(r"C:\Users\viking\spam.txt")), + }, + Example { + description: "Append a filename to a path inside a column", + example: r"ls | path join spam.txt -c [ name ]", + result: None, + }, + Example { + description: "Join a list of parts into a path", + example: r"[ 'C:' '\' 'Users' 'viking' 'spam.txt' ] | path join", + result: Some(Value::test_string(r"C:\Users\viking\spam.txt")), + }, + Example { + description: "Join a structured path into a path", + example: r"[ [parent stem extension]; ['C:\Users\viking' 'spam' 'txt']] | path join", + result: Some(Value::List { + vals: vec![Value::test_string(r"C:\Users\viking\spam.txt")], + span: Span::test_data(), + }), + }, + ] + } + + #[cfg(not(windows))] + fn examples(&self) -> Vec { + vec![ + Example { + description: "Append a filename to a path", + example: r"'/home/viking' | path join spam.txt", + result: Some(Value::test_string(r"/home/viking/spam.txt")), + }, + Example { + description: "Append a filename to a path inside a column", + example: r"ls | path join spam.txt -c [ name ]", + result: None, + }, + Example { + description: "Join a list of parts into a path", + example: r"[ '/' 'home' 'viking' 'spam.txt' ] | path join", + result: Some(Value::test_string(r"/home/viking/spam.txt")), + }, + Example { + description: "Join a structured path into a path", + example: r"[[ parent stem extension ]; [ '/home/viking' 'spam' 'txt' ]] | path join", + result: Some(Value::List { + vals: vec![Value::test_string(r"/home/viking/spam.txt")], + span: Span::test_data(), + }), + }, + ] + } +} + +fn handle_value(v: Value, args: &Arguments, head: Span) -> Value { + match v { + Value::String { ref val, span } => join_single(Path::new(val), span, args), + Value::Record { cols, vals, span } => join_record(&cols, &vals, span, args), + Value::List { vals, span } => join_list(&vals, span, args), + + _ => super::handle_invalid_values(v, head), + } +} + +fn join_single(path: &Path, span: Span, args: &Arguments) -> Value { + let path = if let Some(ref append) = args.append { + path.join(Path::new(&append.item)) + } else { + path.to_path_buf() + }; + + Value::string(path.to_string_lossy(), span) +} + +fn join_list(parts: &[Value], span: Span, args: &Arguments) -> Value { + let path: Result = parts.iter().map(Value::as_string).collect(); + + match path { + Ok(ref path) => join_single(path, span, args), + Err(_) => { + let records: Result, ShellError> = parts.iter().map(Value::as_record).collect(); + match records { + Ok(vals) => { + let vals = vals + .iter() + .map(|(k, v)| join_record(k, v, span, args)) + .collect(); + + Value::List { vals, span } + } + Err(_) => Value::Error { + error: ShellError::PipelineMismatch("string or record".into(), span, span), + }, + } + } + } +} + +fn join_record(cols: &[String], vals: &[Value], span: Span, args: &Arguments) -> Value { + if args.columns.is_some() { + super::operate( + &join_single, + args, + Value::Record { + cols: cols.to_vec(), + vals: vals.to_vec(), + span, + }, + span, + ) + } else { + match merge_record(cols, vals, span) { + Ok(p) => join_single(p.as_path(), span, args), + Err(error) => Value::Error { error }, + } + } +} + +fn merge_record(cols: &[String], vals: &[Value], span: Span) -> Result { + for key in cols { + if !super::ALLOWED_COLUMNS.contains(&key.as_str()) { + let allowed_cols = super::ALLOWED_COLUMNS.join(", "); + let msg = format!( + "Column '{}' is not valid for a structured path. Allowed columns are: {}", + key, allowed_cols + ); + return Err(ShellError::UnsupportedInput(msg, span)); + } + } + + let entries: HashMap<&str, &Value> = cols.iter().map(String::as_str).zip(vals).collect(); + let mut result = PathBuf::new(); + + #[cfg(windows)] + if let Some(val) = entries.get("prefix") { + let p = val.as_string()?; + if !p.is_empty() { + result.push(p); + } + } + + if let Some(val) = entries.get("parent") { + let p = val.as_string()?; + if !p.is_empty() { + result.push(p); + } + } + + let mut basename = String::new(); + if let Some(val) = entries.get("stem") { + let p = val.as_string()?; + if !p.is_empty() { + basename.push_str(&p); + } + } + + if let Some(val) = entries.get("extension") { + let p = val.as_string()?; + if !p.is_empty() { + basename.push('.'); + basename.push_str(&p); + } + } + + if !basename.is_empty() { + result.push(basename); + } + + Ok(result) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/path/mod.rs b/crates/nu-command/src/path/mod.rs new file mode 100644 index 0000000000..5d194153cc --- /dev/null +++ b/crates/nu-command/src/path/mod.rs @@ -0,0 +1,95 @@ +mod basename; +mod dirname; +mod exists; +mod expand; +mod join; +mod parse; +pub mod path_; +mod relative_to; +mod split; +mod r#type; + +use std::path::Path as StdPath; + +pub use basename::SubCommand as PathBasename; +pub use dirname::SubCommand as PathDirname; +pub use exists::SubCommand as PathExists; +pub use expand::SubCommand as PathExpand; +pub use join::SubCommand as PathJoin; +pub use parse::SubCommand as PathParse; +pub use path_::PathCommand as Path; +pub use r#type::SubCommand as PathType; +pub use relative_to::SubCommand as PathRelativeTo; +pub use split::SubCommand as PathSplit; + +use nu_protocol::{ShellError, Span, Value}; + +#[cfg(windows)] +const ALLOWED_COLUMNS: [&str; 4] = ["prefix", "parent", "stem", "extension"]; +#[cfg(not(windows))] +const ALLOWED_COLUMNS: [&str; 3] = ["parent", "stem", "extension"]; + +trait PathSubcommandArguments { + fn get_columns(&self) -> Option>; +} + +fn operate(cmd: &F, args: &A, v: Value, name: Span) -> Value +where + F: Fn(&StdPath, Span, &A) -> Value + Send + Sync + 'static, + A: PathSubcommandArguments + Send + Sync + 'static, +{ + match v { + Value::String { val, span } => cmd(StdPath::new(&val), span, args), + Value::Record { cols, vals, span } => { + let col = if let Some(col) = args.get_columns() { + col + } else { + vec![] + }; + if col.is_empty() { + return Value::Error { + error: ShellError::UnsupportedInput( + String::from("when the input is a table, you must specify the columns"), + name, + ), + }; + } + + let mut output_cols = vec![]; + let mut output_vals = vec![]; + + for (k, v) in cols.iter().zip(vals) { + output_cols.push(k.clone()); + if col.contains(k) { + let new_val = match v { + Value::String { val, span } => cmd(StdPath::new(&val), span, args), + _ => return handle_invalid_values(v, name), + }; + output_vals.push(new_val); + } else { + output_vals.push(v); + } + } + + Value::Record { + cols: output_cols, + vals: output_vals, + span, + } + } + _ => handle_invalid_values(v, name), + } +} + +fn handle_invalid_values(rest: Value, name: Span) -> Value { + Value::Error { + error: err_from_value(&rest, name), + } +} + +fn err_from_value(rest: &Value, name: Span) -> ShellError { + match rest.span() { + Ok(span) => ShellError::PipelineMismatch("string, row or list".into(), name, span), + Err(error) => error, + } +} diff --git a/crates/nu-command/src/path/parse.rs b/crates/nu-command/src/path/parse.rs new file mode 100644 index 0000000000..ef5a014d00 --- /dev/null +++ b/crates/nu-command/src/path/parse.rs @@ -0,0 +1,201 @@ +use std::path::Path; + +use indexmap::IndexMap; +use nu_engine::CallExt; +use nu_protocol::{ + engine::Command, Example, ShellError, Signature, Span, Spanned, SyntaxShape, Value, +}; + +use super::PathSubcommandArguments; + +struct Arguments { + columns: Option>, + extension: Option>, +} + +impl PathSubcommandArguments for Arguments { + fn get_columns(&self) -> Option> { + self.columns.clone() + } +} + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "path parse" + } + + fn signature(&self) -> Signature { + Signature::build("path parse") + .named( + "columns", + SyntaxShape::Table, + "Optionally operate by column path", + Some('c'), + ) + .named( + "extension", + SyntaxShape::String, + "Manually supply the extension (without the dot)", + Some('e'), + ) + } + + fn usage(&self) -> &str { + "Convert a path into structured data." + } + + fn extra_usage(&self) -> &str { + r#"Each path is split into a table with 'parent', 'stem' and 'extension' fields. +On Windows, an extra 'prefix' column is added."# + } + + fn run( + &self, + engine_state: &nu_protocol::engine::EngineState, + stack: &mut nu_protocol::engine::Stack, + call: &nu_protocol::ast::Call, + input: nu_protocol::PipelineData, + ) -> Result { + let head = call.head; + let args = Arguments { + columns: call.get_flag(engine_state, stack, "columns")?, + extension: call.get_flag(engine_state, stack, "extension")?, + }; + + input.map( + move |value| super::operate(&parse, &args, value, head), + engine_state.ctrlc.clone(), + ) + } + + #[cfg(windows)] + fn examples(&self) -> Vec { + vec![ + Example { + description: "Parse a single path", + example: r"'C:\Users\viking\spam.txt' | path parse", + result: None, + }, + Example { + description: "Replace a complex extension", + example: r"'C:\Users\viking\spam.tar.gz' | path parse -e tar.gz | update extension { 'txt' }", + result: None, + }, + Example { + description: "Ignore the extension", + example: r"'C:\Users\viking.d' | path parse -e ''", + result: None, + }, + Example { + description: "Parse all paths under the 'name' column", + example: r"ls | path parse -c [ name ]", + result: None, + }, + ] + } + + #[cfg(not(windows))] + fn examples(&self) -> Vec { + vec![ + Example { + description: "Parse a path", + example: r"'/home/viking/spam.txt' | path parse", + result: None, + }, + Example { + description: "Replace a complex extension", + example: r"'/home/viking/spam.tar.gz' | path parse -e tar.gz | update extension { 'txt' }", + result: None, + }, + Example { + description: "Ignore the extension", + example: r"'/etc/conf.d' | path parse -e ''", + result: None, + }, + Example { + description: "Parse all paths under the 'name' column", + example: r"ls | path parse -c [ name ]", + result: None, + }, + ] + } +} + +fn parse(path: &Path, span: Span, args: &Arguments) -> Value { + let mut map: IndexMap = IndexMap::new(); + + #[cfg(windows)] + { + use std::path::Component; + + let prefix = match path.components().next() { + Some(Component::Prefix(prefix_component)) => { + prefix_component.as_os_str().to_string_lossy() + } + _ => "".into(), + }; + map.insert("prefix".into(), Value::string(prefix, span)); + } + + let parent = path + .parent() + .unwrap_or_else(|| "".as_ref()) + .to_string_lossy(); + + map.insert("parent".into(), Value::string(parent, span)); + + let basename = path + .file_name() + .unwrap_or_else(|| "".as_ref()) + .to_string_lossy(); + + match &args.extension { + Some(Spanned { + item: extension, + span: extension_span, + }) => { + let ext_with_dot = [".", extension].concat(); + if basename.ends_with(&ext_with_dot) && !extension.is_empty() { + let stem = basename.trim_end_matches(&ext_with_dot); + map.insert("stem".into(), Value::string(stem, span)); + map.insert( + "extension".into(), + Value::string(extension, *extension_span), + ); + } else { + map.insert("stem".into(), Value::string(basename, span)); + map.insert("extension".into(), Value::string("", span)); + } + } + None => { + let stem = path + .file_stem() + .unwrap_or_else(|| "".as_ref()) + .to_string_lossy(); + let extension = path + .extension() + .unwrap_or_else(|| "".as_ref()) + .to_string_lossy(); + + map.insert("stem".into(), Value::string(stem, span)); + map.insert("extension".into(), Value::string(extension, span)); + } + } + + Value::from(Spanned { item: map, span }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/path/path_.rs b/crates/nu-command/src/path/path_.rs new file mode 100644 index 0000000000..b9af3b3949 --- /dev/null +++ b/crates/nu-command/src/path/path_.rs @@ -0,0 +1,57 @@ +use nu_engine::get_full_help; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + IntoPipelineData, PipelineData, Signature, Value, +}; + +#[derive(Clone)] +pub struct PathCommand; + +impl Command for PathCommand { + fn name(&self) -> &str { + "path" + } + + fn signature(&self) -> Signature { + Signature::build("path") + } + + fn usage(&self) -> &str { + "Explore and manipulate paths." + } + + fn extra_usage(&self) -> &str { + r#"There are three ways to represent a path: + +* As a path literal, e.g., '/home/viking/spam.txt' +* As a structured path: a table with 'parent', 'stem', and 'extension' (and +* 'prefix' on Windows) columns. This format is produced by the 'path parse' + subcommand. +* As an inner list of path parts, e.g., '[[ / home viking spam.txt ]]'. + Splitting into parts is done by the `path split` command. + +All subcommands accept all three variants as an input. Furthermore, the 'path +join' subcommand can be used to join the structured path or path parts back into +the path literal."# + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(Value::String { + val: get_full_help( + &PathCommand.signature(), + &PathCommand.examples(), + engine_state, + stack, + ), + span: call.head, + } + .into_pipeline_data()) + } +} diff --git a/crates/nu-command/src/path/relative_to.rs b/crates/nu-command/src/path/relative_to.rs new file mode 100644 index 0000000000..6f19132214 --- /dev/null +++ b/crates/nu-command/src/path/relative_to.rs @@ -0,0 +1,135 @@ +use std::path::Path; + +use nu_engine::CallExt; +use nu_protocol::{ + engine::Command, Example, ShellError, Signature, Span, Spanned, SyntaxShape, Value, +}; + +use super::PathSubcommandArguments; + +struct Arguments { + path: Spanned, + columns: Option>, +} + +impl PathSubcommandArguments for Arguments { + fn get_columns(&self) -> Option> { + self.columns.clone() + } +} + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "path relative-to" + } + + fn signature(&self) -> Signature { + Signature::build("path relative-to") + .required( + "path", + SyntaxShape::String, + "Parent shared with the input path", + ) + .named( + "columns", + SyntaxShape::Table, + "Optionally operate by column path", + Some('c'), + ) + } + + fn usage(&self) -> &str { + "Get a path as relative to another path." + } + + fn extra_usage(&self) -> &str { + r#"Can be used only when the input and the argument paths are either both +absolute or both relative. The argument path needs to be a parent of the input +path."# + } + + fn run( + &self, + engine_state: &nu_protocol::engine::EngineState, + stack: &mut nu_protocol::engine::Stack, + call: &nu_protocol::ast::Call, + input: nu_protocol::PipelineData, + ) -> Result { + let head = call.head; + let args = Arguments { + path: call.req(engine_state, stack, 0)?, + columns: call.get_flag(engine_state, stack, "columns")?, + }; + + input.map( + move |value| super::operate(&relative_to, &args, value, head), + engine_state.ctrlc.clone(), + ) + } + + #[cfg(windows)] + fn examples(&self) -> Vec { + vec![ + Example { + description: "Find a relative path from two absolute paths", + example: r"'C:\Users\viking' | path relative-to 'C:\Users'", + result: Some(Value::test_string(r"viking")), + }, + Example { + description: "Find a relative path from two absolute paths in a column", + example: "ls ~ | path relative-to ~ -c [ name ]", + result: None, + }, + Example { + description: "Find a relative path from two relative paths", + example: r"'eggs\bacon\sausage\spam' | path relative-to 'eggs\bacon\sausage'", + result: Some(Value::test_string(r"spam")), + }, + ] + } + + #[cfg(not(windows))] + fn examples(&self) -> Vec { + vec![ + Example { + description: "Find a relative path from two absolute paths", + example: r"'/home/viking' | path relative-to '/home'", + result: Some(Value::test_string(r"viking")), + }, + Example { + description: "Find a relative path from two absolute paths in a column", + example: "ls ~ | path relative-to ~ -c [ name ]", + result: None, + }, + Example { + description: "Find a relative path from two relative paths", + example: r"'eggs/bacon/sausage/spam' | path relative-to 'eggs/bacon/sausage'", + result: Some(Value::test_string(r"spam")), + }, + ] + } +} + +fn relative_to(path: &Path, span: Span, args: &Arguments) -> Value { + match path.strip_prefix(Path::new(&args.path.item)) { + Ok(p) => Value::string(p.to_string_lossy(), span), + Err(e) => Value::Error { + error: ShellError::CantConvert(e.to_string(), "string".into(), span), + }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/path/split.rs b/crates/nu-command/src/path/split.rs new file mode 100644 index 0000000000..dc98a3f8ba --- /dev/null +++ b/crates/nu-command/src/path/split.rs @@ -0,0 +1,130 @@ +use std::path::Path; + +use nu_engine::CallExt; +use nu_protocol::{engine::Command, Example, ShellError, Signature, Span, SyntaxShape, Value}; + +use super::PathSubcommandArguments; + +struct Arguments { + columns: Option>, +} + +impl PathSubcommandArguments for Arguments { + fn get_columns(&self) -> Option> { + self.columns.clone() + } +} + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "path split" + } + + fn signature(&self) -> Signature { + Signature::build("path split").named( + "columns", + SyntaxShape::Table, + "Optionally operate by column path", + Some('c'), + ) + } + + fn usage(&self) -> &str { + "Split a path into parts by a separator." + } + + fn run( + &self, + engine_state: &nu_protocol::engine::EngineState, + stack: &mut nu_protocol::engine::Stack, + call: &nu_protocol::ast::Call, + input: nu_protocol::PipelineData, + ) -> Result { + let head = call.head; + let args = Arguments { + columns: call.get_flag(engine_state, stack, "columns")?, + }; + + input.map( + move |value| super::operate(&split, &args, value, head), + engine_state.ctrlc.clone(), + ) + } + + #[cfg(windows)] + fn examples(&self) -> Vec { + vec![ + Example { + description: "Split a path into parts", + example: r"'C:\Users\viking\spam.txt' | path split", + result: Some(Value::List { + vals: vec![ + Value::test_string("C:"), + Value::test_string(r"\"), + Value::test_string("Users"), + Value::test_string("viking"), + Value::test_string("spam.txt"), + ], + span: Span::test_data(), + }), + }, + Example { + description: "Split all paths under the 'name' column", + example: r"ls ('.' | path expand) | path split -c [ name ]", + result: None, + }, + ] + } + + #[cfg(not(windows))] + fn examples(&self) -> Vec { + vec![ + Example { + description: "Split a path into parts", + example: r"'/home/viking/spam.txt' | path split", + result: Some(Value::List { + vals: vec![ + Value::test_string("/"), + Value::test_string("home"), + Value::test_string("viking"), + Value::test_string("spam.txt"), + ], + span: Span::test_data(), + }), + }, + Example { + description: "Split all paths under the 'name' column", + example: r"ls ('.' | path expand) | path split -c [ name ]", + result: None, + }, + ] + } +} + +fn split(path: &Path, span: Span, _: &Arguments) -> Value { + Value::List { + vals: path + .components() + .map(|comp| { + let s = comp.as_os_str().to_string_lossy(); + Value::string(s, span) + }) + .collect(), + span, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/path/type.rs b/crates/nu-command/src/path/type.rs new file mode 100644 index 0000000000..c00123b3d3 --- /dev/null +++ b/crates/nu-command/src/path/type.rs @@ -0,0 +1,122 @@ +use std::path::Path; + +use nu_engine::CallExt; +use nu_protocol::{engine::Command, Example, ShellError, Signature, Span, SyntaxShape, Value}; + +use super::PathSubcommandArguments; + +struct Arguments { + columns: Option>, +} + +impl PathSubcommandArguments for Arguments { + fn get_columns(&self) -> Option> { + self.columns.clone() + } +} + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "path type" + } + + fn signature(&self) -> Signature { + Signature::build("path type").named( + "columns", + SyntaxShape::Table, + "Optionally operate by column path", + Some('c'), + ) + } + + fn usage(&self) -> &str { + "Get the type of the object a path refers to (e.g., file, dir, symlink)" + } + + fn run( + &self, + engine_state: &nu_protocol::engine::EngineState, + stack: &mut nu_protocol::engine::Stack, + call: &nu_protocol::ast::Call, + input: nu_protocol::PipelineData, + ) -> Result { + let head = call.head; + let args = Arguments { + columns: call.get_flag(engine_state, stack, "columns")?, + }; + + input.map( + move |value| super::operate(&r#type, &args, value, head), + engine_state.ctrlc.clone(), + ) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Show type of a filepath", + example: "'.' | path type", + result: Some(Value::test_string("dir")), + }, + Example { + description: "Show type of a filepath in a column", + example: "ls | path type -c [ name ]", + result: None, + }, + ] + } +} + +fn r#type(path: &Path, span: Span, _: &Arguments) -> Value { + let meta = std::fs::symlink_metadata(path); + + Value::string( + match &meta { + Ok(data) => get_file_type(data), + Err(_) => "", + }, + span, + ) +} + +fn get_file_type(md: &std::fs::Metadata) -> &str { + let ft = md.file_type(); + let mut file_type = "unknown"; + if ft.is_dir() { + file_type = "dir"; + } else if ft.is_file() { + file_type = "file"; + } else if ft.is_symlink() { + file_type = "symlink"; + } else { + #[cfg(unix)] + { + use std::os::unix::fs::FileTypeExt; + if ft.is_block_device() { + file_type = "block device"; + } else if ft.is_char_device() { + file_type = "char device"; + } else if ft.is_fifo() { + file_type = "pipe"; + } else if ft.is_socket() { + file_type = "socket"; + } + } + } + file_type +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/platform/ansi/ansi_.rs b/crates/nu-command/src/platform/ansi/ansi_.rs new file mode 100644 index 0000000000..e6b42e16ac --- /dev/null +++ b/crates/nu-command/src/platform/ansi/ansi_.rs @@ -0,0 +1,456 @@ +use lazy_static::lazy_static; +use nu_ansi_term::*; +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, engine::Command, Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, + PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; +use std::collections::HashMap; + +#[derive(Clone)] +pub struct AnsiCommand; + +struct AnsiCode { + short_name: Option<&'static str>, + long_name: &'static str, + code: String, +} + +lazy_static! { + static ref CODE_LIST: Vec = vec!{ + AnsiCode{ short_name: Some("g"), long_name: "green", code: Color::Green.prefix().to_string()}, + AnsiCode{ short_name: Some("gb"), long_name: "green_bold", code: Color::Green.bold().prefix().to_string()}, + AnsiCode{ short_name: Some("gu"), long_name: "green_underline", code: Color::Green.underline().prefix().to_string()}, + AnsiCode{ short_name: Some("gi"), long_name: "green_italic", code: Color::Green.italic().prefix().to_string()}, + AnsiCode{ short_name: Some("gd"), long_name: "green_dimmed", code: Color::Green.dimmed().prefix().to_string()}, + AnsiCode{ short_name: Some("gr"), long_name: "green_reverse", code: Color::Green.reverse().prefix().to_string()}, + + AnsiCode{ short_name: Some("lg"), long_name: "light_green", code: Color::LightGreen.prefix().to_string()}, + AnsiCode{ short_name: Some("lgb"), long_name: "light_green_bold", code: Color::LightGreen.bold().prefix().to_string()}, + AnsiCode{ short_name: Some("lgu"), long_name: "light_green_underline", code: Color::LightGreen.underline().prefix().to_string()}, + AnsiCode{ short_name: Some("lgi"), long_name: "light_green_italic", code: Color::LightGreen.italic().prefix().to_string()}, + AnsiCode{ short_name: Some("lgd"), long_name: "light_green_dimmed", code: Color::LightGreen.dimmed().prefix().to_string()}, + AnsiCode{ short_name: Some("lgr"), long_name: "light_green_reverse", code: Color::LightGreen.reverse().prefix().to_string()}, + + AnsiCode{ short_name: Some("r"), long_name: "red", code: Color::Red.prefix().to_string()}, + AnsiCode{ short_name: Some("rb"), long_name: "red_bold", code: Color::Red.bold().prefix().to_string()}, + AnsiCode{ short_name: Some("ru"), long_name: "red_underline", code: Color::Red.underline().prefix().to_string()}, + AnsiCode{ short_name: Some("ri"), long_name: "red_italic", code: Color::Red.italic().prefix().to_string()}, + AnsiCode{ short_name: Some("rd"), long_name: "red_dimmed", code: Color::Red.dimmed().prefix().to_string()}, + AnsiCode{ short_name: Some("rr"), long_name: "red_reverse", code: Color::Red.reverse().prefix().to_string()}, + + AnsiCode{ short_name: Some("lr"), long_name: "light_red", code: Color::LightRed.prefix().to_string()}, + AnsiCode{ short_name: Some("lrb"), long_name: "light_red_bold", code: Color::LightRed.bold().prefix().to_string()}, + AnsiCode{ short_name: Some("lru"), long_name: "light_red_underline", code: Color::LightRed.underline().prefix().to_string()}, + AnsiCode{ short_name: Some("lri"), long_name: "light_red_italic", code: Color::LightRed.italic().prefix().to_string()}, + AnsiCode{ short_name: Some("lrd"), long_name: "light_red_dimmed", code: Color::LightRed.dimmed().prefix().to_string()}, + AnsiCode{ short_name: Some("lrr"), long_name: "light_red_reverse", code: Color::LightRed.reverse().prefix().to_string()}, + + AnsiCode{ short_name: Some("u"), long_name: "blue", code: Color::Blue.prefix().to_string()}, + AnsiCode{ short_name: Some("ub"), long_name: "blue_bold", code: Color::Blue.bold().prefix().to_string()}, + AnsiCode{ short_name: Some("uu"), long_name: "blue_underline", code: Color::Blue.underline().prefix().to_string()}, + AnsiCode{ short_name: Some("ui"), long_name: "blue_italic", code: Color::Blue.italic().prefix().to_string()}, + AnsiCode{ short_name: Some("ud"), long_name: "blue_dimmed", code: Color::Blue.dimmed().prefix().to_string()}, + AnsiCode{ short_name: Some("ur"), long_name: "blue_reverse", code: Color::Blue.reverse().prefix().to_string()}, + + AnsiCode{ short_name: Some("lu"), long_name: "light_blue", code: Color::LightBlue.prefix().to_string()}, + AnsiCode{ short_name: Some("lub"), long_name: "light_blue_bold", code: Color::LightBlue.bold().prefix().to_string()}, + AnsiCode{ short_name: Some("luu"), long_name: "light_blue_underline", code: Color::LightBlue.underline().prefix().to_string()}, + AnsiCode{ short_name: Some("lui"), long_name: "light_blue_italic", code: Color::LightBlue.italic().prefix().to_string()}, + AnsiCode{ short_name: Some("lud"), long_name: "light_blue_dimmed", code: Color::LightBlue.dimmed().prefix().to_string()}, + AnsiCode{ short_name: Some("lur"), long_name: "light_blue_reverse", code: Color::LightBlue.reverse().prefix().to_string()}, + + AnsiCode{ short_name: Some("b"), long_name: "black", code: Color::Black.prefix().to_string()}, + AnsiCode{ short_name: Some("bb"), long_name: "black_bold", code: Color::Black.bold().prefix().to_string()}, + AnsiCode{ short_name: Some("bu"), long_name: "black_underline", code: Color::Black.underline().prefix().to_string()}, + AnsiCode{ short_name: Some("bi"), long_name: "black_italic", code: Color::Black.italic().prefix().to_string()}, + AnsiCode{ short_name: Some("bd"), long_name: "black_dimmed", code: Color::Black.dimmed().prefix().to_string()}, + AnsiCode{ short_name: Some("br"), long_name: "black_reverse", code: Color::Black.reverse().prefix().to_string()}, + + AnsiCode{ short_name: Some("ligr"), long_name: "light_gray", code: Color::LightGray.prefix().to_string()}, + AnsiCode{ short_name: Some("ligrb"), long_name: "light_gray_bold", code: Color::LightGray.bold().prefix().to_string()}, + AnsiCode{ short_name: Some("ligru"), long_name: "light_gray_underline", code: Color::LightGray.underline().prefix().to_string()}, + AnsiCode{ short_name: Some("ligri"), long_name: "light_gray_italic", code: Color::LightGray.italic().prefix().to_string()}, + AnsiCode{ short_name: Some("ligrd"), long_name: "light_gray_dimmed", code: Color::LightGray.dimmed().prefix().to_string()}, + AnsiCode{ short_name: Some("ligrr"), long_name: "light_gray_reverse", code: Color::LightGray.reverse().prefix().to_string()}, + + AnsiCode{ short_name: Some("y"), long_name: "yellow", code: Color::Yellow.prefix().to_string()}, + AnsiCode{ short_name: Some("yb"), long_name: "yellow_bold", code: Color::Yellow.bold().prefix().to_string()}, + AnsiCode{ short_name: Some("yu"), long_name: "yellow_underline", code: Color::Yellow.underline().prefix().to_string()}, + AnsiCode{ short_name: Some("yi"), long_name: "yellow_italic", code: Color::Yellow.italic().prefix().to_string()}, + AnsiCode{ short_name: Some("yd"), long_name: "yellow_dimmed", code: Color::Yellow.dimmed().prefix().to_string()}, + AnsiCode{ short_name: Some("yr"), long_name: "yellow_reverse", code: Color::Yellow.reverse().prefix().to_string()}, + + AnsiCode{ short_name: Some("ly"), long_name: "light_yellow", code: Color::LightYellow.prefix().to_string()}, + AnsiCode{ short_name: Some("lyb"), long_name: "light_yellow_bold", code: Color::LightYellow.bold().prefix().to_string()}, + AnsiCode{ short_name: Some("lyu"), long_name: "light_yellow_underline", code: Color::LightYellow.underline().prefix().to_string()}, + AnsiCode{ short_name: Some("lyi"), long_name: "light_yellow_italic", code: Color::LightYellow.italic().prefix().to_string()}, + AnsiCode{ short_name: Some("lyd"), long_name: "light_yellow_dimmed", code: Color::LightYellow.dimmed().prefix().to_string()}, + AnsiCode{ short_name: Some("lyr"), long_name: "light_yellow_reverse", code: Color::LightYellow.reverse().prefix().to_string()}, + + AnsiCode{ short_name: Some("p"), long_name: "purple", code: Color::Purple.prefix().to_string()}, + AnsiCode{ short_name: Some("pb"), long_name: "purple_bold", code: Color::Purple.bold().prefix().to_string()}, + AnsiCode{ short_name: Some("pu"), long_name: "purple_underline", code: Color::Purple.underline().prefix().to_string()}, + AnsiCode{ short_name: Some("pi"), long_name: "purple_italic", code: Color::Purple.italic().prefix().to_string()}, + AnsiCode{ short_name: Some("pd"), long_name: "purple_dimmed", code: Color::Purple.dimmed().prefix().to_string()}, + AnsiCode{ short_name: Some("pr"), long_name: "purple_reverse", code: Color::Purple.reverse().prefix().to_string()}, + + AnsiCode{ short_name: Some("lp"), long_name: "light_purple", code: Color::LightPurple.prefix().to_string()}, + AnsiCode{ short_name: Some("lpb"), long_name: "light_purple_bold", code: Color::LightPurple.bold().prefix().to_string()}, + AnsiCode{ short_name: Some("lpu"), long_name: "light_purple_underline", code: Color::LightPurple.underline().prefix().to_string()}, + AnsiCode{ short_name: Some("lpi"), long_name: "light_purple_italic", code: Color::LightPurple.italic().prefix().to_string()}, + AnsiCode{ short_name: Some("lpd"), long_name: "light_purple_dimmed", code: Color::LightPurple.dimmed().prefix().to_string()}, + AnsiCode{ short_name: Some("lpr"), long_name: "light_purple_reverse", code: Color::LightPurple.reverse().prefix().to_string()}, + + AnsiCode{ short_name: Some("c"), long_name: "cyan", code: Color::Cyan.prefix().to_string()}, + AnsiCode{ short_name: Some("cb"), long_name: "cyan_bold", code: Color::Cyan.bold().prefix().to_string()}, + AnsiCode{ short_name: Some("cu"), long_name: "cyan_underline", code: Color::Cyan.underline().prefix().to_string()}, + AnsiCode{ short_name: Some("ci"), long_name: "cyan_italic", code: Color::Cyan.italic().prefix().to_string()}, + AnsiCode{ short_name: Some("cd"), long_name: "cyan_dimmed", code: Color::Cyan.dimmed().prefix().to_string()}, + AnsiCode{ short_name: Some("cr"), long_name: "cyan_reverse", code: Color::Cyan.reverse().prefix().to_string()}, + + AnsiCode{ short_name: Some("lc"), long_name: "light_cyan", code: Color::LightCyan.prefix().to_string()}, + AnsiCode{ short_name: Some("lcb"), long_name: "light_cyan_bold", code: Color::LightCyan.bold().prefix().to_string()}, + AnsiCode{ short_name: Some("lcu"), long_name: "light_cyan_underline", code: Color::LightCyan.underline().prefix().to_string()}, + AnsiCode{ short_name: Some("lci"), long_name: "light_cyan_italic", code: Color::LightCyan.italic().prefix().to_string()}, + AnsiCode{ short_name: Some("lcd"), long_name: "light_cyan_dimmed", code: Color::LightCyan.dimmed().prefix().to_string()}, + AnsiCode{ short_name: Some("lcr"), long_name: "light_cyan_reverse", code: Color::LightCyan.reverse().prefix().to_string()}, + + AnsiCode{ short_name: Some("w"), long_name: "white", code: Color::White.prefix().to_string()}, + AnsiCode{ short_name: Some("wb"), long_name: "white_bold", code: Color::White.bold().prefix().to_string()}, + AnsiCode{ short_name: Some("wu"), long_name: "white_underline", code: Color::White.underline().prefix().to_string()}, + AnsiCode{ short_name: Some("wi"), long_name: "white_italic", code: Color::White.italic().prefix().to_string()}, + AnsiCode{ short_name: Some("wd"), long_name: "white_dimmed", code: Color::White.dimmed().prefix().to_string()}, + AnsiCode{ short_name: Some("wr"), long_name: "white_reverse", code: Color::White.reverse().prefix().to_string()}, + + AnsiCode{ short_name: Some("dgr"), long_name: "dark_gray", code: Color::DarkGray.prefix().to_string()}, + AnsiCode{ short_name: Some("dgrb"), long_name: "dark_gray_bold", code: Color::DarkGray.bold().prefix().to_string()}, + AnsiCode{ short_name: Some("dgru"), long_name: "dark_gray_underline", code: Color::DarkGray.underline().prefix().to_string()}, + AnsiCode{ short_name: Some("dgri"), long_name: "dark_gray_italic", code: Color::DarkGray.italic().prefix().to_string()}, + AnsiCode{ short_name: Some("dgrd"), long_name: "dark_gray_dimmed", code: Color::DarkGray.dimmed().prefix().to_string()}, + AnsiCode{ short_name: Some("dgrr"), long_name: "dark_gray_reverse", code: Color::DarkGray.reverse().prefix().to_string()}, + + AnsiCode{ short_name: None, long_name: "reset", code: "\x1b[0m".to_owned()}, + // Reference for ansi codes https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797 + // Another good reference http://ascii-table.com/ansi-escape-sequences.php + + // For setting title like `echo [(char title) (pwd) (char bel)] | str collect` + AnsiCode{short_name: None, long_name:"title", code: "\x1b]2;".to_string()}, // ESC]2; xterm sets window title using OSC syntax escapes + + // Ansi Erase Sequences + AnsiCode{ short_name: None, long_name:"clear_screen", code: "\x1b[J".to_string()}, // clears the screen + AnsiCode{ short_name: None, long_name:"clear_screen_from_cursor_to_end", code: "\x1b[0J".to_string()}, // clears from cursor until end of screen + AnsiCode{ short_name: None, long_name:"clear_screen_from_cursor_to_beginning", code: "\x1b[1J".to_string()}, // clears from cursor to beginning of screen + AnsiCode{ short_name: Some("cls"), long_name:"clear_entire_screen", code: "\x1b[2J".to_string()}, // clears the entire screen + AnsiCode{ short_name: None, long_name:"erase_line", code: "\x1b[K".to_string()}, // clears the current line + AnsiCode{ short_name: None, long_name:"erase_line_from_cursor_to_end", code: "\x1b[0K".to_string()}, // clears from cursor to end of line + AnsiCode{ short_name: None, long_name:"erase_line_from_cursor_to_beginning", code: "\x1b[1K".to_string()}, // clears from cursor to start of line + AnsiCode{ short_name: None, long_name:"erase_entire_line", code: "\x1b[2K".to_string()}, // clears entire line + + // Turn on/off cursor + AnsiCode{ short_name: None, long_name:"cursor_off", code: "\x1b[?25l".to_string()}, + AnsiCode{ short_name: None, long_name:"cursor_on", code: "\x1b[?25h".to_string()}, + + // Turn on/off blinking + AnsiCode{ short_name: None, long_name:"cursor_blink_off", code: "\x1b[?12l".to_string()}, + AnsiCode{ short_name: None, long_name:"cursor_blink_on", code: "\x1b[?12h".to_string()}, + + // Cursor position in ESC [ ;R where r = row and c = column + AnsiCode{ short_name: None, long_name:"cursor_position", code: "\x1b[6n".to_string()}, + + // Report Terminal Identity + AnsiCode{ short_name: None, long_name:"identity", code: "\x1b[0c".to_string()}, + + // Ansi escape only - CSI command + AnsiCode{ short_name: Some("escape"), long_name: "escape_left", code: "\x1b[".to_string()}, + // OSC escape (Operating system command) + AnsiCode{ short_name: Some("osc"), long_name:"escape_right", code: "\x1b]".to_string()}, + // OSC string terminator + AnsiCode{ short_name: Some("st"), long_name:"string_terminator", code: "\x1b\\".to_string()}, + + // Ansi Rgb - Needs to be 32;2;r;g;b or 48;2;r;g;b + // assuming the rgb will be passed via command and no here + AnsiCode{ short_name: None, long_name:"rgb_fg", code: "\x1b[38;2;".to_string()}, + AnsiCode{ short_name: None, long_name:"rgb_bg", code: "\x1b[48;2;".to_string()}, + + // Ansi color index - Needs 38;5;idx or 48;5;idx where idx = 0 to 255 + AnsiCode{ short_name: Some("idx_fg"), long_name: "color_idx_fg", code: "\x1b[38;5;".to_string()}, + AnsiCode{ short_name: Some("idx_bg"), long_name:"color_idx_bg", code: "\x1b[48;5;".to_string()}, + + // Returns terminal size like "[;R" where r is rows and c is columns + // This should work assuming your terminal is not greater than 999x999 + AnsiCode{ short_name: None, long_name:"size", code: "\x1b[s\x1b[999;999H\x1b[6n\x1b[u".to_string()},}; + + static ref CODE_MAP: HashMap<&'static str, &'static str > = build_ansi_hashmap(&CODE_LIST); +} + +impl Command for AnsiCommand { + fn name(&self) -> &str { + "ansi" + } + + fn signature(&self) -> Signature { + Signature::build("ansi") + .optional( + "code", + SyntaxShape::Any, + "the name of the code to use like 'green' or 'reset' to reset the color", + ) + .switch( + "escape", // \x1b[ + "escape sequence without the escape character(s)", + Some('e'), + ) + .switch( + "osc", // \x1b] + "operating system command (ocs) escape sequence without the escape character(s)", + Some('o'), + ) + .switch("list", "list available ansi code names", Some('l')) + .category(Category::Platform) + } + + fn usage(&self) -> &str { + "Output ANSI codes to change color." + } + + fn extra_usage(&self) -> &str { + r#"For escape sequences: +Escape: '\x1b[' is not required for --escape parameter +Format: #(;#)m +Example: 1;31m for bold red or 2;37;41m for dimmed white fg with red bg +There can be multiple text formatting sequence numbers +separated by a ; and ending with an m where the # is of the +following values: + attribute_number, abbreviation, description + 0 reset / normal display + 1 b bold or increased intensity + 2 d faint or decreased intensity + 3 i italic on (non-mono font) + 4 u underline on + 5 l slow blink on + 6 fast blink on + 7 r reverse video on + 8 h nondisplayed (invisible) on + 9 s strike-through on + + foreground/bright colors background/bright colors + 30/90 black 40/100 black + 31/91 red 41/101 red + 32/92 green 42/102 green + 33/93 yellow 43/103 yellow + 34/94 blue 44/104 blue + 35/95 magenta 45/105 magenta + 36/96 cyan 46/106 cyan + 37/97 white 47/107 white + https://en.wikipedia.org/wiki/ANSI_escape_code + +OSC: '\x1b]' is not required for --osc parameter +Example: echo [(ansi -o '0') 'some title' (char bel)] | str collect +Format: # + 0 Set window title and icon name + 1 Set icon name + 2 Set window title + 4 Set/read color palette + 9 iTerm2 Grown notifications + 10 Set foreground color (x11 color spec) + 11 Set background color (x11 color spec) + ... others"# + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Change color to green", + example: r#"ansi green"#, + result: Some(Value::test_string("\u{1b}[32m")), + }, + Example { + description: "Reset the color", + example: r#"ansi reset"#, + result: Some(Value::test_string("\u{1b}[0m")), + }, + Example { + description: + "Use ansi to color text (rb = red bold, gb = green bold, pb = purple bold)", + example: r#"echo [(ansi rb) Hello " " (ansi gb) Nu " " (ansi pb) World (ansi reset)] | str collect"#, + result: Some(Value::test_string( + "\u{1b}[1;31mHello \u{1b}[1;32mNu \u{1b}[1;35mWorld\u{1b}[0m", + )), + }, + Example { + description: "Use ansi to color text (italic bright yellow on red 'Hello' with green bold 'Nu' and purble bold 'World')", + example: r#"echo [(ansi -e '3;93;41m') Hello (ansi reset) " " (ansi gb) Nu " " (ansi pb) World (ansi reset)] | str collect"#, + result: Some(Value::test_string( + "\u{1b}[3;93;41mHello\u{1b}[0m \u{1b}[1;32mNu \u{1b}[1;35mWorld\u{1b}[0m", + )), + }, + Example { + description: "Use ansi to color text with a style (blue on red in bold)", + example: r#"$"(ansi -e { fg: '#0000ff' bg: '#ff0000' attr: b })Hello Nu World(ansi reset)""#, + result: Some(Value::test_string( + "\u{1b}[1;48;2;255;0;0;38;2;0;0;255mHello Nu World\u{1b}[0m", + )), + }, + ] + } + + fn run( + &self, + engine_state: &nu_protocol::engine::EngineState, + stack: &mut nu_protocol::engine::Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let list: bool = call.has_flag("list"); + let escape: bool = call.has_flag("escape"); + let osc: bool = call.has_flag("osc"); + + if list { + return generate_ansi_code_list(engine_state, call.head); + } + + // The code can now be one of the ansi abbreviations like green_bold + // or it can be a record like this: { fg: "#ff0000" bg: "#00ff00" attr: bli } + // this record is defined in nu-color-config crate + let code: Value = match call.opt(engine_state, stack, 0)? { + Some(c) => c, + None => return Err(ShellError::MissingParameter("code".into(), call.head)), + }; + + let param_is_string = matches!(code, Value::String { val: _, span: _ }); + + if escape && osc { + return Err(ShellError::IncompatibleParameters { + left_message: "escape".into(), + left_span: call + .get_named_arg("escape") + .expect("Unexpected missing argument") + .span, + right_message: "osc".into(), + right_span: call + .get_named_arg("osc") + .expect("Unexpected missing argument") + .span, + }); + } + + let code_string = if param_is_string { + code.as_string().expect("error getting code as string") + } else { + "".to_string() + }; + + let param_is_valid_string = param_is_string && !code_string.is_empty(); + + if (escape || osc) && (param_is_valid_string) { + let code_vec: Vec = code_string.chars().collect(); + if code_vec[0] == '\\' { + return Err(ShellError::UnsupportedInput( + String::from("no need for escape characters"), + call.get_flag_expr("escape") + .expect("Unexpected missing argument") + .span, + )); + } + } + + let output = if escape && param_is_valid_string { + format!("\x1b[{}", code_string) + } else if osc && param_is_valid_string { + // Operating system command aka osc ESC ] <- note the right brace, not left brace for osc + // OCS's need to end with a bell '\x07' char + format!("\x1b]{};", code_string) + } else if param_is_valid_string { + match str_to_ansi(&code_string) { + Some(c) => c, + None => { + return Err(ShellError::UnsupportedInput( + String::from("Unknown ansi code"), + call.nth(0).expect("Unexpected missing argument").span, + )) + } + } + } else { + // This is a record that should look like + // { fg: "#ff0000" bg: "#00ff00" attr: bli } + let record = code.as_record()?; + // create a NuStyle to parse the information into + let mut nu_style = nu_color_config::NuStyle { + fg: None, + bg: None, + attr: None, + }; + // Iterate and populate NuStyle with real values + for (k, v) in record.0.iter().zip(record.1) { + match k.as_str() { + "fg" => nu_style.fg = Some(v.as_string()?), + "bg" => nu_style.bg = Some(v.as_string()?), + "attr" => nu_style.attr = Some(v.as_string()?), + _ => { + return Err(ShellError::IncompatibleParametersSingle( + format!("problem with key: {}", k), + code.span().expect("error with span"), + )) + } + } + } + // Now create a nu_ansi_term::Style from the NuStyle + let style = nu_color_config::parse_nustyle(nu_style); + // Return the prefix string. The prefix is the Ansi String. The suffix would be 0m, reset/stop coloring. + style.prefix().to_string() + }; + + Ok(Value::string(output, call.head).into_pipeline_data()) + } +} + +pub fn str_to_ansi(s: &str) -> Option { + CODE_MAP.get(s).map(|x| String::from(*x)) +} + +fn generate_ansi_code_list( + engine_state: &nu_protocol::engine::EngineState, + call_span: Span, +) -> Result { + return Ok(CODE_LIST + .iter() + .map(move |ansi_code| { + let cols = vec!["name".into(), "short name".into(), "code".into()]; + let name: Value = Value::string(String::from(ansi_code.long_name), call_span); + let short_name = Value::string(ansi_code.short_name.unwrap_or(""), call_span); + let code_string = String::from(&ansi_code.code.replace("\u{1b}", "")); + let code = Value::string(code_string, call_span); + let vals = vec![name, short_name, code]; + Value::Record { + cols, + vals, + span: call_span, + } + }) + .into_pipeline_data(engine_state.ctrlc.clone())); +} + +fn build_ansi_hashmap(v: &'static [AnsiCode]) -> HashMap<&'static str, &'static str> { + let mut result = HashMap::new(); + for code in v.iter() { + let value: &'static str = &code.code; + if let Some(sn) = code.short_name { + result.insert(sn, value); + } + result.insert(code.long_name, value); + } + result +} + +#[cfg(test)] +mod tests { + use crate::platform::ansi::ansi_::AnsiCommand; + + #[test] + fn examples_work_as_expected() { + use crate::test_examples; + + test_examples(AnsiCommand {}) + } +} diff --git a/crates/nu-command/src/platform/ansi/gradient.rs b/crates/nu-command/src/platform/ansi/gradient.rs new file mode 100644 index 0000000000..92defdcf87 --- /dev/null +++ b/crates/nu-command/src/platform/ansi/gradient.rs @@ -0,0 +1,316 @@ +use nu_ansi_term::{build_all_gradient_text, gradient::TargetGround, Gradient, Rgb}; +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, ast::CellPath, engine::Command, engine::EngineState, engine::Stack, Category, + Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "ansi gradient" + } + + fn signature(&self) -> Signature { + Signature::build("ansi gradient") + .named( + "fgstart", + SyntaxShape::String, + "foreground gradient start color in hex (0x123456)", + Some('a'), + ) + .named( + "fgend", + SyntaxShape::String, + "foreground gradient end color in hex", + Some('b'), + ) + .named( + "bgstart", + SyntaxShape::String, + "background gradient start color in hex", + Some('c'), + ) + .named( + "bgend", + SyntaxShape::String, + "background gradient end color in hex", + Some('d'), + ) + .rest( + "column path", + SyntaxShape::CellPath, + "optionally, draw gradients using text from column paths", + ) + .category(Category::Platform) + } + + fn usage(&self) -> &str { + "draw text with a provided start and end code making a gradient" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "draw text in a gradient with foreground start and end colors", + example: + "echo 'Hello, Nushell! This is a gradient.' | ansi gradient --fgstart 0x40c9ff --fgend 0xe81cff", + result: None, + }, + Example { + description: "draw text in a gradient with foreground start and end colors and background start and end colors", + example: + "echo 'Hello, Nushell! This is a gradient.' | ansi gradient --fgstart 0x40c9ff --fgend 0xe81cff --bgstart 0xe81cff --bgend 0x40c9ff", + result: None, + }, + Example { + description: "draw text in a gradient by specifying foreground start color - end color is assumed to be black", + example: + "echo 'Hello, Nushell! This is a gradient.' | ansi gradient --fgstart 0x40c9ff", + result: None, + }, + Example { + description: "draw text in a gradient by specifying foreground end color - start color is assumed to be black", + example: + "echo 'Hello, Nushell! This is a gradient.' | ansi gradient --fgend 0xe81cff", + result: None, + }, + ] + } +} + +fn value_to_color(v: Option) -> Result, ShellError> { + let s = match v { + None => return Ok(None), + Some(x) => x.as_string()?, + }; + Ok(Some(Rgb::from_hex_string(s))) +} + +fn operate( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let fgstart: Option = call.get_flag(engine_state, stack, "fgstart")?; + let fgend: Option = call.get_flag(engine_state, stack, "fgend")?; + let bgstart: Option = call.get_flag(engine_state, stack, "bgstart")?; + let bgend: Option = call.get_flag(engine_state, stack, "bgend")?; + let column_paths: Vec = call.rest(engine_state, stack, 0)?; + + let fgs_hex = value_to_color(fgstart)?; + let fge_hex = value_to_color(fgend)?; + let bgs_hex = value_to_color(bgstart)?; + let bge_hex = value_to_color(bgend)?; + let head = call.head; + input.map( + move |v| { + if column_paths.is_empty() { + action(&v, fgs_hex, fge_hex, bgs_hex, bge_hex, &head) + } else { + let mut ret = v; + for path in &column_paths { + let r = ret.update_cell_path( + &path.members, + Box::new(move |old| action(old, fgs_hex, fge_hex, bgs_hex, bge_hex, &head)), + ); + if let Err(error) = r { + return Value::Error { error }; + } + } + ret + } + }, + engine_state.ctrlc.clone(), + ) +} + +fn action( + input: &Value, + fg_start: Option, + fg_end: Option, + bg_start: Option, + bg_end: Option, + command_span: &Span, +) -> Value { + match input { + Value::String { val, span } => { + match (fg_start, fg_end, bg_start, bg_end) { + (None, None, None, None) => { + // Error - no colors + Value::Error { + error: ShellError::MissingParameter( + "please supply foreground and/or background color parameters".into(), + *command_span, + ), + } + } + (None, None, None, Some(bg_end)) => { + // Error - missing bg_start, so assume black + let bg_start = Rgb::new(0, 0, 0); + let gradient = Gradient::new(bg_start, bg_end); + let gradient_string = gradient.build(val, TargetGround::Background); + Value::string(gradient_string, *span) + } + (None, None, Some(bg_start), None) => { + // Error - missing bg_end, so assume black + let bg_end = Rgb::new(0, 0, 0); + let gradient = Gradient::new(bg_start, bg_end); + let gradient_string = gradient.build(val, TargetGround::Background); + Value::string(gradient_string, *span) + } + (None, None, Some(bg_start), Some(bg_end)) => { + // Background Only + let gradient = Gradient::new(bg_start, bg_end); + let gradient_string = gradient.build(val, TargetGround::Background); + Value::string(gradient_string, *span) + } + (None, Some(fg_end), None, None) => { + // Error - missing fg_start, so assume black + let fg_start = Rgb::new(0, 0, 0); + let gradient = Gradient::new(fg_start, fg_end); + let gradient_string = gradient.build(val, TargetGround::Foreground); + Value::string(gradient_string, *span) + } + (None, Some(fg_end), None, Some(bg_end)) => { + // missin fg_start and bg_start, so assume black + let fg_start = Rgb::new(0, 0, 0); + let bg_start = Rgb::new(0, 0, 0); + let fg_gradient = Gradient::new(fg_start, fg_end); + let bg_gradient = Gradient::new(bg_start, bg_end); + let gradient_string = build_all_gradient_text(val, fg_gradient, bg_gradient); + Value::string(gradient_string, *span) + } + (None, Some(fg_end), Some(bg_start), None) => { + // Error - missing fg_start and bg_end + let fg_start = Rgb::new(0, 0, 0); + let bg_end = Rgb::new(0, 0, 0); + let fg_gradient = Gradient::new(fg_start, fg_end); + let bg_gradient = Gradient::new(bg_start, bg_end); + let gradient_string = build_all_gradient_text(val, fg_gradient, bg_gradient); + Value::string(gradient_string, *span) + } + (None, Some(fg_end), Some(bg_start), Some(bg_end)) => { + // Error - missing fg_start, so assume black + let fg_start = Rgb::new(0, 0, 0); + let fg_gradient = Gradient::new(fg_start, fg_end); + let bg_gradient = Gradient::new(bg_start, bg_end); + let gradient_string = build_all_gradient_text(val, fg_gradient, bg_gradient); + Value::string(gradient_string, *span) + } + (Some(fg_start), None, None, None) => { + // Error - missing fg_end, so assume black + let fg_end = Rgb::new(0, 0, 0); + let gradient = Gradient::new(fg_start, fg_end); + let gradient_string = gradient.build(val, TargetGround::Foreground); + Value::string(gradient_string, *span) + } + (Some(fg_start), None, None, Some(bg_end)) => { + // Error - missing fg_end, bg_start, so assume black + let fg_end = Rgb::new(0, 0, 0); + let bg_start = Rgb::new(0, 0, 0); + let fg_gradient = Gradient::new(fg_start, fg_end); + let bg_gradient = Gradient::new(bg_start, bg_end); + let gradient_string = build_all_gradient_text(val, fg_gradient, bg_gradient); + Value::string(gradient_string, *span) + } + (Some(fg_start), None, Some(bg_start), None) => { + // Error - missing fg_end, bg_end, so assume black + let fg_end = Rgb::new(0, 0, 0); + let bg_end = Rgb::new(0, 0, 0); + let fg_gradient = Gradient::new(fg_start, fg_end); + let bg_gradient = Gradient::new(bg_start, bg_end); + let gradient_string = build_all_gradient_text(val, fg_gradient, bg_gradient); + Value::string(gradient_string, *span) + } + (Some(fg_start), None, Some(bg_start), Some(bg_end)) => { + // Error - missing fg_end, so assume black + let fg_end = Rgb::new(0, 0, 0); + let fg_gradient = Gradient::new(fg_start, fg_end); + let bg_gradient = Gradient::new(bg_start, bg_end); + let gradient_string = build_all_gradient_text(val, fg_gradient, bg_gradient); + Value::string(gradient_string, *span) + } + (Some(fg_start), Some(fg_end), None, None) => { + // Foreground Only + let gradient = Gradient::new(fg_start, fg_end); + let gradient_string = gradient.build(val, TargetGround::Foreground); + Value::string(gradient_string, *span) + } + (Some(fg_start), Some(fg_end), None, Some(bg_end)) => { + // Error - missing bg_start, so assume black + let bg_start = Rgb::new(0, 0, 0); + let fg_gradient = Gradient::new(fg_start, fg_end); + let bg_gradient = Gradient::new(bg_start, bg_end); + let gradient_string = build_all_gradient_text(val, fg_gradient, bg_gradient); + Value::string(gradient_string, *span) + } + (Some(fg_start), Some(fg_end), Some(bg_start), None) => { + // Error - missing bg_end, so assume black + let bg_end = Rgb::new(0, 0, 0); + let fg_gradient = Gradient::new(fg_start, fg_end); + let bg_gradient = Gradient::new(bg_start, bg_end); + let gradient_string = build_all_gradient_text(val, fg_gradient, bg_gradient); + Value::string(gradient_string, *span) + } + (Some(fg_start), Some(fg_end), Some(bg_start), Some(bg_end)) => { + // Foreground and Background Gradient + let fg_gradient = Gradient::new(fg_start, fg_end); + let bg_gradient = Gradient::new(bg_start, bg_end); + let gradient_string = build_all_gradient_text(val, fg_gradient, bg_gradient); + Value::string(gradient_string, *span) + } + } + } + other => { + let got = format!("value is {}, not string", other.get_type()); + + Value::Error { + error: ShellError::TypeMismatch(got, other.span().unwrap_or(*command_span)), + } + } + } +} + +#[cfg(test)] +mod tests { + use super::{action, SubCommand}; + use nu_ansi_term::Rgb; + use nu_protocol::{Span, Value}; + + #[test] + fn examples_work_as_expected() { + use crate::test_examples; + + test_examples(SubCommand {}) + } + + #[test] + fn test_fg_gradient() { + let input_string = Value::test_string("Hello, World!"); + let expected = Value::test_string("\u{1b}[38;2;64;201;255mH\u{1b}[38;2;76;187;254me\u{1b}[38;2;89;174;254ml\u{1b}[38;2;102;160;254ml\u{1b}[38;2;115;147;254mo\u{1b}[38;2;128;133;254m,\u{1b}[38;2;141;120;254m \u{1b}[38;2;153;107;254mW\u{1b}[38;2;166;94;254mo\u{1b}[38;2;179;80;254mr\u{1b}[38;2;192;67;254ml\u{1b}[38;2;205;53;254md\u{1b}[38;2;218;40;254m!\u{1b}[0m"); + let fg_start = Rgb::from_hex_string("0x40c9ff".to_string()); + let fg_end = Rgb::from_hex_string("0xe81cff".to_string()); + let actual = action( + &input_string, + Some(fg_start), + Some(fg_end), + None, + None, + &Span::test_data(), + ); + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-command/src/platform/ansi/mod.rs b/crates/nu-command/src/platform/ansi/mod.rs new file mode 100644 index 0000000000..4613686bc7 --- /dev/null +++ b/crates/nu-command/src/platform/ansi/mod.rs @@ -0,0 +1,7 @@ +mod ansi_; +mod gradient; +mod strip; + +pub use ansi_::AnsiCommand as Ansi; +pub use gradient::SubCommand as AnsiGradient; +pub use strip::SubCommand as AnsiStrip; diff --git a/crates/nu-command/src/platform/ansi/strip.rs b/crates/nu-command/src/platform/ansi/strip.rs new file mode 100644 index 0000000000..b69e1b06ae --- /dev/null +++ b/crates/nu-command/src/platform/ansi/strip.rs @@ -0,0 +1,123 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, ast::CellPath, engine::Command, engine::EngineState, engine::Stack, Category, + Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; +use strip_ansi_escapes::strip; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "ansi strip" + } + + fn signature(&self) -> Signature { + Signature::build("ansi strip") + .rest( + "column path", + SyntaxShape::CellPath, + "optionally, remove ansi sequences by column paths", + ) + .category(Category::Platform) + } + + fn usage(&self) -> &str { + "strip ansi escape sequences from string" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "strip ansi escape sequences from string", + example: r#"echo [ (ansi green) (ansi cursor_on) "hello" ] | str collect | ansi strip"#, + result: Some(Value::test_string("hello")), + }] + } +} + +fn operate( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let column_paths: Vec = call.rest(engine_state, stack, 0)?; + let head = call.head; + input.map( + move |v| { + if column_paths.is_empty() { + action(&v, &head) + } else { + let mut ret = v; + + for path in &column_paths { + let r = ret + .update_cell_path(&path.members, Box::new(move |old| action(old, &head))); + if let Err(error) = r { + return Value::Error { error }; + } + } + + ret + } + }, + engine_state.ctrlc.clone(), + ) +} + +fn action(input: &Value, command_span: &Span) -> Value { + match input { + Value::String { val, span } => { + let stripped_string = { + if let Ok(bytes) = strip(&val) { + String::from_utf8_lossy(&bytes).to_string() + } else { + val.to_string() + } + }; + + Value::string(stripped_string, *span) + } + other => { + let got = format!("value is {}, not string", other.get_type()); + + Value::Error { + error: ShellError::TypeMismatch(got, other.span().unwrap_or(*command_span)), + } + } + } +} + +#[cfg(test)] +mod tests { + use super::{action, SubCommand}; + use nu_protocol::{Span, Value}; + + #[test] + fn examples_work_as_expected() { + use crate::test_examples; + + test_examples(SubCommand {}) + } + + #[test] + fn test_stripping() { + let input_string = + Value::test_string("\u{1b}[3;93;41mHello\u{1b}[0m \u{1b}[1;32mNu \u{1b}[1;35mWorld"); + let expected = Value::test_string("Hello Nu World"); + + let actual = action(&input_string, &Span::test_data()); + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-command/src/platform/clear.rs b/crates/nu-command/src/platform/clear.rs new file mode 100644 index 0000000000..fdc540bd63 --- /dev/null +++ b/crates/nu-command/src/platform/clear.rs @@ -0,0 +1,53 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Value, +}; +use std::process::Command as CommandSys; + +#[derive(Clone)] +pub struct Clear; + +impl Command for Clear { + fn name(&self) -> &str { + "clear" + } + + fn usage(&self) -> &str { + "Clear the terminal." + } + + fn signature(&self) -> Signature { + Signature::build("clear").category(Category::Platform) + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + if cfg!(windows) { + CommandSys::new("cmd") + .args(["/C", "cls"]) + .status() + .expect("failed to execute process"); + } else if cfg!(unix) { + CommandSys::new("/bin/sh") + .args(["-c", "clear"]) + .status() + .expect("failed to execute process"); + } + + Ok(Value::Nothing { span: call.head }.into_pipeline_data()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Clear the terminal", + example: "clear", + result: None, + }] + } +} diff --git a/crates/nu-command/src/platform/dir_info.rs b/crates/nu-command/src/platform/dir_info.rs new file mode 100644 index 0000000000..a18081510e --- /dev/null +++ b/crates/nu-command/src/platform/dir_info.rs @@ -0,0 +1,292 @@ +use filesize::file_real_size_fast; +use glob::Pattern; +use nu_protocol::{ShellError, Span, Value}; +use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; + +#[derive(Debug, Clone)] +pub struct DirBuilder { + pub tag: Span, + pub min: Option, + pub deref: bool, + pub exclude: Option, + pub all: bool, +} + +impl DirBuilder { + pub fn new( + tag: Span, + min: Option, + deref: bool, + exclude: Option, + all: bool, + ) -> DirBuilder { + DirBuilder { + tag, + min, + deref, + exclude, + all, + } + } +} + +#[derive(Debug, Clone)] +pub struct DirInfo { + dirs: Vec, + files: Vec, + errors: Vec, + size: u64, + blocks: u64, + path: PathBuf, + tag: Span, +} + +#[derive(Debug, Clone)] +pub struct FileInfo { + path: PathBuf, + size: u64, + blocks: Option, + tag: Span, +} + +impl FileInfo { + pub fn new(path: impl Into, deref: bool, tag: Span) -> Result { + let path = path.into(); + let m = if deref { + std::fs::metadata(&path) + } else { + std::fs::symlink_metadata(&path) + }; + + match m { + Ok(d) => { + let block_size = file_real_size_fast(&path, &d).ok(); + + Ok(FileInfo { + path, + blocks: block_size, + size: d.len(), + tag, + }) + } + Err(e) => Err(e.into()), + } + } +} + +impl DirInfo { + pub fn new( + path: impl Into, + params: &DirBuilder, + depth: Option, + ctrl_c: Option>, + ) -> Self { + let path = path.into(); + + let mut s = Self { + dirs: Vec::new(), + errors: Vec::new(), + files: Vec::new(), + size: 0, + blocks: 0, + tag: params.tag, + path, + }; + + match std::fs::metadata(&s.path) { + Ok(d) => { + s.size = d.len(); // dir entry size + s.blocks = file_real_size_fast(&s.path, &d).ok().unwrap_or(0); + } + Err(e) => s = s.add_error(e.into()), + }; + + match std::fs::read_dir(&s.path) { + Ok(d) => { + for f in d { + match ctrl_c { + Some(ref cc) => { + if cc.load(Ordering::SeqCst) { + break; + } + } + None => continue, + } + + match f { + Ok(i) => match i.file_type() { + Ok(t) if t.is_dir() => { + s = s.add_dir(i.path(), depth, params, ctrl_c.clone()) + } + Ok(_t) => s = s.add_file(i.path(), params), + Err(e) => s = s.add_error(e.into()), + }, + Err(e) => s = s.add_error(e.into()), + } + } + } + Err(e) => s = s.add_error(e.into()), + } + s + } + + fn add_dir( + mut self, + path: impl Into, + mut depth: Option, + params: &DirBuilder, + ctrl_c: Option>, + ) -> Self { + if let Some(current) = depth { + if let Some(new) = current.checked_sub(1) { + depth = Some(new); + } else { + return self; + } + } + + let d = DirInfo::new(path, params, depth, ctrl_c); + self.size += d.size; + self.blocks += d.blocks; + self.dirs.push(d); + self + } + + fn add_file(mut self, f: impl Into, params: &DirBuilder) -> Self { + let f = f.into(); + let include = params + .exclude + .as_ref() + .map_or(true, |x| !x.matches_path(&f)); + if include { + match FileInfo::new(f, params.deref, self.tag) { + Ok(file) => { + let inc = params.min.map_or(true, |s| file.size >= s); + if inc { + self.size += file.size; + self.blocks += file.blocks.unwrap_or(0); + if params.all { + self.files.push(file); + } + } + } + Err(e) => self = self.add_error(e), + } + } + self + } + + fn add_error(mut self, e: ShellError) -> Self { + self.errors.push(e); + self + } + + pub fn get_size(&self) -> u64 { + self.size + } +} + +impl From for Value { + fn from(d: DirInfo) -> Self { + let mut cols = vec![]; + let mut vals = vec![]; + + cols.push("path".into()); + vals.push(Value::string(d.path.display().to_string(), d.tag)); + + cols.push("apparent".into()); + vals.push(Value::Filesize { + val: d.size as i64, + span: d.tag, + }); + + cols.push("physical".into()); + vals.push(Value::Filesize { + val: d.blocks as i64, + span: d.tag, + }); + + cols.push("directories".into()); + vals.push(value_from_vec(d.dirs, &d.tag)); + + cols.push("files".into()); + vals.push(value_from_vec(d.files, &d.tag)); + + // if !d.errors.is_empty() { + // let v = d + // .errors + // .into_iter() + // .map(move |e| Value::Error { error: e }) + // .collect::>(); + + // cols.push("errors".into()); + // vals.push(Value::List { + // vals: v, + // span: d.tag, + // }) + // } + + Value::Record { + cols, + vals, + span: d.tag, + } + } +} + +impl From for Value { + fn from(f: FileInfo) -> Self { + let mut cols = vec![]; + let mut vals = vec![]; + + cols.push("path".into()); + vals.push(Value::string(f.path.display().to_string(), f.tag)); + + cols.push("apparent".into()); + vals.push(Value::Filesize { + val: f.size as i64, + span: f.tag, + }); + + cols.push("physical".into()); + vals.push(Value::Filesize { + val: match f.blocks { + Some(b) => b as i64, + None => 0i64, + }, + span: f.tag, + }); + + cols.push("directories".into()); + vals.push(Value::nothing(Span::test_data())); + + cols.push("files".into()); + vals.push(Value::nothing(Span::test_data())); + + // cols.push("errors".into()); + // vals.push(Value::nothing(Span::test_data())); + + Value::Record { + cols, + vals, + span: f.tag, + } + } +} + +fn value_from_vec(vec: Vec, tag: &Span) -> Value +where + V: Into, +{ + if vec.is_empty() { + Value::nothing(*tag) + } else { + let values = vec.into_iter().map(Into::into).collect::>(); + Value::List { + vals: values, + span: *tag, + } + } +} diff --git a/crates/nu-command/src/platform/du.rs b/crates/nu-command/src/platform/du.rs new file mode 100644 index 0000000000..d47dd7e727 --- /dev/null +++ b/crates/nu-command/src/platform/du.rs @@ -0,0 +1,183 @@ +use crate::{DirBuilder, DirInfo, FileInfo}; +use glob::{GlobError, MatchOptions, Pattern}; +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Spanned, + SyntaxShape, Value, +}; +use serde::Deserialize; +use std::path::PathBuf; + +const GLOB_PARAMS: MatchOptions = MatchOptions { + case_sensitive: true, + require_literal_separator: true, + require_literal_leading_dot: false, +}; + +#[derive(Clone)] +pub struct Du; + +#[derive(Deserialize, Clone, Debug)] +pub struct DuArgs { + path: Option>, + all: bool, + deref: bool, + exclude: Option>, + #[serde(rename = "max-depth")] + max_depth: Option, + #[serde(rename = "min-size")] + min_size: Option, +} + +impl Command for Du { + fn name(&self) -> &str { + "du" + } + + fn usage(&self) -> &str { + "Find disk usage sizes of specified items." + } + + fn signature(&self) -> Signature { + Signature::build("du") + .optional("path", SyntaxShape::GlobPattern, "starting directory") + .switch( + "all", + "Output file sizes as well as directory sizes", + Some('a'), + ) + .switch( + "deref", + "Dereference symlinks to their targets for size", + Some('r'), + ) + .named( + "exclude", + SyntaxShape::GlobPattern, + "Exclude these file names", + Some('x'), + ) + .named( + "max-depth", + SyntaxShape::Int, + "Directory recursion limit", + Some('d'), + ) + .named( + "min-size", + SyntaxShape::Int, + "Exclude files below this size", + Some('m'), + ) + .category(Category::Core) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let tag = call.head; + let args = DuArgs { + path: call.opt(engine_state, stack, 0)?, + all: call.has_flag("all"), + deref: call.has_flag("deref"), + exclude: call.get_flag(engine_state, stack, "exclude")?, + max_depth: call + .get_flag::(engine_state, stack, "max-depth")? + .map(|n| (n as u64).try_into().expect("error converting i64 to u64")), + min_size: call.get_flag(engine_state, stack, "min_size")?, + }; + + let exclude = args.exclude.map_or(Ok(None), move |x| { + Pattern::new(&x.item).map(Some).map_err(|e| { + ShellError::SpannedLabeledError(e.msg.to_string(), "glob error".to_string(), x.span) + }) + })?; + + let include_files = args.all; + let mut paths = match args.path { + Some(p) => { + let p = p.item.to_str().expect("Why isn't this encoded properly?"); + glob::glob_with(p, GLOB_PARAMS) + } + None => glob::glob_with("*", GLOB_PARAMS), + } + .map_err(|e| { + ShellError::SpannedLabeledError(e.msg.to_string(), "glob error".to_string(), tag) + })? + .filter(move |p| { + if include_files { + true + } else { + match p { + Ok(f) if f.is_dir() => true, + Err(e) if e.path().is_dir() => true, + _ => false, + } + } + }) + .map(|v| v.map_err(glob_err_into)); + + let all = args.all; + let deref = args.deref; + let max_depth = args.max_depth.map(|f| f as u64); + let min_size = args.min_size.map(|f| f as u64); + + let params = DirBuilder { + tag, + min: min_size, + deref, + exclude, + all, + }; + + let mut output: Vec = vec![]; + for p in paths.by_ref() { + match p { + Ok(a) => { + if a.is_dir() { + output.push( + DirInfo::new(a, ¶ms, max_depth, engine_state.ctrlc.clone()).into(), + ); + } else if let Ok(v) = FileInfo::new(a, deref, tag) { + output.push(v.into()); + } + } + Err(e) => { + output.push(Value::Error { error: e }); + } + } + } + + Ok(output.into_pipeline_data(engine_state.ctrlc.clone())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Disk usage of the current directory", + example: "du", + result: None, + }] + } +} + +fn glob_err_into(e: GlobError) -> ShellError { + let e = e.into_error(); + ShellError::from(e) +} + +#[cfg(test)] +mod tests { + use super::Du; + + #[test] + fn examples_work_as_expected() { + use crate::test_examples; + test_examples(Du {}) + } +} diff --git a/crates/nu-command/src/platform/input.rs b/crates/nu-command/src/platform/input.rs new file mode 100644 index 0000000000..9a36031a69 --- /dev/null +++ b/crates/nu-command/src/platform/input.rs @@ -0,0 +1,119 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, Value, +}; +use std::io::{Read, Write}; + +#[derive(Clone)] +pub struct Input; + +impl Command for Input { + fn name(&self) -> &str { + "input" + } + + fn usage(&self) -> &str { + "Get input from the user." + } + + fn signature(&self) -> Signature { + Signature::build("input") + .optional("prompt", SyntaxShape::String, "prompt to show the user") + .named( + "bytes-until", + SyntaxShape::String, + "read bytes (not text) until a stop byte", + Some('u'), + ) + .category(Category::Platform) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let prompt: Option = call.opt(engine_state, stack, 0)?; + let bytes_until: Option = call.get_flag(engine_state, stack, "bytes-until")?; + + if let Some(bytes_until) = bytes_until { + let _ = crossterm::terminal::enable_raw_mode(); + + if let Some(prompt) = prompt { + print!("{}", prompt); + let _ = std::io::stdout().flush(); + } + if let Some(c) = bytes_until.bytes().next() { + let mut buf = [0u8; 1]; + let mut buffer = vec![]; + + let mut stdin = std::io::stdin(); + + loop { + if let Err(err) = stdin.read_exact(&mut buf) { + let _ = crossterm::terminal::disable_raw_mode(); + return Err(ShellError::IOError(err.to_string())); + } + buffer.push(buf[0]); + + if buf[0] == c { + let _ = crossterm::terminal::disable_raw_mode(); + break; + } + } + + Ok(Value::Binary { + val: buffer, + span: call.head, + } + .into_pipeline_data()) + } else { + let _ = crossterm::terminal::disable_raw_mode(); + Err(ShellError::IOError( + "input can't stop on this byte".to_string(), + )) + } + } else { + if let Some(prompt) = prompt { + print!("{}", prompt); + let _ = std::io::stdout().flush(); + } + + // Just read a normal line of text + let mut buf = String::new(); + let input = std::io::stdin().read_line(&mut buf); + + match input { + Ok(_) => Ok(Value::String { + val: buf, + span: call.head, + } + .into_pipeline_data()), + Err(err) => Err(ShellError::IOError(err.to_string())), + } + } + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Get input from the user, and assign to a variable", + example: "let user-input = (input)", + result: None, + }] + } +} + +#[cfg(test)] +mod tests { + use super::Input; + + #[test] + fn examples_work_as_expected() { + use crate::test_examples; + test_examples(Input {}) + } +} diff --git a/crates/nu-command/src/platform/kill.rs b/crates/nu-command/src/platform/kill.rs new file mode 100644 index 0000000000..405e19caa4 --- /dev/null +++ b/crates/nu-command/src/platform/kill.rs @@ -0,0 +1,166 @@ +use nu_engine::CallExt; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ast::Call, span}; +use nu_protocol::{ + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Spanned, SyntaxShape, + Value, +}; +use std::process::{Command as CommandSys, Stdio}; + +#[derive(Clone)] +pub struct Kill; + +impl Command for Kill { + fn name(&self) -> &str { + "kill" + } + + fn usage(&self) -> &str { + "Kill a process using the process id." + } + + fn signature(&self) -> Signature { + let signature = Signature::build("kill") + .required( + "pid", + SyntaxShape::Int, + "process id of process that is to be killed", + ) + .rest("rest", SyntaxShape::Int, "rest of processes to kill") + .switch("force", "forcefully kill the process", Some('f')) + .switch("quiet", "won't print anything to the console", Some('q')) + .category(Category::Platform); + + if cfg!(windows) { + return signature; + } + + signature.named( + "signal", + SyntaxShape::Int, + "signal decimal number to be sent instead of the default 15 (unsupported on Windows)", + Some('s'), + ) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let pid: i64 = call.req(engine_state, stack, 0)?; + let rest: Vec = call.rest(engine_state, stack, 1)?; + let force: bool = call.has_flag("force"); + let signal: Option> = call.get_flag(engine_state, stack, "signal")?; + let quiet: bool = call.has_flag("quiet"); + + let mut cmd = if cfg!(windows) { + let mut cmd = CommandSys::new("taskkill"); + + if force { + cmd.arg("/F"); + } + + cmd.arg("/PID"); + cmd.arg(pid.to_string()); + + // each pid must written as `/PID 0` otherwise + // taskkill will act as `killall` unix command + for id in &rest { + cmd.arg("/PID"); + cmd.arg(id.to_string()); + } + + cmd + } else { + let mut cmd = CommandSys::new("kill"); + if force { + if let Some(Spanned { + item: _, + span: signal_span, + }) = signal + { + return Err(ShellError::IncompatibleParameters { + left_message: "force".to_string(), + left_span: call + .get_named_arg("force") + .ok_or_else(|| { + ShellError::SpannedLabeledError( + "Flag error".into(), + "flag force not found".into(), + call.head, + ) + })? + .span, + right_message: "signal".to_string(), + right_span: span(&[ + call.get_named_arg("signal") + .ok_or_else(|| { + ShellError::SpannedLabeledError( + "Flag error".into(), + "flag signal not found".into(), + call.head, + ) + })? + .span, + signal_span, + ]), + }); + } + cmd.arg("-9"); + } else if let Some(signal_value) = signal { + cmd.arg(format!("-{}", signal_value.item)); + } + + cmd.arg(pid.to_string()); + + cmd.args(rest.iter().map(move |id| id.to_string())); + + cmd + }; + + // pipe everything to null + if quiet { + cmd.stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + } + + cmd.status().expect("failed to execute shell command"); + + Ok(Value::Nothing { span: call.head }.into_pipeline_data()) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Kill the pid using the most memory", + example: "ps | sort-by mem | last | kill $it.pid", + result: None, + }, + Example { + description: "Force kill a given pid", + example: "kill --force 12345", + result: None, + }, + Example { + description: "Send INT signal", + example: "kill -s 2 12345", + result: None, + }, + ] + } +} + +#[cfg(test)] +mod tests { + use super::Kill; + + #[test] + fn examples_work_as_expected() { + use crate::test_examples; + test_examples(Kill {}) + } +} diff --git a/crates/nu-command/src/platform/mod.rs b/crates/nu-command/src/platform/mod.rs new file mode 100644 index 0000000000..085f4f972b --- /dev/null +++ b/crates/nu-command/src/platform/mod.rs @@ -0,0 +1,19 @@ +mod ansi; +mod clear; +mod dir_info; +mod du; +mod input; +mod kill; +mod reedline_commands; +mod sleep; +mod term_size; + +pub use ansi::{Ansi, AnsiGradient, AnsiStrip}; +pub use clear::Clear; +pub use dir_info::{DirBuilder, DirInfo, FileInfo}; +pub use du::Du; +pub use input::Input; +pub use kill::Kill; +pub use reedline_commands::{Keybindings, KeybindingsDefault, KeybindingsList, KeybindingsListen}; +pub use sleep::Sleep; +pub use term_size::TermSize; diff --git a/crates/nu-command/src/platform/reedline_commands/keybindings.rs b/crates/nu-command/src/platform/reedline_commands/keybindings.rs new file mode 100644 index 0000000000..be5af78445 --- /dev/null +++ b/crates/nu-command/src/platform/reedline_commands/keybindings.rs @@ -0,0 +1,42 @@ +use nu_engine::get_full_help; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, IntoPipelineData, PipelineData, Signature, Value, +}; + +#[derive(Clone)] +pub struct Keybindings; + +impl Command for Keybindings { + fn name(&self) -> &str { + "keybindings" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Platform) + } + + fn usage(&self) -> &str { + "Keybindings related commands" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(Value::String { + val: get_full_help( + &Keybindings.signature(), + &Keybindings.examples(), + engine_state, + stack, + ), + span: call.head, + } + .into_pipeline_data()) + } +} diff --git a/crates/nu-command/src/platform/reedline_commands/keybindings_default.rs b/crates/nu-command/src/platform/reedline_commands/keybindings_default.rs new file mode 100644 index 0000000000..39e545383f --- /dev/null +++ b/crates/nu-command/src/platform/reedline_commands/keybindings_default.rs @@ -0,0 +1,81 @@ +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, IntoPipelineData, PipelineData, Signature, Value, +}; +use reedline::get_reedline_default_keybindings; + +#[derive(Clone)] +pub struct KeybindingsDefault; + +impl Command for KeybindingsDefault { + fn name(&self) -> &str { + "keybindings default" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Platform) + } + + fn usage(&self) -> &str { + "List default keybindings" + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Get list with default keybindings", + example: "keybindings default", + result: None, + }] + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let records = get_reedline_default_keybindings() + .into_iter() + .map(|(mode, modifier, code, event)| { + let mode = Value::String { + val: mode, + span: call.head, + }; + + let modifier = Value::String { + val: modifier, + span: call.head, + }; + + let code = Value::String { + val: code, + span: call.head, + }; + + let event = Value::String { + val: event, + span: call.head, + }; + + Value::Record { + cols: vec![ + "mode".to_string(), + "modifier".to_string(), + "code".to_string(), + "event".to_string(), + ], + vals: vec![mode, modifier, code, event], + span: call.head, + } + }) + .collect(); + + Ok(Value::List { + vals: records, + span: call.head, + } + .into_pipeline_data()) + } +} diff --git a/crates/nu-command/src/platform/reedline_commands/keybindings_list.rs b/crates/nu-command/src/platform/reedline_commands/keybindings_list.rs new file mode 100644 index 0000000000..1a2b0e02d7 --- /dev/null +++ b/crates/nu-command/src/platform/reedline_commands/keybindings_list.rs @@ -0,0 +1,129 @@ +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, IntoPipelineData, PipelineData, Signature, Span, Value, +}; +use reedline::{ + get_reedline_edit_commands, get_reedline_keybinding_modifiers, get_reedline_keycodes, + get_reedline_prompt_edit_modes, get_reedline_reedline_events, +}; + +#[derive(Clone)] +pub struct KeybindingsList; + +impl Command for KeybindingsList { + fn name(&self) -> &str { + "keybindings list" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .switch("modifiers", "list of modifiers", Some('m')) + .switch("keycodes", "list of keycodes", Some('k')) + .switch("modes", "list of edit modes", Some('o')) + .switch("events", "list of reedline event", Some('e')) + .switch("edits", "list of edit commands", Some('d')) + .category(Category::Platform) + } + + fn usage(&self) -> &str { + "List available options that can be used to create keybindings" + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Get list of key modifiers", + example: "keybindings list -m", + result: None, + }, + Example { + description: "Get list of reedline events and edit commands", + example: "keybindings list -e -d", + result: None, + }, + Example { + description: "Get list with all the available options", + example: "keybindings list", + result: None, + }, + ] + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let records = if call.named.is_empty() { + let all_options = vec!["modifiers", "keycodes", "edits", "modes", "events"]; + all_options + .iter() + .flat_map(|argument| get_records(argument, &call.head)) + .collect() + } else { + call.named + .iter() + .flat_map(|(argument, _)| get_records(argument.item.as_str(), &call.head)) + .collect() + }; + + Ok(Value::List { + vals: records, + span: call.head, + } + .into_pipeline_data()) + } +} + +fn get_records(entry_type: &str, span: &Span) -> Vec { + let values = match entry_type { + "modifiers" => get_reedline_keybinding_modifiers().sorted(), + "keycodes" => get_reedline_keycodes().sorted(), + "edits" => get_reedline_edit_commands().sorted(), + "modes" => get_reedline_prompt_edit_modes().sorted(), + "events" => get_reedline_reedline_events().sorted(), + _ => Vec::new(), + }; + + values + .iter() + .map(|edit| edit.split('\n')) + .flat_map(|edit| edit.map(|edit| convert_to_record(edit, entry_type, span))) + .collect() +} + +fn convert_to_record(edit: &str, entry_type: &str, span: &Span) -> Value { + let entry_type = Value::String { + val: entry_type.to_string(), + span: *span, + }; + + let name = Value::String { + val: edit.to_string(), + span: *span, + }; + + Value::Record { + cols: vec!["type".to_string(), "name".to_string()], + vals: vec![entry_type, name], + span: *span, + } +} + +// Helper to sort a vec and return a vec +trait SortedImpl { + fn sorted(self) -> Self; +} + +impl SortedImpl for Vec +where + E: std::cmp::Ord, +{ + fn sorted(mut self) -> Self { + self.sort(); + self + } +} diff --git a/crates/nu-command/src/platform/reedline_commands/keybindings_listen.rs b/crates/nu-command/src/platform/reedline_commands/keybindings_listen.rs new file mode 100644 index 0000000000..75c9262870 --- /dev/null +++ b/crates/nu-command/src/platform/reedline_commands/keybindings_listen.rs @@ -0,0 +1,151 @@ +use crossterm::QueueableCommand; +use crossterm::{event::Event, event::KeyCode, event::KeyEvent, terminal}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Value, +}; +use std::io::{stdout, Write}; + +#[derive(Clone)] +pub struct KeybindingsListen; + +impl Command for KeybindingsListen { + fn name(&self) -> &str { + "keybindings listen" + } + + fn usage(&self) -> &str { + "Get input from the user." + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Platform) + } + + fn run( + &self, + _engine_state: &EngineState, + stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + println!("Type any key combination to see key details. Press ESC to abort."); + + match print_events(stack) { + Ok(v) => Ok(v.into_pipeline_data()), + Err(e) => { + terminal::disable_raw_mode()?; + Err(ShellError::LabeledError( + "Error with input".to_string(), + e.to_string(), + )) + } + } + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Type and see key event codes", + example: "keybindings listen", + result: None, + }] + } +} + +pub fn print_events(stack: &mut Stack) -> Result { + let config = stack.get_config()?; + + stdout().flush()?; + terminal::enable_raw_mode()?; + let mut stdout = std::io::BufWriter::new(std::io::stderr()); + + loop { + let event = crossterm::event::read()?; + if event == Event::Key(KeyCode::Esc.into()) { + break; + } + // stdout.queue(crossterm::style::Print(format!("event: {:?}", &event)))?; + // stdout.queue(crossterm::style::Print("\r\n"))?; + + // Get a record + let v = print_events_helper(event)?; + // Print out the record + let o = match v { + Value::Record { cols, vals, .. } => cols + .iter() + .zip(vals.iter()) + .map(|(x, y)| format!("{}: {}", x, y.into_string("", &config))) + .collect::>() + .join(", "), + + _ => "".to_string(), + }; + stdout.queue(crossterm::style::Print(o))?; + stdout.queue(crossterm::style::Print("\r\n"))?; + stdout.flush()?; + } + terminal::disable_raw_mode()?; + + Ok(Value::nothing(Span::test_data())) +} + +// this fn is totally ripped off from crossterm's examples +// it's really a diagnostic routine to see if crossterm is +// even seeing the events. if you press a key and no events +// are printed, it's a good chance your terminal is eating +// those events. +fn print_events_helper(event: Event) -> Result { + if let Event::Key(KeyEvent { code, modifiers }) = event { + match code { + KeyCode::Char(c) => { + let record = Value::Record { + cols: vec![ + "char".into(), + "code".into(), + "modifier".into(), + "flags".into(), + ], + vals: vec![ + Value::string(format!("{}", c), Span::test_data()), + Value::string(format!("{:#08x}", u32::from(c)), Span::test_data()), + Value::string(format!("{:?}", modifiers), Span::test_data()), + Value::string(format!("{:#08b}", modifiers), Span::test_data()), + ], + span: Span::test_data(), + }; + Ok(record) + } + _ => { + let record = Value::Record { + cols: vec!["code".into(), "modifier".into(), "flags".into()], + vals: vec![ + Value::string(format!("{:?}", code), Span::test_data()), + Value::string(format!("{:?}", modifiers), Span::test_data()), + Value::string(format!("{:#08b}", modifiers), Span::test_data()), + ], + span: Span::test_data(), + }; + Ok(record) + } + } + } else { + let record = Value::Record { + cols: vec!["event".into()], + vals: vec![Value::string(format!("{:?}", event), Span::test_data())], + span: Span::test_data(), + }; + Ok(record) + } +} + +#[cfg(test)] +mod tests { + use crate::KeybindingsListen; + + #[test] + fn examples_work_as_expected() { + use crate::test_examples; + test_examples(KeybindingsListen {}) + } +} diff --git a/crates/nu-command/src/platform/reedline_commands/mod.rs b/crates/nu-command/src/platform/reedline_commands/mod.rs new file mode 100644 index 0000000000..b063d9ea26 --- /dev/null +++ b/crates/nu-command/src/platform/reedline_commands/mod.rs @@ -0,0 +1,9 @@ +mod keybindings; +mod keybindings_default; +mod keybindings_list; +mod keybindings_listen; + +pub use keybindings::Keybindings; +pub use keybindings_default::KeybindingsDefault; +pub use keybindings_list::KeybindingsList; +pub use keybindings_listen::KeybindingsListen; diff --git a/crates/nu-command/src/platform/sleep.rs b/crates/nu-command/src/platform/sleep.rs new file mode 100644 index 0000000000..9591de75a3 --- /dev/null +++ b/crates/nu-command/src/platform/sleep.rs @@ -0,0 +1,108 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, Value, +}; +use std::{ + sync::atomic::Ordering, + thread, + time::{Duration, Instant}, +}; + +const CTRL_C_CHECK_INTERVAL: Duration = Duration::from_millis(100); + +#[derive(Clone)] +pub struct Sleep; + +impl Command for Sleep { + fn name(&self) -> &str { + "sleep" + } + + fn usage(&self) -> &str { + "Delay for a specified amount of time." + } + + fn signature(&self) -> Signature { + Signature::build("sleep") + .required("duration", SyntaxShape::Duration, "time to sleep") + .rest("rest", SyntaxShape::Duration, "additional time") + .category(Category::Platform) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + fn duration_from_i64(val: i64) -> Duration { + Duration::from_nanos(if val < 0 { 0 } else { val as u64 }) + } + + let duration: i64 = call.req(engine_state, stack, 0)?; + let rest: Vec = call.rest(engine_state, stack, 1)?; + + let total_dur = + duration_from_i64(duration) + rest.into_iter().map(duration_from_i64).sum::(); + + let ctrlc_ref = &engine_state.ctrlc.clone(); + let start = Instant::now(); + loop { + thread::sleep(CTRL_C_CHECK_INTERVAL); + if start.elapsed() >= total_dur { + break; + } + + if let Some(ctrlc) = ctrlc_ref { + if ctrlc.load(Ordering::SeqCst) { + break; + } + } + } + + Ok(Value::Nothing { span: call.head }.into_pipeline_data()) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Sleep for 1sec", + example: "sleep 1sec", + result: None, + }, + Example { + description: "Sleep for 3sec", + example: "sleep 1sec 1sec 1sec", + result: None, + }, + Example { + description: "Send output after 1sec", + example: "sleep 1sec; echo done", + result: Some(Value::test_string("done")), + }, + ] + } +} + +#[cfg(test)] +mod tests { + use super::Sleep; + + #[test] + fn examples_work_as_expected() { + use crate::test_examples; + use std::time::Instant; + + let start = Instant::now(); + test_examples(Sleep {}); + + let elapsed = start.elapsed(); + + // only examples with actual output are run + assert!(elapsed >= std::time::Duration::from_secs(1)); + assert!(elapsed < std::time::Duration::from_secs(2)); + } +} diff --git a/crates/nu-command/src/platform/term_size.rs b/crates/nu-command/src/platform/term_size.rs new file mode 100644 index 0000000000..2b6506aedd --- /dev/null +++ b/crates/nu-command/src/platform/term_size.rs @@ -0,0 +1,113 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, IntoPipelineData, PipelineData, Signature, Span, Value}; +use terminal_size::{terminal_size, Height, Width}; + +#[derive(Clone)] +pub struct TermSize; + +impl Command for TermSize { + fn name(&self) -> &str { + "term size" + } + + fn usage(&self) -> &str { + "Returns the terminal size" + } + + fn signature(&self) -> Signature { + Signature::build("term size") + .switch( + "columns", + "Report only the width of the terminal", + Some('c'), + ) + .switch("rows", "Report only the height of the terminal", Some('r')) + .category(Category::Platform) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Return the width height of the terminal", + example: "term size", + result: None, + }, + Example { + description: "Return the width (columns) of the terminal", + example: "term size -c", + result: None, + }, + Example { + description: "Return the height (rows) of the terminal", + example: "term size -r", + result: None, + }, + ] + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let head = call.head; + let wide = call.has_flag("columns"); + let tall = call.has_flag("rows"); + + let (cols, rows) = match terminal_size() { + Some((w, h)) => (Width(w.0), Height(h.0)), + None => (Width(0), Height(0)), + }; + + Ok((match (wide, tall) { + (true, false) => Value::Record { + cols: vec!["columns".into()], + vals: vec![Value::Int { + val: cols.0 as i64, + span: Span::test_data(), + }], + span: head, + }, + (true, true) => Value::Record { + cols: vec!["columns".into(), "rows".into()], + vals: vec![ + Value::Int { + val: cols.0 as i64, + span: Span::test_data(), + }, + Value::Int { + val: rows.0 as i64, + span: Span::test_data(), + }, + ], + span: head, + }, + (false, true) => Value::Record { + cols: vec!["rows".into()], + vals: vec![Value::Int { + val: rows.0 as i64, + span: Span::test_data(), + }], + span: head, + }, + (false, false) => Value::Record { + cols: vec!["columns".into(), "rows".into()], + vals: vec![ + Value::Int { + val: cols.0 as i64, + span: Span::test_data(), + }, + Value::Int { + val: rows.0 as i64, + span: Span::test_data(), + }, + ], + span: head, + }, + }) + .into_pipeline_data()) + } +} diff --git a/crates/nu-command/src/prelude.rs b/crates/nu-command/src/prelude.rs deleted file mode 100644 index 26dc518bcf..0000000000 --- a/crates/nu-command/src/prelude.rs +++ /dev/null @@ -1,32 +0,0 @@ -#[macro_export] -macro_rules! return_err { - ($expr:expr) => { - match $expr { - Err(_) => return, - Ok(expr) => expr, - }; - }; -} - -pub(crate) use bigdecimal::BigDecimal; -pub(crate) use indexmap::{indexmap, IndexMap}; -pub(crate) use itertools::Itertools; -pub(crate) use nu_data::config; -pub(crate) use nu_data::value; -pub(crate) use nu_engine::EvaluationContext; -pub(crate) use nu_engine::Example; -pub(crate) use nu_engine::Host; -pub(crate) use nu_engine::RunnableContext; -pub(crate) use nu_engine::{get_full_help, CommandArgs, Scope, WholeStreamCommand}; -pub(crate) use nu_parser::ParserScope; -pub(crate) use nu_protocol::{out, row}; -pub(crate) use nu_source::{AnchorLocation, PrettyDebug, Span, SpannedItem, Tag, TaggedItem}; -pub(crate) use nu_stream::{ActionStream, InputStream, Interruptible, OutputStream}; -pub(crate) use nu_stream::{IntoActionStream, IntoInputStream, IntoOutputStream}; -pub(crate) use nu_value_ext::ValueExt; -pub(crate) use num_bigint::BigInt; -pub(crate) use num_traits::cast::ToPrimitive; -pub(crate) use serde::Deserialize; -pub(crate) use std::collections::VecDeque; -pub(crate) use std::sync::atomic::AtomicBool; -pub(crate) use std::sync::Arc; diff --git a/crates/nu-command/src/random/bool.rs b/crates/nu-command/src/random/bool.rs new file mode 100644 index 0000000000..439b9469e0 --- /dev/null +++ b/crates/nu-command/src/random/bool.rs @@ -0,0 +1,100 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Value, +}; +use rand::prelude::{thread_rng, Rng}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "random bool" + } + + fn signature(&self) -> Signature { + Signature::build("random bool") + .named( + "bias", + SyntaxShape::Number, + "Adjusts the probability of a \"true\" outcome", + Some('b'), + ) + .category(Category::Random) + } + + fn usage(&self) -> &str { + "Generate a random boolean value" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + bool(engine_state, stack, call) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Generate a random boolean value", + example: "random bool", + result: None, + }, + Example { + description: "Generate a random boolean value with a 75% chance of \"true\"", + example: "random bool --bias 0.75", + result: None, + }, + ] + } +} + +fn bool( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, +) -> Result { + let span = call.head; + let bias: Option> = call.get_flag(engine_state, stack, "bias")?; + + let mut probability = 0.5; + + if let Some(prob) = bias { + probability = prob.item; + + let probability_is_valid = (0.0..=1.0).contains(&probability); + + if !probability_is_valid { + return Err(ShellError::InvalidProbability(prob.span)); + } + } + + let mut rng = thread_rng(); + let bool_result: bool = rng.gen_bool(probability); + + Ok(PipelineData::Value( + Value::Bool { + val: bool_result, + span, + }, + None, + )) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/random/chars.rs b/crates/nu-command/src/random/chars.rs new file mode 100644 index 0000000000..5c3aba0314 --- /dev/null +++ b/crates/nu-command/src/random/chars.rs @@ -0,0 +1,92 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Value}; +use rand::{ + distributions::{Alphanumeric, Distribution}, + thread_rng, +}; + +const DEFAULT_CHARS_LENGTH: usize = 25; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "random chars" + } + + fn signature(&self) -> Signature { + Signature::build("random chars") + .named("length", SyntaxShape::Int, "Number of chars", Some('l')) + .category(Category::Random) + } + + fn usage(&self) -> &str { + "Generate random chars" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + chars(engine_state, stack, call) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Generate random chars", + example: "random chars", + result: None, + }, + Example { + description: "Generate random chars with specified length", + example: "random chars -l 20", + result: None, + }, + ] + } +} + +fn chars( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, +) -> Result { + let span = call.head; + let length: Option = call.get_flag(engine_state, stack, "length")?; + + let chars_length = length.unwrap_or(DEFAULT_CHARS_LENGTH); + let mut rng = thread_rng(); + + let random_string = Alphanumeric + .sample_iter(&mut rng) + .take(chars_length) + .map(char::from) + .collect::(); + + Ok(PipelineData::Value( + Value::String { + val: random_string, + span, + }, + None, + )) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/random/decimal.rs b/crates/nu-command/src/random/decimal.rs new file mode 100644 index 0000000000..845584cfc7 --- /dev/null +++ b/crates/nu-command/src/random/decimal.rs @@ -0,0 +1,116 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, PipelineData, Range, ShellError, Signature, Span, SyntaxShape, Value, +}; +use rand::prelude::{thread_rng, Rng}; +use std::cmp::Ordering; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "random decimal" + } + + fn signature(&self) -> Signature { + Signature::build("random decimal") + .optional("range", SyntaxShape::Range, "Range of values") + .category(Category::Random) + } + + fn usage(&self) -> &str { + "Generate a random decimal within a range [min..max]" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + decimal(engine_state, stack, call) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Generate a default decimal value between 0 and 1", + example: "random decimal", + result: None, + }, + Example { + description: "Generate a random decimal less than or equal to 500", + example: "random decimal ..500", + result: None, + }, + Example { + description: "Generate a random decimal greater than or equal to 100000", + example: "random decimal 100000..", + result: None, + }, + Example { + description: "Generate a random decimal between 1.0 and 1.1", + example: "random decimal 1.0..1.1", + result: None, + }, + ] + } +} + +fn decimal( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, +) -> Result { + let span = call.head; + let range: Option = call.opt(engine_state, stack, 0)?; + + let (min, max) = if let Some(r) = range { + (r.from.as_float()?, r.to.as_float()?) + } else { + (0.0, 1.0) + }; + + match min.partial_cmp(&max) { + Some(Ordering::Greater) => Err(ShellError::InvalidRange( + min.to_string(), + max.to_string(), + span, + )), + Some(Ordering::Equal) => Ok(PipelineData::Value( + Value::Float { + val: min, + span: Span::new(64, 64), + }, + None, + )), + _ => { + let mut thread_rng = thread_rng(); + let result: f64 = thread_rng.gen_range(min..max); + + Ok(PipelineData::Value( + Value::Float { + val: result, + span: Span::new(64, 64), + }, + None, + )) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/random/dice.rs b/crates/nu-command/src/random/dice.rs new file mode 100644 index 0000000000..af127bce45 --- /dev/null +++ b/crates/nu-command/src/random/dice.rs @@ -0,0 +1,98 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, ListStream, PipelineData, ShellError, Signature, SyntaxShape, Value, +}; +use rand::prelude::{thread_rng, Rng}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "random dice" + } + + fn signature(&self) -> Signature { + Signature::build("random dice") + .named( + "dice", + SyntaxShape::Int, + "The amount of dice being rolled", + Some('d'), + ) + .named( + "sides", + SyntaxShape::Int, + "The amount of sides a die has", + Some('s'), + ) + .category(Category::Random) + } + + fn usage(&self) -> &str { + "Generate a random dice roll" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + dice(engine_state, stack, call) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Roll 1 dice with 6 sides each", + example: "random dice", + result: None, + }, + Example { + description: "Roll 10 dice with 12 sides each", + example: "random dice -d 10 -s 12", + result: None, + }, + ] + } +} + +fn dice( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, +) -> Result { + let span = call.head; + + let dice: usize = call.get_flag(engine_state, stack, "dice")?.unwrap_or(1); + let sides: usize = call.get_flag(engine_state, stack, "sides")?.unwrap_or(6); + + let iter = (0..dice).map(move |_| { + let mut thread_rng = thread_rng(); + Value::Int { + val: thread_rng.gen_range(1..sides + 1) as i64, + span, + } + }); + + Ok(PipelineData::ListStream( + ListStream::from_stream(iter, engine_state.ctrlc.clone()), + None, + )) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/random/integer.rs b/crates/nu-command/src/random/integer.rs new file mode 100644 index 0000000000..93be91ad97 --- /dev/null +++ b/crates/nu-command/src/random/integer.rs @@ -0,0 +1,104 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, PipelineData, Range, ShellError, Signature, SyntaxShape, Value, +}; +use rand::prelude::{thread_rng, Rng}; +use std::cmp::Ordering; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "random integer" + } + + fn signature(&self) -> Signature { + Signature::build("random integer") + .optional("range", SyntaxShape::Range, "Range of values") + .category(Category::Random) + } + + fn usage(&self) -> &str { + "Generate a random integer [min..max]" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + integer(engine_state, stack, call) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Generate an unconstrained random integer", + example: "random integer", + result: None, + }, + Example { + description: "Generate a random integer less than or equal to 500", + example: "random integer ..500", + result: None, + }, + Example { + description: "Generate a random integer greater than or equal to 100000", + example: "random integer 100000..", + result: None, + }, + Example { + description: "Generate a random integer between 1 and 10", + example: "random integer 1..10", + result: None, + }, + ] + } +} + +fn integer( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, +) -> Result { + let span = call.head; + let range: Option = call.opt(engine_state, stack, 0)?; + + let (min, max) = if let Some(r) = range { + (r.from.as_integer()?, r.to.as_integer()?) + } else { + (0, i64::MAX) + }; + + match min.partial_cmp(&max) { + Some(Ordering::Greater) => Err(ShellError::InvalidRange( + min.to_string(), + max.to_string(), + span, + )), + Some(Ordering::Equal) => Ok(PipelineData::Value(Value::Int { val: min, span }, None)), + _ => { + let mut thread_rng = thread_rng(); + let result: i64 = thread_rng.gen_range(min..=max); + + Ok(PipelineData::Value(Value::Int { val: result, span }, None)) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/random/mod.rs b/crates/nu-command/src/random/mod.rs new file mode 100644 index 0000000000..19e2f36103 --- /dev/null +++ b/crates/nu-command/src/random/mod.rs @@ -0,0 +1,15 @@ +mod bool; +mod chars; +mod decimal; +mod dice; +mod integer; +mod random_; +mod uuid; + +pub use self::bool::SubCommand as RandomBool; +pub use self::chars::SubCommand as RandomChars; +pub use self::decimal::SubCommand as RandomDecimal; +pub use self::dice::SubCommand as RandomDice; +pub use self::integer::SubCommand as RandomInteger; +pub use self::uuid::SubCommand as RandomUuid; +pub use random_::RandomCommand as Random; diff --git a/crates/nu-command/src/random/random_.rs b/crates/nu-command/src/random/random_.rs new file mode 100644 index 0000000000..8e8f26129b --- /dev/null +++ b/crates/nu-command/src/random/random_.rs @@ -0,0 +1,42 @@ +use nu_engine::get_full_help; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, IntoPipelineData, PipelineData, Signature, Value, +}; + +#[derive(Clone)] +pub struct RandomCommand; + +impl Command for RandomCommand { + fn name(&self) -> &str { + "random" + } + + fn signature(&self) -> Signature { + Signature::build("random").category(Category::Random) + } + + fn usage(&self) -> &str { + "Generate a random values." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(Value::String { + val: get_full_help( + &RandomCommand.signature(), + &RandomCommand.examples(), + engine_state, + stack, + ), + span: call.head, + } + .into_pipeline_data()) + } +} diff --git a/crates/nu-command/src/random/uuid.rs b/crates/nu-command/src/random/uuid.rs new file mode 100644 index 0000000000..fd7a2929f6 --- /dev/null +++ b/crates/nu-command/src/random/uuid.rs @@ -0,0 +1,61 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Value}; +use uuid::Uuid; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "random uuid" + } + + fn signature(&self) -> Signature { + Signature::build("random uuid").category(Category::Random) + } + + fn usage(&self) -> &str { + "Generate a random uuid4 string" + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + uuid(call) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Generate a random uuid4 string", + example: "random uuid", + result: None, + }] + } +} + +fn uuid(call: &Call) -> Result { + let span = call.head; + let uuid_4 = Uuid::new_v4().to_hyphenated().to_string(); + + Ok(PipelineData::Value( + Value::String { val: uuid_4, span }, + None, + )) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/shells/enter.rs b/crates/nu-command/src/shells/enter.rs new file mode 100644 index 0000000000..2a586c7104 --- /dev/null +++ b/crates/nu-command/src/shells/enter.rs @@ -0,0 +1,107 @@ +use nu_engine::{current_dir, CallExt}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, PipelineData, ShellError, Signature, SyntaxShape, Value}; + +/// Source a file for environment variables. +#[derive(Clone)] +pub struct Enter; + +impl Command for Enter { + fn name(&self) -> &str { + "enter" + } + + fn signature(&self) -> Signature { + Signature::build("enter") + .required( + "path", + SyntaxShape::Filepath, + "the path to enter as a new shell", + ) + .category(Category::Shells) + } + + fn usage(&self) -> &str { + "Enters a new shell at the given path." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let new_path: Value = call.req(engine_state, stack, 0)?; + let path_span = new_path.span()?; + + let new_path = new_path.as_path()?; + if !new_path.exists() { + return Err(ShellError::DirectoryNotFound(path_span)); + } + + if !new_path.is_dir() { + return Err(ShellError::DirectoryNotFoundCustom( + "not a directory".to_string(), + path_span, + )); + } + + let cwd = current_dir(engine_state, stack)?; + let new_path = nu_path::canonicalize_with(new_path, &cwd)?; + + let new_path = Value::String { + val: new_path.to_string_lossy().to_string(), + span: call.head, + }; + + let cwd = Value::String { + val: cwd.to_string_lossy().to_string(), + span: call.head, + }; + + let shells = stack.get_env_var(engine_state, "NUSHELL_SHELLS"); + let mut shells = if let Some(v) = shells { + v.as_list() + .map(|x| x.to_vec()) + .unwrap_or_else(|_| vec![cwd]) + } else { + vec![cwd] + }; + + let current_shell = stack.get_env_var(engine_state, "NUSHELL_CURRENT_SHELL"); + let mut current_shell = if let Some(v) = current_shell { + v.as_integer().unwrap_or_default() as usize + } else { + 0 + }; + + if current_shell + 1 > shells.len() { + shells.push(new_path.clone()); + current_shell = shells.len(); + } else { + shells.insert(current_shell + 1, new_path.clone()); + current_shell += 1; + } + + stack.add_env_var( + "NUSHELL_SHELLS".into(), + Value::List { + vals: shells, + span: call.head, + }, + ); + stack.add_env_var( + "NUSHELL_CURRENT_SHELL".into(), + Value::Int { + val: current_shell as i64, + span: call.head, + }, + ); + + stack.add_env_var("PWD".into(), new_path); + + Ok(PipelineData::new(call.head)) + } +} diff --git a/crates/nu-command/src/shells/exit.rs b/crates/nu-command/src/shells/exit.rs new file mode 100644 index 0000000000..e2a355922d --- /dev/null +++ b/crates/nu-command/src/shells/exit.rs @@ -0,0 +1,100 @@ +use nu_engine::{current_dir, CallExt}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, PipelineData, ShellError, Signature, SyntaxShape, Value}; + +/// Source a file for environment variables. +#[derive(Clone)] +pub struct Exit; + +impl Command for Exit { + fn name(&self) -> &str { + "exit" + } + + fn signature(&self) -> Signature { + Signature::build("exit") + .optional( + "exit-code", + SyntaxShape::Int, + "Exit code to return immediately with", + ) + .switch("now", "Exit out of the shell immediately", Some('n')) + .category(Category::Shells) + } + + fn usage(&self) -> &str { + "Runs a script file in the current context." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let exit_code: Option = call.opt(engine_state, stack, 0)?; + + if let Some(exit_code) = exit_code { + std::process::exit(exit_code as i32); + } + + if call.has_flag("now") { + std::process::exit(0); + } + + let cwd = current_dir(engine_state, stack)?; + let cwd = Value::String { + val: cwd.to_string_lossy().to_string(), + span: call.head, + }; + + let shells = stack.get_env_var(engine_state, "NUSHELL_SHELLS"); + let mut shells = if let Some(v) = shells { + v.as_list() + .map(|x| x.to_vec()) + .unwrap_or_else(|_| vec![cwd]) + } else { + vec![cwd] + }; + + let current_shell = stack.get_env_var(engine_state, "NUSHELL_CURRENT_SHELL"); + let mut current_shell = if let Some(v) = current_shell { + v.as_integer().unwrap_or_default() as usize + } else { + 0 + }; + + shells.remove(current_shell); + + if current_shell == shells.len() && !shells.is_empty() { + current_shell -= 1; + } + + if shells.is_empty() { + std::process::exit(0); + } else { + let new_path = shells[current_shell].clone(); + + stack.add_env_var( + "NUSHELL_SHELLS".into(), + Value::List { + vals: shells, + span: call.head, + }, + ); + stack.add_env_var( + "NUSHELL_CURRENT_SHELL".into(), + Value::Int { + val: current_shell as i64, + span: call.head, + }, + ); + + stack.add_env_var("PWD".into(), new_path); + + Ok(PipelineData::new(call.head)) + } + } +} diff --git a/crates/nu-command/src/shells/g.rs b/crates/nu-command/src/shells/g.rs new file mode 100644 index 0000000000..64bc10169e --- /dev/null +++ b/crates/nu-command/src/shells/g.rs @@ -0,0 +1,78 @@ +use nu_engine::{current_dir, CallExt}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Value}; + +/// Source a file for environment variables. +#[derive(Clone)] +pub struct GotoShell; + +impl Command for GotoShell { + fn name(&self) -> &str { + "g" + } + + fn signature(&self) -> Signature { + Signature::build("g") + .required( + "shell-number", + SyntaxShape::Int, + "shell number to change to", + ) + .category(Category::Shells) + } + + fn usage(&self) -> &str { + "Switch to a given shell." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let new_shell: Spanned = call.req(engine_state, stack, 0)?; + + let cwd = current_dir(engine_state, stack)?; + let cwd = Value::String { + val: cwd.to_string_lossy().to_string(), + span: call.head, + }; + + let shells = stack.get_env_var(engine_state, "NUSHELL_SHELLS"); + let shells = if let Some(v) = shells { + v.as_list() + .map(|x| x.to_vec()) + .unwrap_or_else(|_| vec![cwd]) + } else { + vec![cwd] + }; + + let new_path = if let Some(v) = shells.get(new_shell.item as usize) { + v.clone() + } else { + return Err(ShellError::NotFound(new_shell.span)); + }; + + stack.add_env_var( + "NUSHELL_SHELLS".into(), + Value::List { + vals: shells, + span: call.head, + }, + ); + stack.add_env_var( + "NUSHELL_CURRENT_SHELL".into(), + Value::Int { + val: new_shell.item, + span: call.head, + }, + ); + + stack.add_env_var("PWD".into(), new_path); + + Ok(PipelineData::new(call.head)) + } +} diff --git a/crates/nu-command/src/shells/mod.rs b/crates/nu-command/src/shells/mod.rs new file mode 100644 index 0000000000..12d4f15fee --- /dev/null +++ b/crates/nu-command/src/shells/mod.rs @@ -0,0 +1,13 @@ +mod enter; +mod exit; +mod g; +mod n; +mod p; +mod shells_; + +pub use enter::Enter; +pub use exit::Exit; +pub use g::GotoShell; +pub use n::NextShell; +pub use p::PrevShell; +pub use shells_::Shells; diff --git a/crates/nu-command/src/shells/n.rs b/crates/nu-command/src/shells/n.rs new file mode 100644 index 0000000000..9354c7595c --- /dev/null +++ b/crates/nu-command/src/shells/n.rs @@ -0,0 +1,79 @@ +use nu_engine::current_dir; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, PipelineData, ShellError, Signature, Value}; + +/// Source a file for environment variables. +#[derive(Clone)] +pub struct NextShell; + +impl Command for NextShell { + fn name(&self) -> &str { + "n" + } + + fn signature(&self) -> Signature { + Signature::build("n").category(Category::Shells) + } + + fn usage(&self) -> &str { + "Switch to the next shell." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let cwd = current_dir(engine_state, stack)?; + let cwd = Value::String { + val: cwd.to_string_lossy().to_string(), + span: call.head, + }; + + let shells = stack.get_env_var(engine_state, "NUSHELL_SHELLS"); + let shells = if let Some(v) = shells { + v.as_list() + .map(|x| x.to_vec()) + .unwrap_or_else(|_| vec![cwd]) + } else { + vec![cwd] + }; + + let current_shell = stack.get_env_var(engine_state, "NUSHELL_CURRENT_SHELL"); + let mut current_shell = if let Some(v) = current_shell { + v.as_integer().unwrap_or_default() as usize + } else { + 0 + }; + + current_shell += 1; + + if current_shell == shells.len() { + current_shell = 0; + } + + let new_path = shells[current_shell].clone(); + + stack.add_env_var( + "NUSHELL_SHELLS".into(), + Value::List { + vals: shells, + span: call.head, + }, + ); + stack.add_env_var( + "NUSHELL_CURRENT_SHELL".into(), + Value::Int { + val: current_shell as i64, + span: call.head, + }, + ); + + stack.add_env_var("PWD".into(), new_path); + + Ok(PipelineData::new(call.head)) + } +} diff --git a/crates/nu-command/src/shells/p.rs b/crates/nu-command/src/shells/p.rs new file mode 100644 index 0000000000..bd04b82b74 --- /dev/null +++ b/crates/nu-command/src/shells/p.rs @@ -0,0 +1,79 @@ +use nu_engine::current_dir; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, PipelineData, ShellError, Signature, Value}; + +/// Source a file for environment variables. +#[derive(Clone)] +pub struct PrevShell; + +impl Command for PrevShell { + fn name(&self) -> &str { + "p" + } + + fn signature(&self) -> Signature { + Signature::build("p").category(Category::Shells) + } + + fn usage(&self) -> &str { + "Switch to the previous shell." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let cwd = current_dir(engine_state, stack)?; + let cwd = Value::String { + val: cwd.to_string_lossy().to_string(), + span: call.head, + }; + + let shells = stack.get_env_var(engine_state, "NUSHELL_SHELLS"); + let shells = if let Some(v) = shells { + v.as_list() + .map(|x| x.to_vec()) + .unwrap_or_else(|_| vec![cwd]) + } else { + vec![cwd] + }; + + let current_shell = stack.get_env_var(engine_state, "NUSHELL_CURRENT_SHELL"); + let mut current_shell = if let Some(v) = current_shell { + v.as_integer().unwrap_or_default() as usize + } else { + 0 + }; + + if current_shell == 0 { + current_shell = shells.len() - 1; + } else { + current_shell -= 1; + } + + let new_path = shells[current_shell].clone(); + + stack.add_env_var( + "NUSHELL_SHELLS".into(), + Value::List { + vals: shells, + span: call.head, + }, + ); + stack.add_env_var( + "NUSHELL_CURRENT_SHELL".into(), + Value::Int { + val: current_shell as i64, + span: call.head, + }, + ); + + stack.add_env_var("PWD".into(), new_path); + + Ok(PipelineData::new(call.head)) + } +} diff --git a/crates/nu-command/src/shells/shells_.rs b/crates/nu-command/src/shells/shells_.rs new file mode 100644 index 0000000000..b50a500884 --- /dev/null +++ b/crates/nu-command/src/shells/shells_.rs @@ -0,0 +1,72 @@ +use nu_engine::current_dir; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Value, +}; + +/// Source a file for environment variables. +#[derive(Clone)] +pub struct Shells; + +impl Command for Shells { + fn name(&self) -> &str { + "shells" + } + + fn signature(&self) -> Signature { + Signature::build("shells").category(Category::Shells) + } + + fn usage(&self) -> &str { + "Lists all open shells." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let span = call.head; + let cwd = current_dir(engine_state, stack)?; + let cwd = Value::String { + val: cwd.to_string_lossy().to_string(), + span, + }; + + let shells = stack.get_env_var(engine_state, "NUSHELL_SHELLS"); + let shells = if let Some(v) = shells { + v.as_list() + .map(|x| x.to_vec()) + .unwrap_or_else(|_| vec![cwd]) + } else { + vec![cwd] + }; + + let current_shell = stack.get_env_var(engine_state, "NUSHELL_CURRENT_SHELL"); + let current_shell = if let Some(v) = current_shell { + v.as_integer().unwrap_or_default() as usize + } else { + 0 + }; + + let output = shells + .into_iter() + .enumerate() + .map(move |(idx, val)| Value::Record { + cols: vec!["active".to_string(), "path".to_string()], + vals: vec![ + Value::Bool { + val: idx == current_shell, + span, + }, + val, + ], + span, + }); + + Ok(output.into_pipeline_data(None)) + } +} diff --git a/crates/nu-command/src/strings/build_string.rs b/crates/nu-command/src/strings/build_string.rs new file mode 100644 index 0000000000..8046a8f0f9 --- /dev/null +++ b/crates/nu-command/src/strings/build_string.rs @@ -0,0 +1,82 @@ +use nu_engine::eval_expression; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, SyntaxShape, + Value, +}; + +#[derive(Clone)] +pub struct BuildString; + +impl Command for BuildString { + fn name(&self) -> &str { + "build-string" + } + + fn usage(&self) -> &str { + "Create a string from the arguments." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("build-string") + .rest("rest", SyntaxShape::String, "list of string") + .category(Category::Strings) + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "build-string a b c", + description: "Builds a string from letters a b c", + result: Some(Value::String { + val: "abc".to_string(), + span: Span::test_data(), + }), + }, + Example { + example: "build-string (1 + 2) = one ' ' plus ' ' two", + description: "Builds a string from letters a b c", + result: Some(Value::String { + val: "3=one plus two".to_string(), + span: Span::test_data(), + }), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let config = stack.get_config().unwrap_or_default(); + let output = call + .positional + .iter() + .map(|expr| { + eval_expression(engine_state, stack, expr).map(|val| val.into_string(", ", &config)) + }) + .collect::, ShellError>>()?; + + Ok(Value::String { + val: output.join(""), + span: call.head, + } + .into_pipeline_data()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(BuildString {}) + } +} diff --git a/crates/nu-command/src/strings/char_.rs b/crates/nu-command/src/strings/char_.rs new file mode 100644 index 0000000000..e2c637f658 --- /dev/null +++ b/crates/nu-command/src/strings/char_.rs @@ -0,0 +1,283 @@ +use indexmap::indexmap; +use indexmap::map::IndexMap; +use lazy_static::lazy_static; +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, engine::Command, Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, + PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +// Character used to separate directories in a Path Environment variable on windows is ";" +#[cfg(target_family = "windows")] +const ENV_PATH_SEPARATOR_CHAR: char = ';'; +// Character used to separate directories in a Path Environment variable on linux/mac/unix is ":" +#[cfg(not(target_family = "windows"))] +const ENV_PATH_SEPARATOR_CHAR: char = ':'; + +#[derive(Clone)] +pub struct Char; + +lazy_static! { + static ref CHAR_MAP: IndexMap<&'static str, String> = indexmap! { + // These are some regular characters that either can't be used or + // it's just easier to use them like this. + + // This are the "normal" characters section + "newline" => '\n'.to_string(), + "enter" => '\n'.to_string(), + "nl" => '\n'.to_string(), + "line_feed" => '\n'.to_string(), + "lf" => '\n'.to_string(), + "carriage_return" => '\r'.to_string(), + "cr" => '\r'.to_string(), + "crlf" => "\r\n".to_string(), + "tab" => '\t'.to_string(), + "sp" => ' '.to_string(), + "space" => ' '.to_string(), + "pipe" => '|'.to_string(), + "left_brace" => '{'.to_string(), + "lbrace" => '{'.to_string(), + "right_brace" => '}'.to_string(), + "rbrace" => '}'.to_string(), + "left_paren" => '('.to_string(), + "lp" => '('.to_string(), + "lparen" => '('.to_string(), + "right_paren" => ')'.to_string(), + "rparen" => ')'.to_string(), + "rp" => ')'.to_string(), + "left_bracket" => '['.to_string(), + "lbracket" => '['.to_string(), + "right_bracket" => ']'.to_string(), + "rbracket" => ']'.to_string(), + "single_quote" => '\''.to_string(), + "squote" => '\''.to_string(), + "sq" => '\''.to_string(), + "double_quote" => '\"'.to_string(), + "dquote" => '\"'.to_string(), + "dq" => '\"'.to_string(), + "path_sep" => std::path::MAIN_SEPARATOR.to_string(), + "psep" => std::path::MAIN_SEPARATOR.to_string(), + "separator" => std::path::MAIN_SEPARATOR.to_string(), + "esep" => ENV_PATH_SEPARATOR_CHAR.to_string(), + "env_sep" => ENV_PATH_SEPARATOR_CHAR.to_string(), + "tilde" => '~'.to_string(), // ~ + "twiddle" => '~'.to_string(), // ~ + "squiggly" => '~'.to_string(), // ~ + "home" => '~'.to_string(), // ~ + "hash" => '#'.to_string(), // # + "hashtag" => '#'.to_string(), // # + "pound_sign" => '#'.to_string(), // # + "sharp" => '#'.to_string(), // # + "root" => '#'.to_string(), // # + + // This is the unicode section + // Unicode names came from https://www.compart.com/en/unicode + // Private Use Area (U+E000-U+F8FF) + // Unicode can't be mixed with Ansi or it will break width calculation + "nf-branch" => '\u{e0a0}'.to_string(), //  + "nf-segment" => '\u{e0b0}'.to_string(), //  + "nf-left-segment" => '\u{e0b0}'.to_string(), //  + "nf-left-segment-thin" => '\u{e0b1}'.to_string(), //  + "nf-right-segment" => '\u{e0b2}'.to_string(), //  + "nf-right-segment-thin" => '\u{e0b3}'.to_string(), //  + "nf-git" => '\u{f1d3}'.to_string(), //  + "nf-git-branch" => "\u{e709}\u{e0a0}".to_string(), //  + "nf-folder1" => '\u{f07c}'.to_string(), //  + "nf-folder2" => '\u{f115}'.to_string(), //  + "nf-house1" => '\u{f015}'.to_string(), //  + "nf-house2" => '\u{f7db}'.to_string(), //  + + "identical_to" => '\u{2261}'.to_string(), // ≡ + "hamburger" => '\u{2261}'.to_string(), // ≡ + "not_identical_to" => '\u{2262}'.to_string(), // ≢ + "branch_untracked" => '\u{2262}'.to_string(), // ≢ + "strictly_equivalent_to" => '\u{2263}'.to_string(), // ≣ + "branch_identical" => '\u{2263}'.to_string(), // ≣ + + "upwards_arrow" => '\u{2191}'.to_string(), // ↑ + "branch_ahead" => '\u{2191}'.to_string(), // ↑ + "downwards_arrow" => '\u{2193}'.to_string(), // ↓ + "branch_behind" => '\u{2193}'.to_string(), // ↓ + "up_down_arrow" => '\u{2195}'.to_string(), // ↕ + "branch_ahead_behind" => '\u{2195}'.to_string(), // ↕ + + "black_right_pointing_triangle" => '\u{25b6}'.to_string(), // ▶ + "prompt" => '\u{25b6}'.to_string(), // ▶ + "vector_or_cross_product" => '\u{2a2f}'.to_string(), // ⨯ + "failed" => '\u{2a2f}'.to_string(), // ⨯ + "high_voltage_sign" => '\u{26a1}'.to_string(), // ⚡ + "elevated" => '\u{26a1}'.to_string(), // ⚡ + + // This is the emoji section + // Weather symbols + // https://www.babelstone.co.uk/Unicode/whatisit.html + "sun" => "☀️".to_string(), //2600 + fe0f + "sunny" => "☀️".to_string(), //2600 + fe0f + "sunrise" => "☀️".to_string(), //2600 + fe0f + "moon" => "🌛".to_string(), //1f31b + "cloudy" => "☁️".to_string(), //2601 + fe0f + "cloud" => "☁️".to_string(), //2601 + fe0f + "clouds" => "☁️".to_string(), //2601 + fe0f + "rainy" => "🌦️".to_string(), //1f326 + fe0f + "rain" => "🌦️".to_string(), //1f326 + fe0f + "foggy" => "🌫️".to_string(), //1f32b + fe0f + "fog" => "🌫️".to_string(), //1f32b + fe0f + "mist" => '\u{2591}'.to_string(), //2591 + "haze" => '\u{2591}'.to_string(), //2591 + "snowy" => "❄️".to_string(), //2744 + fe0f + "snow" => "❄️".to_string(), //2744 + fe0f + "thunderstorm" => "🌩️".to_string(),//1f329 + fe0f + "thunder" => "🌩️".to_string(), //1f329 + fe0f + + // This is the "other" section + "bel" => '\x07'.to_string(), // Terminal Bell + "backspace" => '\x08'.to_string(), // Backspace + }; +} + +impl Command for Char { + fn name(&self) -> &str { + "char" + } + + fn signature(&self) -> Signature { + Signature::build("char") + .optional( + "character", + SyntaxShape::Any, + "the name of the character to output", + ) + .rest("rest", SyntaxShape::String, "multiple Unicode bytes") + .switch("list", "List all supported character names", Some('l')) + .switch("unicode", "Unicode string i.e. 1f378", Some('u')) + .category(Category::Strings) + } + + fn usage(&self) -> &str { + "Output special characters (e.g., 'newline')." + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Output newline", + example: r#"char newline"#, + result: Some(Value::test_string("\n")), + }, + Example { + description: "Output prompt character, newline and a hamburger character", + example: r#"echo [(char prompt) (char newline) (char hamburger)] | str collect"#, + result: Some(Value::test_string("\u{25b6}\n\u{2261}")), + }, + Example { + description: "Output Unicode character", + example: r#"char -u 1f378"#, + result: Some(Value::test_string("\u{1f378}")), + }, + Example { + description: "Output multi-byte Unicode character", + example: r#"char -u 1F468 200D 1F466 200D 1F466"#, + result: Some(Value::test_string( + "\u{1F468}\u{200D}\u{1F466}\u{200D}\u{1F466}", + )), + }, + ] + } + + fn run( + &self, + engine_state: &nu_protocol::engine::EngineState, + stack: &mut nu_protocol::engine::Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let call_span = call.head; + // handle -l flag + if call.has_flag("list") { + return Ok(CHAR_MAP + .iter() + .map(move |(name, s)| { + let cols = vec!["name".into(), "character".into(), "unicode".into()]; + let name: Value = Value::string(String::from(*name), call_span); + let character = Value::string(s, call_span); + let unicode = Value::string( + s.chars() + .map(|c| format!("{:x}", c as u32)) + .collect::>() + .join(" "), + call_span, + ); + let vals = vec![name, character, unicode]; + Value::Record { + cols, + vals, + span: call_span, + } + }) + .into_pipeline_data(engine_state.ctrlc.clone())); + } + // handle -u flag + let args: Vec = call.rest(engine_state, stack, 0)?; + if call.has_flag("unicode") { + if args.is_empty() { + return Err(ShellError::MissingParameter( + "missing at least one unicode character".into(), + call_span, + )); + } + let mut multi_byte = String::new(); + for (i, arg) in args.iter().enumerate() { + let span = call.nth(i).expect("Unexpected missing argument").span; + multi_byte.push(string_to_unicode_char(arg, &span)?) + } + Ok(Value::string(multi_byte, call_span).into_pipeline_data()) + } else { + if args.is_empty() { + return Err(ShellError::MissingParameter( + "missing name of the character".into(), + call_span, + )); + } + let special_character = str_to_character(&args[0]); + if let Some(output) = special_character { + Ok(Value::string(output, call_span).into_pipeline_data()) + } else { + Err(ShellError::UnsupportedInput( + "error finding named character".into(), + call.nth(0).expect("Unexpected missing argument").span, + )) + } + } + } +} + +fn string_to_unicode_char(s: &str, t: &Span) -> Result { + let decoded_char = u32::from_str_radix(s, 16) + .ok() + .and_then(std::char::from_u32); + + if let Some(ch) = decoded_char { + Ok(ch) + } else { + Err(ShellError::UnsupportedInput( + "error decoding Unicode character".into(), + *t, + )) + } +} + +fn str_to_character(s: &str) -> Option { + CHAR_MAP.get(s).map(|s| s.into()) +} + +#[cfg(test)] +mod tests { + use super::Char; + + #[test] + fn examples_work_as_expected() { + use crate::test_examples; + + test_examples(Char {}) + } +} diff --git a/crates/nu-command/src/strings/decode.rs b/crates/nu-command/src/strings/decode.rs new file mode 100644 index 0000000000..7b54791b5f --- /dev/null +++ b/crates/nu-command/src/strings/decode.rs @@ -0,0 +1,107 @@ +use encoding_rs::Encoding; +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Spanned, SyntaxShape, + Value, +}; + +#[derive(Clone)] +pub struct Decode; + +impl Command for Decode { + fn name(&self) -> &str { + "decode" + } + + fn usage(&self) -> &str { + "Decode bytes as a string." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("decode") + .required("encoding", SyntaxShape::String, "the text encoding to use") + .category(Category::Strings) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Decode the output of an external command", + example: "cat myfile.q | decode utf-8", + result: None, + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let encoding: Spanned = call.req(engine_state, stack, 0)?; + + match input { + PipelineData::RawStream(stream, ..) => { + let bytes: Vec = stream.into_bytes()?.item; + + let encoding = match Encoding::for_label(encoding.item.as_bytes()) { + None => Err(ShellError::SpannedLabeledError( + format!( + r#"{} is not a valid encoding, refer to https://docs.rs/encoding_rs/0.8.23/encoding_rs/#statics for a valid list of encodings"#, + encoding.item + ), + "invalid encoding".into(), + encoding.span, + )), + Some(encoding) => Ok(encoding), + }?; + + let result = encoding.decode(&bytes); + + Ok(Value::String { + val: result.0.to_string(), + span: head, + } + .into_pipeline_data()) + } + PipelineData::Value(Value::Binary { val: bytes, .. }, ..) => { + let encoding = match Encoding::for_label(encoding.item.as_bytes()) { + None => Err(ShellError::SpannedLabeledError( + format!( + r#"{} is not a valid encoding, refer to https://docs.rs/encoding_rs/0.8.23/encoding_rs/#statics for a valid list of encodings"#, + encoding.item + ), + "invalid encoding".into(), + encoding.span, + )), + Some(encoding) => Ok(encoding), + }?; + + let result = encoding.decode(&bytes); + + Ok(Value::String { + val: result.0.to_string(), + span: head, + } + .into_pipeline_data()) + } + _ => Err(ShellError::UnsupportedInput( + "non-binary input".into(), + head, + )), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + crate::test_examples(Decode) + } +} diff --git a/crates/nu-command/src/strings/detect_columns.rs b/crates/nu-command/src/strings/detect_columns.rs new file mode 100644 index 0000000000..bdd533e78b --- /dev/null +++ b/crates/nu-command/src/strings/detect_columns.rs @@ -0,0 +1,314 @@ +use std::iter::Peekable; +use std::str::CharIndices; + +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span, Spanned, + SyntaxShape, Value, +}; + +type Input<'t> = Peekable>; + +#[derive(Clone)] +pub struct DetectColumns; + +impl Command for DetectColumns { + fn name(&self) -> &str { + "detect columns" + } + + fn signature(&self) -> Signature { + Signature::build("detect columns") + .named( + "skip", + SyntaxShape::Int, + "number of rows to skip before detecting", + Some('s'), + ) + .switch("no_headers", "don't detect headers", Some('n')) + .category(Category::Strings) + } + + fn usage(&self) -> &str { + "splits contents across multiple columns via the separator." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + detect_columns(engine_state, stack, call, input) + } +} + +fn detect_columns( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let name_span = call.head; + let num_rows_to_skip: Option = call.get_flag(engine_state, stack, "skip")?; + let noheader = call.has_flag("no_headers"); + let ctrlc = engine_state.ctrlc.clone(); + let config = stack.get_config()?; + let input = input.collect_string("", &config)?; + + #[allow(clippy::needless_collect)] + let input: Vec<_> = input + .lines() + .skip(num_rows_to_skip.unwrap_or_default()) + .map(|x| x.to_string()) + .collect(); + + let mut input = input.into_iter(); + let headers = input.next(); + + if let Some(orig_headers) = headers { + let mut headers = find_columns(&orig_headers); + + if noheader { + for header in headers.iter_mut().enumerate() { + header.1.item = format!("Column{}", header.0); + } + } + + Ok((if noheader { + vec![orig_headers].into_iter().chain(input) + } else { + vec![].into_iter().chain(input) + }) + .map(move |x| { + let row = find_columns(&x); + + let mut cols = vec![]; + let mut vals = vec![]; + + if headers.len() == row.len() { + for (header, val) in headers.iter().zip(row.iter()) { + cols.push(header.item.clone()); + vals.push(Value::String { + val: val.item.clone(), + span: name_span, + }); + } + } else { + let mut pre_output = vec![]; + + // column counts don't line up, so see if we can figure out why + for cell in row { + for header in &headers { + if cell.span.start <= header.span.end && cell.span.end > header.span.start { + pre_output.push(( + header.item.to_string(), + Value::string(&cell.item, name_span), + )); + } + } + } + + for header in &headers { + let mut found = false; + for pre_o in &pre_output { + if pre_o.0 == header.item { + found = true; + break; + } + } + + if !found { + pre_output.push((header.item.to_string(), Value::nothing(name_span))); + } + } + + for header in &headers { + for pre_o in &pre_output { + if pre_o.0 == header.item { + cols.push(header.item.clone()); + vals.push(pre_o.1.clone()) + } + } + } + } + + Value::Record { + cols, + vals, + span: name_span, + } + }) + .into_pipeline_data(ctrlc)) + } else { + Ok(PipelineData::new(name_span)) + } +} + +pub fn find_columns(input: &str) -> Vec> { + let mut chars = input.char_indices().peekable(); + let mut output = vec![]; + + while let Some((_, c)) = chars.peek() { + if c.is_whitespace() { + // If the next character is non-newline whitespace, skip it. + + let _ = chars.next(); + } else { + // Otherwise, try to consume an unclassified token. + + let result = baseline(&mut chars); + + output.push(result); + } + } + + output +} + +#[derive(Clone, Copy)] +enum BlockKind { + Paren, + CurlyBracket, + SquareBracket, +} + +fn baseline(src: &mut Input) -> Spanned { + let mut token_contents = String::new(); + + let start_offset = if let Some((pos, _)) = src.peek() { + *pos + } else { + 0 + }; + + // This variable tracks the starting character of a string literal, so that + // we remain inside the string literal lexer mode until we encounter the + // closing quote. + let mut quote_start: Option = None; + + // This Vec tracks paired delimiters + let mut block_level: Vec = vec![]; + + // A baseline token is terminated if it's not nested inside of a paired + // delimiter and the next character is one of: `|`, `;`, `#` or any + // whitespace. + fn is_termination(block_level: &[BlockKind], c: char) -> bool { + block_level.is_empty() && (c.is_whitespace()) + } + + // The process of slurping up a baseline token repeats: + // + // - String literal, which begins with `'`, `"` or `\``, and continues until + // the same character is encountered again. + // - Delimiter pair, which begins with `[`, `(`, or `{`, and continues until + // the matching closing delimiter is found, skipping comments and string + // literals. + // - When not nested inside of a delimiter pair, when a terminating + // character (whitespace, `|`, `;` or `#`) is encountered, the baseline + // token is done. + // - Otherwise, accumulate the character into the current baseline token. + while let Some((_, c)) = src.peek() { + let c = *c; + + if quote_start.is_some() { + // If we encountered the closing quote character for the current + // string, we're done with the current string. + if Some(c) == quote_start { + quote_start = None; + } + } else if c == '\n' { + if is_termination(&block_level, c) { + break; + } + } else if c == '\'' || c == '"' || c == '`' { + // We encountered the opening quote of a string literal. + quote_start = Some(c); + } else if c == '[' { + // We encountered an opening `[` delimiter. + block_level.push(BlockKind::SquareBracket); + } else if c == ']' { + // We encountered a closing `]` delimiter. Pop off the opening `[` + // delimiter. + if let Some(BlockKind::SquareBracket) = block_level.last() { + let _ = block_level.pop(); + } + } else if c == '{' { + // We encountered an opening `{` delimiter. + block_level.push(BlockKind::CurlyBracket); + } else if c == '}' { + // We encountered a closing `}` delimiter. Pop off the opening `{`. + if let Some(BlockKind::CurlyBracket) = block_level.last() { + let _ = block_level.pop(); + } + } else if c == '(' { + // We enceountered an opening `(` delimiter. + block_level.push(BlockKind::Paren); + } else if c == ')' { + // We encountered a closing `)` delimiter. Pop off the opening `(`. + if let Some(BlockKind::Paren) = block_level.last() { + let _ = block_level.pop(); + } + } else if is_termination(&block_level, c) { + break; + } + + // Otherwise, accumulate the character into the current token. + token_contents.push(c); + + // Consume the character. + let _ = src.next(); + } + + let span = Span::new(start_offset, start_offset + token_contents.len()); + + // If there is still unclosed opening delimiters, close them and add + // synthetic closing characters to the accumulated token. + if block_level.last().is_some() { + // let delim: char = (*block).closing(); + // let cause = ParseError::unexpected_eof(delim.to_string(), span); + + // while let Some(bk) = block_level.pop() { + // token_contents.push(bk.closing()); + // } + + return Spanned { + item: token_contents, + span, + }; + } + + if quote_start.is_some() { + // The non-lite parse trims quotes on both sides, so we add the expected quote so that + // anyone wanting to consume this partial parse (e.g., completions) will be able to get + // correct information from the non-lite parse. + // token_contents.push(delimiter); + + // return ( + // token_contents.spanned(span), + // Some(ParseError::unexpected_eof(delimiter.to_string(), span)), + // ); + return Spanned { + item: token_contents, + span, + }; + } + + Spanned { + item: token_contents, + span, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + crate::test_examples(DetectColumns) + } +} diff --git a/crates/nu-command/src/strings/format/command.rs b/crates/nu-command/src/strings/format/command.rs new file mode 100644 index 0000000000..6846ac2a7a --- /dev/null +++ b/crates/nu-command/src/strings/format/command.rs @@ -0,0 +1,202 @@ +use nu_engine::CallExt; +use nu_protocol::ast::{Call, PathMember}; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, ListStream, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Format; + +impl Command for Format { + fn name(&self) -> &str { + "format" + } + + fn signature(&self) -> Signature { + Signature::build("format") + .required( + "pattern", + SyntaxShape::String, + "the pattern to output. e.g.) \"{foo}: {bar}\"", + ) + .category(Category::Strings) + } + + fn usage(&self) -> &str { + "Format columns into a string using a simple pattern." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let specified_pattern: Result = call.req(engine_state, stack, 0); + match specified_pattern { + Err(e) => Err(e), + Ok(pattern) => { + let string_pattern = pattern.as_string()?; + let ops = extract_formatting_operations(string_pattern); + format(input, &ops, call.head) + } + } + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Print filenames with their sizes", + example: "ls | format '{name}: {size}'", + result: None, + }, + Example { + description: "Print elements from some columns of a table", + example: "echo [[col1, col2]; [v1, v2] [v3, v4]] | format '{col2}'", + result: Some(Value::List { + vals: vec![Value::test_string("v2"), Value::test_string("v4")], + span: Span::test_data(), + }), + }, + ] + } +} + +#[derive(Debug)] +enum FormatOperation { + FixedText(String), + ValueFromColumn(String), +} + +/// Given a pattern that is fed into the Format command, we can process it and subdivide it +/// in two kind of operations. +/// FormatOperation::FixedText contains a portion of the patter that has to be placed +/// there without any further processing. +/// FormatOperation::ValueFromColumn contains the name of a column whose values will be +/// formatted according to the input pattern. +fn extract_formatting_operations(input: String) -> Vec { + let mut output = vec![]; + + let mut characters = input.chars(); + 'outer: loop { + let mut before_bracket = String::new(); + + for ch in &mut characters { + if ch == '{' { + break; + } + before_bracket.push(ch); + } + + if !before_bracket.is_empty() { + output.push(FormatOperation::FixedText(before_bracket.to_string())); + } + + let mut column_name = String::new(); + + for ch in &mut characters { + if ch == '}' { + break; + } + column_name.push(ch); + } + + if !column_name.is_empty() { + output.push(FormatOperation::ValueFromColumn(column_name.clone())); + } + + if before_bracket.is_empty() && column_name.is_empty() { + break 'outer; + } + } + output +} + +/// Format the incoming PipelineData according to the pattern +fn format( + input_data: PipelineData, + format_operations: &[FormatOperation], + span: Span, +) -> Result { + let data_as_value = input_data.into_value(span); + + // We can only handle a Record or a List of Record's + match data_as_value { + Value::Record { .. } => match format_record(format_operations, &data_as_value, span) { + Ok(value) => Ok(PipelineData::Value(Value::string(value, span), None)), + Err(value) => Err(value), + }, + + Value::List { vals, .. } => { + let mut list = vec![]; + for val in vals.iter() { + match val { + Value::Record { .. } => match format_record(format_operations, val, span) { + Ok(value) => { + list.push(Value::string(value, span)); + } + Err(value) => { + return Err(value); + } + }, + + _ => { + return Err(ShellError::UnsupportedInput( + "Input data is not supported by this command.".to_string(), + span, + )) + } + } + } + + Ok(PipelineData::ListStream( + ListStream::from_stream(list.into_iter(), None), + None, + )) + } + _ => Err(ShellError::UnsupportedInput( + "Input data is not supported by this command.".to_string(), + span, + )), + } +} + +fn format_record( + format_operations: &[FormatOperation], + data_as_value: &Value, + span: Span, +) -> Result { + let mut output = String::new(); + for op in format_operations { + match op { + FormatOperation::FixedText(s) => output.push_str(s.as_str()), + + // The referenced code suggest to use the correct Span's + // See: https://github.com/nushell/nushell/blob/c4af5df828135159633d4bc3070ce800518a42a2/crates/nu-command/src/commands/strings/format/command.rs#L61 + FormatOperation::ValueFromColumn(col_name) => { + match data_as_value + .clone() + .follow_cell_path(&[PathMember::String { + val: col_name.clone(), + span, + }]) { + Ok(value_at_column) => output.push_str(value_at_column.as_string()?.as_str()), + Err(se) => return Err(se), + } + } + } + } + Ok(output) +} + +#[cfg(test)] +mod test { + #[test] + fn test_examples() { + use super::Format; + use crate::test_examples; + test_examples(Format {}) + } +} diff --git a/crates/nu-command/src/strings/format/mod.rs b/crates/nu-command/src/strings/format/mod.rs new file mode 100644 index 0000000000..71be06ceb0 --- /dev/null +++ b/crates/nu-command/src/strings/format/mod.rs @@ -0,0 +1,3 @@ +pub mod command; + +pub use command::Format; diff --git a/crates/nu-command/src/strings/mod.rs b/crates/nu-command/src/strings/mod.rs new file mode 100644 index 0000000000..1cdcb4a18f --- /dev/null +++ b/crates/nu-command/src/strings/mod.rs @@ -0,0 +1,19 @@ +mod build_string; +mod char_; +mod decode; +mod detect_columns; +mod format; +mod parse; +mod size; +mod split; +mod str_; + +pub use build_string::BuildString; +pub use char_::Char; +pub use decode::*; +pub use detect_columns::*; +pub use format::*; +pub use parse::*; +pub use size::Size; +pub use split::*; +pub use str_::*; diff --git a/crates/nu-command/src/strings/parse.rs b/crates/nu-command/src/strings/parse.rs new file mode 100644 index 0000000000..874f8b06c7 --- /dev/null +++ b/crates/nu-command/src/strings/parse.rs @@ -0,0 +1,238 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, ListStream, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, + Value, +}; +use regex::Regex; + +#[derive(Clone)] +pub struct Parse; + +impl Command for Parse { + fn name(&self) -> &str { + "parse" + } + + fn usage(&self) -> &str { + "Parse columns from string data using a simple pattern." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("parse") + .required( + "pattern", + SyntaxShape::String, + "the pattern to match. Eg) \"{foo}: {bar}\"", + ) + .switch("regex", "use full regex syntax for patterns", Some('r')) + .category(Category::Strings) + } + + fn examples(&self) -> Vec { + let result = Value::List { + vals: vec![Value::Record { + cols: vec!["foo".to_string(), "bar".to_string()], + vals: vec![Value::test_string("hi"), Value::test_string("there")], + span: Span::test_data(), + }], + span: Span::test_data(), + }; + + vec![ + Example { + description: "Parse a string into two named columns", + example: "echo \"hi there\" | parse \"{foo} {bar}\"", + result: Some(result.clone()), + }, + Example { + description: "Parse a string using regex pattern", + example: "echo \"hi there\" | parse -r \"(?P\\w+) (?P\\w+)\"", + result: Some(result), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input) + } +} + +fn operate( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let head = call.head; + let pattern: Spanned = call.req(engine_state, stack, 0)?; + let regex: bool = call.has_flag("regex"); + let ctrlc = engine_state.ctrlc.clone(); + + let pattern_item = pattern.item; + let pattern_span = pattern.span; + + let item_to_parse = if regex { + pattern_item + } else { + build_regex(&pattern_item, pattern_span)? + }; + + let regex_pattern = + Regex::new(&item_to_parse).map_err(|e| parse_regex_error(e, pattern_span))?; + + let columns = column_names(®ex_pattern); + let mut parsed: Vec = Vec::new(); + + for v in input { + match v.as_string() { + Ok(s) => { + let results = regex_pattern.captures_iter(&s); + + for c in results { + let mut cols = Vec::with_capacity(columns.len()); + let mut vals = Vec::with_capacity(c.len()); + + for (column_name, cap) in columns.iter().zip(c.iter().skip(1)) { + let cap_string = cap.map(|v| v.as_str()).unwrap_or("").to_string(); + cols.push(column_name.clone()); + vals.push(Value::String { + val: cap_string, + span: v.span()?, + }); + } + + parsed.push(Value::Record { + cols, + vals, + span: head, + }); + } + } + Err(_) => { + return Err(ShellError::PipelineMismatch( + "string".into(), + head, + v.span()?, + )) + } + } + } + + Ok(PipelineData::ListStream( + ListStream::from_stream(parsed.into_iter(), ctrlc), + None, + )) +} + +fn build_regex(input: &str, span: Span) -> Result { + let mut output = "(?s)\\A".to_string(); + + //let mut loop_input = input; + let mut loop_input = input.chars().peekable(); + loop { + let mut before = String::new(); + while let Some(c) = loop_input.next() { + if c == '{' { + // If '{{', still creating a plaintext parse command, but just for a single '{' char + if loop_input.peek() == Some(&'{') { + let _ = loop_input.next(); + } else { + break; + } + } + before.push(c); + } + + if !before.is_empty() { + output.push_str(®ex::escape(&before)); + } + + // Look for column as we're now at one + let mut column = String::new(); + while let Some(c) = loop_input.next() { + if c == '}' { + break; + } + column.push(c); + + if loop_input.peek().is_none() { + return Err(ShellError::DelimiterError( + "Found opening `{` without an associated closing `}`".to_owned(), + span, + )); + } + } + + if !column.is_empty() { + output.push_str("(?P<"); + output.push_str(&column); + output.push_str(">.*?)"); + } + + if before.is_empty() && column.is_empty() { + break; + } + } + + output.push_str("\\z"); + Ok(output) +} + +fn column_names(regex: &Regex) -> Vec { + regex + .capture_names() + .enumerate() + .skip(1) + .map(|(i, name)| { + name.map(String::from) + .unwrap_or_else(|| format!("Capture{}", i)) + }) + .collect() +} + +fn parse_regex_error(e: regex::Error, base_span: Span) -> ShellError { + match e { + regex::Error::Syntax(msg) => { + let mut lines = msg.lines(); + + let main_msg = lines + .next() + .map(|l| l.replace(':', "")) + .expect("invalid regex pattern"); + + let span = lines.nth(1).and_then(|l| l.find('^')).map(|space| { + let start = base_span.start + space - 3; + Span::new(start, start + 1) + }); + + let msg = lines + .next() + .and_then(|l| l.split(':').nth(1)) + .map(|s| format!("{}: {}", main_msg, s.trim())); + + match (msg, span) { + (Some(msg), Some(span)) => ShellError::DelimiterError(msg, span), + _ => ShellError::DelimiterError("Invalid regex".to_owned(), base_span), + } + } + _ => ShellError::DelimiterError("Invalid regex".to_owned(), base_span), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + crate::test_examples(Parse) + } +} diff --git a/crates/nu-command/src/strings/size.rs b/crates/nu-command/src/strings/size.rs new file mode 100644 index 0000000000..db9faabdd8 --- /dev/null +++ b/crates/nu-command/src/strings/size.rs @@ -0,0 +1,173 @@ +extern crate unicode_segmentation; + +use unicode_segmentation::UnicodeSegmentation; + +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Span, Value}; + +#[derive(Clone)] +pub struct Size; + +impl Command for Size { + fn name(&self) -> &str { + "size" + } + + fn signature(&self) -> Signature { + Signature::build("size").category(Category::Strings) + } + + fn usage(&self) -> &str { + "Gather word count statistics on the text." + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + size(engine_state, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Count the number of words in a string", + example: r#""There are seven words in this sentence" | size"#, + result: Some(Value::Record { + cols: vec![ + "lines".into(), + "words".into(), + "chars".into(), + "bytes".into(), + ], + vals: vec![ + Value::Int { + val: 0, + span: Span::test_data(), + }, + Value::Int { + val: 7, + span: Span::test_data(), + }, + Value::Int { + val: 38, + span: Span::test_data(), + }, + Value::Int { + val: 38, + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }, + Example { + description: "Counts Unicode characters correctly in a string", + example: r#""Amélie Amelie" | size"#, + result: Some(Value::Record { + cols: vec![ + "lines".into(), + "words".into(), + "chars".into(), + "bytes".into(), + ], + vals: vec![ + Value::Int { + val: 0, + span: Span::test_data(), + }, + Value::Int { + val: 2, + span: Span::test_data(), + }, + Value::Int { + val: 13, + span: Span::test_data(), + }, + Value::Int { + val: 15, + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }, + ] + } +} + +fn size( + engine_state: &EngineState, + call: &Call, + input: PipelineData, +) -> Result { + let span = call.head; + input.map( + move |v| match v.as_string() { + Ok(s) => count(&s, span), + Err(_) => Value::Error { + error: ShellError::PipelineMismatch("string".into(), span, span), + }, + }, + engine_state.ctrlc.clone(), + ) +} + +fn count(contents: &str, span: Span) -> Value { + let mut lines: i64 = 0; + let mut words: i64 = 0; + let mut chars: i64 = 0; + let bytes = contents.len() as i64; + let mut end_of_word = true; + + for c in UnicodeSegmentation::graphemes(contents, true) { + chars += 1; + + match c { + "\n" => { + lines += 1; + end_of_word = true; + } + " " => end_of_word = true, + _ => { + if end_of_word { + words += 1; + } + end_of_word = false; + } + } + } + + let mut cols = vec![]; + let mut vals = vec![]; + + cols.push("lines".into()); + vals.push(Value::Int { val: lines, span }); + + cols.push("words".into()); + vals.push(Value::Int { val: words, span }); + + cols.push("chars".into()); + vals.push(Value::Int { val: chars, span }); + + cols.push("bytes".into()); + vals.push(Value::Int { val: bytes, span }); + + Value::Record { cols, vals, span } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Size {}) + } +} diff --git a/crates/nu-command/src/strings/split/chars.rs b/crates/nu-command/src/strings/split/chars.rs new file mode 100644 index 0000000000..c633dc6d4f --- /dev/null +++ b/crates/nu-command/src/strings/split/chars.rs @@ -0,0 +1,93 @@ +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Value, +}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "split chars" + } + + fn signature(&self) -> Signature { + Signature::build("split chars").category(Category::Strings) + } + + fn usage(&self) -> &str { + "splits a string's characters into separate rows" + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Split the string's characters into separate rows", + example: "'hello' | split chars", + result: Some(Value::List { + vals: vec![ + Value::test_string("h"), + Value::test_string("e"), + Value::test_string("l"), + Value::test_string("l"), + Value::test_string("o"), + ], + span: Span::test_data(), + }), + }] + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + split_chars(engine_state, call, input) + } +} + +fn split_chars( + engine_state: &EngineState, + call: &Call, + input: PipelineData, +) -> Result { + let span = call.head; + + input.flat_map( + move |x| split_chars_helper(&x, span), + engine_state.ctrlc.clone(), + ) +} + +fn split_chars_helper(v: &Value, name: Span) -> Vec { + match v.span() { + Ok(v_span) => { + if let Ok(s) = v.as_string() { + s.chars() + .collect::>() + .into_iter() + .map(move |x| Value::string(x, v_span)) + .collect() + } else { + vec![Value::Error { + error: ShellError::PipelineMismatch("string".into(), name, v_span), + }] + } + } + Err(error) => vec![Value::Error { error }], + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/strings/split/column.rs b/crates/nu-command/src/strings/split/column.rs new file mode 100644 index 0000000000..6c2f00df0c --- /dev/null +++ b/crates/nu-command/src/strings/split/column.rs @@ -0,0 +1,128 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "split column" + } + + fn signature(&self) -> Signature { + Signature::build("split column") + .required( + "separator", + SyntaxShape::String, + "the character that denotes what separates columns", + ) + .switch("collapse-empty", "remove empty columns", Some('c')) + .rest( + "rest", + SyntaxShape::String, + "column names to give the new columns", + ) + .category(Category::Strings) + } + + fn usage(&self) -> &str { + "splits contents across multiple columns via the separator." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + split_column(engine_state, stack, call, input) + } +} + +fn split_column( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let name_span = call.head; + let separator: Spanned = call.req(engine_state, stack, 0)?; + let rest: Vec> = call.rest(engine_state, stack, 1)?; + let collapse_empty = call.has_flag("collapse-empty"); + + input.flat_map( + move |x| split_column_helper(&x, &separator, &rest, collapse_empty, name_span), + engine_state.ctrlc.clone(), + ) +} + +fn split_column_helper( + v: &Value, + separator: &Spanned, + rest: &[Spanned], + collapse_empty: bool, + head: Span, +) -> Vec { + if let Ok(s) = v.as_string() { + let splitter = separator.item.replace("\\n", "\n"); + + let split_result: Vec<_> = if collapse_empty { + s.split(&splitter).filter(|s| !s.is_empty()).collect() + } else { + s.split(&splitter).collect() + }; + + let positional: Vec<_> = rest.iter().map(|f| f.item.clone()).collect(); + + // If they didn't provide column names, make up our own + + let mut cols = vec![]; + let mut vals = vec![]; + if positional.is_empty() { + let mut gen_columns = vec![]; + for i in 0..split_result.len() { + gen_columns.push(format!("Column{}", i + 1)); + } + + for (&k, v) in split_result.iter().zip(&gen_columns) { + cols.push(v.to_string()); + vals.push(Value::string(k, head)); + } + } else { + for (&k, v) in split_result.iter().zip(&positional) { + cols.push(v.into()); + vals.push(Value::string(k, head)); + } + } + vec![Value::Record { + cols, + vals, + span: head, + }] + } else { + match v.span() { + Ok(span) => vec![Value::Error { + error: ShellError::PipelineMismatch("string".into(), head, span), + }], + Err(error) => vec![Value::Error { error }], + } + } +} + +// #[cfg(test)] +// mod tests { +// use super::ShellError; +// use super::SubCommand; + +// #[test] +// fn examples_work_as_expected() -> Result<(), ShellError> { +// use crate::examples::test as test_examples; + +// test_examples(SubCommand {}) +// } +// } diff --git a/crates/nu-command/src/strings/split/command.rs b/crates/nu-command/src/strings/split/command.rs new file mode 100644 index 0000000000..30d4c86908 --- /dev/null +++ b/crates/nu-command/src/strings/split/command.rs @@ -0,0 +1,55 @@ +use nu_engine::get_full_help; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, IntoPipelineData, PipelineData, Signature, Value, +}; + +#[derive(Clone)] +pub struct SplitCommand; + +impl Command for SplitCommand { + fn name(&self) -> &str { + "split" + } + + fn signature(&self) -> Signature { + Signature::build("split").category(Category::Strings) + } + + fn usage(&self) -> &str { + "Split contents across desired subcommand (like row, column) via the separator." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(Value::String { + val: get_full_help( + &SplitCommand.signature(), + &SplitCommand.examples(), + engine_state, + stack, + ), + span: call.head, + } + .into_pipeline_data()) + } +} + +// #[cfg(test)] +// mod tests { +// use super::Command; +// use super::ShellError; + +// #[test] +// fn examples_work_as_expected() -> Result<(), ShellError> { +// use crate::examples::test as test_examples; + +// test_examples(Command {}) +// } +// } diff --git a/crates/nu-command/src/strings/split/mod.rs b/crates/nu-command/src/strings/split/mod.rs new file mode 100644 index 0000000000..c6e6da0171 --- /dev/null +++ b/crates/nu-command/src/strings/split/mod.rs @@ -0,0 +1,9 @@ +pub mod chars; +pub mod column; +pub mod command; +pub mod row; + +pub use chars::SubCommand as SplitChars; +pub use column::SubCommand as SplitColumn; +pub use command::SplitCommand as Split; +pub use row::SubCommand as SplitRow; diff --git a/crates/nu-command/src/strings/split/row.rs b/crates/nu-command/src/strings/split/row.rs new file mode 100644 index 0000000000..d81679a259 --- /dev/null +++ b/crates/nu-command/src/strings/split/row.rs @@ -0,0 +1,91 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "split row" + } + + fn signature(&self) -> Signature { + Signature::build("split row") + .required( + "separator", + SyntaxShape::String, + "the character that denotes what separates rows", + ) + .category(Category::Strings) + } + + fn usage(&self) -> &str { + "splits contents over multiple rows via the separator." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + split_row(engine_state, stack, call, input) + } +} + +fn split_row( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let name_span = call.head; + let separator: Spanned = call.req(engine_state, stack, 0)?; + + input.flat_map( + move |x| split_row_helper(&x, &separator, name_span), + engine_state.ctrlc.clone(), + ) +} + +fn split_row_helper(v: &Value, separator: &Spanned, name: Span) -> Vec { + match v.span() { + Ok(v_span) => { + if let Ok(s) = v.as_string() { + let splitter = separator.item.replace("\\n", "\n"); + s.split(&splitter) + .filter_map(|s| { + if s.trim() != "" { + Some(Value::string(s, v_span)) + } else { + None + } + }) + .collect() + } else { + vec![Value::Error { + error: ShellError::PipelineMismatch("string".into(), name, v_span), + }] + } + } + Err(error) => vec![Value::Error { error }], + } +} + +// #[cfg(test)] +// mod tests { +// use super::ShellError; +// use super::SubCommand; + +// #[test] +// fn examples_work_as_expected() -> Result<(), ShellError> { +// use crate::examples::test as test_examples; + +// test_examples(SubCommand {}) +// } +// } diff --git a/crates/nu-command/src/strings/str_/capitalize.rs b/crates/nu-command/src/strings/str_/capitalize.rs new file mode 100644 index 0000000000..fb11969058 --- /dev/null +++ b/crates/nu-command/src/strings/str_/capitalize.rs @@ -0,0 +1,145 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::ast::CellPath; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::Category; +use nu_protocol::{Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "str capitalize" + } + + fn signature(&self) -> Signature { + Signature::build("str capitalize") + .rest( + "rest", + SyntaxShape::CellPath, + "optionally capitalize text by column paths", + ) + .category(Category::Strings) + } + + fn usage(&self) -> &str { + "capitalizes text" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Capitalize contents", + example: "'good day' | str capitalize", + result: Some(Value::String { + val: "Good day".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "Capitalize contents", + example: "'anton' | str capitalize", + result: Some(Value::String { + val: "Anton".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "Capitalize a column in a table", + example: "[[lang, gems]; [nu_test, 100]] | str capitalize lang", + result: Some(Value::List { + vals: vec![Value::Record { + span: Span::test_data(), + cols: vec!["lang".to_string(), "gems".to_string()], + vals: vec![ + Value::String { + val: "Nu_test".to_string(), + span: Span::test_data(), + }, + Value::test_int(100), + ], + }], + span: Span::test_data(), + }), + }, + ] + } +} + +fn operate( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let head = call.head; + let column_paths: Vec = call.rest(engine_state, stack, 0)?; + input.map( + move |v| { + if column_paths.is_empty() { + action(&v, head) + } else { + let mut ret = v; + for path in &column_paths { + let r = + ret.update_cell_path(&path.members, Box::new(move |old| action(old, head))); + if let Err(error) = r { + return Value::Error { error }; + } + } + ret + } + }, + engine_state.ctrlc.clone(), + ) +} + +fn action(input: &Value, head: Span) -> Value { + match input { + Value::String { val, .. } => Value::String { + val: uppercase_helper(val), + span: head, + }, + other => Value::Error { + error: ShellError::UnsupportedInput( + format!( + "Input's type is {}. This command only works with strings.", + other.get_type() + ), + head, + ), + }, + } +} + +fn uppercase_helper(s: &str) -> String { + // apparently more performant https://stackoverflow.com/questions/38406793/why-is-capitalizing-the-first-letter-of-a-string-so-convoluted-in-rust + let mut chars = s.chars(); + match chars.next() { + None => String::new(), + Some(f) => f.to_uppercase().collect::() + chars.as_str(), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/strings/str_/case/camel_case.rs b/crates/nu-command/src/strings/str_/case/camel_case.rs new file mode 100644 index 0000000000..5ca2e5c9af --- /dev/null +++ b/crates/nu-command/src/strings/str_/case/camel_case.rs @@ -0,0 +1,100 @@ +use inflector::cases::camelcase::to_camel_case; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +use crate::operate; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "str camel-case" + } + + fn signature(&self) -> Signature { + Signature::build("str camel-case") + .rest( + "rest", + SyntaxShape::CellPath, + "optionally convert text to camelCase by column paths", + ) + .category(Category::Strings) + } + + fn usage(&self) -> &str { + "converts a string to camelCase" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input, &to_camel_case) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "convert a string to camelCase", + example: " 'NuShell' | str camel-case", + result: Some(Value::String { + val: "nuShell".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "convert a string to camelCase", + example: "'this-is-the-first-case' | str camel-case", + result: Some(Value::String { + val: "thisIsTheFirstCase".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "convert a string to camelCase", + example: " 'this_is_the_second_case' | str camel-case", + result: Some(Value::String { + val: "thisIsTheSecondCase".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "convert a column from a table to camelCase", + example: r#"[[lang, gems]; [nu_test, 100]] | str camel-case lang"#, + result: Some(Value::List { + vals: vec![Value::Record { + span: Span::test_data(), + cols: vec!["lang".to_string(), "gems".to_string()], + vals: vec![ + Value::String { + val: "nuTest".to_string(), + span: Span::test_data(), + }, + Value::test_int(100), + ], + }], + span: Span::test_data(), + }), + }, + ] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/strings/str_/case/kebab_case.rs b/crates/nu-command/src/strings/str_/case/kebab_case.rs new file mode 100644 index 0000000000..377cf4b182 --- /dev/null +++ b/crates/nu-command/src/strings/str_/case/kebab_case.rs @@ -0,0 +1,99 @@ +use inflector::cases::kebabcase::to_kebab_case; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +use crate::operate; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "str kebab-case" + } + + fn signature(&self) -> Signature { + Signature::build("str kebab-case") + .rest( + "rest", + SyntaxShape::CellPath, + "optionally convert text to kebab-case by column paths", + ) + .category(Category::Strings) + } + + fn usage(&self) -> &str { + "converts a string to kebab-case" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input, &to_kebab_case) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "convert a string to kebab-case", + example: "'NuShell' | str kebab-case", + result: Some(Value::String { + val: "nu-shell".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "convert a string to kebab-case", + example: "'thisIsTheFirstCase' | str kebab-case", + result: Some(Value::String { + val: "this-is-the-first-case".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "convert a string to kebab-case", + example: "'THIS_IS_THE_SECOND_CASE' | str kebab-case", + result: Some(Value::String { + val: "this-is-the-second-case".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "convert a column from a table to kebab-case", + example: r#"[[lang, gems]; [nuTest, 100]] | str kebab-case lang"#, + result: Some(Value::List { + vals: vec![Value::Record { + span: Span::test_data(), + cols: vec!["lang".to_string(), "gems".to_string()], + vals: vec![ + Value::String { + val: "nu-test".to_string(), + span: Span::test_data(), + }, + Value::test_int(100), + ], + }], + span: Span::test_data(), + }), + }, + ] + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/strings/str_/case/mod.rs b/crates/nu-command/src/strings/str_/case/mod.rs new file mode 100644 index 0000000000..9fb318b91c --- /dev/null +++ b/crates/nu-command/src/strings/str_/case/mod.rs @@ -0,0 +1,75 @@ +pub mod camel_case; +pub mod kebab_case; +pub mod pascal_case; +pub mod screaming_snake_case; +pub mod snake_case; +pub mod str_; + +pub use camel_case::SubCommand as StrCamelCase; +pub use kebab_case::SubCommand as StrKebabCase; +pub use pascal_case::SubCommand as StrPascalCase; +pub use screaming_snake_case::SubCommand as StrScreamingSnakeCase; +pub use snake_case::SubCommand as StrSnakeCase; +pub use str_::Str; + +use nu_engine::CallExt; + +use nu_protocol::ast::{Call, CellPath}; +use nu_protocol::engine::{EngineState, Stack}; +use nu_protocol::{PipelineData, ShellError, Span, Value}; + +pub fn operate( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + case_operation: &'static F, +) -> Result +where + F: Fn(&str) -> String + Send + Sync + 'static, +{ + let head = call.head; + let column_paths: Vec = call.rest(engine_state, stack, 0)?; + + input.map( + move |v| { + if column_paths.is_empty() { + action(&v, case_operation, head) + } else { + let mut ret = v; + for path in &column_paths { + let r = ret.update_cell_path( + &path.members, + Box::new(move |old| action(old, case_operation, head)), + ); + if let Err(error) = r { + return Value::Error { error }; + } + } + ret + } + }, + engine_state.ctrlc.clone(), + ) +} + +pub fn action(input: &Value, case_operation: &F, head: Span) -> Value +where + F: Fn(&str) -> String + Send + Sync + 'static, +{ + match input { + Value::String { val, .. } => Value::String { + val: case_operation(val), + span: head, + }, + other => Value::Error { + error: ShellError::UnsupportedInput( + format!( + "Input's type is {}. This command only works with strings.", + other.get_type() + ), + head, + ), + }, + } +} diff --git a/crates/nu-command/src/strings/str_/case/pascal_case.rs b/crates/nu-command/src/strings/str_/case/pascal_case.rs new file mode 100644 index 0000000000..2f9729c5ab --- /dev/null +++ b/crates/nu-command/src/strings/str_/case/pascal_case.rs @@ -0,0 +1,100 @@ +use inflector::cases::pascalcase::to_pascal_case; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +use crate::operate; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "str pascal-case" + } + + fn signature(&self) -> Signature { + Signature::build("str pascal-case") + .rest( + "rest", + SyntaxShape::CellPath, + "optionally convert text to PascalCase by column paths", + ) + .category(Category::Strings) + } + + fn usage(&self) -> &str { + "converts a string to PascalCase" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input, &to_pascal_case) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "convert a string to PascalCase", + example: "'nu-shell' | str pascal-case", + result: Some(Value::String { + val: "NuShell".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "convert a string to PascalCase", + example: "'this-is-the-first-case' | str pascal-case", + result: Some(Value::String { + val: "ThisIsTheFirstCase".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "convert a string to PascalCase", + example: "'this_is_the_second_case' | str pascal-case", + result: Some(Value::String { + val: "ThisIsTheSecondCase".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "convert a column from a table to PascalCase", + example: r#"[[lang, gems]; [nu_test, 100]] | str pascal-case lang"#, + result: Some(Value::List { + vals: vec![Value::Record { + span: Span::test_data(), + cols: vec!["lang".to_string(), "gems".to_string()], + vals: vec![ + Value::String { + val: "NuTest".to_string(), + span: Span::test_data(), + }, + Value::test_int(100), + ], + }], + span: Span::test_data(), + }), + }, + ] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/strings/str_/case/screaming_snake_case.rs b/crates/nu-command/src/strings/str_/case/screaming_snake_case.rs new file mode 100644 index 0000000000..8083440f74 --- /dev/null +++ b/crates/nu-command/src/strings/str_/case/screaming_snake_case.rs @@ -0,0 +1,99 @@ +use inflector::cases::screamingsnakecase::to_screaming_snake_case; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +use crate::operate; +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "str screaming-snake-case" + } + + fn signature(&self) -> Signature { + Signature::build("str screaming-snake-case") + .rest( + "rest", + SyntaxShape::CellPath, + "optionally convert text to SCREAMING_SNAKE_CASE by column paths", + ) + .category(Category::Strings) + } + + fn usage(&self) -> &str { + "converts a string to SCREAMING_SNAKE_CASE" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input, &to_screaming_snake_case) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "convert a string to camelCase", + example: r#" "NuShell" | str screaming-snake-case"#, + result: Some(Value::String { + val: "NU_SHELL".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "convert a string to camelCase", + example: r#" "this_is_the_second_case" | str screaming-snake-case"#, + result: Some(Value::String { + val: "THIS_IS_THE_SECOND_CASE".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "convert a string to camelCase", + example: r#""this-is-the-first-case" | str screaming-snake-case"#, + result: Some(Value::String { + val: "THIS_IS_THE_FIRST_CASE".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "convert a column from a table to SCREAMING_SNAKE_CASE", + example: r#"[[lang, gems]; [nu_test, 100]] | str screaming-snake-case lang"#, + result: Some(Value::List { + vals: vec![Value::Record { + span: Span::test_data(), + cols: vec!["lang".to_string(), "gems".to_string()], + vals: vec![ + Value::String { + val: "NU_TEST".to_string(), + span: Span::test_data(), + }, + Value::test_int(100), + ], + }], + span: Span::test_data(), + }), + }, + ] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/strings/str_/case/snake_case.rs b/crates/nu-command/src/strings/str_/case/snake_case.rs new file mode 100644 index 0000000000..5dda6e799d --- /dev/null +++ b/crates/nu-command/src/strings/str_/case/snake_case.rs @@ -0,0 +1,98 @@ +use inflector::cases::snakecase::to_snake_case; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +use crate::operate; +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "str snake-case" + } + + fn signature(&self) -> Signature { + Signature::build("str snake-case") + .rest( + "rest", + SyntaxShape::CellPath, + "optionally convert text to snake_case by column paths", + ) + .category(Category::Strings) + } + fn usage(&self) -> &str { + "converts a string to snake_case" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input, &to_snake_case) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "convert a string to camelCase", + example: r#" "NuShell" | str snake-case"#, + result: Some(Value::String { + val: "nu_shell".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "convert a string to camelCase", + example: r#" "this_is_the_second_case" | str snake-case"#, + result: Some(Value::String { + val: "this_is_the_second_case".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "convert a string to camelCase", + example: r#""this-is-the-first-case" | str snake-case"#, + result: Some(Value::String { + val: "this_is_the_first_case".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "convert a column from a table to snake-case", + example: r#"[[lang, gems]; [nuTest, 100]] | str snake-case lang"#, + result: Some(Value::List { + vals: vec![Value::Record { + span: Span::test_data(), + cols: vec!["lang".to_string(), "gems".to_string()], + vals: vec![ + Value::String { + val: "nu_test".to_string(), + span: Span::test_data(), + }, + Value::test_int(100), + ], + }], + span: Span::test_data(), + }), + }, + ] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/strings/str_/case/str_.rs b/crates/nu-command/src/strings/str_/case/str_.rs new file mode 100644 index 0000000000..153f580928 --- /dev/null +++ b/crates/nu-command/src/strings/str_/case/str_.rs @@ -0,0 +1,49 @@ +use nu_engine::get_full_help; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, IntoPipelineData, PipelineData, Signature, Value, +}; + +#[derive(Clone)] +pub struct Str; + +impl Command for Str { + fn name(&self) -> &str { + "str" + } + + fn signature(&self) -> Signature { + Signature::build("str").category(Category::Strings) + } + + fn usage(&self) -> &str { + "Various commands for working with string data." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(Value::String { + val: get_full_help(&Str.signature(), &Str.examples(), engine_state, stack), + span: call.head, + } + .into_pipeline_data()) + } +} + +#[cfg(test)] +mod test { + use crate::Str; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Str {}) + } +} diff --git a/crates/nu-command/src/strings/str_/collect.rs b/crates/nu-command/src/strings/str_/collect.rs new file mode 100644 index 0000000000..3881ca54cd --- /dev/null +++ b/crates/nu-command/src/strings/str_/collect.rs @@ -0,0 +1,102 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, SyntaxShape, + Value, +}; + +#[derive(Clone)] +pub struct StrCollect; + +impl Command for StrCollect { + fn name(&self) -> &str { + "str collect" + } + + fn signature(&self) -> Signature { + Signature::build("str collect") + .optional( + "separator", + SyntaxShape::String, + "optional separator to use when creating string", + ) + .category(Category::Strings) + } + + fn usage(&self) -> &str { + "creates a string from the input, optionally using a separator" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let separator: Option = call.opt(engine_state, stack, 0)?; + + let config = stack.get_config().unwrap_or_default(); + + // let output = input.collect_string(&separator.unwrap_or_default(), &config)?; + // Hmm, not sure what we actually want. If you don't use debug_string, Date comes out as human readable + // which feels funny + let mut strings: Vec = vec![]; + + for value in input { + match value { + Value::Error { error } => { + return Err(error); + } + value => { + strings.push(value.debug_string("\n", &config)); + } + } + } + + let output = if let Some(separator) = separator { + strings.join(&separator) + } else { + strings.join("") + }; + + Ok(Value::String { + val: output, + span: call.head, + } + .into_pipeline_data()) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Create a string from input", + example: "['nu', 'shell'] | str collect", + result: Some(Value::String { + val: "nushell".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "Create a string from input with a separator", + example: "['nu', 'shell'] | str collect '-'", + result: Some(Value::String { + val: "nu-shell".to_string(), + span: Span::test_data(), + }), + }, + ] + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(StrCollect {}) + } +} diff --git a/crates/nu-command/src/strings/str_/contains.rs b/crates/nu-command/src/strings/str_/contains.rs new file mode 100644 index 0000000000..eb95733eab --- /dev/null +++ b/crates/nu-command/src/strings/str_/contains.rs @@ -0,0 +1,196 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::ast::CellPath; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::Category; +use nu_protocol::{ + Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "str contains" + } + + fn signature(&self) -> Signature { + Signature::build("str contains") + .required("pattern", SyntaxShape::String, "the pattern to find") + .rest( + "rest", + SyntaxShape::CellPath, + "optionally check if string contains pattern by column paths", + ) + .switch("insensitive", "search is case insensitive", Some('i')) + .category(Category::Strings) + } + + fn usage(&self) -> &str { + "Checks if string contains pattern" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Check if string contains pattern", + example: "'my_library.rb' | str contains '.rb'", + result: Some(Value::Bool { + val: true, + span: Span::test_data(), + }), + }, + Example { + description: "Check if string contains pattern case insensitive", + example: "'my_library.rb' | str contains -i '.RB'", + result: Some(Value::Bool { + val: true, + span: Span::test_data(), + }), + }, + Example { + description: "Check if string contains pattern in a table", + example: " [[ColA ColB]; [test 100]] | str contains 'e' ColA", + result: Some(Value::List { + vals: vec![Value::Record { + cols: vec!["ColA".to_string(), "ColB".to_string()], + vals: vec![ + Value::Bool { + val: true, + span: Span::test_data(), + }, + Value::test_int(100), + ], + span: Span::test_data(), + }], + span: Span::test_data(), + }), + }, + Example { + description: "Check if string contains pattern in a table", + example: " [[ColA ColB]; [test 100]] | str contains -i 'E' ColA", + result: Some(Value::List { + vals: vec![Value::Record { + cols: vec!["ColA".to_string(), "ColB".to_string()], + vals: vec![ + Value::Bool { + val: true, + span: Span::test_data(), + }, + Value::test_int(100), + ], + span: Span::test_data(), + }], + span: Span::test_data(), + }), + }, + Example { + description: "Check if string contains pattern in a table", + example: " [[ColA ColB]; [test hello]] | str contains 'e' ColA ColB", + result: Some(Value::List { + vals: vec![Value::Record { + cols: vec!["ColA".to_string(), "ColB".to_string()], + vals: vec![ + Value::Bool { + val: true, + span: Span::test_data(), + }, + Value::Bool { + val: true, + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }], + span: Span::test_data(), + }), + }, + Example { + description: "Check if string contains pattern", + example: "'hello' | str contains 'banana'", + result: Some(Value::Bool { + val: false, + span: Span::test_data(), + }), + }, + ] + } +} + +fn operate( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let head = call.head; + let pattern: Spanned = call.req(engine_state, stack, 0)?; + let column_paths: Vec = call.rest(engine_state, stack, 1)?; + let case_insensitive = call.has_flag("insensitive"); + + input.map( + move |v| { + if column_paths.is_empty() { + action(&v, case_insensitive, &pattern.item, head) + } else { + let mut ret = v; + for path in &column_paths { + let p = pattern.item.clone(); + let r = ret.update_cell_path( + &path.members, + Box::new(move |old| action(old, case_insensitive, &p, head)), + ); + if let Err(error) = r { + return Value::Error { error }; + } + } + ret + } + }, + engine_state.ctrlc.clone(), + ) +} + +fn action(input: &Value, case_insensitive: bool, pattern: &str, head: Span) -> Value { + match input { + Value::String { val, .. } => Value::Bool { + val: match case_insensitive { + true => val.to_lowercase().contains(pattern.to_lowercase().as_str()), + false => val.contains(pattern), + }, + span: head, + }, + other => Value::Error { + error: ShellError::UnsupportedInput( + format!( + "Input's type is {}. This command only works with strings.", + other.get_type() + ), + head, + ), + }, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/strings/str_/downcase.rs b/crates/nu-command/src/strings/str_/downcase.rs new file mode 100644 index 0000000000..d048d8c814 --- /dev/null +++ b/crates/nu-command/src/strings/str_/downcase.rs @@ -0,0 +1,159 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::ast::CellPath; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::Category; +use nu_protocol::{Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "str downcase" + } + + fn signature(&self) -> Signature { + Signature::build("str downcase") + .rest( + "rest", + SyntaxShape::CellPath, + "optionally downcase text by column paths", + ) + .category(Category::Strings) + } + + fn usage(&self) -> &str { + "downcases text" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Downcase contents", + example: "'NU' | str downcase", + result: Some(Value::String { + val: "nu".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "Downcase contents", + example: "'TESTa' | str downcase", + result: Some(Value::String { + val: "testa".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "Downcase contents", + example: "[[ColA ColB]; [Test ABC]] | str downcase ColA", + result: Some(Value::List { + vals: vec![Value::Record { + cols: vec!["ColA".to_string(), "ColB".to_string()], + vals: vec![ + Value::String { + val: "test".to_string(), + span: Span::test_data(), + }, + Value::String { + val: "ABC".to_string(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }], + span: Span::test_data(), + }), + }, + Example { + description: "Downcase contents", + example: "[[ColA ColB]; [Test ABC]] | str downcase ColA ColB", + result: Some(Value::List { + vals: vec![Value::Record { + cols: vec!["ColA".to_string(), "ColB".to_string()], + vals: vec![ + Value::String { + val: "test".to_string(), + span: Span::test_data(), + }, + Value::String { + val: "abc".to_string(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }], + span: Span::test_data(), + }), + }, + ] + } +} + +fn operate( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let head = call.head; + let column_paths: Vec = call.rest(engine_state, stack, 0)?; + input.map( + move |v| { + if column_paths.is_empty() { + action(&v, head) + } else { + let mut ret = v; + for path in &column_paths { + let r = + ret.update_cell_path(&path.members, Box::new(move |old| action(old, head))); + if let Err(error) = r { + return Value::Error { error }; + } + } + ret + } + }, + engine_state.ctrlc.clone(), + ) +} + +fn action(input: &Value, head: Span) -> Value { + match input { + Value::String { val, .. } => Value::String { + val: val.to_ascii_lowercase(), + span: head, + }, + other => Value::Error { + error: ShellError::UnsupportedInput( + format!( + "Input's type is {}. This command only works with strings.", + other.get_type() + ), + head, + ), + }, + } +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/strings/str_/ends_with.rs b/crates/nu-command/src/strings/str_/ends_with.rs new file mode 100644 index 0000000000..342f177f95 --- /dev/null +++ b/crates/nu-command/src/strings/str_/ends_with.rs @@ -0,0 +1,125 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::ast::CellPath; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::Category; +use nu_protocol::Spanned; +use nu_protocol::{Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "str ends-with" + } + + fn signature(&self) -> Signature { + Signature::build("str ends-with") + .required("pattern", SyntaxShape::String, "the pattern to match") + .rest( + "rest", + SyntaxShape::CellPath, + "optionally matches suffix of text by column paths", + ) + .category(Category::Strings) + } + + fn usage(&self) -> &str { + "checks if string ends with pattern" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Checks if string ends with '.rb' pattern", + example: "'my_library.rb' | str ends-with '.rb'", + result: Some(Value::Bool { + val: true, + span: Span::test_data(), + }), + }, + Example { + description: "Checks if string ends with '.txt' pattern", + example: "'my_library.rb' | str ends-with '.txt'", + result: Some(Value::Bool { + val: false, + span: Span::test_data(), + }), + }, + ] + } +} + +fn operate( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let head = call.head; + let pattern: Spanned = call.req(engine_state, stack, 0)?; + let column_paths: Vec = call.rest(engine_state, stack, 1)?; + + input.map( + move |v| { + if column_paths.is_empty() { + action(&v, &pattern.item, head) + } else { + let mut ret = v; + for path in &column_paths { + let p = pattern.item.clone(); + let r = ret.update_cell_path( + &path.members, + Box::new(move |old| action(old, &p, head)), + ); + if let Err(error) = r { + return Value::Error { error }; + } + } + ret + } + }, + engine_state.ctrlc.clone(), + ) +} + +fn action(input: &Value, pattern: &str, head: Span) -> Value { + match input { + Value::String { val, .. } => Value::Bool { + val: val.ends_with(pattern), + span: head, + }, + other => Value::Error { + error: ShellError::UnsupportedInput( + format!( + "Input's type is {}. This command only works with strings.", + other.get_type() + ), + head, + ), + }, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/strings/str_/find_replace.rs b/crates/nu-command/src/strings/str_/find_replace.rs new file mode 100644 index 0000000000..2d0ac02c01 --- /dev/null +++ b/crates/nu-command/src/strings/str_/find_replace.rs @@ -0,0 +1,221 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::ast::CellPath; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::Category; +use nu_protocol::Spanned; +use nu_protocol::{Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value}; +use regex::Regex; +use std::sync::Arc; + +struct Arguments { + all: bool, + find: String, + replace: String, + column_paths: Vec, +} + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "str find-replace" + } + + fn signature(&self) -> Signature { + Signature::build("str find-replace") + .required("find", SyntaxShape::String, "the pattern to find") + .required("replace", SyntaxShape::String, "the replacement pattern") + .rest( + "rest", + SyntaxShape::CellPath, + "optionally find and replace text by column paths", + ) + .switch("all", "replace all occurrences of find string", Some('a')) + .category(Category::Strings) + } + + fn usage(&self) -> &str { + "finds and replaces text" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Find and replace contents with capture group", + example: "'my_library.rb' | str find-replace '(.+).rb' '$1.nu'", + result: Some(Value::String { + val: "my_library.nu".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "Find and replace all occurrences of find string", + example: "'abc abc abc' | str find-replace -a 'b' 'z'", + result: Some(Value::String { + val: "azc azc azc".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "Find and replace all occurrences of find string in table", + example: + "[[ColA ColB ColC]; [abc abc ads]] | str find-replace -a 'b' 'z' ColA ColC", + result: Some(Value::List { + vals: vec![Value::Record { + cols: vec!["ColA".to_string(), "ColB".to_string(), "ColC".to_string()], + vals: vec![ + Value::String { + val: "azc".to_string(), + span: Span::test_data(), + }, + Value::String { + val: "abc".to_string(), + span: Span::test_data(), + }, + Value::String { + val: "ads".to_string(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }], + span: Span::test_data(), + }), + }, + ] + } +} + +fn operate( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let find: Spanned = call.req(engine_state, stack, 0)?; + let replace: Spanned = call.req(engine_state, stack, 1)?; + + let options = Arc::new(Arguments { + all: call.has_flag("all"), + find: find.item, + replace: replace.item, + column_paths: call.rest(engine_state, stack, 2)?, + }); + let head = call.head; + input.map( + move |v| { + if options.column_paths.is_empty() { + action(&v, &options, head) + } else { + let mut ret = v; + for path in &options.column_paths { + let opt = options.clone(); + let r = ret.update_cell_path( + &path.members, + Box::new(move |old| action(old, &opt, head)), + ); + if let Err(error) = r { + return Value::Error { error }; + } + } + ret + } + }, + engine_state.ctrlc.clone(), + ) +} + +struct FindReplace<'a>(&'a str, &'a str); + +fn action( + input: &Value, + Arguments { + find, replace, all, .. + }: &Arguments, + head: Span, +) -> Value { + match input { + Value::String { val, .. } => { + let FindReplace(find, replacement) = FindReplace(find, replace); + let regex = Regex::new(find); + + match regex { + Ok(re) => { + if *all { + Value::String { + val: re.replace_all(val, replacement).to_string(), + span: head, + } + } else { + Value::String { + val: re.replace(val, replacement).to_string(), + span: head, + } + } + } + Err(_) => Value::String { + val: val.to_string(), + span: head, + }, + } + } + other => Value::Error { + error: ShellError::UnsupportedInput( + format!( + "Input's type is {}. This command only works with strings.", + other.get_type() + ), + head, + ), + }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use super::{action, Arguments, SubCommand}; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } + + #[test] + fn can_have_capture_groups() { + let word = Value::String { + val: "Cargo.toml".to_string(), + span: Span::test_data(), + }; + + let options = Arguments { + find: String::from("Cargo.(.+)"), + replace: String::from("Carga.$1"), + column_paths: vec![], + all: false, + }; + + let actual = action(&word, &options, Span::test_data()); + assert_eq!( + actual, + Value::String { + val: "Carga.toml".to_string(), + span: Span::test_data() + } + ); + } +} diff --git a/crates/nu-command/src/strings/str_/index_of.rs b/crates/nu-command/src/strings/str_/index_of.rs new file mode 100644 index 0000000000..26c46648d4 --- /dev/null +++ b/crates/nu-command/src/strings/str_/index_of.rs @@ -0,0 +1,415 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::ast::CellPath; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::Category; +use nu_protocol::Spanned; +use nu_protocol::{Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value}; +use std::sync::Arc; + +struct Arguments { + end: bool, + pattern: String, + range: Option, + column_paths: Vec, +} + +#[derive(Clone)] +pub struct SubCommand; + +#[derive(Clone)] +pub struct IndexOfOptionalBounds(i32, i32); + +impl Command for SubCommand { + fn name(&self) -> &str { + "str index-of" + } + + fn signature(&self) -> Signature { + Signature::build("str index-of") + .required( + "pattern", + SyntaxShape::String, + "the pattern to find index of", + ) + .rest( + "rest", + SyntaxShape::CellPath, + "optionally returns index of pattern in string by column paths", + ) + .named( + "range", + SyntaxShape::Any, + "optional start and/or end index", + Some('r'), + ) + .switch("end", "search from the end of the string", Some('e')) + .category(Category::Strings) + } + + fn usage(&self) -> &str { + "Returns starting index of given pattern in string counting from 0. Returns -1 when there are no results." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Returns index of pattern in string", + example: " 'my_library.rb' | str index-of '.rb'", + result: Some(Value::test_int(10)), + }, + Example { + description: "Returns index of pattern in string with start index", + example: " '.rb.rb' | str index-of '.rb' -r '1,'", + result: Some(Value::test_int(3)), + }, + Example { + description: "Returns index of pattern in string with end index", + example: " '123456' | str index-of '6' -r ',4'", + result: Some(Value::test_int(-1)), + }, + Example { + description: "Returns index of pattern in string with start and end index", + example: " '123456' | str index-of '3' -r '1,4'", + result: Some(Value::test_int(2)), + }, + Example { + description: "Alternatively you can use this form", + example: " '123456' | str index-of '3' -r [1 4]", + result: Some(Value::test_int(2)), + }, + Example { + description: "Returns index of pattern in string", + example: " '/this/is/some/path/file.txt' | str index-of '/' -e", + result: Some(Value::test_int(18)), + }, + ] + } +} + +fn operate( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let pattern: Spanned = call.req(engine_state, stack, 0)?; + + let options = Arc::new(Arguments { + pattern: pattern.item, + range: call.get_flag(engine_state, stack, "range")?, + end: call.has_flag("end"), + column_paths: call.rest(engine_state, stack, 1)?, + }); + let head = call.head; + input.map( + move |v| { + if options.column_paths.is_empty() { + action(&v, &options, head) + } else { + let mut ret = v; + for path in &options.column_paths { + let opt = options.clone(); + let r = ret.update_cell_path( + &path.members, + Box::new(move |old| action(old, &opt, head)), + ); + if let Err(error) = r { + return Value::Error { error }; + } + } + ret + } + }, + engine_state.ctrlc.clone(), + ) +} + +fn action( + input: &Value, + Arguments { + ref pattern, + range, + end, + .. + }: &Arguments, + head: Span, +) -> Value { + let range = match range { + Some(range) => range.clone(), + None => Value::String { + val: "".to_string(), + span: head, + }, + }; + + let r = process_range(input, &range, head); + + match input { + Value::String { val: s, .. } => { + let (start_index, end_index) = match r { + Ok(r) => (r.0 as usize, r.1 as usize), + Err(e) => return Value::Error { error: e }, + }; + + if *end { + if let Some(result) = s[start_index..end_index].rfind(&**pattern) { + Value::Int { + val: result as i64 + start_index as i64, + span: head, + } + } else { + Value::Int { + val: -1, + span: head, + } + } + } else if let Some(result) = s[start_index..end_index].find(&**pattern) { + Value::Int { + val: result as i64 + start_index as i64, + span: head, + } + } else { + Value::Int { + val: -1, + span: head, + } + } + } + other => Value::Error { + error: ShellError::UnsupportedInput( + format!( + "Input's type is {}. This command only works with strings.", + other.get_type() + ), + head, + ), + }, + } +} + +fn process_range( + input: &Value, + range: &Value, + head: Span, +) -> Result { + let input_len = match input { + Value::String { val: s, .. } => s.len(), + _ => 0, + }; + let min_index_str = String::from("0"); + let max_index_str = input_len.to_string(); + let r = match range { + Value::String { val: s, .. } => { + let indexes: Vec<&str> = s.split(',').collect(); + + let start_index = indexes.get(0).unwrap_or(&&min_index_str[..]).to_string(); + + let end_index = indexes.get(1).unwrap_or(&&max_index_str[..]).to_string(); + + Ok((start_index, end_index)) + } + Value::List { vals, .. } => { + if vals.len() > 2 { + Err(ShellError::UnsupportedInput( + String::from("there shouldn't be more than two indexes. too many indexes"), + head, + )) + } else { + let idx: Vec = vals + .iter() + .map(|v| v.as_string().unwrap_or_else(|_| String::from(""))) + .collect(); + + let start_index = idx.get(0).unwrap_or(&min_index_str).to_string(); + let end_index = idx.get(1).unwrap_or(&max_index_str).to_string(); + + Ok((start_index, end_index)) + } + } + other => Err(ShellError::UnsupportedInput( + format!( + "Input's type is {}. This command only works with strings.", + other.get_type() + ), + head, + )), + }?; + + let start_index = r.0.parse::().unwrap_or(0); + let end_index = r.1.parse::().unwrap_or(input_len as i32); + + if start_index < 0 || start_index > end_index { + return Err(ShellError::UnsupportedInput( + String::from( + "start index can't be negative or greater than end index. Invalid start index", + ), + head, + )); + } + + if end_index < 0 || end_index < start_index || end_index > input_len as i32 { + return Err(ShellError::UnsupportedInput( + String::from( + "end index can't be negative, smaller than start index or greater than input length. Invalid end index"), + head, + )); + } + Ok(IndexOfOptionalBounds(start_index, end_index)) +} + +#[cfg(test)] +mod tests { + use super::*; + use super::{action, Arguments, SubCommand}; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } + + #[test] + fn returns_index_of_substring() { + let word = Value::String { + val: String::from("Cargo.tomL"), + span: Span::test_data(), + }; + + let options = Arguments { + pattern: String::from(".tomL"), + + range: Some(Value::String { + val: String::from(""), + span: Span::test_data(), + }), + column_paths: vec![], + end: false, + }; + + let actual = action(&word, &options, Span::test_data()); + + assert_eq!(actual, Value::test_int(5)); + } + #[test] + fn index_of_does_not_exist_in_string() { + let word = Value::String { + val: String::from("Cargo.tomL"), + span: Span::test_data(), + }; + + let options = Arguments { + pattern: String::from("Lm"), + + range: Some(Value::String { + val: String::from(""), + span: Span::test_data(), + }), + column_paths: vec![], + end: false, + }; + + let actual = action(&word, &options, Span::test_data()); + + assert_eq!(actual, Value::test_int(-1)); + } + + #[test] + fn returns_index_of_next_substring() { + let word = Value::String { + val: String::from("Cargo.Cargo"), + span: Span::test_data(), + }; + + let options = Arguments { + pattern: String::from("Cargo"), + + range: Some(Value::String { + val: String::from("1"), + span: Span::test_data(), + }), + column_paths: vec![], + end: false, + }; + + let actual = action(&word, &options, Span::test_data()); + assert_eq!(actual, Value::test_int(6)); + } + + #[test] + fn index_does_not_exist_due_to_end_index() { + let word = Value::String { + val: String::from("Cargo.Banana"), + span: Span::test_data(), + }; + + let options = Arguments { + pattern: String::from("Banana"), + + range: Some(Value::String { + val: String::from(",5"), + span: Span::test_data(), + }), + column_paths: vec![], + end: false, + }; + + let actual = action(&word, &options, Span::test_data()); + assert_eq!(actual, Value::test_int(-1)); + } + + #[test] + fn returns_index_of_nums_in_middle_due_to_index_limit_from_both_ends() { + let word = Value::String { + val: String::from("123123123"), + span: Span::test_data(), + }; + + let options = Arguments { + pattern: String::from("123"), + + range: Some(Value::String { + val: String::from("2,6"), + span: Span::test_data(), + }), + column_paths: vec![], + end: false, + }; + + let actual = action(&word, &options, Span::test_data()); + assert_eq!(actual, Value::test_int(3)); + } + + #[test] + fn index_does_not_exists_due_to_strict_bounds() { + let word = Value::String { + val: String::from("123456"), + span: Span::test_data(), + }; + + let options = Arguments { + pattern: String::from("1"), + + range: Some(Value::String { + val: String::from("2,4"), + span: Span::test_data(), + }), + column_paths: vec![], + end: false, + }; + + let actual = action(&word, &options, Span::test_data()); + assert_eq!(actual, Value::test_int(-1)); + } +} diff --git a/crates/nu-command/src/strings/str_/length.rs b/crates/nu-command/src/strings/str_/length.rs new file mode 100644 index 0000000000..b318f2b185 --- /dev/null +++ b/crates/nu-command/src/strings/str_/length.rs @@ -0,0 +1,115 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::ast::CellPath; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::Category; +use nu_protocol::{Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "str length" + } + + fn signature(&self) -> Signature { + Signature::build("str length") + .rest( + "rest", + SyntaxShape::CellPath, + "optionally find length of text by column paths", + ) + .category(Category::Strings) + } + + fn usage(&self) -> &str { + "outputs the lengths of the strings in the pipeline" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Return the lengths of multiple strings", + example: "'hello' | str length", + result: Some(Value::test_int(5)), + }, + Example { + description: "Return the lengths of multiple strings", + example: "['hi' 'there'] | str length", + result: Some(Value::List { + vals: vec![Value::test_int(2), Value::test_int(5)], + span: Span::test_data(), + }), + }, + ] + } +} + +fn operate( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let head = call.head; + let column_paths: Vec = call.rest(engine_state, stack, 0)?; + input.map( + move |v| { + if column_paths.is_empty() { + action(&v, head) + } else { + let mut ret = v; + for path in &column_paths { + let r = + ret.update_cell_path(&path.members, Box::new(move |old| action(old, head))); + if let Err(error) = r { + return Value::Error { error }; + } + } + ret + } + }, + engine_state.ctrlc.clone(), + ) +} + +fn action(input: &Value, head: Span) -> Value { + match input { + Value::String { val, .. } => Value::Int { + val: val.len() as i64, + span: head, + }, + other => Value::Error { + error: ShellError::UnsupportedInput( + format!( + "Input's type is {}. This command only works with strings.", + other.get_type() + ), + head, + ), + }, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/strings/str_/lpad.rs b/crates/nu-command/src/strings/str_/lpad.rs new file mode 100644 index 0000000000..8c2bd4fa69 --- /dev/null +++ b/crates/nu-command/src/strings/str_/lpad.rs @@ -0,0 +1,183 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::ast::CellPath; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::Category; +use nu_protocol::{Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value}; +use std::sync::Arc; + +struct Arguments { + length: Option, + character: Option, + column_paths: Vec, +} + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "str lpad" + } + + fn signature(&self) -> Signature { + Signature::build("str lpad") + .required_named("length", SyntaxShape::Int, "length to pad to", Some('l')) + .required_named( + "character", + SyntaxShape::String, + "character to pad with", + Some('c'), + ) + .rest( + "rest", + SyntaxShape::CellPath, + "optionally check if string contains pattern by column paths", + ) + .category(Category::Strings) + } + + fn usage(&self) -> &str { + "pad a string with a character a certain length" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Left pad a string with a character a number of places", + example: "'nushell' | str lpad -l 10 -c '*'", + result: Some(Value::String { + val: "***nushell".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "Left pad a string with a character a number of places", + example: "'123' | str lpad -l 10 -c '0'", + result: Some(Value::String { + val: "0000000123".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "Use lpad to truncate a string", + example: "'123456789' | str lpad -l 3 -c '0'", + result: Some(Value::String { + val: "123".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "Use lpad to pad Unicode", + example: "'▉' | str lpad -l 10 -c '▉'", + result: Some(Value::String { + val: "▉▉▉▉▉▉▉▉▉▉".to_string(), + span: Span::test_data(), + }), + }, + ] + } +} + +fn operate( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let options = Arc::new(Arguments { + length: call.get_flag(engine_state, stack, "length")?, + character: call.get_flag(engine_state, stack, "character")?, + column_paths: call.rest(engine_state, stack, 0)?, + }); + + let head = call.head; + input.map( + move |v| { + if options.column_paths.is_empty() { + action(&v, &options, head) + } else { + let mut ret = v; + for path in &options.column_paths { + let opt = options.clone(); + let r = ret.update_cell_path( + &path.members, + Box::new(move |old| action(old, &opt, head)), + ); + if let Err(error) = r { + return Value::Error { error }; + } + } + ret + } + }, + engine_state.ctrlc.clone(), + ) +} + +fn action( + input: &Value, + Arguments { + character, length, .. + }: &Arguments, + head: Span, +) -> Value { + match &input { + Value::String { val, .. } => match length { + Some(x) => { + let s = *x as usize; + if s < val.len() { + Value::String { + val: val.chars().take(s).collect::(), + span: head, + } + } else { + let c = character.as_ref().expect("we already know this flag needs to exist because the command is type checked before we call the action function"); + let mut res = c.repeat(s - val.chars().count()); + res += val; + Value::String { + val: res, + span: head, + } + } + } + None => Value::Error { + error: ShellError::UnsupportedInput( + String::from("Length argument is missing"), + head, + ), + }, + }, + other => Value::Error { + error: ShellError::UnsupportedInput( + format!( + "Input's type is {}. This command only works with strings.", + other.get_type() + ), + head, + ), + }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/strings/str_/mod.rs b/crates/nu-command/src/strings/str_/mod.rs new file mode 100644 index 0000000000..1d1d39911b --- /dev/null +++ b/crates/nu-command/src/strings/str_/mod.rs @@ -0,0 +1,33 @@ +mod capitalize; +mod case; +mod collect; +mod contains; +mod downcase; +mod ends_with; +mod find_replace; +mod index_of; +mod length; +mod lpad; +mod reverse; +mod rpad; +mod starts_with; +mod substring; +mod trim; +mod upcase; + +pub use capitalize::SubCommand as StrCapitalize; +pub use case::*; +pub use collect::*; +pub use contains::SubCommand as StrContains; +pub use downcase::SubCommand as StrDowncase; +pub use ends_with::SubCommand as StrEndswith; +pub use find_replace::SubCommand as StrFindReplace; +pub use index_of::SubCommand as StrIndexOf; +pub use length::SubCommand as StrLength; +pub use lpad::SubCommand as StrLpad; +pub use reverse::SubCommand as StrReverse; +pub use rpad::SubCommand as StrRpad; +pub use starts_with::SubCommand as StrStartsWith; +pub use substring::SubCommand as StrSubstring; +pub use trim::Trim as StrTrim; +pub use upcase::SubCommand as StrUpcase; diff --git a/crates/nu-command/src/strings/str_/reverse.rs b/crates/nu-command/src/strings/str_/reverse.rs new file mode 100644 index 0000000000..f89d4a1215 --- /dev/null +++ b/crates/nu-command/src/strings/str_/reverse.rs @@ -0,0 +1,109 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::ast::CellPath; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::Category; +use nu_protocol::{Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "str reverse" + } + + fn signature(&self) -> Signature { + Signature::build("str reverse") + .rest( + "rest", + SyntaxShape::CellPath, + "optionally reverse text by column paths", + ) + .category(Category::Strings) + } + + fn usage(&self) -> &str { + "outputs the reversals of the strings in the pipeline" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Return the reversals of multiple strings", + example: "'Nushell' | str reverse", + result: Some(Value::String { + val: "llehsuN".to_string(), + span: Span::test_data(), + }), + }] + } +} + +fn operate( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let head = call.head; + let column_paths: Vec = call.rest(engine_state, stack, 0)?; + input.map( + move |v| { + if column_paths.is_empty() { + action(&v, head) + } else { + let mut ret = v; + for path in &column_paths { + let r = + ret.update_cell_path(&path.members, Box::new(move |old| action(old, head))); + if let Err(error) = r { + return Value::Error { error }; + } + } + ret + } + }, + engine_state.ctrlc.clone(), + ) +} + +fn action(input: &Value, head: Span) -> Value { + match input { + Value::String { val, .. } => Value::String { + val: val.chars().rev().collect::(), + span: head, + }, + + other => Value::Error { + error: ShellError::UnsupportedInput( + format!( + "Input's type is {}. This command only works with strings.", + other.get_type() + ), + head, + ), + }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/strings/str_/rpad.rs b/crates/nu-command/src/strings/str_/rpad.rs new file mode 100644 index 0000000000..add8be3c70 --- /dev/null +++ b/crates/nu-command/src/strings/str_/rpad.rs @@ -0,0 +1,182 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::ast::CellPath; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::Category; +use nu_protocol::{Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value}; +use std::sync::Arc; + +struct Arguments { + length: Option, + character: Option, + column_paths: Vec, +} + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "str rpad" + } + + fn signature(&self) -> Signature { + Signature::build("str rpad") + .required_named("length", SyntaxShape::Int, "length to pad to", Some('l')) + .required_named( + "character", + SyntaxShape::String, + "character to pad with", + Some('c'), + ) + .rest( + "rest", + SyntaxShape::CellPath, + "optionally check if string contains pattern by column paths", + ) + .category(Category::Strings) + } + + fn usage(&self) -> &str { + "pad a string with a character a certain length" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Right pad a string with a character a number of places", + example: "'nushell' | str rpad -l 10 -c '*'", + result: Some(Value::String { + val: "nushell***".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "Right pad a string with a character a number of places", + example: "'123' | str rpad -l 10 -c '0'", + result: Some(Value::String { + val: "1230000000".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "Use rpad to truncate a string", + example: "'123456789' | str rpad -l 3 -c '0'", + result: Some(Value::String { + val: "123".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "Use rpad to pad Unicode", + example: "'▉' | str rpad -l 10 -c '▉'", + result: Some(Value::String { + val: "▉▉▉▉▉▉▉▉▉▉".to_string(), + span: Span::test_data(), + }), + }, + ] + } +} + +fn operate( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let options = Arc::new(Arguments { + length: call.get_flag(engine_state, stack, "length")?, + character: call.get_flag(engine_state, stack, "character")?, + column_paths: call.rest(engine_state, stack, 0)?, + }); + + let head = call.head; + input.map( + move |v| { + if options.column_paths.is_empty() { + action(&v, &options, head) + } else { + let mut ret = v; + for path in &options.column_paths { + let opt = options.clone(); + let r = ret.update_cell_path( + &path.members, + Box::new(move |old| action(old, &opt, head)), + ); + if let Err(error) = r { + return Value::Error { error }; + } + } + ret + } + }, + engine_state.ctrlc.clone(), + ) +} + +fn action( + input: &Value, + Arguments { + character, length, .. + }: &Arguments, + head: Span, +) -> Value { + match &input { + Value::String { val, .. } => match length { + Some(x) => { + let s = *x as usize; + if s < val.len() { + Value::String { + val: val.chars().take(s).collect::(), + span: head, + } + } else { + let mut res = val.to_string(); + res += &character.as_ref().expect("we already know this flag needs to exist because the command is type checked before we call the action function").repeat(s - val.chars().count()); + Value::String { + val: res, + span: head, + } + } + } + None => Value::Error { + error: ShellError::UnsupportedInput( + String::from("Length argument is missing"), + head, + ), + }, + }, + other => Value::Error { + error: ShellError::UnsupportedInput( + format!( + "Input's type is {}. This command only works with strings.", + other.get_type() + ), + head, + ), + }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/strings/str_/starts_with.rs b/crates/nu-command/src/strings/str_/starts_with.rs new file mode 100644 index 0000000000..645ea4d6c9 --- /dev/null +++ b/crates/nu-command/src/strings/str_/starts_with.rs @@ -0,0 +1,146 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::ast::CellPath; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::Category; +use nu_protocol::Spanned; +use nu_protocol::{Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value}; +use std::sync::Arc; + +struct Arguments { + pattern: String, + column_paths: Vec, +} + +#[derive(Clone)] + +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "str starts-with" + } + + fn signature(&self) -> Signature { + Signature::build("str starts-with") + .required("pattern", SyntaxShape::String, "the pattern to match") + .rest( + "rest", + SyntaxShape::CellPath, + "optionally matches prefix of text by column paths", + ) + .category(Category::Strings) + } + + fn usage(&self) -> &str { + "checks if string starts with pattern" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Checks if string starts with 'my' pattern", + example: "'my_library.rb' | str starts-with 'my'", + result: Some(Value::Bool { + val: true, + span: Span::test_data(), + }), + }, + Example { + description: "Checks if string starts with 'my' pattern", + example: "'Cargo.toml' | str starts-with 'Car'", + result: Some(Value::Bool { + val: true, + span: Span::test_data(), + }), + }, + Example { + description: "Checks if string starts with 'my' pattern", + example: "'Cargo.toml' | str starts-with '.toml'", + result: Some(Value::Bool { + val: false, + span: Span::test_data(), + }), + }, + ] + } +} + +fn operate( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let pattern: Spanned = call.req(engine_state, stack, 0)?; + + let options = Arc::new(Arguments { + pattern: pattern.item, + column_paths: call.rest(engine_state, stack, 1)?, + }); + let head = call.head; + input.map( + move |v| { + if options.column_paths.is_empty() { + action(&v, &options, head) + } else { + let mut ret = v; + for path in &options.column_paths { + let opt = options.clone(); + let r = ret.update_cell_path( + &path.members, + Box::new(move |old| action(old, &opt, head)), + ); + if let Err(error) = r { + return Value::Error { error }; + } + } + ret + } + }, + engine_state.ctrlc.clone(), + ) +} + +fn action(input: &Value, Arguments { pattern, .. }: &Arguments, head: Span) -> Value { + match input { + Value::String { val: s, .. } => { + let starts_with = s.starts_with(pattern); + Value::Bool { + val: starts_with, + span: head, + } + } + other => Value::Error { + error: ShellError::UnsupportedInput( + format!( + "Input's type is {}. This command only works with strings.", + other.get_type() + ), + head, + ), + }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/strings/str_/substring.rs b/crates/nu-command/src/strings/str_/substring.rs new file mode 100644 index 0000000000..ec5df7ef43 --- /dev/null +++ b/crates/nu-command/src/strings/str_/substring.rs @@ -0,0 +1,356 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::ast::CellPath; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value}; +use std::cmp::Ordering; +use std::sync::Arc; + +#[derive(Clone)] +pub struct SubCommand; + +struct Arguments { + range: Value, + column_paths: Vec, +} + +#[derive(Clone)] +struct Substring(isize, isize); + +impl From<(isize, isize)> for Substring { + fn from(input: (isize, isize)) -> Substring { + Substring(input.0, input.1) + } +} + +struct SubstringText(String, String); + +impl Command for SubCommand { + fn name(&self) -> &str { + "str substring" + } + + fn signature(&self) -> Signature { + Signature::build("str substring") + .required( + "range", + SyntaxShape::Any, + "the indexes to substring [start end]", + ) + .rest( + "rest", + SyntaxShape::CellPath, + "optionally substring text by column paths", + ) + } + + fn usage(&self) -> &str { + "substrings text" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Get a substring from the text", + example: " 'good nushell' | str substring [5 12]", + result: Some(Value::test_string("nushell")), + }, + Example { + description: "Alternatively, you can use the form", + example: " 'good nushell' | str substring '5,12'", + result: Some(Value::test_string("nushell")), + }, + Example { + description: "Drop the last `n` characters from the string", + example: " 'good nushell' | str substring ',-5'", + result: Some(Value::test_string("good nu")), + }, + Example { + description: "Get the remaining characters from a starting index", + example: " 'good nushell' | str substring '5,'", + result: Some(Value::test_string("nushell")), + }, + Example { + description: "Get the characters from the beginning until ending index", + example: " 'good nushell' | str substring ',7'", + result: Some(Value::test_string("good nu")), + }, + ] + } +} + +fn operate( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let options = Arc::new(Arguments { + range: call.req(engine_state, stack, 0)?, + column_paths: call.rest(engine_state, stack, 1)?, + }); + + let head = call.head; + let indexes: Arc = Arc::new(process_arguments(&options, head)?.into()); + + input.map( + move |v| { + if options.column_paths.is_empty() { + action(&v, &indexes, head) + } else { + let mut ret = v; + for path in &options.column_paths { + let indexes = indexes.clone(); + let r = ret.update_cell_path( + &path.members, + Box::new(move |old| action(old, &indexes, head)), + ); + if let Err(error) = r { + return Value::Error { error }; + } + } + ret + } + }, + engine_state.ctrlc.clone(), + ) +} + +fn action(input: &Value, options: &Substring, head: Span) -> Value { + match input { + Value::String { val: s, .. } => { + let len: isize = s.len() as isize; + + let start: isize = if options.0 < 0 { + options.0 + len + } else { + options.0 + }; + let end: isize = if options.1 < 0 { + std::cmp::max(len + options.1, 0) + } else { + options.1 + }; + + if start < len && end >= 0 { + match start.cmp(&end) { + Ordering::Equal => Value::String { + val: "".to_string(), + span: head, + }, + Ordering::Greater => Value::Error { + error: ShellError::UnsupportedInput( + "End must be greater than or equal to Start".to_string(), + head, + ), + }, + Ordering::Less => Value::String { + val: { + if end == isize::max_value() { + String::from_utf8_lossy( + &s.bytes().skip(start as usize).collect::>(), + ) + .to_string() + } else { + String::from_utf8_lossy( + &s.bytes() + .skip(start as usize) + .take((end - start) as usize) + .collect::>(), + ) + .to_string() + } + }, + span: head, + }, + } + } else { + Value::String { + val: "".to_string(), + span: head, + } + } + } + other => Value::Error { + error: ShellError::UnsupportedInput( + format!( + "Input's type is {}. This command only works with strings.", + other.get_type() + ), + head, + ), + }, + } +} + +fn process_arguments(options: &Arguments, head: Span) -> Result<(isize, isize), ShellError> { + let search = match &options.range { + Value::List { vals, .. } => { + if vals.len() > 2 { + Err(ShellError::UnsupportedInput( + "More than two indices given".to_string(), + head, + )) + } else { + let idx: Vec = vals + .iter() + .map(|v| { + match v { + Value::Int { val, .. } => Ok(val.to_string()), + Value::String { val, .. } => Ok(val.to_string()), + _ => Err(ShellError::UnsupportedInput( + "could not perform substring. Expecting a string or int" + .to_string(), + head, + )), + } + .unwrap_or_else(|_| String::from("")) + }) + .collect(); + + let start = idx + .get(0) + .ok_or_else(|| { + ShellError::UnsupportedInput( + "could not perform substring".to_string(), + head, + ) + })? + .to_string(); + let end = idx + .get(1) + .ok_or_else(|| { + ShellError::UnsupportedInput( + "could not perform substring".to_string(), + head, + ) + })? + .to_string(); + Ok(SubstringText(start, end)) + } + } + Value::String { val, .. } => { + let idx: Vec<&str> = val.split(',').collect(); + + let start = idx + .get(0) + .ok_or_else(|| { + ShellError::UnsupportedInput("could not perform substring".to_string(), head) + })? + .to_string(); + let end = idx + .get(1) + .ok_or_else(|| { + ShellError::UnsupportedInput("could not perform substring".to_string(), head) + })? + .to_string(); + + Ok(SubstringText(start, end)) + } + _ => Err(ShellError::UnsupportedInput( + "could not perform substring".to_string(), + head, + )), + }?; + let start = match &search { + SubstringText(start, _) if start.is_empty() || start == "_" => 0, + SubstringText(start, _) => start.trim().parse().map_err(|_| { + ShellError::UnsupportedInput("could not perform substring".to_string(), head) + })?, + }; + + let end = match &search { + SubstringText(_, end) if end.is_empty() || end == "_" => isize::max_value(), + SubstringText(_, end) => end.trim().parse().map_err(|_| { + ShellError::UnsupportedInput("could not perform substring".to_string(), head) + })?, + }; + + Ok((start, end)) +} + +#[cfg(test)] +mod tests { + use super::{action, Span, SubCommand, Substring, Value}; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } + struct Expectation<'a> { + options: (isize, isize), + expected: &'a str, + } + + impl Expectation<'_> { + fn options(&self) -> Substring { + Substring(self.options.0, self.options.1) + } + } + + fn expectation(word: &str, indexes: (isize, isize)) -> Expectation { + Expectation { + options: indexes, + expected: word, + } + } + + #[test] + fn substrings_indexes() { + let word = Value::String { + val: "andres".to_string(), + span: Span::test_data(), + }; + + let cases = vec![ + expectation("a", (0, 1)), + expectation("an", (0, 2)), + expectation("and", (0, 3)), + expectation("andr", (0, 4)), + expectation("andre", (0, 5)), + expectation("andres", (0, 6)), + expectation("", (0, -6)), + expectation("a", (0, -5)), + expectation("an", (0, -4)), + expectation("and", (0, -3)), + expectation("andr", (0, -2)), + expectation("andre", (0, -1)), + // str substring [ -4 , _ ] + // str substring -4 , + expectation("dres", (-4, isize::max_value())), + expectation("", (0, -110)), + expectation("", (6, 0)), + expectation("", (6, -1)), + expectation("", (6, -2)), + expectation("", (6, -3)), + expectation("", (6, -4)), + expectation("", (6, -5)), + expectation("", (6, -6)), + ]; + + for expectation in &cases { + let expected = expectation.expected; + let actual = action(&word, &expectation.options(), Span::test_data()); + + assert_eq!( + actual, + Value::String { + val: expected.to_string(), + span: Span::test_data() + } + ); + } + } +} diff --git a/crates/nu-command/src/strings/str_/trim/mod.rs b/crates/nu-command/src/strings/str_/trim/mod.rs new file mode 100644 index 0000000000..33d949ef5c --- /dev/null +++ b/crates/nu-command/src/strings/str_/trim/mod.rs @@ -0,0 +1,2 @@ +mod trim_; +pub use trim_::SubCommand as Trim; diff --git a/crates/nu-command/src/strings/str_/trim/trim_.rs b/crates/nu-command/src/strings/str_/trim/trim_.rs new file mode 100644 index 0000000000..61d7b3c570 --- /dev/null +++ b/crates/nu-command/src/strings/str_/trim/trim_.rs @@ -0,0 +1,1169 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::{Call, CellPath}, + engine::{Command, EngineState, Stack}, + Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct SubCommand; + +struct Arguments { + character: Option>, + column_paths: Vec, +} + +#[derive(Default, Debug, Copy, Clone)] +pub struct ClosureFlags { + all_flag: bool, + left_trim: bool, + right_trim: bool, + format_flag: bool, + both_flag: bool, +} + +impl Command for SubCommand { + fn name(&self) -> &str { + "str trim" + } + + fn signature(&self) -> Signature { + Signature::build("str trim") + .rest( + "rest", + SyntaxShape::CellPath, + "optionally trim text by column paths", + ) + .named( + "char", + SyntaxShape::String, + "character to trim (default: whitespace)", + Some('c'), + ) + .switch( + "left", + "trims characters only from the beginning of the string (default: whitespace)", + Some('l'), + ) + .switch( + "right", + "trims characters only from the end of the string (default: whitespace)", + Some('r'), + ) + .switch( + "all", + "trims all characters from both sides of the string *and* in the middle (default: whitespace)", + Some('a'), + ) + .switch("both", "trims all characters from left and right side of the string (default: whitespace)", Some('b')) + .switch("format", "trims spaces replacing multiple characters with singles in the middle (default: whitespace)", Some('f')) + } + fn usage(&self) -> &str { + "trims text" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input, &trim) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Trim whitespace", + example: "'Nu shell ' | str trim", + result: Some(Value::test_string("Nu shell")), + }, + Example { + description: "Trim a specific character", + example: "'=== Nu shell ===' | str trim -c '=' | str trim", + result: Some(Value::test_string("Nu shell")), + }, + Example { + description: "Trim all characters", + example: "' Nu shell ' | str trim -a", + result: Some(Value::test_string("Nushell")), + }, + Example { + description: "Trim whitespace from the beginning of string", + example: "' Nu shell ' | str trim -l", + result: Some(Value::test_string("Nu shell ")), + }, + Example { + description: "Trim a specific character", + example: "'=== Nu shell ===' | str trim -c '='", + result: Some(Value::test_string(" Nu shell ")), + }, + Example { + description: "Trim whitespace from the end of string", + example: "' Nu shell ' | str trim -r", + result: Some(Value::test_string(" Nu shell")), + }, + Example { + description: "Trim a specific character", + example: "'=== Nu shell ===' | str trim -r -c '='", + result: Some(Value::test_string("=== Nu shell ")), + }, + ] + } +} + +pub fn operate( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + trim_operation: &'static F, +) -> Result +where + F: Fn(&str, Option, &ClosureFlags) -> String + Send + Sync + 'static, +{ + let head = call.head; + let (options, closure_flags, input) = ( + Arguments { + character: call.get_flag(engine_state, stack, "char")?, + column_paths: call.rest(engine_state, stack, 0)?, + }, + ClosureFlags { + all_flag: call.has_flag("all"), + left_trim: call.has_flag("left"), + right_trim: call.has_flag("right"), + format_flag: call.has_flag("format"), + both_flag: call.has_flag("both") + || (!call.has_flag("all") + && !call.has_flag("left") + && !call.has_flag("right") + && !call.has_flag("format")), // this is the case if no flags are provided + }, + input, + ); + let to_trim = match options.character.as_ref() { + Some(v) => { + if v.item.chars().count() > 1 { + return Err(ShellError::SpannedLabeledError( + "Trim only works with single character".into(), + "needs single character".into(), + v.span, + )); + } + v.item.chars().next() + } + None => None, + }; + + input.map( + move |v| { + if options.column_paths.is_empty() { + action( + &v, + head, + to_trim, + &closure_flags, + &trim_operation, + ActionMode::Global, + ) + } else { + let mut ret = v; + for path in &options.column_paths { + let r = ret.update_cell_path( + &path.members, + Box::new(move |old| { + action( + old, + head, + to_trim, + &closure_flags, + &trim_operation, + ActionMode::Local, + ) + }), + ); + if let Err(error) = r { + return Value::Error { error }; + } + } + ret + } + }, + engine_state.ctrlc.clone(), + ) +} + +#[derive(Debug, Copy, Clone)] +pub enum ActionMode { + Local, + Global, +} + +pub fn action( + input: &Value, + head: Span, + char_: Option, + closure_flags: &ClosureFlags, + trim_operation: &F, + mode: ActionMode, +) -> Value +where + F: Fn(&str, Option, &ClosureFlags) -> String + Send + Sync + 'static, +{ + match input { + Value::String { val: s, .. } => Value::String { + val: trim_operation(s, char_, closure_flags), + span: head, + }, + other => match mode { + ActionMode::Global => match other { + Value::Record { cols, vals, span } => { + let new_vals = vals + .iter() + .map(|v| action(v, head, char_, closure_flags, trim_operation, mode)) + .collect(); + + Value::Record { + cols: cols.to_vec(), + vals: new_vals, + span: *span, + } + } + Value::List { vals, span } => { + let new_vals = vals + .iter() + .map(|v| action(v, head, char_, closure_flags, trim_operation, mode)) + .collect(); + + Value::List { + vals: new_vals, + span: *span, + } + } + _ => input.clone(), + }, + ActionMode::Local => { + let got = format!("Input must be a string. Found {}", other.get_type()); + Value::Error { + error: ShellError::UnsupportedInput(got, head), + } + } + }, + } +} + +fn trim(s: &str, char_: Option, closure_flags: &ClosureFlags) -> String { + let ClosureFlags { + left_trim, + right_trim, + all_flag, + both_flag, + format_flag, + } = closure_flags; + let delimiters = match char_ { + Some(c) => vec![c], + // Trying to make this trim work like rust default trim() + // which uses is_whitespace() as a default + None => vec![ + ' ', // space + '\x09', // horizontal tab + '\x0A', // new line, line feed + '\x0B', // vertical tab + '\x0C', // form feed, new page + '\x0D', // carriage return + ], //whitespace + }; + + if *left_trim { + s.trim_start_matches(&delimiters[..]).to_string() + } else if *right_trim { + s.trim_end_matches(&delimiters[..]).to_string() + } else if *all_flag { + s.split(&delimiters[..]) + .filter(|s| !s.is_empty()) + .collect::() + } else if *both_flag { + s.trim_matches(&delimiters[..]).to_string() + } else if *format_flag { + // The idea here is to use regex to go through these delimiters and + // where there are multiple, replace them with singles + + // create our return string which is a copy of the original string + let mut return_string = String::from(s); + // Iterate through the delimiters replacing them with regex friendly names + for r in &delimiters { + let reg = match r { + ' ' => r"\s".to_string(), + '\x09' => r"\t".to_string(), + '\x0A' => r"\n".to_string(), + '\x0B' => r"\v".to_string(), + '\x0C' => r"\f".to_string(), + '\x0D' => r"\r".to_string(), + _ => format!(r"\{}", r), + }; + // create a regex string that looks for 2 or more of each of these characters + let re_str = format!("{}{{2,}}", reg); + // create the regex + let re = regex::Regex::new(&re_str).expect("Error creating regular expression"); + // replace all mutliple occurances with single occurences represented by r + let new_str = re.replace_all(&return_string, r.to_string()); + // update the return string so the next loop has the latest changes + return_string = new_str.to_string(); + } + // for good measure, trim_matches, which gets the start and end + // theoretically we shouldn't have to do this but from my testing, we do. + return_string.trim_matches(&delimiters[..]).to_string() + } else { + s.trim().to_string() + } +} + +#[cfg(test)] +mod tests { + use crate::strings::str_::trim::trim_::*; + use nu_protocol::{Span, Value}; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } + + fn make_record(cols: Vec<&str>, vals: Vec<&str>) -> Value { + Value::Record { + cols: cols.iter().map(|x| x.to_string()).collect(), + vals: vals + .iter() + .map(|x| Value::String { + val: x.to_string(), + span: Span::test_data(), + }) + .collect(), + span: Span::test_data(), + } + } + + fn make_list(vals: Vec<&str>) -> Value { + Value::List { + vals: vals + .iter() + .map(|x| Value::String { + val: x.to_string(), + span: Span::test_data(), + }) + .collect(), + span: Span::test_data(), + } + } + + #[test] + fn trims() { + let word = Value::test_string("andres "); + let expected = Value::test_string("andres"); + let closure_flags = ClosureFlags { + both_flag: true, + ..Default::default() + }; + + let actual = action( + &word, + Span::test_data(), + None, + &closure_flags, + &trim, + ActionMode::Local, + ); + assert_eq!(actual, expected); + } + + #[test] + fn trims_global() { + let word = Value::test_string(" global "); + let expected = Value::test_string("global"); + let closure_flags = ClosureFlags { + both_flag: true, + ..Default::default() + }; + + let actual = action( + &word, + Span::test_data(), + None, + &closure_flags, + &trim, + ActionMode::Global, + ); + assert_eq!(actual, expected); + } + + #[test] + fn global_trim_ignores_numbers() { + let number = Value::test_int(2020); + let expected = Value::test_int(2020); + let closure_flags = ClosureFlags { + both_flag: true, + ..Default::default() + }; + + let actual = action( + &number, + Span::test_data(), + None, + &closure_flags, + &trim, + ActionMode::Global, + ); + assert_eq!(actual, expected); + } + + #[test] + fn global_trim_row() { + let row = make_record(vec!["a", "b"], vec![" c ", " d "]); + // ["a".to_string() => string(" c "), " b ".to_string() => string(" d ")]; + let expected = make_record(vec!["a", "b"], vec!["c", "d"]); + let closure_flags = ClosureFlags { + both_flag: true, + ..Default::default() + }; + + let actual = action( + &row, + Span::test_data(), + None, + &closure_flags, + &trim, + ActionMode::Global, + ); + assert_eq!(actual, expected); + } + + #[test] + fn global_trim_table() { + let row = make_list(vec![" a ", "d"]); + let expected = make_list(vec!["a", "d"]); + let closure_flags = ClosureFlags { + both_flag: true, + ..Default::default() + }; + + let actual = action( + &row, + Span::test_data(), + None, + &closure_flags, + &trim, + ActionMode::Global, + ); + assert_eq!(actual, expected); + } + + #[test] + fn trims_custom_character_both_ends() { + let word = Value::test_string("!#andres#!"); + let expected = Value::test_string("#andres#"); + let closure_flags = ClosureFlags { + both_flag: true, + ..Default::default() + }; + + let actual = action( + &word, + Span::test_data(), + Some('!'), + &closure_flags, + &trim, + ActionMode::Local, + ); + assert_eq!(actual, expected); + } + #[test] + fn trims_all_white_space() { + let word = Value::test_string(" Value1 a lot of spaces "); + let expected = Value::test_string("Value1alotofspaces"); + let closure_flags = ClosureFlags { + all_flag: true, + ..Default::default() + }; + + let actual = action( + &word, + Span::test_data(), + Some(' '), + &closure_flags, + &trim, + ActionMode::Local, + ); + assert_eq!(actual, expected); + } + + #[test] + fn global_trims_row_all_white_space() { + let row = make_record( + vec!["a", "b"], + vec![" nu shell ", " b c d e "], + ); + let expected = make_record(vec!["a", "b"], vec!["nushell", "bcde"]); + + let closure_flags = ClosureFlags { + all_flag: true, + ..Default::default() + }; + + let actual = action( + &row, + Span::test_data(), + None, + &closure_flags, + &trim, + ActionMode::Global, + ); + assert_eq!(actual, expected); + } + + #[test] + fn global_trims_table_all_white_space() { + let row = Value::List { + vals: vec![ + Value::String { + val: " nu shell ".to_string(), + span: Span::test_data(), + }, + Value::Int { + val: 65, + span: Span::test_data(), + }, + Value::String { + val: " d".to_string(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }; + let expected = Value::List { + vals: vec![ + Value::String { + val: "nushell".to_string(), + span: Span::test_data(), + }, + Value::Int { + val: 65, + span: Span::test_data(), + }, + Value::String { + val: "d".to_string(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }; + + let closure_flags = ClosureFlags { + all_flag: true, + ..Default::default() + }; + + let actual = action( + &row, + Span::test_data(), + None, + &closure_flags, + &trim, + ActionMode::Global, + ); + assert_eq!(actual, expected); + } + + #[test] + fn trims_all_custom_character() { + let word = Value::test_string(".Value1.a.lot..of...dots."); + let expected = Value::test_string("Value1alotofdots"); + let closure_flags = ClosureFlags { + all_flag: true, + ..Default::default() + }; + + let actual = action( + &word, + Span::test_data(), + Some('.'), + &closure_flags, + &trim, + ActionMode::Local, + ); + assert_eq!(actual, expected); + } + + #[test] + fn global_trims_row_all_custom_character() { + let row = make_record(vec!["a", "b"], vec!["!!!!nu!!shell!!!", "!!b!c!!d!e!!"]); + let expected = make_record(vec!["a", "b"], vec!["nushell", "bcde"]); + let closure_flags = ClosureFlags { + all_flag: true, + ..Default::default() + }; + + let actual = action( + &row, + Span::test_data(), + Some('!'), + &closure_flags, + &trim, + ActionMode::Global, + ); + assert_eq!(actual, expected); + } + + #[test] + fn global_trims_table_all_custom_character() { + let row = Value::List { + vals: vec![ + Value::String { + val: "##nu####shell##".to_string(), + span: Span::test_data(), + }, + Value::Int { + val: 65, + span: Span::test_data(), + }, + Value::String { + val: "#d".to_string(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }; + let expected = Value::List { + vals: vec![ + Value::String { + val: "nushell".to_string(), + span: Span::test_data(), + }, + Value::Int { + val: 65, + span: Span::test_data(), + }, + Value::String { + val: "d".to_string(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }; + + let closure_flags = ClosureFlags { + all_flag: true, + ..Default::default() + }; + + let actual = action( + &row, + Span::test_data(), + Some('#'), + &closure_flags, + &trim, + ActionMode::Global, + ); + assert_eq!(actual, expected); + } + + #[test] + fn trims_whitespace_from_left() { + let word = Value::test_string(" andres "); + let expected = Value::test_string("andres "); + let closure_flags = ClosureFlags { + left_trim: true, + ..Default::default() + }; + + let actual = action( + &word, + Span::test_data(), + None, + &closure_flags, + &trim, + ActionMode::Local, + ); + assert_eq!(actual, expected); + } + + #[test] + fn global_trim_left_ignores_numbers() { + let number = Value::test_int(2020); + let expected = Value::test_int(2020); + let closure_flags = ClosureFlags { + left_trim: true, + ..Default::default() + }; + + let actual = action( + &number, + Span::test_data(), + None, + &closure_flags, + &trim, + ActionMode::Global, + ); + assert_eq!(actual, expected); + } + + #[test] + fn trims_left_global() { + let word = Value::test_string(" global "); + let expected = Value::test_string("global "); + let closure_flags = ClosureFlags { + left_trim: true, + ..Default::default() + }; + + let actual = action( + &word, + Span::test_data(), + None, + &closure_flags, + &trim, + ActionMode::Global, + ); + assert_eq!(actual, expected); + } + + #[test] + fn global_trim_left_row() { + let row = make_record(vec!["a", "b"], vec![" c ", " d "]); + let expected = make_record(vec!["a", "b"], vec!["c ", "d "]); + let closure_flags = ClosureFlags { + left_trim: true, + ..Default::default() + }; + + let actual = action( + &row, + Span::test_data(), + None, + &closure_flags, + &trim, + ActionMode::Global, + ); + assert_eq!(actual, expected); + } + + #[test] + fn global_trim_left_table() { + let row = Value::List { + vals: vec![ + Value::String { + val: " a ".to_string(), + span: Span::test_data(), + }, + Value::Int { + val: 65, + span: Span::test_data(), + }, + Value::String { + val: " d".to_string(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }; + let expected = Value::List { + vals: vec![ + Value::String { + val: "a ".to_string(), + span: Span::test_data(), + }, + Value::Int { + val: 65, + span: Span::test_data(), + }, + Value::String { + val: "d".to_string(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }; + + let closure_flags = ClosureFlags { + left_trim: true, + ..Default::default() + }; + + let actual = action( + &row, + Span::test_data(), + None, + &closure_flags, + &trim, + ActionMode::Global, + ); + assert_eq!(actual, expected); + } + + #[test] + fn trims_custom_chars_from_left() { + let word = Value::test_string("!!! andres !!!"); + let expected = Value::test_string(" andres !!!"); + let closure_flags = ClosureFlags { + left_trim: true, + ..Default::default() + }; + + let actual = action( + &word, + Span::test_data(), + Some('!'), + &closure_flags, + &trim, + ActionMode::Local, + ); + assert_eq!(actual, expected); + } + #[test] + fn trims_whitespace_from_right() { + let word = Value::test_string(" andres "); + let expected = Value::test_string(" andres"); + let closure_flags = ClosureFlags { + right_trim: true, + ..Default::default() + }; + + let actual = action( + &word, + Span::test_data(), + None, + &closure_flags, + &trim, + ActionMode::Local, + ); + assert_eq!(actual, expected); + } + + #[test] + fn trims_right_global() { + let word = Value::test_string(" global "); + let expected = Value::test_string(" global"); + let closure_flags = ClosureFlags { + right_trim: true, + ..Default::default() + }; + + let actual = action( + &word, + Span::test_data(), + None, + &closure_flags, + &trim, + ActionMode::Global, + ); + assert_eq!(actual, expected); + } + + #[test] + fn global_trim_right_ignores_numbers() { + let number = Value::test_int(2020); + let expected = Value::test_int(2020); + let closure_flags = ClosureFlags { + right_trim: true, + ..Default::default() + }; + + let actual = action( + &number, + Span::test_data(), + None, + &closure_flags, + &trim, + ActionMode::Global, + ); + assert_eq!(actual, expected); + } + + #[test] + fn global_trim_right_row() { + let row = make_record(vec!["a", "b"], vec![" c ", " d "]); + let expected = make_record(vec!["a", "b"], vec![" c", " d"]); + let closure_flags = ClosureFlags { + right_trim: true, + ..Default::default() + }; + + let actual = action( + &row, + Span::test_data(), + None, + &closure_flags, + &trim, + ActionMode::Global, + ); + assert_eq!(actual, expected); + } + + #[test] + fn global_trim_right_table() { + let row = Value::List { + vals: vec![ + Value::String { + val: " a ".to_string(), + span: Span::test_data(), + }, + Value::Int { + val: 65, + span: Span::test_data(), + }, + Value::String { + val: " d".to_string(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }; + let expected = Value::List { + vals: vec![ + Value::String { + val: " a".to_string(), + span: Span::test_data(), + }, + Value::Int { + val: 65, + span: Span::test_data(), + }, + Value::String { + val: " d".to_string(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }; + + let closure_flags = ClosureFlags { + right_trim: true, + ..Default::default() + }; + + let actual = action( + &row, + Span::test_data(), + None, + &closure_flags, + &trim, + ActionMode::Global, + ); + assert_eq!(actual, expected); + } + + #[test] + fn trims_custom_chars_from_right() { + let word = Value::test_string("#@! andres !@#"); + let expected = Value::test_string("#@! andres !@"); + let closure_flags = ClosureFlags { + right_trim: true, + ..Default::default() + }; + + let actual = action( + &word, + Span::test_data(), + Some('#'), + &closure_flags, + &trim, + ActionMode::Local, + ); + assert_eq!(actual, expected); + } + + #[test] + fn trims_whitespace_format_flag() { + let word = Value::test_string(" nushell is great "); + let expected = Value::test_string("nushell is great"); + let closure_flags = ClosureFlags { + format_flag: true, + ..Default::default() + }; + + let actual = action( + &word, + Span::test_data(), + Some(' '), + &closure_flags, + &trim, + ActionMode::Local, + ); + assert_eq!(actual, expected); + } + + #[test] + fn trims_format_flag_global() { + let word = Value::test_string("global "); + let expected = Value::test_string("global"); + let closure_flags = ClosureFlags { + format_flag: true, + ..Default::default() + }; + + let actual = action( + &word, + Span::test_data(), + Some(' '), + &closure_flags, + &trim, + ActionMode::Local, + ); + assert_eq!(actual, expected); + } + #[test] + fn global_trim_format_flag_ignores_numbers() { + let number = Value::test_int(2020); + let expected = Value::test_int(2020); + let closure_flags = ClosureFlags { + format_flag: true, + ..Default::default() + }; + + let actual = action( + &number, + Span::test_data(), + None, + &closure_flags, + &trim, + ActionMode::Global, + ); + assert_eq!(actual, expected); + } + + #[test] + fn global_trim_format_flag_row() { + let row = make_record(vec!["a", "b"], vec![" c ", " b c d e "]); + let expected = make_record(vec!["a", "b"], vec!["c", "b c d e"]); + let closure_flags = ClosureFlags { + format_flag: true, + ..Default::default() + }; + + let actual = action( + &row, + Span::test_data(), + None, + &closure_flags, + &trim, + ActionMode::Global, + ); + assert_eq!(actual, expected); + } + + #[test] + fn global_trim_format_flag_table() { + let row = Value::List { + vals: vec![ + Value::String { + val: " a b c d ".to_string(), + span: Span::test_data(), + }, + Value::Int { + val: 65, + span: Span::test_data(), + }, + Value::String { + val: " b c d e f".to_string(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }; + let expected = Value::List { + vals: vec![ + Value::String { + val: "a b c d".to_string(), + span: Span::test_data(), + }, + Value::Int { + val: 65, + span: Span::test_data(), + }, + Value::String { + val: "b c d e f".to_string(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }; + + let closure_flags = ClosureFlags { + format_flag: true, + ..Default::default() + }; + + let actual = action( + &row, + Span::test_data(), + None, + &closure_flags, + &trim, + ActionMode::Global, + ); + assert_eq!(actual, expected); + } + + #[test] + fn trims_custom_chars_format_flag() { + let word = Value::test_string(".Value1.a..lot...of....dots."); + let expected = Value::test_string("Value1.a.lot.of.dots"); + let closure_flags = ClosureFlags { + format_flag: true, + ..Default::default() + }; + + let actual = action( + &word, + Span::test_data(), + Some('.'), + &closure_flags, + &trim, + ActionMode::Local, + ); + assert_eq!(actual, expected); + } + + #[test] + fn trims_all_format_flag_whitespace() { + let word = Value::test_string(" nushell is great "); + let expected = Value::test_string("nushellisgreat"); + let closure_flags = ClosureFlags { + format_flag: true, + all_flag: true, + ..Default::default() + }; + + let actual = action( + &word, + Span::test_data(), + Some(' '), + &closure_flags, + &trim, + ActionMode::Local, + ); + assert_eq!(actual, expected); + } + + #[test] + fn trims_all_format_flag_global() { + let word = Value::test_string(" nushell is great "); + let expected = Value::test_string("nushellisgreat"); + let closure_flags = ClosureFlags { + format_flag: true, + all_flag: true, + ..Default::default() + }; + + let actual = action( + &word, + Span::test_data(), + Some(' '), + &closure_flags, + &trim, + ActionMode::Global, + ); + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-command/src/strings/str_/upcase.rs b/crates/nu-command/src/strings/str_/upcase.rs new file mode 100644 index 0000000000..94f8a58dcb --- /dev/null +++ b/crates/nu-command/src/strings/str_/upcase.rs @@ -0,0 +1,109 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::ast::CellPath; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "str upcase" + } + + fn signature(&self) -> Signature { + Signature::build("str upcase").rest( + "rest", + SyntaxShape::CellPath, + "optionally upcase text by column paths", + ) + } + + fn usage(&self) -> &str { + "upcases text" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Upcase contents", + example: "'nu' | str upcase", + result: Some(Value::test_string("NU")), + }] + } +} + +fn operate( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let head = call.head; + let column_paths: Vec = call.rest(engine_state, stack, 0)?; + input.map( + move |v| { + if column_paths.is_empty() { + action(&v, head) + } else { + let mut ret = v; + for path in &column_paths { + let r = + ret.update_cell_path(&path.members, Box::new(move |old| action(old, head))); + if let Err(error) = r { + return Value::Error { error }; + } + } + ret + } + }, + engine_state.ctrlc.clone(), + ) +} + +fn action(input: &Value, head: Span) -> Value { + match input { + Value::String { val: s, .. } => Value::String { + val: s.to_uppercase(), + span: head, + }, + other => { + let got = format!("Expected string but got {}", other.get_type()); + Value::Error { + error: ShellError::UnsupportedInput(got, head), + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use super::{action, SubCommand}; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } + + #[test] + fn upcases() { + let word = Value::test_string("andres"); + + let actual = action(&word, Span::test_data()); + let expected = Value::test_string("ANDRES"); + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-command/src/system/benchmark.rs b/crates/nu-command/src/system/benchmark.rs new file mode 100644 index 0000000000..bb42f2522e --- /dev/null +++ b/crates/nu-command/src/system/benchmark.rs @@ -0,0 +1,59 @@ +use std::time::Instant; + +use nu_engine::{eval_block, CallExt}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{CaptureBlock, Command, EngineState, Stack}; +use nu_protocol::{Category, IntoPipelineData, PipelineData, Signature, SyntaxShape, Value}; + +#[derive(Clone)] +pub struct Benchmark; + +impl Command for Benchmark { + fn name(&self) -> &str { + "benchmark" + } + + fn usage(&self) -> &str { + "Time the running time of a block" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("benchmark") + .required( + "block", + SyntaxShape::Block(Some(vec![])), + "the block to run", + ) + .category(Category::System) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let capture_block: CaptureBlock = call.req(engine_state, stack, 0)?; + let block = engine_state.get_block(capture_block.block_id); + + let mut stack = stack.captures_to_stack(&capture_block.captures); + let start_time = Instant::now(); + eval_block( + engine_state, + &mut stack, + block, + PipelineData::new(call.head), + )? + .into_value(call.head); + + let end_time = Instant::now(); + + let output = Value::Duration { + val: (end_time - start_time).as_nanos() as i64, + span: call.head, + }; + + Ok(output.into_pipeline_data()) + } +} diff --git a/crates/nu-command/src/system/exec.rs b/crates/nu-command/src/system/exec.rs new file mode 100644 index 0000000000..56b0a71f55 --- /dev/null +++ b/crates/nu-command/src/system/exec.rs @@ -0,0 +1,114 @@ +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, SyntaxShape, +}; + +#[derive(Clone)] +pub struct Exec; + +impl Command for Exec { + fn name(&self) -> &str { + "exec" + } + + fn signature(&self) -> Signature { + Signature::build("exec") + .required("command", SyntaxShape::String, "the command to execute") + .rest( + "rest", + SyntaxShape::String, + "any additional arguments for the command", + ) + .category(Category::System) + } + + fn usage(&self) -> &str { + "Execute a command, replacing the current process." + } + + fn extra_usage(&self) -> &str { + "Currently supported only on Unix-based systems." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + exec(engine_state, stack, call) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Execute external 'ps aux' tool", + example: "exec ps aux", + result: None, + }, + Example { + description: "Execute 'nautilus'", + example: "exec nautilus", + result: None, + }, + ] + } +} + +#[cfg(unix)] +fn exec( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, +) -> Result { + use std::os::unix::process::CommandExt; + + use nu_engine::{current_dir, env_to_strings, CallExt}; + use nu_protocol::Spanned; + + use super::run_external::ExternalCommand; + + let name: Spanned = call.req(engine_state, stack, 0)?; + let name_span = name.span; + + let args: Vec> = call.rest(engine_state, stack, 1)?; + + let cwd = current_dir(engine_state, stack)?; + let config = stack.get_config()?; + let env_vars = env_to_strings(engine_state, stack, &config)?; + let current_dir = current_dir(engine_state, stack)?; + + let external_command = ExternalCommand { + name, + args, + env_vars, + last_expression: true, + }; + + let mut command = external_command.spawn_simple_command(&cwd.to_string_lossy().to_string())?; + command.current_dir(current_dir); + + println!("{:#?}", command); + let err = command.exec(); // this replaces our process, should not return + + Err(ShellError::SpannedLabeledError( + "Error on exec".to_string(), + err.to_string(), + name_span, + )) +} + +#[cfg(not(unix))] +fn exec( + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, +) -> Result { + Err(ShellError::SpannedLabeledError( + "Error on exec".to_string(), + "exec is not supported on your platform".to_string(), + call.head, + )) +} diff --git a/crates/nu-command/src/system/mod.rs b/crates/nu-command/src/system/mod.rs new file mode 100644 index 0000000000..76572089fb --- /dev/null +++ b/crates/nu-command/src/system/mod.rs @@ -0,0 +1,13 @@ +mod benchmark; +mod exec; +mod ps; +mod run_external; +mod sys; +mod which_; + +pub use benchmark::Benchmark; +pub use exec::Exec; +pub use ps::Ps; +pub use run_external::{External, ExternalCommand}; +pub use sys::Sys; +pub use which_::Which; diff --git a/crates/nu-command/src/system/ps.rs b/crates/nu-command/src/system/ps.rs new file mode 100644 index 0000000000..ef52b9dd54 --- /dev/null +++ b/crates/nu-command/src/system/ps.rs @@ -0,0 +1,132 @@ +use std::time::Duration; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Value, +}; + +#[derive(Clone)] +pub struct Ps; + +impl Command for Ps { + fn name(&self) -> &str { + "ps" + } + + fn signature(&self) -> Signature { + Signature::build("ps") + .desc("View information about system processes.") + .switch( + "long", + "list all available columns for each entry", + Some('l'), + ) + .filter() + .category(Category::System) + } + + fn usage(&self) -> &str { + "View information about system processes." + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + run_ps(engine_state, call) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "List the system processes", + example: "ps", + result: None, + }] + } +} + +fn run_ps(engine_state: &EngineState, call: &Call) -> Result { + let mut output = vec![]; + let span = call.head; + let long = call.has_flag("long"); + + for proc in nu_system::collect_proc(Duration::from_millis(100), false) { + let mut cols = vec![]; + let mut vals = vec![]; + + cols.push("pid".to_string()); + vals.push(Value::Int { + val: proc.pid() as i64, + span, + }); + + cols.push("name".to_string()); + vals.push(Value::String { + val: proc.name(), + span, + }); + + #[cfg(not(windows))] + { + // Hide status on Windows until we can find a good way to support it + cols.push("status".to_string()); + vals.push(Value::String { + val: proc.status(), + span, + }); + } + + cols.push("cpu".to_string()); + vals.push(Value::Float { + val: proc.cpu_usage(), + span, + }); + + cols.push("mem".to_string()); + vals.push(Value::Filesize { + val: proc.mem_size() as i64, + span, + }); + + cols.push("virtual".to_string()); + vals.push(Value::Filesize { + val: proc.virtual_size() as i64, + span, + }); + + if long { + cols.push("command".to_string()); + vals.push(Value::String { + val: proc.command(), + span, + }); + #[cfg(windows)] + { + cols.push("cwd".to_string()); + vals.push(Value::String { + val: proc.cwd(), + span, + }); + cols.push("environment".to_string()); + vals.push(Value::List { + vals: proc + .environ() + .iter() + .map(|x| Value::string(x.to_string(), span)) + .collect(), + span, + }); + } + } + + output.push(Value::Record { cols, vals, span }); + } + + Ok(output + .into_iter() + .into_pipeline_data(engine_state.ctrlc.clone())) +} diff --git a/crates/nu-command/src/system/run_external.rs b/crates/nu-command/src/system/run_external.rs new file mode 100644 index 0000000000..4731789c42 --- /dev/null +++ b/crates/nu-command/src/system/run_external.rs @@ -0,0 +1,479 @@ +use std::collections::HashMap; +use std::io::{BufRead, BufReader, Write}; +use std::path::PathBuf; +use std::process::{Command as CommandSys, Stdio}; +use std::sync::atomic::Ordering; +use std::sync::mpsc; + +use nu_engine::env_to_strings; +use nu_protocol::engine::{EngineState, Stack}; +use nu_protocol::{ast::Call, engine::Command, ShellError, Signature, SyntaxShape, Value}; +use nu_protocol::{Category, PipelineData, RawStream, Span, Spanned}; + +use itertools::Itertools; + +use nu_engine::CallExt; +use pathdiff::diff_paths; +use regex::Regex; + +const OUTPUT_BUFFER_SIZE: usize = 1024; + +#[derive(Clone)] +pub struct External; + +impl Command for External { + fn name(&self) -> &str { + "run_external" + } + + fn usage(&self) -> &str { + "Runs external command" + } + + fn is_private(&self) -> bool { + true + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("run_external") + .switch("last_expression", "last_expression", None) + .rest("rest", SyntaxShape::Any, "external command to run") + .category(Category::System) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let name: Spanned = call.req(engine_state, stack, 0)?; + let args: Vec = call.rest(engine_state, stack, 1)?; + let last_expression = call.has_flag("last_expression"); + + // Translate environment variables from Values to Strings + let config = stack.get_config().unwrap_or_default(); + let env_vars_str = env_to_strings(engine_state, stack, &config)?; + + let mut args_strs = vec![]; + + for arg in args { + let span = if let Ok(span) = arg.span() { + span + } else { + Span { start: 0, end: 0 } + }; + + if let Ok(s) = arg.as_string() { + args_strs.push(Spanned { item: s, span }); + } else if let Value::List { vals, span } = arg { + // Interpret a list as a series of arguments + for val in vals { + if let Ok(s) = val.as_string() { + args_strs.push(Spanned { item: s, span }); + } else { + return Err(ShellError::ExternalCommand( + "Cannot convert argument to a string".into(), + "All arguments to an external command need to be string-compatible" + .into(), + val.span()?, + )); + } + } + } else { + return Err(ShellError::ExternalCommand( + "Cannot convert argument to a string".into(), + "All arguments to an external command need to be string-compatible".into(), + arg.span()?, + )); + } + } + + let command = ExternalCommand { + name, + args: args_strs, + last_expression, + env_vars: env_vars_str, + }; + command.run_with_input(engine_state, stack, input) + } +} + +pub struct ExternalCommand { + pub name: Spanned, + pub args: Vec>, + pub last_expression: bool, + pub env_vars: HashMap, +} + +impl ExternalCommand { + pub fn run_with_input( + &self, + engine_state: &EngineState, + stack: &mut Stack, + input: PipelineData, + ) -> Result { + let head = self.name.span; + + let ctrlc = engine_state.ctrlc.clone(); + + let mut process = if let Some(d) = self.env_vars.get("PWD") { + let mut process = self.create_command(d)?; + process.current_dir(d); + process + } else { + return Err(ShellError::SpannedLabeledErrorHelp( + "Current directory not found".to_string(), + "did not find PWD environment variable".to_string(), + head, + concat!( + "The environment variable 'PWD' was not found. ", + "It is required to define the current directory when running an external command." + ).to_string(), + )); + }; + + process.envs(&self.env_vars); + + // If the external is not the last command, its output will get piped + // either as a string or binary + if !self.last_expression { + process.stdout(Stdio::piped()); + } + + // If there is an input from the pipeline. The stdin from the process + // is piped so it can be used to send the input information + if !matches!(input, PipelineData::Value(Value::Nothing { .. }, ..)) { + process.stdin(Stdio::piped()); + } + + let child; + + #[cfg(windows)] + { + match process.spawn() { + Err(_) => { + let mut process = self.spawn_cmd_command(); + if let Some(d) = self.env_vars.get("PWD") { + process.current_dir(d); + } else { + return Err(ShellError::SpannedLabeledErrorHelp( + "Current directory not found".to_string(), + "did not find PWD environment variable".to_string(), + head, + concat!( + "The environment variable 'PWD' was not found. ", + "It is required to define the current directory when running an external command." + ).to_string(), + )); + }; + + process.envs(&self.env_vars); + + // If the external is not the last command, its output will get piped + // either as a string or binary + if !self.last_expression { + process.stdout(Stdio::piped()); + } + + // If there is an input from the pipeline. The stdin from the process + // is piped so it can be used to send the input information + if !matches!(input, PipelineData::Value(Value::Nothing { .. }, ..)) { + process.stdin(Stdio::piped()); + } + + child = process.spawn(); + } + Ok(process) => { + child = Ok(process); + } + } + } + + #[cfg(not(windows))] + { + child = process.spawn() + } + + match child { + Err(err) => Err(ShellError::ExternalCommand( + "can't run executable".to_string(), + err.to_string(), + self.name.span, + )), + Ok(mut child) => { + if !input.is_nothing() { + let engine_state = engine_state.clone(); + let mut stack = stack.clone(); + stack.update_config( + "use_ansi_coloring", + Value::Bool { + val: false, + span: Span::new(0, 0), + }, + ); + // if there is a string or a stream, that is sent to the pipe std + if let Some(mut stdin_write) = child.stdin.take() { + std::thread::spawn(move || { + let input = crate::Table::run( + &crate::Table, + &engine_state, + &mut stack, + &Call::new(head), + input, + ); + + if let Ok(input) = input { + for value in input.into_iter() { + if let Value::String { val, span: _ } = value { + if stdin_write.write(val.as_bytes()).is_err() { + return Ok(()); + } + } else { + return Err(()); + } + } + } + + Ok(()) + }); + } + } + + let last_expression = self.last_expression; + let span = self.name.span; + let output_ctrlc = ctrlc.clone(); + let (tx, rx) = mpsc::channel(); + + std::thread::spawn(move || { + // If this external is not the last expression, then its output is piped to a channel + // and we create a ValueStream that can be consumed + if !last_expression { + let stdout = child.stdout.take().ok_or_else(|| { + ShellError::ExternalCommand( + "Error taking stdout from external".to_string(), + "Redirects need access to stdout of an external command" + .to_string(), + span, + ) + })?; + + // Stdout is read using the Buffer reader. It will do so until there is an + // error or there are no more bytes to read + let mut buf_read = BufReader::with_capacity(OUTPUT_BUFFER_SIZE, stdout); + while let Ok(bytes) = buf_read.fill_buf() { + if bytes.is_empty() { + break; + } + + // The Cow generated from the function represents the conversion + // from bytes to String. If no replacements are required, then the + // borrowed value is a proper UTF-8 string. The Owned option represents + // a string where the values had to be replaced, thus marking it as bytes + let bytes = bytes.to_vec(); + let length = bytes.len(); + buf_read.consume(length); + + if let Some(ctrlc) = &ctrlc { + if ctrlc.load(Ordering::SeqCst) { + break; + } + } + + match tx.send(bytes) { + Ok(_) => continue, + Err(_) => break, + } + } + } + + match child.wait() { + Err(err) => Err(ShellError::ExternalCommand( + "External command exited with error".into(), + err.to_string(), + span, + )), + Ok(_) => Ok(()), + } + }); + let receiver = ChannelReceiver::new(rx); + + Ok(PipelineData::RawStream( + RawStream::new(Box::new(receiver), output_ctrlc, head), + head, + None, + )) + } + } + } + + fn create_command(&self, cwd: &str) -> Result { + // in all the other cases shell out + if cfg!(windows) { + //TODO. This should be modifiable from the config file. + // We could give the option to call from powershell + // for minimal builds cwd is unused + if self.name.item.ends_with(".cmd") || self.name.item.ends_with(".bat") { + Ok(self.spawn_cmd_command()) + } else { + self.spawn_simple_command(cwd) + } + } else if self.name.item.ends_with(".sh") { + Ok(self.spawn_sh_command()) + } else { + self.spawn_simple_command(cwd) + } + } + + /// Spawn a command without shelling out to an external shell + pub fn spawn_simple_command(&self, cwd: &str) -> Result { + let head = trim_enclosing_quotes(&self.name.item); + let head = if head.starts_with('~') || head.starts_with("..") { + nu_path::expand_path_with(head, cwd) + .to_string_lossy() + .to_string() + } else { + head + }; + + let mut process = std::process::Command::new(&head); + + for arg in self.args.iter() { + let mut arg = Spanned { + item: trim_enclosing_quotes(&arg.item), + span: arg.span, + }; + arg.item = if arg.item.starts_with('~') || arg.item.starts_with("..") { + nu_path::expand_path_with(&arg.item, cwd) + .to_string_lossy() + .to_string() + } else { + arg.item + }; + + let cwd = PathBuf::from(cwd); + + if arg.item.contains('*') { + if let Ok((prefix, matches)) = nu_engine::glob_from(&arg, &cwd, self.name.span) { + let matches: Vec<_> = matches.collect(); + + // Following shells like bash, if we can't expand a glob pattern, we don't assume an empty arg + // Instead, we throw an error. This helps prevent issues with things like `ls unknowndir/*` accidentally + // listening the current directory. + if matches.is_empty() { + return Err(ShellError::FileNotFoundCustom( + "pattern not found".to_string(), + arg.span, + )); + } + for m in matches { + if let Ok(arg) = m { + let arg = if let Some(prefix) = &prefix { + if let Ok(remainder) = arg.strip_prefix(&prefix) { + let new_prefix = if let Some(pfx) = diff_paths(&prefix, &cwd) { + pfx + } else { + prefix.to_path_buf() + }; + + new_prefix.join(remainder).to_string_lossy().to_string() + } else { + arg.to_string_lossy().to_string() + } + } else { + arg.to_string_lossy().to_string() + }; + + process.arg(&arg); + } else { + process.arg(&arg.item); + } + } + } + } else { + process.arg(&arg.item); + } + } + + Ok(process) + } + + /// Spawn a cmd command with `cmd /c args...` + pub fn spawn_cmd_command(&self) -> std::process::Command { + let mut process = std::process::Command::new("cmd"); + process.arg("/c"); + process.arg(&self.name.item); + for arg in &self.args { + // Clean the args before we use them: + // https://stackoverflow.com/questions/1200235/how-to-pass-a-quoted-pipe-character-to-cmd-exe + // cmd.exe needs to have a caret to escape a pipe + let arg = arg.item.replace("|", "^|"); + process.arg(&arg); + } + process + } + + /// Spawn a sh command with `sh -c args...` + pub fn spawn_sh_command(&self) -> std::process::Command { + let joined_and_escaped_arguments = self + .args + .iter() + .map(|arg| shell_arg_escape(&arg.item)) + .join(" "); + let cmd_with_args = vec![self.name.item.clone(), joined_and_escaped_arguments].join(" "); + let mut process = std::process::Command::new("sh"); + process.arg("-c").arg(cmd_with_args); + process + } +} + +fn has_unsafe_shell_characters(arg: &str) -> bool { + let re: Regex = Regex::new(r"[^\w@%+=:,./-]").expect("regex to be valid"); + + re.is_match(arg) +} + +fn shell_arg_escape(arg: &str) -> String { + match arg { + "" => String::from("''"), + s if !has_unsafe_shell_characters(s) => String::from(s), + _ => { + let single_quotes_escaped = arg.split('\'').join("'\"'\"'"); + format!("'{}'", single_quotes_escaped) + } + } +} + +fn trim_enclosing_quotes(input: &str) -> String { + let mut chars = input.chars(); + + match (chars.next(), chars.next_back()) { + (Some('"'), Some('"')) => chars.collect(), + (Some('\''), Some('\'')) => chars.collect(), + _ => input.to_string(), + } +} + +// Receiver used for the ValueStream +// It implements iterator so it can be used as a ValueStream +struct ChannelReceiver { + rx: mpsc::Receiver>, +} + +impl ChannelReceiver { + pub fn new(rx: mpsc::Receiver>) -> Self { + Self { rx } + } +} + +impl Iterator for ChannelReceiver { + type Item = Result, ShellError>; + + fn next(&mut self) -> Option { + match self.rx.recv() { + Ok(v) => Some(Ok(v)), + Err(_) => None, + } + } +} diff --git a/crates/nu-command/src/system/sys.rs b/crates/nu-command/src/system/sys.rs new file mode 100644 index 0000000000..c6c44237e7 --- /dev/null +++ b/crates/nu-command/src/system/sys.rs @@ -0,0 +1,361 @@ +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Value, +}; +use sysinfo::{ComponentExt, DiskExt, NetworkExt, ProcessorExt, System, SystemExt, UserExt}; + +#[derive(Clone)] +pub struct Sys; + +impl Command for Sys { + fn name(&self) -> &str { + "sys" + } + + fn signature(&self) -> Signature { + Signature::build("sys") + .desc("View information about the current system.") + .filter() + .category(Category::System) + } + + fn usage(&self) -> &str { + "View information about the system." + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + run_sys(call) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Show info about the system", + example: "sys", + result: None, + }] + } +} + +fn run_sys(call: &Call) -> Result { + let span = call.head; + let mut sys = System::new(); + + let mut headers = vec![]; + let mut values = vec![]; + + if let Some(value) = host(&mut sys, span) { + headers.push("host".into()); + values.push(value); + } + if let Some(value) = cpu(&mut sys, span) { + headers.push("cpu".into()); + values.push(value); + } + if let Some(value) = disks(&mut sys, span) { + headers.push("disks".into()); + values.push(value); + } + if let Some(value) = mem(&mut sys, span) { + headers.push("mem".into()); + values.push(value); + } + if let Some(value) = temp(&mut sys, span) { + headers.push("temp".into()); + values.push(value); + } + if let Some(value) = net(&mut sys, span) { + headers.push("net".into()); + values.push(value); + } + + Ok(Value::Record { + cols: headers, + vals: values, + span, + } + .into_pipeline_data()) +} + +pub fn trim_cstyle_null(s: String) -> String { + s.trim_matches(char::from(0)).to_string() +} + +pub fn disks(sys: &mut System, span: Span) -> Option { + sys.refresh_disks(); + sys.refresh_disks_list(); + + let mut output = vec![]; + for disk in sys.disks() { + let mut cols = vec![]; + let mut vals = vec![]; + + cols.push("device".into()); + vals.push(Value::String { + val: trim_cstyle_null(disk.name().to_string_lossy().to_string()), + span, + }); + + cols.push("type".into()); + vals.push(Value::String { + val: trim_cstyle_null(String::from_utf8_lossy(disk.file_system()).to_string()), + span, + }); + + cols.push("mount".into()); + vals.push(Value::String { + val: disk.mount_point().to_string_lossy().to_string(), + span, + }); + + cols.push("total".into()); + vals.push(Value::Filesize { + val: disk.total_space() as i64, + span, + }); + + cols.push("free".into()); + vals.push(Value::Filesize { + val: disk.available_space() as i64, + span, + }); + + output.push(Value::Record { cols, vals, span }); + } + if !output.is_empty() { + Some(Value::List { vals: output, span }) + } else { + None + } +} + +pub fn net(sys: &mut System, span: Span) -> Option { + sys.refresh_networks(); + sys.refresh_networks_list(); + + let mut output = vec![]; + for (iface, data) in sys.networks() { + let mut cols = vec![]; + let mut vals = vec![]; + + cols.push("name".into()); + vals.push(Value::String { + val: trim_cstyle_null(iface.to_string()), + span, + }); + + cols.push("sent".into()); + vals.push(Value::Filesize { + val: data.total_transmitted() as i64, + span, + }); + + cols.push("recv".into()); + vals.push(Value::Filesize { + val: data.total_received() as i64, + span, + }); + + output.push(Value::Record { cols, vals, span }); + } + if !output.is_empty() { + Some(Value::List { vals: output, span }) + } else { + None + } +} + +pub fn cpu(sys: &mut System, span: Span) -> Option { + sys.refresh_cpu(); + + let mut output = vec![]; + for cpu in sys.processors() { + let mut cols = vec![]; + let mut vals = vec![]; + + cols.push("name".into()); + vals.push(Value::String { + val: trim_cstyle_null(cpu.name().to_string()), + span, + }); + + cols.push("brand".into()); + vals.push(Value::String { + val: trim_cstyle_null(cpu.brand().to_string()), + span, + }); + + cols.push("freq".into()); + vals.push(Value::Int { + val: cpu.frequency() as i64, + span, + }); + + output.push(Value::Record { cols, vals, span }); + } + if !output.is_empty() { + Some(Value::List { vals: output, span }) + } else { + None + } +} + +pub fn mem(sys: &mut System, span: Span) -> Option { + sys.refresh_memory(); + + let mut cols = vec![]; + let mut vals = vec![]; + + let total_mem = sys.total_memory(); + let free_mem = sys.free_memory(); + let total_swap = sys.total_swap(); + let free_swap = sys.free_swap(); + + cols.push("total".into()); + vals.push(Value::Filesize { + val: total_mem as i64 * 1000, + span, + }); + + cols.push("free".into()); + vals.push(Value::Filesize { + val: free_mem as i64 * 1000, + span, + }); + + cols.push("swap total".into()); + vals.push(Value::Filesize { + val: total_swap as i64 * 1000, + span, + }); + + cols.push("swap free".into()); + vals.push(Value::Filesize { + val: free_swap as i64 * 1000, + span, + }); + + Some(Value::Record { cols, vals, span }) +} + +pub fn host(sys: &mut System, span: Span) -> Option { + sys.refresh_users_list(); + + let mut cols = vec![]; + let mut vals = vec![]; + + if let Some(name) = sys.name() { + cols.push("name".into()); + vals.push(Value::String { + val: trim_cstyle_null(name), + span, + }); + } + if let Some(version) = sys.os_version() { + cols.push("os version".into()); + vals.push(Value::String { + val: trim_cstyle_null(version), + span, + }); + } + if let Some(version) = sys.kernel_version() { + cols.push("kernel version".into()); + vals.push(Value::String { + val: trim_cstyle_null(version), + span, + }); + } + if let Some(hostname) = sys.host_name() { + cols.push("hostname".into()); + vals.push(Value::String { + val: trim_cstyle_null(hostname), + span, + }); + } + cols.push("uptime".into()); + vals.push(Value::Duration { + val: 1000000000 * sys.uptime() as i64, + span, + }); + + let mut users = vec![]; + for user in sys.users() { + let mut cols = vec![]; + let mut vals = vec![]; + + cols.push("name".into()); + vals.push(Value::String { + val: trim_cstyle_null(user.name().to_string()), + span, + }); + + let mut groups = vec![]; + for group in user.groups() { + groups.push(Value::String { + val: trim_cstyle_null(group.to_string()), + span, + }); + } + + cols.push("groups".into()); + vals.push(Value::List { vals: groups, span }); + + users.push(Value::Record { cols, vals, span }); + } + if !users.is_empty() { + cols.push("sessions".into()); + vals.push(Value::List { vals: users, span }); + } + + Some(Value::Record { cols, vals, span }) +} + +pub fn temp(sys: &mut System, span: Span) -> Option { + sys.refresh_components(); + sys.refresh_components_list(); + + let mut output = vec![]; + + for component in sys.components() { + let mut cols = vec![]; + let mut vals = vec![]; + + cols.push("unit".into()); + vals.push(Value::String { + val: component.label().to_string(), + span, + }); + + cols.push("temp".into()); + vals.push(Value::Float { + val: component.temperature() as f64, + span, + }); + + cols.push("high".into()); + vals.push(Value::Float { + val: component.max() as f64, + span, + }); + + if let Some(critical) = component.critical() { + cols.push("critical".into()); + vals.push(Value::Float { + val: critical as f64, + span, + }); + } + output.push(Value::Record { cols, vals, span }); + } + if !output.is_empty() { + Some(Value::List { vals: output, span }) + } else { + None + } +} diff --git a/crates/nu-command/src/system/which_.rs b/crates/nu-command/src/system/which_.rs new file mode 100644 index 0000000000..529a38f1d3 --- /dev/null +++ b/crates/nu-command/src/system/which_.rs @@ -0,0 +1,256 @@ +use itertools::Itertools; +use log::trace; +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span, + Spanned, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Which; + +impl Command for Which { + fn name(&self) -> &str { + "which" + } + + fn signature(&self) -> Signature { + Signature::build("which") + .required("application", SyntaxShape::String, "application") + .rest("rest", SyntaxShape::String, "additional applications") + .switch("all", "list all executables", Some('a')) + .category(Category::System) + } + + fn usage(&self) -> &str { + "Finds a program file, alias or custom command." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + which(engine_state, stack, call) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Find if the 'myapp' application is available", + example: "which myapp", + result: None, + }] + } +} + +/// Shortcuts for creating an entry to the output table +fn entry(arg: impl Into, path: Value, builtin: bool, span: Span) -> Value { + let mut cols = vec![]; + let mut vals = vec![]; + + cols.push("arg".to_string()); + vals.push(Value::string(arg.into(), span)); + + cols.push("path".to_string()); + vals.push(path); + + cols.push("builtin".to_string()); + vals.push(Value::Bool { val: builtin, span }); + + Value::Record { cols, vals, span } +} + +macro_rules! create_entry { + ($arg:expr, $path:expr, $span:expr, $is_builtin:expr) => { + entry( + $arg.clone(), + Value::string($path.to_string(), $span), + $is_builtin, + $span, + ) + }; +} + +fn get_entries_in_aliases(engine_state: &EngineState, name: &str, span: Span) -> Vec { + let aliases = engine_state.find_aliases(name); + + let aliases = aliases + .into_iter() + .map(|spans| { + spans + .into_iter() + .map(|span| { + String::from_utf8_lossy(engine_state.get_span_contents(&span)).to_string() + }) + .join(" ") + }) + .map(|alias| create_entry!(name, format!("Nushell alias: {}", alias), span, false)) + .collect::>(); + trace!("Found {} aliases", aliases.len()); + aliases +} + +fn get_entries_in_custom_command(engine_state: &EngineState, name: &str, span: Span) -> Vec { + let custom_commands = engine_state.find_custom_commands(name); + + custom_commands + .into_iter() + .map(|_| create_entry!(name, "Nushell custom command", span, false)) + .collect::>() +} + +fn get_entry_in_commands(engine_state: &EngineState, name: &str, span: Span) -> Option { + if engine_state.find_decl(name.as_bytes()).is_some() { + Some(create_entry!(name, "Nushell built-in command", span, true)) + } else { + None + } +} + +fn get_entries_in_nu( + engine_state: &EngineState, + name: &str, + span: Span, + skip_after_first_found: bool, +) -> Vec { + let mut all_entries = vec![]; + + all_entries.extend(get_entries_in_aliases(engine_state, name, span)); + if !all_entries.is_empty() && skip_after_first_found { + return all_entries; + } + + all_entries.extend(get_entries_in_custom_command(engine_state, name, span)); + if !all_entries.is_empty() && skip_after_first_found { + return all_entries; + } + + if let Some(entry) = get_entry_in_commands(engine_state, name, span) { + all_entries.push(entry); + } + + all_entries +} + +#[allow(unused)] +macro_rules! entry_path { + ($arg:expr, $path:expr, $span:expr) => { + entry($arg.clone(), Value::string($path, $span), false, $span) + }; +} + +#[cfg(feature = "which")] +fn get_first_entry_in_path(item: &str, span: Span) -> Option { + which::which(item) + .map(|path| entry_path!(item, path.to_string_lossy().to_string(), span)) + .ok() +} + +#[cfg(not(feature = "which"))] +fn get_first_entry_in_path(_: &str, _: Span) -> Option { + None +} + +#[cfg(feature = "which")] +fn get_all_entries_in_path(item: &str, span: Span) -> Vec { + which::which_all(&item) + .map(|iter| { + iter.map(|path| entry_path!(item, path.to_string_lossy().to_string(), span)) + .collect() + }) + .unwrap_or_default() +} +#[cfg(not(feature = "which"))] +fn get_all_entries_in_path(_: &str, _: Span) -> Vec { + vec![] +} + +#[derive(Debug)] +struct WhichArgs { + applications: Vec>, + all: bool, +} + +fn which_single(application: Spanned, all: bool, engine_state: &EngineState) -> Vec { + let (external, prog_name) = if application.item.starts_with('^') { + (true, application.item[1..].to_string()) + } else { + (false, application.item.clone()) + }; + + //If prog_name is an external command, don't search for nu-specific programs + //If all is false, we can save some time by only searching for the first matching + //program + //This match handles all different cases + match (all, external) { + (true, true) => get_all_entries_in_path(&prog_name, application.span), + (true, false) => { + let mut output: Vec = vec![]; + output.extend(get_entries_in_nu( + engine_state, + &prog_name, + application.span, + false, + )); + output.extend(get_all_entries_in_path(&prog_name, application.span)); + output + } + (false, true) => { + if let Some(entry) = get_first_entry_in_path(&prog_name, application.span) { + return vec![entry]; + } + vec![] + } + (false, false) => { + let nu_entries = get_entries_in_nu(engine_state, &prog_name, application.span, true); + if !nu_entries.is_empty() { + return vec![nu_entries[0].clone()]; + } else if let Some(entry) = get_first_entry_in_path(&prog_name, application.span) { + return vec![entry]; + } + vec![] + } + } +} + +fn which( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, +) -> Result { + let which_args = WhichArgs { + applications: call.rest(engine_state, stack, 0)?, + all: call.has_flag("all"), + }; + let ctrlc = engine_state.ctrlc.clone(); + + if which_args.applications.is_empty() { + return Err(ShellError::MissingParameter( + "application".into(), + call.head, + )); + } + + let mut output = vec![]; + + for app in which_args.applications { + let values = which_single(app, which_args.all, engine_state); + output.extend(values); + } + + Ok(output.into_iter().into_pipeline_data(ctrlc)) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + crate::test_examples(Which) + } +} diff --git a/crates/nu-command/src/utils.rs b/crates/nu-command/src/utils.rs deleted file mode 100644 index ed6dc62fcf..0000000000 --- a/crates/nu-command/src/utils.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod suggestions; -pub mod test_bins; diff --git a/crates/nu-command/src/utils/suggestions.rs b/crates/nu-command/src/utils/suggestions.rs deleted file mode 100644 index b96ee86ec0..0000000000 --- a/crates/nu-command/src/utils/suggestions.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::did_you_mean; -use nu_errors::ShellError; -use nu_protocol::Value; -use nu_source::Tagged; - -pub fn suggestions(tried: Tagged<&str>, for_value: &Value) -> ShellError { - let possibilities = did_you_mean(for_value, tried.to_string()); - - match possibilities { - Some(p) => ShellError::labeled_error( - "Unknown column", - format!("did you mean '{}'?", p[0]), - tried.tag(), - ), - None => ShellError::labeled_error( - "Unknown column", - "row does not contain this column", - tried.tag(), - ), - } -} diff --git a/crates/nu-command/src/viewers/griddle.rs b/crates/nu-command/src/viewers/griddle.rs new file mode 100644 index 0000000000..1da6f0829e --- /dev/null +++ b/crates/nu-command/src/viewers/griddle.rs @@ -0,0 +1,303 @@ +// use super::icons::{icon_for_file, iconify_style_ansi_to_nu}; +use super::icons::icon_for_file; +use lscolors::{LsColors, Style}; +use nu_engine::env_to_string; +use nu_engine::CallExt; +use nu_protocol::{ + ast::{Call, PathMember}, + engine::{Command, EngineState, Stack}, + Category, Config, IntoPipelineData, PipelineData, ShellError, Signature, Span, SyntaxShape, + Value, +}; +use nu_term_grid::grid::{Alignment, Cell, Direction, Filling, Grid, GridOptions}; +use terminal_size::{Height, Width}; + +#[derive(Clone)] +pub struct Griddle; + +impl Command for Griddle { + fn name(&self) -> &str { + "grid" + } + + fn usage(&self) -> &str { + "Renders the output to a textual terminal grid." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("grid") + .named( + "width", + SyntaxShape::Int, + "number of columns wide", + Some('w'), + ) + .switch("color", "draw output with color", Some('c')) + .named( + "separator", + SyntaxShape::String, + "character to separate grid with", + Some('s'), + ) + .category(Category::Viewers) + } + + fn extra_usage(&self) -> &str { + r#"grid was built to give a concise gridded layout for ls. however, +it determines what to put in the grid by looking for a column named +'name'. this works great for tables and records but for lists we +need to do something different. such as with '[one two three] | grid' +it creates a fake column called 'name' for these values so that it +prints out the list properly."# + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let width_param: Option = call.get_flag(engine_state, stack, "width")?; + let color_param: bool = call.has_flag("color"); + let separator_param: Option = call.get_flag(engine_state, stack, "separator")?; + let config = stack.get_config().unwrap_or_default(); + let env_str = match stack.get_env_var(engine_state, "LS_COLORS") { + Some(v) => Some(env_to_string("LS_COLORS", v, engine_state, stack, &config)?), + None => None, + }; + let use_grid_icons = config.use_grid_icons; + + match input { + PipelineData::Value(Value::List { vals, .. }, ..) => { + // dbg!("value::list"); + let data = convert_to_list(vals, &config, call.head); + if let Some(items) = data { + Ok(create_grid_output( + items, + call, + width_param, + color_param, + separator_param, + env_str, + use_grid_icons, + )?) + } else { + Ok(PipelineData::new(call.head)) + } + } + PipelineData::ListStream(stream, ..) => { + // dbg!("value::stream"); + let data = convert_to_list(stream, &config, call.head); + if let Some(items) = data { + Ok(create_grid_output( + items, + call, + width_param, + color_param, + separator_param, + env_str, + use_grid_icons, + )?) + } else { + // dbg!(data); + Ok(PipelineData::new(call.head)) + } + } + PipelineData::Value(Value::Record { cols, vals, .. }, ..) => { + // dbg!("value::record"); + let mut items = vec![]; + + for (i, (c, v)) in cols.into_iter().zip(vals.into_iter()).enumerate() { + items.push((i, c, v.into_string(", ", &config))) + } + + Ok(create_grid_output( + items, + call, + width_param, + color_param, + separator_param, + env_str, + use_grid_icons, + )?) + } + x => { + // dbg!("other value"); + // dbg!(x.get_type()); + Ok(x) + } + } + } +} + +fn strip_ansi(astring: &str) -> String { + if let Ok(bytes) = strip_ansi_escapes::strip(astring) { + String::from_utf8_lossy(&bytes).to_string() + } else { + astring.to_string() + } +} + +fn create_grid_output( + items: Vec<(usize, String, String)>, + call: &Call, + width_param: Option, + color_param: bool, + separator_param: Option, + env_str: Option, + use_grid_icons: bool, +) -> Result { + let ls_colors = match env_str { + Some(s) => LsColors::from_string(&s), + None => LsColors::default(), + }; + + let cols = if let Some(col) = width_param { + col.parse::().unwrap_or(80) + } else if let Some((Width(w), Height(_h))) = terminal_size::terminal_size() { + w + } else { + 80u16 + }; + let sep = if let Some(separator) = separator_param { + separator + } else { + " │ ".to_string() + }; + + let mut grid = Grid::new(GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Text(sep), + }); + + for (_row_index, header, value) in items { + // only output value if the header name is 'name' + if header == "name" { + if color_param { + if use_grid_icons { + let no_ansi = strip_ansi(&value); + let path = std::path::Path::new(&no_ansi); + let icon = icon_for_file(path, call.head)?; + let ls_colors_style = ls_colors.style_for_path(path); + // eprintln!("ls_colors_style: {:?}", &ls_colors_style); + + let icon_style = match ls_colors_style { + Some(c) => c.to_crossterm_style(), + None => crossterm::style::ContentStyle::default(), + }; + // eprintln!("icon_style: {:?}", &icon_style); + + let ansi_style = ls_colors_style + .map(Style::to_crossterm_style) + .unwrap_or_default(); + // eprintln!("ansi_style: {:?}", &ansi_style); + + let item = format!("{} {}", icon_style.apply(icon), ansi_style.apply(value)); + + let mut cell = Cell::from(item); + cell.alignment = Alignment::Left; + grid.add(cell); + } else { + let style = ls_colors.style_for_path(value.clone()); + let ansi_style = style.map(Style::to_crossterm_style).unwrap_or_default(); + let mut cell = Cell::from(ansi_style.apply(value).to_string()); + cell.alignment = Alignment::Left; + grid.add(cell); + } + } else { + let mut cell = Cell::from(value); + cell.alignment = Alignment::Left; + grid.add(cell); + } + } + } + + Ok( + if let Some(grid_display) = grid.fit_into_width(cols as usize) { + Value::String { + val: grid_display.to_string(), + span: call.head, + } + } else { + Value::String { + val: format!("Couldn't fit grid into {} columns!", cols), + span: call.head, + } + } + .into_pipeline_data(), + ) +} + +fn convert_to_list( + iter: impl IntoIterator, + config: &Config, + head: Span, +) -> Option> { + let mut iter = iter.into_iter().peekable(); + + if let Some(first) = iter.peek() { + let mut headers = first.columns(); + + if !headers.is_empty() { + headers.insert(0, "#".into()); + } + + let mut data = vec![]; + + for (row_num, item) in iter.enumerate() { + let mut row = vec![row_num.to_string()]; + + if headers.is_empty() { + row.push(item.into_string(", ", config)) + } else { + for header in headers.iter().skip(1) { + let result = match item { + Value::Record { .. } => { + item.clone().follow_cell_path(&[PathMember::String { + val: header.into(), + span: head, + }]) + } + _ => Ok(item.clone()), + }; + + match result { + Ok(value) => row.push(value.into_string(", ", config)), + Err(_) => row.push(String::new()), + } + } + } + + data.push(row); + } + + let mut h: Vec = headers.into_iter().collect(); + + // This is just a list + if h.is_empty() { + // let's fake the header + h.push("#".to_string()); + h.push("name".to_string()); + } + + // this tuple is (row_index, header_name, value) + let mut interleaved = vec![]; + for (i, v) in data.into_iter().enumerate() { + for (n, s) in v.into_iter().enumerate() { + if h.len() == 1 { + // always get the 1th element since this is a simple list + // and we hacked the header above because it was empty + // 0th element is an index, 1th element is the value + interleaved.push((i, h[1].clone(), s)) + } else { + interleaved.push((i, h[n].clone(), s)) + } + } + } + + Some(interleaved) + } else { + None + } +} diff --git a/crates/nu-command/src/viewers/icons.rs b/crates/nu-command/src/viewers/icons.rs new file mode 100644 index 0000000000..ed6a4b62d2 --- /dev/null +++ b/crates/nu-command/src/viewers/icons.rs @@ -0,0 +1,605 @@ +use lazy_static::lazy_static; +use nu_protocol::{ShellError, Span}; +use std::collections::HashMap; +use std::path::Path; + +// Attribution: Thanks exa. Most of this file is taken from around here +// https://github.com/ogham/exa/blob/dbd11d38042284cc890fdd91760c2f93b65e8553/src/output/icons.rs + +pub trait FileIcon { + fn icon_file(&self, file: &Path) -> Option; +} + +#[derive(Copy, Clone)] +pub enum Icons { + Audio, + Image, + Video, +} + +impl Icons { + pub fn value(self) -> char { + match self { + Self::Audio => '\u{f001}', + Self::Image => '\u{f1c5}', + Self::Video => '\u{f03d}', + } + } +} + +// keeping this for now in case we have to revert to ansi style instead of crossterm style +// Helper function to convert ansi_term style to nu_ansi_term. unfortunately +// this is necessary because ls_colors has a dependency on ansi_term vs nu_ansi_term +// double unfortunately, now we have a dependency on both. we may have to bring +// in ls_colors crate to nushell +// pub fn iconify_style_ansi_to_nu<'a>(style: ansi_term::Style) -> nu_ansi_term::Style { +// let bg = match style.background { +// Some(c) => match c { +// ansi_term::Color::Black => Some(nu_ansi_term::Color::Black), +// ansi_term::Color::Red => Some(nu_ansi_term::Color::Red), +// ansi_term::Color::Green => Some(nu_ansi_term::Color::Green), +// ansi_term::Color::Yellow => Some(nu_ansi_term::Color::Yellow), +// ansi_term::Color::Blue => Some(nu_ansi_term::Color::Blue), +// ansi_term::Color::Purple => Some(nu_ansi_term::Color::Purple), +// ansi_term::Color::Cyan => Some(nu_ansi_term::Color::Cyan), +// ansi_term::Color::White => Some(nu_ansi_term::Color::White), +// ansi_term::Color::Fixed(f) => Some(nu_ansi_term::Color::Fixed(f)), +// ansi_term::Color::RGB(r, g, b) => Some(nu_ansi_term::Color::Rgb(r, g, b)), +// }, +// None => None, +// }; + +// let fg = match style.foreground { +// Some(c) => match c { +// ansi_term::Color::Black => Some(nu_ansi_term::Color::Black), +// ansi_term::Color::Red => Some(nu_ansi_term::Color::Red), +// ansi_term::Color::Green => Some(nu_ansi_term::Color::Green), +// ansi_term::Color::Yellow => Some(nu_ansi_term::Color::Yellow), +// ansi_term::Color::Blue => Some(nu_ansi_term::Color::Blue), +// ansi_term::Color::Purple => Some(nu_ansi_term::Color::Purple), +// ansi_term::Color::Cyan => Some(nu_ansi_term::Color::Cyan), +// ansi_term::Color::White => Some(nu_ansi_term::Color::White), +// ansi_term::Color::Fixed(f) => Some(nu_ansi_term::Color::Fixed(f)), +// ansi_term::Color::RGB(r, g, b) => Some(nu_ansi_term::Color::Rgb(r, g, b)), +// }, +// None => None, +// }; + +// let nu_style = nu_ansi_term::Style { +// foreground: fg, +// background: bg, +// is_blink: style.is_blink, +// is_bold: style.is_bold, +// is_dimmed: style.is_dimmed, +// is_hidden: style.is_hidden, +// is_italic: style.is_italic, +// is_underline: style.is_underline, +// is_reverse: style.is_reverse, +// is_strikethrough: style.is_strikethrough, +// }; + +// nu_style +// .background +// .or(nu_style.foreground) +// .map(nu_ansi_term::Style::from) +// .unwrap_or_default() +// } + +lazy_static! { + static ref MAP_BY_NAME: HashMap<&'static str, char> = { + let mut m = HashMap::new(); + m.insert(".Trash", '\u{f1f8}'); //  + m.insert(".atom", '\u{e764}'); //  + m.insert(".bashprofile", '\u{e615}'); //  + m.insert(".bashrc", '\u{f489}'); //  + m.insert(".git", '\u{f1d3}'); //  + m.insert(".gitattributes", '\u{f1d3}'); //  + m.insert(".gitconfig", '\u{f1d3}'); //  + m.insert(".github", '\u{f408}'); //  + m.insert(".gitignore", '\u{f1d3}'); //  + m.insert(".gitmodules", '\u{f1d3}'); //  + m.insert(".rvm", '\u{e21e}'); //  + m.insert(".vimrc", '\u{e62b}'); //  + m.insert(".vscode", '\u{e70c}'); //  + m.insert(".zshrc", '\u{f489}'); //  + m.insert("Cargo.lock", '\u{e7a8}'); //  + m.insert("bin", '\u{e5fc}'); //  + m.insert("config", '\u{e5fc}'); //  + m.insert("docker-compose.yml", '\u{f308}'); //  + m.insert("Dockerfile", '\u{f308}'); //  + m.insert("ds_store", '\u{f179}'); //  + m.insert("gitignore_global", '\u{f1d3}'); //  + m.insert("gradle", '\u{e70e}'); //  + m.insert("gruntfile.coffee", '\u{e611}'); //  + m.insert("gruntfile.js", '\u{e611}'); //  + m.insert("gruntfile.ls", '\u{e611}'); //  + m.insert("gulpfile.coffee", '\u{e610}'); //  + m.insert("gulpfile.js", '\u{e610}'); //  + m.insert("gulpfile.ls", '\u{e610}'); //  + m.insert("hidden", '\u{f023}'); //  + m.insert("include", '\u{e5fc}'); //  + m.insert("lib", '\u{f121}'); //  + m.insert("localized", '\u{f179}'); //  + m.insert("Makefile", '\u{e779}'); //  + m.insert("node_modules", '\u{e718}'); //  + m.insert("npmignore", '\u{e71e}'); //  + m.insert("rubydoc", '\u{e73b}'); //  + m.insert("yarn.lock", '\u{e718}'); //  + + m + }; +} + +pub fn icon_for_file(file_path: &Path, span: Span) -> Result { + let extensions = Box::new(FileExtensions); + let fp = format!("{}", file_path.display()); + + if let Some(icon) = MAP_BY_NAME.get(&fp[..]) { + Ok(*icon) + } else if file_path.is_dir() { + let str = file_path + .file_name() + .ok_or_else(|| { + ShellError::SpannedLabeledError( + "File name error".into(), + "Unable to get file name".into(), + span, + ) + })? + .to_str() + .ok_or_else(|| { + ShellError::SpannedLabeledError( + "Unable to get str error".into(), + "Unable to convert to str file name".into(), + span, + ) + })?; + Ok(match str { + "bin" => '\u{e5fc}', //  + ".git" => '\u{f1d3}', //  + ".idea" => '\u{e7b5}', //  + _ => '\u{f115}', //  + }) + } else if let Some(icon) = extensions.icon_file(file_path) { + Ok(icon) + } else if let Some(ext) = file_path.extension().as_ref() { + let str = ext.to_str().ok_or_else(|| { + ShellError::SpannedLabeledError( + "Unable to get str error".into(), + "Unable to convert to str file name".into(), + span, + ) + })?; + Ok(match str { + "ai" => '\u{e7b4}', //  + "android" => '\u{e70e}', //  + "apk" => '\u{e70e}', //  + "apple" => '\u{f179}', //  + "avi" => '\u{f03d}', //  + "avro" => '\u{e60b}', //  + "awk" => '\u{f489}', //  + "bash" => '\u{f489}', //  + "bash_history" => '\u{f489}', //  + "bash_profile" => '\u{f489}', //  + "bashrc" => '\u{f489}', //  + "bat" => '\u{f17a}', //  + "bmp" => '\u{f1c5}', //  + "bz" => '\u{f410}', //  + "bz2" => '\u{f410}', //  + "c" => '\u{e61e}', //  + "c++" => '\u{e61d}', //  + "cab" => '\u{e70f}', //  + "cc" => '\u{e61d}', //  + "cfg" => '\u{e615}', //  + "class" => '\u{e256}', //  + "clj" => '\u{e768}', //  + "cljs" => '\u{e76a}', //  + "cls" => '\u{e600}', //  + "cmd" => '\u{e70f}', //  + "coffee" => '\u{f0f4}', //  + "conf" => '\u{e615}', //  + "cp" => '\u{e61d}', //  + "cpp" => '\u{e61d}', //  + "cs" => '\u{f81a}', //  + "csh" => '\u{f489}', //  + "cshtml" => '\u{f1fa}', //  + "csproj" => '\u{f81a}', //  + "css" => '\u{e749}', //  + "csv" => '\u{f1c3}', //  + "csx" => '\u{f81a}', //  + "cxx" => '\u{e61d}', //  + "d" => '\u{e7af}', //  + "dart" => '\u{e798}', //  + "db" => '\u{f1c0}', //  + "deb" => '\u{e77d}', //  + "diff" => '\u{f440}', //  + "djvu" => '\u{f02d}', //  + "dll" => '\u{e70f}', //  + "doc" => '\u{f1c2}', //  + "docx" => '\u{f1c2}', //  + "ds_store" => '\u{f179}', //  + "DS_store" => '\u{f179}', //  + "dump" => '\u{f1c0}', //  + "ebook" => '\u{e28b}', //  + "editorconfig" => '\u{e615}', //  + "ejs" => '\u{e618}', //  + "elm" => '\u{e62c}', //  + "env" => '\u{f462}', //  + "eot" => '\u{f031}', //  + "epub" => '\u{e28a}', //  + "erb" => '\u{e73b}', //  + "erl" => '\u{e7b1}', //  + "ex" => '\u{e62d}', //  + "exe" => '\u{f17a}', //  + "exs" => '\u{e62d}', //  + "fish" => '\u{f489}', //  + "flac" => '\u{f001}', //  + "flv" => '\u{f03d}', //  + "font" => '\u{f031}', //  + "gdoc" => '\u{f1c2}', //  + "gem" => '\u{e21e}', //  + "gemfile" => '\u{e21e}', //  + "gemspec" => '\u{e21e}', //  + "gform" => '\u{f298}', //  + "gif" => '\u{f1c5}', //  + "git" => '\u{f1d3}', //  + "gitattributes" => '\u{f1d3}', //  + "gitignore" => '\u{f1d3}', //  + "gitmodules" => '\u{f1d3}', //  + "go" => '\u{e626}', //  + "gradle" => '\u{e70e}', //  + "groovy" => '\u{e775}', //  + "gsheet" => '\u{f1c3}', //  + "gslides" => '\u{f1c4}', //  + "guardfile" => '\u{e21e}', //  + "gz" => '\u{f410}', //  + "h" => '\u{f0fd}', //  + "hbs" => '\u{e60f}', //  + "hpp" => '\u{f0fd}', //  + "hs" => '\u{e777}', //  + "htm" => '\u{f13b}', //  + "html" => '\u{f13b}', //  + "hxx" => '\u{f0fd}', //  + "ico" => '\u{f1c5}', //  + "image" => '\u{f1c5}', //  + "iml" => '\u{e7b5}', //  + "ini" => '\u{f17a}', //  + "ipynb" => '\u{e606}', //  + "iso" => '\u{e271}', //  + "jad" => '\u{e256}', //  + "jar" => '\u{e204}', //  + "java" => '\u{e204}', //  + "jpeg" => '\u{f1c5}', //  + "jpg" => '\u{f1c5}', //  + "js" => '\u{e74e}', //  + "json" => '\u{e60b}', //  + "jsx" => '\u{e7ba}', //  + "ksh" => '\u{f489}', //  + "latex" => '\u{e600}', //  + "less" => '\u{e758}', //  + "lhs" => '\u{e777}', //  + "license" => '\u{f718}', //  + "localized" => '\u{f179}', //  + "lock" => '\u{f023}', //  + "log" => '\u{f18d}', //  + "lua" => '\u{e620}', //  + "lz" => '\u{f410}', //  + "lzh" => '\u{f410}', //  + "lzma" => '\u{f410}', //  + "lzo" => '\u{f410}', //  + "m" => '\u{e61e}', //  + "mm" => '\u{e61d}', //  + "m4a" => '\u{f001}', //  + "markdown" => '\u{f48a}', //  + "md" => '\u{f48a}', //  + "mjs" => '\u{e74e}', //  + "mkd" => '\u{f48a}', //  + "mkv" => '\u{f03d}', //  + "mobi" => '\u{e28b}', //  + "mov" => '\u{f03d}', //  + "mp3" => '\u{f001}', //  + "mp4" => '\u{f03d}', //  + "msi" => '\u{e70f}', //  + "mustache" => '\u{e60f}', //  + "nix" => '\u{f313}', //  + "node" => '\u{f898}', //  + "npmignore" => '\u{e71e}', //  + "odp" => '\u{f1c4}', //  + "ods" => '\u{f1c3}', //  + "odt" => '\u{f1c2}', //  + "ogg" => '\u{f001}', //  + "ogv" => '\u{f03d}', //  + "otf" => '\u{f031}', //  + "patch" => '\u{f440}', //  + "pdf" => '\u{f1c1}', //  + "php" => '\u{e73d}', //  + "pl" => '\u{e769}', //  + "png" => '\u{f1c5}', //  + "ppt" => '\u{f1c4}', //  + "pptx" => '\u{f1c4}', //  + "procfile" => '\u{e21e}', //  + "properties" => '\u{e60b}', //  + "ps1" => '\u{f489}', //  + "psd" => '\u{e7b8}', //  + "pxm" => '\u{f1c5}', //  + "py" => '\u{e606}', //  + "pyc" => '\u{e606}', //  + "r" => '\u{f25d}', //  + "rakefile" => '\u{e21e}', //  + "rar" => '\u{f410}', //  + "razor" => '\u{f1fa}', //  + "rb" => '\u{e21e}', //  + "rdata" => '\u{f25d}', //  + "rdb" => '\u{e76d}', //  + "rdoc" => '\u{f48a}', //  + "rds" => '\u{f25d}', //  + "readme" => '\u{f48a}', //  + "rlib" => '\u{e7a8}', //  + "rmd" => '\u{f48a}', //  + "rpm" => '\u{e7bb}', //  + "rs" => '\u{e7a8}', //  + "rspec" => '\u{e21e}', //  + "rspec_parallel" => '\u{e21e}', //  + "rspec_status" => '\u{e21e}', //  + "rss" => '\u{f09e}', //  + "rtf" => '\u{f718}', //  + "ru" => '\u{e21e}', //  + "rubydoc" => '\u{e73b}', //  + "sass" => '\u{e603}', //  + "scala" => '\u{e737}', //  + "scss" => '\u{e749}', //  + "sh" => '\u{f489}', //  + "shell" => '\u{f489}', //  + "slim" => '\u{e73b}', //  + "sln" => '\u{e70c}', //  + "so" => '\u{f17c}', //  + "sql" => '\u{f1c0}', //  + "sqlite3" => '\u{e7c4}', //  + "styl" => '\u{e600}', //  + "stylus" => '\u{e600}', //  + "svg" => '\u{f1c5}', //  + "swift" => '\u{e755}', //  + "tar" => '\u{f410}', //  + "taz" => '\u{f410}', //  + "tbz" => '\u{f410}', //  + "tbz2" => '\u{f410}', //  + "tex" => '\u{e600}', //  + "tiff" => '\u{f1c5}', //  + "toml" => '\u{e615}', //  + "ts" => '\u{e628}', //  + "tsv" => '\u{f1c3}', //  + "tsx" => '\u{e7ba}', //  + "ttf" => '\u{f031}', //  + "twig" => '\u{e61c}', //  + "txt" => '\u{f15c}', //  + "tz" => '\u{f410}', //  + "tzo" => '\u{f410}', //  + "video" => '\u{f03d}', //  + "vim" => '\u{e62b}', //  + "vue" => '\u{fd42}', // ﵂ + "war" => '\u{e256}', //  + "wav" => '\u{f001}', //  + "webm" => '\u{f03d}', //  + "webp" => '\u{f1c5}', //  + "windows" => '\u{f17a}', //  + "woff" => '\u{f031}', //  + "woff2" => '\u{f031}', //  + "xhtml" => '\u{f13b}', //  + "xls" => '\u{f1c3}', //  + "xlsx" => '\u{f1c3}', //  + "xml" => '\u{fabf}', // 謹 + "xul" => '\u{fabf}', // 謹 + "xz" => '\u{f410}', //  + "yaml" => '\u{f481}', //  + "yml" => '\u{f481}', //  + "zip" => '\u{f410}', //  + "zsh" => '\u{f489}', //  + "zsh-theme" => '\u{f489}', //  + "zshrc" => '\u{f489}', //  + _ => '\u{f15b}', //  + }) + } else { + Ok('\u{f016}') + } +} + +/// Whether this file’s extension is any of the strings that get passed in. +/// +/// This will always return `false` if the file has no extension. +pub fn extension_is_one_of(path: &Path, choices: &[&str]) -> bool { + match path.extension() { + Some(os_ext) => match os_ext.to_str() { + Some(ext) => choices.contains(&ext), + None => false, + }, + None => false, + } +} + +/// Whether this file’s name, including extension, is any of the strings +/// that get passed in. +// pub fn name_is_one_of(name: &str, choices: &[&str]) -> bool { +// choices.contains(&&name[..]) +// } + +#[derive(Debug, Default, PartialEq)] +pub struct FileExtensions; + +// TODO: We may want to re-add these FileExtensions impl fns back. I have disabled +// it now because it's hard coding colors which kind of defeats the LS_COLORS +// functionality. We may want to enable and augment at some point. + +impl FileExtensions { + // /// An “immediate” file is something that can be run or activated somehow + // /// in order to kick off the build of a project. It’s usually only present + // /// in directories full of source code. + // #[allow(clippy::case_sensitive_file_extension_comparisons)] + // #[allow(dead_code)] + // fn is_immediate(&self, file_path: &Path) -> bool { + // file_path + // .file_name() + // .unwrap() + // .to_str() + // .unwrap() + // .to_lowercase() + // .starts_with("readme") + // || file_path + // .file_name() + // .unwrap() + // .to_str() + // .unwrap() + // .ends_with(".ninja") + // || name_is_one_of( + // file_path.file_name().unwrap().to_str().unwrap(), + // &[ + // "Makefile", + // "Cargo.toml", + // "SConstruct", + // "CMakeLists.txt", + // "build.gradle", + // "pom.xml", + // "Rakefile", + // "package.json", + // "Gruntfile.js", + // "Gruntfile.coffee", + // "BUILD", + // "BUILD.bazel", + // "WORKSPACE", + // "build.xml", + // "Podfile", + // "webpack.config.js", + // "meson.build", + // "composer.json", + // "RoboFile.php", + // "PKGBUILD", + // "Justfile", + // "Procfile", + // "Dockerfile", + // "Containerfile", + // "Vagrantfile", + // "Brewfile", + // "Gemfile", + // "Pipfile", + // "build.sbt", + // "mix.exs", + // "bsconfig.json", + // "tsconfig.json", + // ], + // ) + // } + + fn is_image(&self, file: &Path) -> bool { + extension_is_one_of( + file, + &[ + "png", "jfi", "jfif", "jif", "jpe", "jpeg", "jpg", "gif", "bmp", "tiff", "tif", + "ppm", "pgm", "pbm", "pnm", "webp", "raw", "arw", "svg", "stl", "eps", "dvi", "ps", + "cbr", "jpf", "cbz", "xpm", "ico", "cr2", "orf", "nef", "heif", "avif", "jxl", + ], + ) + } + + fn is_video(&self, file: &Path) -> bool { + extension_is_one_of( + file, + &[ + "avi", "flv", "m2v", "m4v", "mkv", "mov", "mp4", "mpeg", "mpg", "ogm", "ogv", + "vob", "wmv", "webm", "m2ts", "heic", + ], + ) + } + + fn is_music(&self, file: &Path) -> bool { + extension_is_one_of(file, &["aac", "m4a", "mp3", "ogg", "wma", "mka", "opus"]) + } + + // Lossless music, rather than any other kind of data... + fn is_lossless(&self, file: &Path) -> bool { + extension_is_one_of(file, &["alac", "ape", "flac", "wav"]) + } + + // #[allow(dead_code)] + // fn is_crypto(&self, file: &Path) -> bool { + // extension_is_one_of( + // file, + // &["asc", "enc", "gpg", "pgp", "sig", "signature", "pfx", "p12"], + // ) + // } + + // #[allow(dead_code)] + // fn is_document(&self, file: &Path) -> bool { + // extension_is_one_of( + // file, + // &[ + // "djvu", "doc", "docx", "dvi", "eml", "eps", "fotd", "key", "keynote", "numbers", + // "odp", "odt", "pages", "pdf", "ppt", "pptx", "rtf", "xls", "xlsx", + // ], + // ) + // } + + // #[allow(dead_code)] + // fn is_compressed(&self, file: &Path) -> bool { + // extension_is_one_of( + // file, + // &[ + // "zip", "tar", "Z", "z", "gz", "bz2", "a", "ar", "7z", "iso", "dmg", "tc", "rar", + // "par", "tgz", "xz", "txz", "lz", "tlz", "lzma", "deb", "rpm", "zst", "lz4", + // ], + // ) + // } + + // #[allow(dead_code)] + // fn is_temp(&self, file: &Path) -> bool { + // file.file_name().unwrap().to_str().unwrap().ends_with('~') + // || (file.file_name().unwrap().to_str().unwrap().starts_with('#') + // && file.file_name().unwrap().to_str().unwrap().ends_with('#')) + // || extension_is_one_of(file, &["tmp", "swp", "swo", "swn", "bak", "bkp", "bk"]) + // } + + // #[allow(dead_code)] + // fn is_compiled(&self, file: &Path) -> bool { + // if extension_is_one_of(file, &["class", "elc", "hi", "o", "pyc", "zwc", "ko"]) { + // true + // // } else if let Some(dir) = file.parent() { + // // file.get_source_files() + // // .iter() + // // .any(|path| dir.contains(path)) + // } else { + // false + // } + // } + // } + + // impl FileColours for FileExtensions { + // fn colour_file(&self, file: &Path) -> OptionChange to a new path.

Usage:
> cd (directory) {flags}

Parameters:
(directory) the directory to change to

Flags:
-h, --help: Display this help message

Examples:
Change to a new directory called 'dirname'
> cd dirname

Change to your home directory
>
cd

Change to your home directory (alternate version)
>
cd
~

Change to the previous directory
>
cd
-

" + r"Usage:
> cd (path)

Flags:
-h, --help
Display this help message

Parameters:
(optional) path: the path to change to

" ); } @@ -71,7 +71,7 @@ fn test_no_color_flag() { ); assert_eq!( actual.out, - r"Change to a new path.

Usage:
> cd (directory) {flags}

Parameters:
(directory) the directory to change to

Flags:
-h, --help: Display this help message

Examples:
Change to a new directory called 'dirname'
> cd dirname

Change to your home directory
> cd

Change to your home directory (alternate version)
> cd ~

Change to the previous directory
> cd -

" + r"Usage:
> cd (path)

Flags:
-h, --help
Display this help message

Parameters:
(optional) path: the path to change to

" ); } @@ -86,6 +86,6 @@ fn test_html_color_where_flag_dark_false() { ); assert_eq!( actual.out, - r"Filter table to match the condition.

Usage:
> where <condition> {flags}

Parameters:
<condition> the condition that must match

Flags:
-h, --help: Display this help message

Examples:
List all files in the current directory with sizes greater than 2kb
> ls | where size > 2kb

List only the files in the current directory
>
ls
| where type == File

List all files with names that contain "Car"
>
ls
| where name =~ "Car"

List all files that were modified in the last two weeks
>
ls
| where modified <= 2wk

" + r"Usage:
> where <cond>

Flags:
-h, --help
Display this help message

Parameters:
cond: condition

" ); } diff --git a/crates/nu-command/tests/format_conversions/ics.rs b/crates/nu-command/tests/format_conversions/ics.rs index 7e727cb723..ccaa6318cf 100644 --- a/crates/nu-command/tests/format_conversions/ics.rs +++ b/crates/nu-command/tests/format_conversions/ics.rs @@ -47,7 +47,7 @@ fn infers_types() { cwd: dirs.test(), pipeline( r#" open calendar.ics - | get events + | get events.0 | length "# )); @@ -86,8 +86,8 @@ fn from_ics_text_to_table() { r#" open calendar.txt | from ics - | get events - | get properties + | get events.0 + | get properties.0 | where name == "SUMMARY" | first | get value diff --git a/crates/nu-command/tests/format_conversions/ods.rs b/crates/nu-command/tests/format_conversions/ods.rs index 5d5ce7ca7a..c322e914e6 100644 --- a/crates/nu-command/tests/format_conversions/ods.rs +++ b/crates/nu-command/tests/format_conversions/ods.rs @@ -22,7 +22,8 @@ fn from_ods_file_to_table_select_sheet() { r#" open sample_data.ods --raw | from ods -s ["SalesOrders"] - | get + | columns + | get 0 "# )); diff --git a/crates/nu-command/tests/format_conversions/vcf.rs b/crates/nu-command/tests/format_conversions/vcf.rs index 9d03dd6254..ee4bf1bb52 100644 --- a/crates/nu-command/tests/format_conversions/vcf.rs +++ b/crates/nu-command/tests/format_conversions/vcf.rs @@ -70,7 +70,7 @@ fn from_vcf_text_to_table() { r#" open contacts.txt | from vcf - | get properties + | get properties.0 | where name == "EMAIL" | first | get value diff --git a/crates/nu-command/tests/format_conversions/xlsx.rs b/crates/nu-command/tests/format_conversions/xlsx.rs index 36b3aca17a..961c305cae 100644 --- a/crates/nu-command/tests/format_conversions/xlsx.rs +++ b/crates/nu-command/tests/format_conversions/xlsx.rs @@ -22,7 +22,8 @@ fn from_excel_file_to_table_select_sheet() { r#" open sample_data.xlsx --raw | from xlsx -s ["SalesOrders"] - | get + | columns + | get 0 "# )); diff --git a/crates/nu-command/tests/format_conversions/xml.rs b/crates/nu-command/tests/format_conversions/xml.rs index 068296195a..15a62d6907 100644 --- a/crates/nu-command/tests/format_conversions/xml.rs +++ b/crates/nu-command/tests/format_conversions/xml.rs @@ -8,7 +8,7 @@ fn table_to_xml_text_and_from_xml_text_back_into_table() { open jonathan.xml | to xml | from xml - | get rss.children.channel.children.0.item.children.0.guid.attributes.isPermaLink + | get rss.children.channel.children.0.3.item.children.guid.4.attributes.isPermaLink "# )); diff --git a/crates/nu-command/tests/main.rs b/crates/nu-command/tests/main.rs index bd02ef653b..62f9eb37d2 100644 --- a/crates/nu-command/tests/main.rs +++ b/crates/nu-command/tests/main.rs @@ -1,18 +1,26 @@ +use nu_command::create_default_context; +use nu_protocol::engine::StateWorkingSet; use quickcheck_macros::quickcheck; mod commands; mod format_conversions; -use nu_engine::EvaluationContext; +// use nu_engine::EvaluationContext; #[quickcheck] fn quickcheck_parse(data: String) -> bool { - let (tokens, err) = nu_parser::lex(&data, 0, nu_parser::NewlineMode::Normal); - let (lite_block, err2) = nu_parser::parse_block(tokens); + let (tokens, err) = nu_parser::lex(data.as_bytes(), 0, b"", b"", true); + let (lite_block, err2) = nu_parser::lite_parse(&tokens); if err.is_none() && err2.is_none() { - let context = EvaluationContext::basic(); - let _ = nu_parser::classify_block(&lite_block, &context.scope); + let cwd = std::env::current_dir().expect("Could not get current working directory."); + let context = create_default_context(cwd); + { + let mut working_set = StateWorkingSet::new(&context); + working_set.add_file("quickcheck".into(), data.as_bytes()); + + let _ = nu_parser::parse_block(&mut working_set, &lite_block, false); + } } true } diff --git a/crates/nu-completion/Cargo.toml b/crates/nu-completion/Cargo.toml deleted file mode 100644 index 96e0a767aa..0000000000 --- a/crates/nu-completion/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -authors = ["The Nu Project Contributors"] -description = "Completions for nushell" -edition = "2018" -license = "MIT" -name = "nu-completion" -version = "0.44.0" - -[lib] -doctest = false - -[dependencies] -nu-engine = { version = "0.44.0", path="../nu-engine" } -nu-data = { version = "0.44.0", path="../nu-data" } -nu-parser = { version = "0.44.0", path="../nu-parser" } -nu-path = { version = "0.44.0", path="../nu-path" } -nu-protocol = { version = "0.44.0", path="../nu-protocol" } -nu-source = { version = "0.44.0", path="../nu-source" } -nu-test-support = { version = "0.44.0", path="../nu-test-support" } -indexmap = { version="1.6.1", features=["serde-1"] } - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -is_executable = "1.0.1" - -[dev-dependencies] -parking_lot = "0.11.1" diff --git a/crates/nu-completion/src/command.rs b/crates/nu-completion/src/command.rs deleted file mode 100644 index 7db121de26..0000000000 --- a/crates/nu-completion/src/command.rs +++ /dev/null @@ -1,91 +0,0 @@ -use super::matchers::Matcher; -use crate::{Completer, CompletionContext, Suggestion}; -use indexmap::set::IndexSet; -#[cfg(not(target_arch = "wasm32"))] -use is_executable::IsExecutable; -use nu_test_support::NATIVE_PATH_ENV_VAR; -use std::iter::FromIterator; -use std::path::Path; - -pub struct CommandCompleter; - -impl Completer for CommandCompleter -where - Context: CompletionContext, -{ - fn complete(&self, ctx: &Context, partial: &str, matcher: &dyn Matcher) -> Vec { - let registry = ctx.signature_registry(); - let mut commands: IndexSet = IndexSet::from_iter(registry.names()); - - // Command suggestions can come from three possible sets: - // 1. internal command names, - // 2. external command names relative to PATH env var, and - // 3. any other executable (that matches what's been typed so far). - - let path_executables = find_path_executables().unwrap_or_default(); - - // TODO quote these, if necessary - commands.extend(path_executables); - - let mut suggestions: Vec<_> = commands - .into_iter() - .filter(|v| matcher.matches(partial, v)) - .map(|v| Suggestion { - replacement: v.clone(), - display: v, - }) - .collect(); - - if !partial.is_empty() { - let path_completer = crate::path::PathCompleter; - let path_results = path_completer.path_suggestions(partial, matcher); - let iter = path_results.into_iter().filter_map(|path_suggestion| { - let path = path_suggestion.path; - if path.is_dir() || is_executable(&path) { - Some(path_suggestion.suggestion) - } else { - None - } - }); - - suggestions.extend(iter); - } - - suggestions - } -} - -#[cfg(not(target_arch = "wasm32"))] -fn is_executable(path: &Path) -> bool { - // This call to a crate essentially checks the PATHEXT on Windows and does some - // low level WinAPI calls to determine if the file is executable. It seems quite - // a bit faster than calling path.metadata(). - // On Unix, this checks the file metadata. The underlying code traverses symlinks. - path.is_executable() -} - -#[cfg(target_arch = "wasm32")] -fn is_executable(_path: &Path) -> bool { - false -} - -// TODO cache these, but watch for changes to PATH -fn find_path_executables() -> Option> { - let path_var = std::env::var_os(NATIVE_PATH_ENV_VAR)?; - let paths: Vec<_> = std::env::split_paths(&path_var).collect(); - - let mut executables: IndexSet = IndexSet::new(); - for path in paths { - if let Ok(mut contents) = std::fs::read_dir(path) { - while let Some(Ok(item)) = contents.next() { - if is_executable(&item.path()) { - if let Ok(name) = item.file_name().into_string() { - executables.insert(name); - } - } - } - } - } - - Some(executables) -} diff --git a/crates/nu-completion/src/completer.rs b/crates/nu-completion/src/completer.rs deleted file mode 100644 index def926f021..0000000000 --- a/crates/nu-completion/src/completer.rs +++ /dev/null @@ -1,191 +0,0 @@ -use std::borrow::Cow; - -use nu_parser::NewlineMode; -use nu_source::{Span, Tag}; - -use crate::command::CommandCompleter; -use crate::engine; -use crate::flag::FlagCompleter; -use crate::matchers; -use crate::matchers::Matcher; -use crate::path::{PathCompleter, PathSuggestion}; -use crate::variable::VariableCompleter; -use crate::{Completer, CompletionContext, Suggestion}; - -pub struct NuCompleter {} - -impl NuCompleter {} - -impl NuCompleter { - pub fn complete( - &self, - line: &str, - pos: usize, - context: &Context, - ) -> (usize, Vec) { - use engine::LocationType; - - let tokens = nu_parser::lex(line, 0, NewlineMode::Normal).0; - - let locations = Some(nu_parser::parse_block(tokens).0) - .map(|block| nu_parser::classify_block(&block, context.scope())) - .map(|(block, _)| engine::completion_location(line, &block, pos)) - .unwrap_or_default(); - - let matcher = nu_data::config::config(Tag::unknown()) - .ok() - .and_then(|cfg| cfg.get("line_editor").cloned()) - .and_then(|le| { - le.row_entries() - .find(|&(idx, _value)| idx == "completion_match_method") - .and_then(|(_idx, value)| value.as_string().ok()) - }) - .unwrap_or_else(String::new); - - let matcher = matcher.as_str(); - let matcher: &dyn Matcher = match matcher { - "case-insensitive" => &matchers::case_insensitive::Matcher, - "case-sensitive" => &matchers::case_sensitive::Matcher, - #[cfg(target_os = "windows")] - _ => &matchers::case_insensitive::Matcher, - #[cfg(not(target_os = "windows"))] - _ => &matchers::case_sensitive::Matcher, - }; - - if locations.is_empty() { - (pos, Vec::new()) - } else { - let mut pos = locations[0].span.start(); - - for location in &locations { - if location.span.start() < pos { - pos = location.span.start(); - } - } - let suggestions = locations - .into_iter() - .flat_map(|location| { - let partial = location.span.slice(line).to_string(); - match location.item { - LocationType::Command => { - let command_completer = CommandCompleter; - command_completer.complete(context, &partial, matcher.to_owned()) - } - - LocationType::Flag(cmd) => { - let flag_completer = FlagCompleter { cmd }; - flag_completer.complete(context, &partial, matcher.to_owned()) - } - - LocationType::Argument(cmd, _arg_name) => { - let path_completer = PathCompleter; - let prepend = Span::new(pos, location.span.start()).slice(line); - - const QUOTE_CHARS: &[char] = &['\'', '"']; - - // TODO Find a better way to deal with quote chars. Can the completion - // engine relay this back to us? Maybe have two spans: inner and - // outer. The former is what we want to complete, the latter what - // we'd need to replace. - let (quote_char, partial) = if partial.starts_with(QUOTE_CHARS) { - let (head, tail) = partial.split_at(1); - (Some(head), tail.to_string()) - } else { - (None, partial) - }; - - let (mut partial, quoted) = if let Some(quote_char) = quote_char { - if partial.ends_with(quote_char) { - (partial[..partial.len() - 1].to_string(), true) - } else { - (partial, false) - } - } else { - (partial, false) - }; - - partial = partial.split('"').collect::>().join(""); - let completed_paths = - path_completer.path_suggestions(&partial, matcher); - match cmd.as_deref().unwrap_or("") { - "cd" => select_directory_suggestions(completed_paths), - _ => completed_paths, - } - .into_iter() - .map(|s| Suggestion { - replacement: format!( - "{}{}", - prepend, - requote(s.suggestion.replacement, quoted) - ), - display: s.suggestion.display, - }) - .collect() - } - - LocationType::Variable => { - let variable_completer = VariableCompleter; - variable_completer.complete(context, &partial, matcher.to_owned()) - } - } - }) - .collect(); - - (pos, suggestions) - } - } -} - -fn select_directory_suggestions(completed_paths: Vec) -> Vec { - completed_paths - .into_iter() - .filter(|suggestion| { - suggestion - .path - .metadata() - .map(|md| md.is_dir()) - .unwrap_or(false) - }) - .collect() -} - -fn requote(orig_value: String, previously_quoted: bool) -> String { - let value: Cow = { - #[cfg(feature = "rustyline-support")] - { - rustyline::completion::unescape(&orig_value, Some('\\')) - } - #[cfg(not(feature = "rustyline-support"))] - { - orig_value.into() - } - }; - - let mut quotes = vec!['"', '\'']; - let mut should_quote = false; - for c in value.chars() { - if c.is_whitespace() || c == '#' { - should_quote = true; - } else if let Some(index) = quotes.iter().position(|q| *q == c) { - should_quote = true; - quotes.swap_remove(index); - } - } - - if should_quote { - if quotes.is_empty() { - // TODO we don't really have an escape character, so there isn't a great option right - // now. One possibility is `{{(char backtick)}}` - value.to_string() - } else { - let quote = quotes[0]; - if previously_quoted { - format!("{}{}", quote, value) - } else { - format!("{}{}{}", quote, value, quote) - } - } - } else { - value.to_string() - } -} diff --git a/crates/nu-completion/src/engine.rs b/crates/nu-completion/src/engine.rs deleted file mode 100644 index 67158548ef..0000000000 --- a/crates/nu-completion/src/engine.rs +++ /dev/null @@ -1,497 +0,0 @@ -use nu_protocol::hir::*; -use nu_source::{Span, Spanned, SpannedItem}; - -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum LocationType { - Command, - Flag(String), // command name - Argument(Option, Option), // command name, argument name - Variable, -} - -pub type CompletionLocation = Spanned; - -// TODO The below is very similar to shapes / expression_to_flat_shape. Check back October 2020 -// to see if we're close enough to just make use of those. - -struct Flatten<'s> { - line: &'s str, - command: Option, - flag: Option, -} - -impl<'s> Flatten<'s> { - /// Converts a SpannedExpression into a completion location for use in NuCompleter - fn expression(&self, e: &SpannedExpression) -> Vec { - match &e.expr { - Expression::Block(block) => self.completion_locations(block), - Expression::Subexpression(block) => self.completion_locations(block), - Expression::List(exprs) => exprs.iter().flat_map(|v| self.expression(v)).collect(), - Expression::Table(headers, cells) => headers - .iter() - .flat_map(|v| self.expression(v)) - .chain( - cells - .iter() - .flat_map(|v| v.iter().flat_map(|v| self.expression(v))), - ) - .collect(), - Expression::Command => vec![LocationType::Command.spanned(e.span)], - Expression::FullColumnPath(path) => self.expression(&path.head), - Expression::Variable(_, _) => vec![LocationType::Variable.spanned(e.span)], - - Expression::Boolean(_) - | Expression::FilePath(_) - | Expression::Literal(Literal::ColumnPath(_)) - | Expression::Literal(Literal::GlobPattern(_)) - | Expression::Literal(Literal::Number(_)) - | Expression::Literal(Literal::Size(_, _)) - | Expression::Literal(Literal::String(_)) => { - vec![ - LocationType::Argument(self.command.clone(), self.flag.clone()).spanned(e.span), - ] - } - - Expression::Binary(binary) => { - let mut result = Vec::new(); - result.append(&mut self.expression(&binary.left)); - result.append(&mut self.expression(&binary.right)); - result - } - Expression::Range(range) => { - let mut result = Vec::new(); - if let Some(left) = &range.left { - result.append(&mut self.expression(left)); - } - if let Some(right) = &range.right { - result.append(&mut self.expression(right)); - } - result - } - - Expression::ExternalWord - | Expression::ExternalCommand(_) - | Expression::Synthetic(_) - | Expression::Literal(Literal::Operator(_)) - | Expression::Literal(Literal::Bare(_)) - | Expression::Garbage => Vec::new(), - } - } - - fn internal_command(&self, internal: &InternalCommand) -> Vec { - let mut result = Vec::new(); - - match internal.args.head.expr { - Expression::Command => { - result.push(LocationType::Command.spanned(internal.name_span)); - } - Expression::Literal(Literal::String(_)) => { - result.push(LocationType::Command.spanned(internal.name_span)); - } - _ => (), - } - - if let Some(positionals) = &internal.args.positional { - let mut positionals = positionals.iter(); - - if internal.name == "run_external" { - if let Some(external_command) = positionals.next() { - result.push(LocationType::Command.spanned(external_command.span)); - } - } - - result.extend(positionals.flat_map(|positional| match positional.expr { - Expression::Garbage => { - let garbage = positional.span.slice(self.line); - let location = if garbage.starts_with('-') { - LocationType::Flag(internal.name.clone()) - } else { - // TODO we may be able to map this to the name of a positional, - // but we'll need a signature - LocationType::Argument(Some(internal.name.clone()), None) - }; - - vec![location.spanned(positional.span)] - } - - _ => self.expression(positional), - })); - } - - if let Some(named) = &internal.args.named { - for (name, kind) in &named.named { - match kind { - NamedValue::PresentSwitch(span) => { - result.push(LocationType::Flag(internal.name.clone()).spanned(*span)); - } - - NamedValue::Value(span, expr) => { - result.push(LocationType::Flag(internal.name.clone()).spanned(*span)); - result.append(&mut self.with_flag(name.clone()).expression(expr)); - } - - _ => (), - } - } - } - - result - } - - fn pipeline(&self, pipeline: &Pipeline) -> Vec { - let mut result = Vec::new(); - - for command in &pipeline.list { - match command { - ClassifiedCommand::Internal(internal) => { - let engine = self.with_command(internal.name.clone()); - result.append(&mut engine.internal_command(internal)); - } - - ClassifiedCommand::Expr(expr) => result.append(&mut self.expression(expr)), - _ => (), - } - } - - result - } - - /// Flattens the block into a Vec of completion locations - pub fn completion_locations(&self, block: &Block) -> Vec { - block - .block - .iter() - .flat_map(|g| g.pipelines.iter().flat_map(|v| self.pipeline(v))) - .collect() - } - - pub fn new(line: &'s str) -> Flatten<'s> { - Flatten { - line, - command: None, - flag: None, - } - } - - pub fn with_command(&self, command: String) -> Flatten<'s> { - Flatten { - line: self.line, - command: Some(command), - flag: None, - } - } - - pub fn with_flag(&self, flag: String) -> Flatten<'s> { - Flatten { - line: self.line, - command: self.command.clone(), - flag: Some(flag), - } - } -} - -/// Characters that precede a command name -const BEFORE_COMMAND_CHARS: &[char] = &['|', '(', ';']; - -/// Determines the completion location for a given block at the given cursor position -pub fn completion_location(line: &str, block: &Block, pos: usize) -> Vec { - let completion_engine = Flatten::new(line); - let locations = completion_engine.completion_locations(block); - - if locations.is_empty() { - vec![LocationType::Command.spanned(Span::unknown())] - } else { - let mut command = None; - let mut prev = None; - - for loc in &locations { - // We don't use span.contains because we want to include the end. This handles the case - // where the cursor is just after the text (i.e., no space between cursor and text) - if loc.span.start() <= pos && pos <= loc.span.end() { - // The parser sees the "-" in `cmd -` as an argument, but the user is likely - // expecting a flag. - return match loc.item { - LocationType::Argument(ref cmd, _) => { - if loc.span.slice(line) == "-" { - let cmd = cmd.clone(); - let span = loc.span; - vec![ - loc.clone(), - LocationType::Flag(cmd.unwrap_or_default()).spanned(span), - ] - } else { - let mut output = vec![]; - - for rloc in locations.iter().rev() { - if let Spanned { - span, - item: LocationType::Command, - } = &rloc - { - if span.start() <= pos { - output.push( - LocationType::Command - .spanned(Span::new(span.start(), pos)), - ); - break; - } - } - } - - output.push({ - let mut partial_loc = loc.clone(); - partial_loc.span = Span::new(loc.span.start(), pos); - partial_loc - }); - output - } - } - _ => vec![{ - let mut partial_loc = loc.clone(); - partial_loc.span = Span::new(loc.span.start(), pos); - partial_loc - }], - }; - } else if pos < loc.span.start() { - break; - } - - if let LocationType::Command = loc.item { - command = Some(String::from(loc.span.slice(line))); - } - - prev = Some(loc); - } - - if let Some(prev) = prev { - let mut locations = vec![]; - // Cursor is between locations (or at the end). Look at the line to see if the cursor - // is after some character that would imply we're in the command position. - let start = prev.span.end(); - - if let Spanned { - item: LocationType::Command, - span, - } = &prev - { - locations.push(LocationType::Command.spanned(Span::new(span.start(), pos))); - } - if line[start..pos].contains(BEFORE_COMMAND_CHARS) { - locations.push(LocationType::Command.spanned(Span::new(pos, pos))); - } else { - // TODO this should be able to be mapped to a command - locations.push(LocationType::Argument(command, None).spanned(Span::new(pos, pos))); - } - locations - } else { - // Cursor is before any possible completion location, so must be a command - vec![LocationType::Command.spanned(Span::unknown())] - } - } -} - -#[cfg(test)] -mod tests { - use std::sync::Arc; - - use super::*; - - use nu_parser::{classify_block, lex, parse_block, ParserScope}; - use nu_protocol::{Signature, SyntaxShape}; - - #[derive(Clone, Debug)] - struct VecRegistry(Vec); - - impl From> for VecRegistry { - fn from(v: Vec) -> Self { - VecRegistry(v) - } - } - - impl ParserScope for VecRegistry { - fn has_signature(&self, name: &str) -> bool { - self.0.iter().any(|v| v.name == name) - } - - fn get_signature(&self, name: &str) -> Option { - self.0.iter().find(|v| v.name == name).cloned() - } - - fn get_alias(&self, _name: &str) -> Option>> { - None - } - - fn add_alias(&self, _name: &str, _replacement: Vec>) { - todo!() - } - - fn remove_alias(&self, _name: &str) { - todo!() - } - - fn add_definition(&self, _block: Arc) {} - - fn get_definitions(&self) -> Vec> { - vec![] - } - - fn enter_scope(&self) {} - - fn exit_scope(&self) {} - } - - mod completion_location { - use super::*; - - fn completion_location( - line: &str, - scope: &dyn ParserScope, - pos: usize, - ) -> Vec { - let (tokens, _) = lex(line, 0, nu_parser::NewlineMode::Normal); - let (lite_block, _) = parse_block(tokens); - - scope.enter_scope(); - let (block, _) = classify_block(&lite_block, scope); - scope.exit_scope(); - - super::completion_location(line, &block, pos) - } - - #[test] - fn completes_internal_command_names() { - let registry: VecRegistry = - vec![Signature::build("echo").rest("rest", SyntaxShape::Any, "the values to echo")] - .into(); - let line = "echo 1 | echo 2"; - - assert_eq!( - completion_location(line, ®istry, 10), - vec![LocationType::Command.spanned(Span::new(9, 10)),], - ); - } - - #[test] - fn completes_external_command_names() { - let registry: VecRegistry = Vec::new().into(); - let line = "echo 1 | echo 2"; - - assert_eq!( - completion_location(line, ®istry, 10), - vec![LocationType::Command.spanned(Span::new(9, 10)),], - ); - } - - #[test] - fn completes_command_names_when_cursor_immediately_after_command_name() { - let registry: VecRegistry = Vec::new().into(); - let line = "echo 1 | echo 2"; - - assert_eq!( - completion_location(line, ®istry, 4), - vec![LocationType::Command.spanned(Span::new(0, 4)),], - ); - } - - #[test] - fn completes_variables() { - let registry: VecRegistry = Vec::new().into(); - let line = "echo $nu.env."; - - assert_eq!( - completion_location(line, ®istry, 13), - vec![LocationType::Variable.spanned(Span::new(5, 13)),], - ); - } - - #[test] - fn completes_flags() { - let registry: VecRegistry = vec![Signature::build("du") - .switch("recursive", "the values to echo", None) - .rest("rest", SyntaxShape::Any, "blah")] - .into(); - - let line = "du --recurs"; - - assert_eq!( - completion_location(line, ®istry, 7), - vec![LocationType::Flag("du".to_string()).spanned(Span::new(3, 7)),], - ); - } - - #[test] - fn completes_incomplete_nested_structure() { - let registry: VecRegistry = vec![Signature::build("sys")].into(); - let line = "echo (sy"; - - assert_eq!( - completion_location(line, ®istry, 8), - vec![LocationType::Command.spanned(Span::new(6, 8)),], - ); - } - - #[test] - fn has_correct_command_name_for_argument() { - let registry: VecRegistry = vec![Signature::build("cd")].into(); - let line = "cd "; - - assert_eq!( - completion_location(line, ®istry, 3), - vec![ - LocationType::Command.spanned(Span::new(0, 3)), - LocationType::Argument(Some("cd".to_string()), None).spanned(Span::new(3, 3)), - ], - ); - } - - #[test] - fn completes_flags_with_just_a_single_hyphen() { - let registry: VecRegistry = vec![Signature::build("du") - .switch("recursive", "the values to echo", None) - .rest("rest", SyntaxShape::Any, "blah")] - .into(); - - let line = "du -"; - - assert_eq!( - completion_location(line, ®istry, 3), - vec![ - LocationType::Argument(Some("du".to_string()), None).spanned(Span::new(3, 4)), - LocationType::Flag("du".to_string()).spanned(Span::new(3, 4)), - ], - ); - } - - #[test] - fn completes_arguments() { - let registry: VecRegistry = - vec![Signature::build("echo").rest("rest", SyntaxShape::Any, "the values to echo")] - .into(); - let line = "echo 1 | echo 2"; - - assert_eq!( - completion_location(line, ®istry, 6), - vec![ - LocationType::Command.spanned(Span::new(0, 6)), - LocationType::Argument(Some("echo".to_string()), None).spanned(Span::new(5, 6)), - ], - ); - } - - #[test] - fn completes_argument_when_cursor_inside_argument() { - let registry: VecRegistry = - vec![Signature::build("echo").rest("rest", SyntaxShape::Any, "the values to echo")] - .into(); - let line = "echo 123"; - - assert_eq!( - completion_location(line, ®istry, 6), - vec![ - LocationType::Command.spanned(Span::new(0, 6)), - LocationType::Argument(Some("echo".to_string()), None).spanned(Span::new(5, 6)), - ], - ); - } - } -} diff --git a/crates/nu-completion/src/flag.rs b/crates/nu-completion/src/flag.rs deleted file mode 100644 index 88b8ddaf8b..0000000000 --- a/crates/nu-completion/src/flag.rs +++ /dev/null @@ -1,35 +0,0 @@ -use super::matchers::Matcher; -use crate::{Completer, CompletionContext, Suggestion}; - -pub struct FlagCompleter { - pub(crate) cmd: String, -} - -impl Completer for FlagCompleter -where - Context: CompletionContext, -{ - fn complete(&self, ctx: &Context, partial: &str, matcher: &dyn Matcher) -> Vec { - if let Some(sig) = ctx.signature_registry().get(&self.cmd) { - let mut suggestions = Vec::new(); - for (name, (named_type, _desc)) in &sig.named { - suggestions.push(format!("--{}", name)); - - if let Some(c) = named_type.get_short() { - suggestions.push(format!("-{}", c)); - } - } - - suggestions - .into_iter() - .filter(|v| matcher.matches(partial, v)) - .map(|v| Suggestion { - replacement: format!("{} ", v), - display: v, - }) - .collect() - } else { - Vec::new() - } - } -} diff --git a/crates/nu-completion/src/lib.rs b/crates/nu-completion/src/lib.rs deleted file mode 100644 index 4774f933fd..0000000000 --- a/crates/nu-completion/src/lib.rs +++ /dev/null @@ -1,40 +0,0 @@ -pub(crate) mod command; -pub(crate) mod completer; -pub(crate) mod engine; -pub(crate) mod flag; -pub(crate) mod matchers; -pub(crate) mod path; -pub(crate) mod variable; - -use nu_engine::EvaluationContext; -use nu_protocol::{SignatureRegistry, VariableRegistry}; - -use matchers::Matcher; - -pub use completer::NuCompleter; - -#[derive(Debug, Eq, PartialEq)] -pub struct Suggestion { - pub display: String, - pub replacement: String, -} - -impl Suggestion { - fn new(display: impl Into, replacement: impl Into) -> Self { - Self { - display: display.into(), - replacement: replacement.into(), - } - } -} - -pub trait CompletionContext { - fn signature_registry(&self) -> &dyn SignatureRegistry; - fn scope(&self) -> &dyn nu_parser::ParserScope; - fn source(&self) -> &EvaluationContext; - fn variable_registry(&self) -> &dyn VariableRegistry; -} - -pub trait Completer { - fn complete(&self, ctx: &Context, partial: &str, matcher: &dyn Matcher) -> Vec; -} diff --git a/crates/nu-completion/src/matchers/case_insensitive.rs b/crates/nu-completion/src/matchers/case_insensitive.rs deleted file mode 100644 index 9c3ad47a57..0000000000 --- a/crates/nu-completion/src/matchers/case_insensitive.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::matchers; - -pub struct Matcher; - -impl matchers::Matcher for Matcher { - fn matches(&self, partial: &str, from: &str) -> bool { - from.to_ascii_lowercase() - .starts_with(&partial.to_ascii_lowercase()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - // TODO: check some unicode matches if this becomes relevant - - // FIXME: could work exhaustively through ['-', '--'. ''] in a loop for each test - #[test] - fn completes_exact_matches() { - let matcher: Box = Box::new(Matcher); - - assert!(matcher.matches("shouldmatch", "shouldmatch")); - assert!(matcher.matches("shouldm", "shouldmatch")); - assert!(matcher.matches("--also-should-m", "--also-should-match")); - assert!(matcher.matches("-also-should-m", "-also-should-match")); - } - - #[test] - fn completes_case_insensitive_matches() { - let matcher: Box = Box::new(Matcher); - - assert!(matcher.matches("thisshould", "Thisshouldmatch")); - assert!(matcher.matches("--Shouldm", "--shouldmatch")); - assert!(matcher.matches("-Shouldm", "-shouldmatch")); - } - - #[test] - fn should_not_match_when_unequal() { - let matcher: Box = Box::new(Matcher); - - assert!(!matcher.matches("ashouldmatch", "Shouldnotmatch")); - assert!(!matcher.matches("--ashouldnotmatch", "--Shouldnotmatch")); - assert!(!matcher.matches("-ashouldnotmatch", "-Shouldnotmatch")); - } -} diff --git a/crates/nu-completion/src/matchers/case_sensitive.rs b/crates/nu-completion/src/matchers/case_sensitive.rs deleted file mode 100644 index 6a00aac6da..0000000000 --- a/crates/nu-completion/src/matchers/case_sensitive.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::matchers; - -pub struct Matcher; - -impl matchers::Matcher for Matcher { - fn matches(&self, partial: &str, from: &str) -> bool { - from.starts_with(partial) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn completes_case_sensitive() { - let matcher: Box = Box::new(Matcher); - - //Should match - assert!(matcher.matches("shouldmatch", "shouldmatch")); - assert!(matcher.matches("shouldm", "shouldmatch")); - assert!(matcher.matches("--also-should-m", "--also-should-match")); - assert!(matcher.matches("-also-should-m", "-also-should-match")); - - // Should not match - assert!(!matcher.matches("--Shouldnot", "--shouldnotmatch")); - } -} diff --git a/crates/nu-completion/src/matchers/mod.rs b/crates/nu-completion/src/matchers/mod.rs deleted file mode 100644 index c14b0d50ce..0000000000 --- a/crates/nu-completion/src/matchers/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub(crate) mod case_insensitive; -pub(crate) mod case_sensitive; - -pub trait Matcher { - fn matches(&self, partial: &str, from: &str) -> bool; -} diff --git a/crates/nu-completion/src/path.rs b/crates/nu-completion/src/path.rs deleted file mode 100644 index df804bf89a..0000000000 --- a/crates/nu-completion/src/path.rs +++ /dev/null @@ -1,77 +0,0 @@ -use std::path::{is_separator, Path, PathBuf}; - -use super::matchers::Matcher; -use crate::{Completer, CompletionContext, Suggestion}; - -const SEP: char = std::path::MAIN_SEPARATOR; - -pub struct PathCompleter; - -#[derive(Debug)] -pub struct PathSuggestion { - pub(crate) path: PathBuf, - pub(crate) suggestion: Suggestion, -} - -impl PathCompleter { - pub fn path_suggestions(&self, partial: &str, matcher: &dyn Matcher) -> Vec { - let (base_dir_name, partial) = { - // If partial is only a word we want to search in the current dir - let (base, rest) = partial.rsplit_once(is_separator).unwrap_or((".", partial)); - // On windows, this standardizes paths to use \ - let mut base = base.replace(is_separator, &SEP.to_string()); - - // rsplit_once removes the separator - base.push(SEP); - (base, rest) - }; - - let base_dir = nu_path::expand_path(&base_dir_name); - // This check is here as base_dir.read_dir() with base_dir == "" will open the current dir - // which we don't want in this case (if we did, base_dir would already be ".") - if base_dir == Path::new("") { - return Vec::new(); - } - - if let Ok(result) = base_dir.read_dir() { - result - .filter_map(|entry| { - entry.ok().and_then(|entry| { - let mut file_name = entry.file_name().to_string_lossy().into_owned(); - if matcher.matches(partial, &file_name) { - let mut path = format!("{}{}", base_dir_name, file_name); - if entry.path().is_dir() { - path.push(SEP); - file_name.push(SEP); - } - - Some(PathSuggestion { - path: entry.path(), - suggestion: Suggestion { - replacement: path, - display: file_name, - }, - }) - } else { - None - } - }) - }) - .collect() - } else { - Vec::new() - } - } -} - -impl Completer for PathCompleter -where - Context: CompletionContext, -{ - fn complete(&self, _ctx: &Context, partial: &str, matcher: &dyn Matcher) -> Vec { - self.path_suggestions(partial, matcher) - .into_iter() - .map(|ps| ps.suggestion) - .collect() - } -} diff --git a/crates/nu-completion/src/variable.rs b/crates/nu-completion/src/variable.rs deleted file mode 100644 index 4d450101b7..0000000000 --- a/crates/nu-completion/src/variable.rs +++ /dev/null @@ -1,187 +0,0 @@ -use nu_engine::value_shell::ValueShell; -use nu_protocol::ColumnPath; -use nu_source::SpannedItem; - -use super::matchers::Matcher; -use crate::{Completer, CompletionContext, Suggestion}; -use std::path::{Path, PathBuf}; - -fn build_path(head: &str, members: &Path, entry: &str) -> String { - let mut full_path = head.to_string(); - full_path.push_str( - &members - .join(entry) - .display() - .to_string() - .replace(std::path::MAIN_SEPARATOR, "."), - ); - full_path -} - -fn collect_entries(value_fs: &ValueShell, head: &str, path: &Path) -> Vec { - value_fs - .members_under(path) - .iter() - .flat_map(|entry| { - entry - .row_entries() - .map(|(entry_name, _)| build_path(head, path, entry_name)) - }) - .collect() -} - -pub struct VariableCompleter; - -impl Completer for VariableCompleter -where - Context: CompletionContext, -{ - fn complete(&self, ctx: &Context, partial: &str, matcher: &dyn Matcher) -> Vec { - let registry = ctx.variable_registry(); - let variables_available = registry.variables(); - let partial_column_path = ColumnPath::with_head(&partial.to_string().spanned_unknown()); - - partial_column_path - .map(|(head, members)| { - variables_available - .iter() - .filter(|candidate| matcher.matches(&head, candidate)) - .into_iter() - .filter_map(|candidate| { - if !partial.ends_with('.') && members.is_empty() { - Some(vec![candidate.to_string()]) - } else { - let value = registry.get_variable(&candidate[..].spanned_unknown()); - let path = PathBuf::from(members.path()); - - value.map(|candidate| { - let fs = ValueShell::new(candidate); - - fs.find(&path) - .map(|fs| collect_entries(fs, &head, &path)) - .or_else(|| { - path.parent().map(|parent| { - fs.find(parent) - .map(|fs| collect_entries(fs, &head, parent)) - .unwrap_or_default() - }) - }) - .unwrap_or_default() - }) - } - }) - .flatten() - .filter_map(|candidate| { - if matcher.matches(partial, &candidate) { - Some(Suggestion::new(&candidate, &candidate)) - } else { - None - } - }) - .collect() - }) - .unwrap_or_default() - } -} - -#[cfg(test)] -mod tests { - use super::{Completer, Suggestion as S, VariableCompleter}; - use crate::matchers::case_insensitive::Matcher as CaseInsensitiveMatcher; - - use nu_engine::EnvVar; - use nu_engine::{ - evaluation_context::EngineState, ConfigHolder, EvaluationContext, FakeHost, Host, Scope, - ShellManager, - }; - use nu_protocol::{SignatureRegistry, VariableRegistry}; - use parking_lot::Mutex; - use std::ffi::OsString; - use std::sync::{atomic::AtomicBool, Arc}; - - struct CompletionContext<'a>(&'a EvaluationContext); - - impl<'a> crate::CompletionContext for CompletionContext<'a> { - fn signature_registry(&self) -> &dyn SignatureRegistry { - &self.0.scope - } - - fn source(&self) -> &nu_engine::EvaluationContext { - self.0 - } - - fn scope(&self) -> &dyn nu_parser::ParserScope { - &self.0.scope - } - - fn variable_registry(&self) -> &dyn VariableRegistry { - self.0 - } - } - - fn create_context_with_host(host: Box) -> EvaluationContext { - let scope = Scope::new(); - let env_vars = host - .vars() - .iter() - .cloned() - .map(|(k, v)| (k, EnvVar::from(v))) - .collect(); - scope.add_env(env_vars); - - EvaluationContext { - scope, - engine_state: Arc::new(EngineState { - host: Arc::new(parking_lot::Mutex::new(host)), - current_errors: Arc::new(Mutex::new(vec![])), - ctrl_c: Arc::new(AtomicBool::new(false)), - configs: Arc::new(Mutex::new(ConfigHolder::new())), - shell_manager: ShellManager::basic(), - windows_drives_previous_cwd: Arc::new(Mutex::new(std::collections::HashMap::new())), - }), - } - } - - fn set_envs(host: &mut FakeHost, values: Vec<(&str, &str)>) { - values.iter().for_each(|(key, value)| { - host.env_set(OsString::from(key), OsString::from(value)); - }); - } - - #[test] - fn structure() { - let mut host = nu_engine::FakeHost::new(); - set_envs(&mut host, vec![("COMPLETER", "VARIABLE"), ("SHELL", "NU")]); - let context = create_context_with_host(Box::new(host)); - - assert_eq!( - VariableCompleter {}.complete( - &CompletionContext(&context), - "$nu.env.", - &CaseInsensitiveMatcher - ), - vec![ - S::new("$nu.env.COMPLETER", "$nu.env.COMPLETER"), - S::new("$nu.env.SHELL", "$nu.env.SHELL") - ] - ); - - assert_eq!( - VariableCompleter {}.complete( - &CompletionContext(&context), - "$nu.env.CO", - &CaseInsensitiveMatcher - ), - vec![S::new("$nu.env.COMPLETER", "$nu.env.COMPLETER"),] - ); - - assert_eq!( - VariableCompleter {}.complete( - &CompletionContext(&context), - "$nu.en", - &CaseInsensitiveMatcher - ), - vec![S::new("$nu.env", "$nu.env"),] - ); - } -} diff --git a/crates/nu-data/Cargo.toml b/crates/nu-data/Cargo.toml deleted file mode 100644 index ad414db261..0000000000 --- a/crates/nu-data/Cargo.toml +++ /dev/null @@ -1,40 +0,0 @@ -[package] -authors = ["The Nu Project Contributors"] -description = "Data for Nushell" -edition = "2018" -license = "MIT" -name = "nu-data" -version = "0.44.0" - -[lib] -doctest = false - -[dependencies] -bigdecimal = { package = "bigdecimal", version = "0.3.0", features = ["serde"] } -byte-unit = "4.0.9" -chrono = "0.4.19" -common-path = "1.0.0" -derive-new = "0.5.8" -directories-next = "2.0.0" -getset = "0.1.1" -indexmap = { version="1.6.1", features=["serde-1"] } -log = "0.4.14" -num-bigint = { version="0.4.3", features=["serde"] } -num-format = "0.4.0" -num-traits = "0.2.14" -serde = { version="1.0.123", features=["derive"] } -sha2 = "0.9.3" -sys-locale = "0.1.0" -toml = "0.5.8" - -nu-errors = { version = "0.44.0", path="../nu-errors" } -nu-path = { version = "0.44.0", path="../nu-path" } -nu-protocol = { version = "0.44.0", path="../nu-protocol" } -nu-source = { version = "0.44.0", path="../nu-source" } -nu-table = { version = "0.44.0", path="../nu-table" } -nu-test-support = { version = "0.44.0", path="../nu-test-support" } -nu-value-ext = { version = "0.44.0", path="../nu-value-ext" } -nu-ansi-term = { version = "0.44.0", path="../nu-ansi-term" } - -[features] -dataframe = ["nu-protocol/dataframe"] diff --git a/crates/nu-data/src/base.rs b/crates/nu-data/src/base.rs deleted file mode 100644 index f0b76d7acb..0000000000 --- a/crates/nu-data/src/base.rs +++ /dev/null @@ -1,466 +0,0 @@ -pub mod shape; - -use bigdecimal::BigDecimal; -use chrono::{DateTime, FixedOffset, Utc}; -use derive_new::new; -use nu_errors::ShellError; -use nu_protocol::{ - hir, Primitive, ShellTypeName, SpannedTypeName, TaggedDictBuilder, UntaggedValue, Value, -}; -use nu_source::{Span, Tag}; -use nu_value_ext::ValueExt; -use num_bigint::BigInt; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone, new, Serialize)] -pub struct Operation { - pub(crate) left: Value, - pub(crate) operator: hir::Operator, - pub(crate) right: Value, -} - -#[derive(Serialize, Deserialize)] -pub enum Switch { - Present, - Absent, -} - -impl std::convert::TryFrom> for Switch { - type Error = ShellError; - - fn try_from(value: Option<&Value>) -> Result { - match value { - None => Ok(Switch::Absent), - Some(value) => match &value.value { - UntaggedValue::Primitive(Primitive::Boolean(true)) => Ok(Switch::Present), - _ => Err(ShellError::type_error("Boolean", value.spanned_type_name())), - }, - } - } -} - -pub fn select_fields(obj: &Value, fields: &[String], tag: impl Into) -> Value { - let mut out = TaggedDictBuilder::new(tag); - - let descs = obj.data_descriptors(); - - for column_name in fields { - match descs.iter().find(|&d| d == column_name) { - None => out.insert_untagged(column_name, UntaggedValue::nothing()), - Some(desc) => out.insert_value(desc.clone(), obj.get_data(desc).borrow().clone()), - } - } - - out.into_value() -} - -pub fn reject_fields(obj: &Value, fields: &[String], tag: impl Into) -> Value { - let mut out = TaggedDictBuilder::new(tag); - - let descs = obj.data_descriptors(); - - for desc in descs { - if fields.iter().any(|field| *field == desc) { - continue; - } else { - out.insert_value(desc.clone(), obj.get_data(&desc).borrow().clone()) - } - } - - out.into_value() -} - -pub enum CompareValues { - Ints(i64, i64), - Filesizes(u64, u64), - BigInts(BigInt, BigInt), - Decimals(BigDecimal, BigDecimal), - String(String, String), - Date(DateTime, DateTime), - DateDuration(DateTime, BigInt), - TimeDuration(BigInt, BigInt), - Booleans(bool, bool), -} - -impl CompareValues { - pub fn compare(&self) -> std::cmp::Ordering { - match self { - CompareValues::BigInts(left, right) => left.cmp(right), - CompareValues::Ints(left, right) => left.cmp(right), - CompareValues::Filesizes(left, right) => left.cmp(right), - CompareValues::Decimals(left, right) => left.cmp(right), - CompareValues::String(left, right) => left.cmp(right), - CompareValues::Date(left, right) => left.cmp(right), - CompareValues::DateDuration(left, right) => { - // FIXME: Not sure if I could do something better with the Span. - let duration = Primitive::into_chrono_duration( - Primitive::Duration(right.clone()), - Span::unknown(), - ) - .expect("Could not convert nushell Duration into chrono Duration."); - let right: DateTime = Utc::now() - .checked_sub_signed(duration) - .expect("Data overflow") - .into(); - right.cmp(left) - } - CompareValues::Booleans(left, right) => left.cmp(right), - CompareValues::TimeDuration(left, right) => left.cmp(right), - } - } -} - -pub fn coerce_compare( - left: &UntaggedValue, - right: &UntaggedValue, -) -> Result { - match (left, right) { - (UntaggedValue::Primitive(left), UntaggedValue::Primitive(right)) => { - coerce_compare_primitive(left, right) - } - - _ => Err((left.type_name(), right.type_name())), - } -} - -pub fn coerce_compare_primitive( - left: &Primitive, - right: &Primitive, -) -> Result { - use Primitive::*; - - Ok(match (left, right) { - (Int(left), Int(right)) => CompareValues::Ints(*left, *right), - (Int(left), BigInt(right)) => { - CompareValues::BigInts(num_bigint::BigInt::from(*left), right.clone()) - } - (BigInt(left), Int(right)) => { - CompareValues::BigInts(left.clone(), num_bigint::BigInt::from(*right)) - } - (BigInt(left), BigInt(right)) => CompareValues::BigInts(left.clone(), right.clone()), - - (Int(left), Decimal(right)) => { - CompareValues::Decimals(BigDecimal::from(*left), right.clone()) - } - (BigInt(left), Decimal(right)) => { - CompareValues::Decimals(BigDecimal::from(left.clone()), right.clone()) - } - (Decimal(left), Decimal(right)) => CompareValues::Decimals(left.clone(), right.clone()), - (Decimal(left), Int(right)) => { - CompareValues::Decimals(left.clone(), BigDecimal::from(*right)) - } - (Decimal(left), BigInt(right)) => { - CompareValues::Decimals(left.clone(), BigDecimal::from(right.clone())) - } - (Decimal(left), Filesize(right)) => { - CompareValues::Decimals(left.clone(), BigDecimal::from(*right)) - } - (Filesize(left), Filesize(right)) => CompareValues::Filesizes(*left, *right), - (Filesize(left), Decimal(right)) => { - CompareValues::Decimals(BigDecimal::from(*left), right.clone()) - } - (Nothing, Nothing) => CompareValues::Booleans(true, true), - (String(left), String(right)) => CompareValues::String(left.clone(), right.clone()), - (Date(left), Date(right)) => CompareValues::Date(*left, *right), - (Date(left), Duration(right)) => CompareValues::DateDuration(*left, right.clone()), - (Boolean(left), Boolean(right)) => CompareValues::Booleans(*left, *right), - (Boolean(left), Nothing) => CompareValues::Ints(if *left { 1 } else { 0 }, -1), - (Nothing, Boolean(right)) => CompareValues::Ints(-1, if *right { 1 } else { 0 }), - (String(left), Nothing) => CompareValues::String(left.clone(), std::string::String::new()), - (Nothing, String(right)) => { - CompareValues::String(std::string::String::new(), right.clone()) - } - (FilePath(left), String(right)) => { - CompareValues::String(left.as_path().display().to_string(), right.clone()) - } - (String(left), FilePath(right)) => { - CompareValues::String(left.clone(), right.as_path().display().to_string()) - } - (Duration(left), Duration(right)) => { - CompareValues::TimeDuration(left.clone(), right.clone()) - } - _ => return Err((left.type_name(), right.type_name())), - }) -} -#[cfg(test)] -mod tests { - use nu_errors::ShellError; - use nu_protocol::UntaggedValue; - use nu_source::SpannedItem; - use nu_test_support::value::*; - use nu_value_ext::ValueExt; - - use indexmap::indexmap; - - #[test] - fn gets_matching_field_from_a_row() -> Result<(), ShellError> { - let row = UntaggedValue::row(indexmap! { - "amigos".into() => table(&[string("andres"),string("jonathan"),string("yehuda")]) - }) - .into_untagged_value(); - - assert_eq!( - row.get_data_by_key("amigos".spanned_unknown()) - .ok_or_else(|| ShellError::unexpected("Failure during testing"))?, - table(&[string("andres"), string("jonathan"), string("yehuda")]) - ); - - Ok(()) - } - - #[test] - fn gets_matching_field_from_nested_rows_inside_a_row() -> Result<(), ShellError> { - let field_path = column_path("package.version").as_column_path()?; - - let (version, tag) = string("0.4.0").into_parts(); - - let value = UntaggedValue::row(indexmap! { - "package".into() => - row(indexmap! { - "name".into() => string("nu"), - "version".into() => string("0.4.0") - }) - }); - - assert_eq!( - *value.into_value(tag).get_data_by_column_path( - &field_path.item, - Box::new(error_callback("package.version")) - )?, - version - ); - - Ok(()) - } - - #[test] - fn gets_first_matching_field_from_rows_with_same_field_inside_a_table() -> Result<(), ShellError> - { - let field_path = column_path("package.authors.name").as_column_path()?; - - let (_, tag) = string("Andrés N. Robalino").into_parts(); - - let value = UntaggedValue::row(indexmap! { - "package".into() => row(indexmap! { - "name".into() => string("nu"), - "version".into() => string("0.4.0"), - "authors".into() => table(&[ - row(indexmap!{"name".into() => string("Andrés N. Robalino")}), - row(indexmap!{"name".into() => string("Jonathan Turner")}), - row(indexmap!{"name".into() => string("Yehuda Katz")}) - ]) - }) - }); - - assert_eq!( - value.into_value(tag).get_data_by_column_path( - &field_path.item, - Box::new(error_callback("package.authors.name")) - )?, - table(&[ - string("Andrés N. Robalino"), - string("Jonathan Turner"), - string("Yehuda Katz") - ]) - ); - - Ok(()) - } - - #[test] - fn column_path_that_contains_just_a_number_gets_a_row_from_a_table() -> Result<(), ShellError> { - let field_path = column_path("package.authors.0").as_column_path()?; - - let (_, tag) = string("Andrés N. Robalino").into_parts(); - - let value = UntaggedValue::row(indexmap! { - "package".into() => row(indexmap! { - "name".into() => string("nu"), - "version".into() => string("0.4.0"), - "authors".into() => table(&[ - row(indexmap!{"name".into() => string("Andrés N. Robalino")}), - row(indexmap!{"name".into() => string("Jonathan Turner")}), - row(indexmap!{"name".into() => string("Yehuda Katz")}) - ]) - }) - }); - - assert_eq!( - *value.into_value(tag).get_data_by_column_path( - &field_path.item, - Box::new(error_callback("package.authors.0")) - )?, - UntaggedValue::row(indexmap! { - "name".into() => string("Andrés N. Robalino") - }) - ); - - Ok(()) - } - - #[test] - fn column_path_that_contains_just_a_number_gets_a_row_from_a_row() -> Result<(), ShellError> { - let field_path = column_path(r#"package.authors."0""#).as_column_path()?; - - let (_, tag) = string("Andrés N. Robalino").into_parts(); - - let value = UntaggedValue::row(indexmap! { - "package".into() => row(indexmap! { - "name".into() => string("nu"), - "version".into() => string("0.4.0"), - "authors".into() => row(indexmap! { - "0".into() => row(indexmap!{"name".into() => string("Andrés N. Robalino")}), - "1".into() => row(indexmap!{"name".into() => string("Jonathan Turner")}), - "2".into() => row(indexmap!{"name".into() => string("Yehuda Katz")}), - }) - }) - }); - - assert_eq!( - *value.into_value(tag).get_data_by_column_path( - &field_path.item, - Box::new(error_callback("package.authors.\"0\"")) - )?, - UntaggedValue::row(indexmap! { - "name".into() => string("Andrés N. Robalino") - }) - ); - - Ok(()) - } - - #[test] - fn replaces_matching_field_from_a_row() -> Result<(), ShellError> { - let field_path = column_path("amigos").as_column_path()?; - - let sample = UntaggedValue::row(indexmap! { - "amigos".into() => table(&[ - string("andres"), - string("jonathan"), - string("yehuda"), - ]), - }); - - let replacement = string("jonas"); - - let actual = sample - .into_untagged_value() - .replace_data_at_column_path(&field_path.item, replacement) - .ok_or_else(|| ShellError::untagged_runtime_error("Could not replace column"))?; - - assert_eq!(actual, row(indexmap! {"amigos".into() => string("jonas")})); - - Ok(()) - } - - #[test] - fn replaces_matching_field_from_nested_rows_inside_a_row() -> Result<(), ShellError> { - let field_path = column_path(r#"package.authors."los.3.caballeros""#).as_column_path()?; - - let sample = UntaggedValue::row(indexmap! { - "package".into() => row(indexmap! { - "authors".into() => row(indexmap! { - "los.3.mosqueteros".into() => table(&[string("andres::yehuda::jonathan")]), - "los.3.amigos".into() => table(&[string("andres::yehuda::jonathan")]), - "los.3.caballeros".into() => table(&[string("andres::yehuda::jonathan")]) - }) - }) - }); - - let replacement = table(&[string("yehuda::jonathan::andres")]); - let tag = replacement.tag.clone(); - - let actual = sample - .into_value(&tag) - .replace_data_at_column_path(&field_path.item, replacement.clone()) - .ok_or_else(|| { - ShellError::labeled_error( - "Could not replace column", - "could not replace column", - &tag, - ) - })?; - - assert_eq!( - actual, - UntaggedValue::row(indexmap! { - "package".into() => row(indexmap! { - "authors".into() => row(indexmap! { - "los.3.mosqueteros".into() => table(&[string("andres::yehuda::jonathan")]), - "los.3.amigos".into() => table(&[string("andres::yehuda::jonathan")]), - "los.3.caballeros".into() => replacement})})}) - .into_value(tag) - ); - - Ok(()) - } - #[test] - fn replaces_matching_field_from_rows_inside_a_table() -> Result<(), ShellError> { - let field_path = - column_path(r#"shell_policy.releases."nu.version.arepa""#).as_column_path()?; - - let sample = UntaggedValue::row(indexmap! { - "shell_policy".into() => row(indexmap! { - "releases".into() => table(&[ - row(indexmap! { - "nu.version.arepa".into() => row(indexmap! { - "code".into() => string("0.4.0"), "tag_line".into() => string("GitHub-era") - }) - }), - row(indexmap! { - "nu.version.taco".into() => row(indexmap! { - "code".into() => string("0.3.0"), "tag_line".into() => string("GitHub-era") - }) - }), - row(indexmap! { - "nu.version.stable".into() => row(indexmap! { - "code".into() => string("0.2.0"), "tag_line".into() => string("GitHub-era") - }) - }) - ]) - }) - }); - - let replacement = row(indexmap! { - "code".into() => string("0.5.0"), - "tag_line".into() => string("CABALLEROS") - }); - let tag = replacement.tag.clone(); - - let actual = sample - .into_value(tag.clone()) - .replace_data_at_column_path(&field_path.item, replacement.clone()) - .ok_or_else(|| { - ShellError::labeled_error( - "Could not replace column", - "could not replace column", - &tag, - ) - })?; - - assert_eq!( - actual, - UntaggedValue::row(indexmap! { - "shell_policy".into() => row(indexmap! { - "releases".into() => table(&[ - row(indexmap! { - "nu.version.arepa".into() => replacement - }), - row(indexmap! { - "nu.version.taco".into() => row(indexmap! { - "code".into() => string("0.3.0"), "tag_line".into() => string("GitHub-era") - }) - }), - row(indexmap! { - "nu.version.stable".into() => row(indexmap! { - "code".into() => string("0.2.0"), "tag_line".into() => string("GitHub-era") - }) - }) - ]) - }) - }).into_value(&tag) - ); - - Ok(()) - } -} diff --git a/crates/nu-data/src/base/shape.rs b/crates/nu-data/src/base/shape.rs deleted file mode 100644 index c2a9122166..0000000000 --- a/crates/nu-data/src/base/shape.rs +++ /dev/null @@ -1,479 +0,0 @@ -use bigdecimal::BigDecimal; -use chrono::{DateTime, FixedOffset}; -use indexmap::map::IndexMap; -use nu_protocol::RangeInclusion; -use nu_protocol::{format_primitive, ColumnPath, Dictionary, Primitive, UntaggedValue, Value}; -use nu_source::{DbgDocBldr, DebugDocBuilder, PrettyDebug, Tag}; -use num_bigint::BigInt; -use num_format::{Locale, ToFormattedString}; -use serde::{Deserialize, Serialize}; -use std::cmp::Ordering; -use std::fmt::Debug; -use std::hash::{Hash, Hasher}; -use std::path::PathBuf; -use sys_locale::get_locale; - -#[cfg(feature = "dataframe")] -use nu_protocol::dataframe::{FrameStruct, NuDataFrame}; - -#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)] -pub struct InlineRange { - from: (InlineShape, RangeInclusion), - to: (InlineShape, RangeInclusion), -} - -#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)] -pub enum InlineShape { - Nothing, - Int(i64), - BigInt(BigInt), - Decimal(BigDecimal), - Range(Box), - Bytesize(u64), - String(String), - Line(String), - ColumnPath(ColumnPath), - GlobPattern(String), - Boolean(bool), - Date(DateTime), - Duration(BigInt), - FilePath(PathBuf), - Binary(usize), - - Row(Row), - Table(Vec), - - // TODO: Block arguments - Block, - // TODO: Error type - Error, - - #[cfg(feature = "dataframe")] - DataFrame(String), - - #[cfg(feature = "dataframe")] - FrameStruct(String), - - // Stream markers (used as bookend markers rather than actual values) - BeginningOfStream, - EndOfStream, -} - -pub struct FormatInlineShape { - shape: InlineShape, - column: Option, -} - -pub fn get_config_filesize_metric() -> bool { - let res = crate::config::config(Tag::unknown()); - if res.is_err() { - return true; - } - let value = res - .unwrap_or_default() - .get("filesize_metric") - .map(|s| s.value.is_true()) - .unwrap_or(true); - value -} - -impl InlineShape { - pub fn from_primitive(primitive: &Primitive) -> InlineShape { - match primitive { - Primitive::Nothing => InlineShape::Nothing, - Primitive::Int(int) => InlineShape::Int(*int), - Primitive::BigInt(int) => InlineShape::BigInt(int.clone()), - Primitive::Range(range) => { - let (left, left_inclusion) = &range.from; - let (right, right_inclusion) = &range.to; - - InlineShape::Range(Box::new(InlineRange { - from: (InlineShape::from_primitive(left), *left_inclusion), - to: (InlineShape::from_primitive(right), *right_inclusion), - })) - } - Primitive::Decimal(decimal) => InlineShape::Decimal(decimal.clone()), - Primitive::Filesize(bytesize) => InlineShape::Bytesize(*bytesize), - Primitive::String(string) => InlineShape::String(string.clone()), - Primitive::ColumnPath(path) => InlineShape::ColumnPath(path.clone()), - Primitive::GlobPattern(pattern) => InlineShape::GlobPattern(pattern.clone()), - Primitive::Boolean(boolean) => InlineShape::Boolean(*boolean), - Primitive::Date(date) => InlineShape::Date(*date), - Primitive::Duration(duration) => InlineShape::Duration(duration.clone()), - Primitive::FilePath(path) => InlineShape::FilePath(path.clone()), - Primitive::Binary(b) => InlineShape::Binary(b.len()), - Primitive::BeginningOfStream => InlineShape::BeginningOfStream, - Primitive::EndOfStream => InlineShape::EndOfStream, - } - } - - pub fn from_dictionary(dictionary: &Dictionary) -> InlineShape { - let mut map = IndexMap::new(); - - for (key, value) in &dictionary.entries { - let column = Column::String(key.clone()); - map.insert(column, InlineShape::from_value(value)); - } - - InlineShape::Row(Row { map }) - } - - pub fn from_table<'a>(table: impl IntoIterator) -> InlineShape { - let vec = table.into_iter().map(InlineShape::from_value).collect(); - - InlineShape::Table(vec) - } - - #[cfg(feature = "dataframe")] - pub fn from_df(df: &NuDataFrame) -> InlineShape { - let msg = format!("{} rows {} cols", df.as_ref().height(), df.as_ref().width()); - - InlineShape::DataFrame(msg) - } - - #[cfg(feature = "dataframe")] - pub fn from_frame_struct(s: &FrameStruct) -> InlineShape { - match s { - FrameStruct::GroupBy(groupby) => { - let msg = groupby.by().join(","); - let msg = format!("groupby {}", msg); - InlineShape::DataFrame(msg) - } - } - } - - pub fn from_value<'a>(value: impl Into<&'a UntaggedValue>) -> InlineShape { - match value.into() { - UntaggedValue::Primitive(p) => InlineShape::from_primitive(p), - UntaggedValue::Row(row) => InlineShape::from_dictionary(row), - UntaggedValue::Table(table) => InlineShape::from_table(table), - UntaggedValue::Error(_) => InlineShape::Error, - UntaggedValue::Block(_) => InlineShape::Block, - #[cfg(feature = "dataframe")] - UntaggedValue::DataFrame(df) => InlineShape::from_df(df), - #[cfg(feature = "dataframe")] - UntaggedValue::FrameStruct(s) => InlineShape::from_frame_struct(s), - } - } - - #[allow(unused)] - pub fn format_for_column(self, column: impl Into) -> FormatInlineShape { - FormatInlineShape { - shape: self, - column: Some(column.into()), - } - } - - pub fn format(self) -> FormatInlineShape { - FormatInlineShape { - shape: self, - column: None, - } - } - - pub fn format_bytes(bytesize: u64, forced_format: Option<&str>) -> (DbgDocBldr, String) { - use bigdecimal::ToPrimitive; - - // get the config value, if it doesn't exist make it 'auto' so it works how it originally did - let filesize_format_var; - if let Some(fmt) = forced_format { - filesize_format_var = fmt.to_ascii_lowercase(); - } else { - filesize_format_var = match crate::config::config(Tag::unknown()) { - Ok(cfg) => cfg - .get("filesize_format") - .map(|val| val.convert_to_string().to_ascii_lowercase()) - .unwrap_or_else(|| "auto".to_string()), - _ => "auto".to_string(), - } - } - - // if there is a value, match it to one of the valid values for byte units - let filesize_format = match filesize_format_var.as_str() { - "b" => (byte_unit::ByteUnit::B, ""), - "kb" => (byte_unit::ByteUnit::KB, ""), - "kib" => (byte_unit::ByteUnit::KiB, ""), - "mb" => (byte_unit::ByteUnit::MB, ""), - "mib" => (byte_unit::ByteUnit::MiB, ""), - "gb" => (byte_unit::ByteUnit::GB, ""), - "gib" => (byte_unit::ByteUnit::GiB, ""), - "tb" => (byte_unit::ByteUnit::TB, ""), - "tib" => (byte_unit::ByteUnit::TiB, ""), - "pb" => (byte_unit::ByteUnit::PB, ""), - "pib" => (byte_unit::ByteUnit::PiB, ""), - "eb" => (byte_unit::ByteUnit::EB, ""), - "eib" => (byte_unit::ByteUnit::EiB, ""), - "zb" => (byte_unit::ByteUnit::ZB, ""), - "zib" => (byte_unit::ByteUnit::ZiB, ""), - _ => (byte_unit::ByteUnit::B, "auto"), - }; - - if let Some(value) = bytesize.to_u128() { - let byte = byte_unit::Byte::from_bytes(value); - let adj_byte = - if filesize_format.0 == byte_unit::ByteUnit::B && filesize_format.1 == "auto" { - byte.get_appropriate_unit(!get_config_filesize_metric()) - } else { - byte.get_adjusted_unit(filesize_format.0) - }; - - match adj_byte.get_unit() { - byte_unit::ByteUnit::B => { - let locale_string = get_locale().unwrap_or_else(|| String::from("en-US")); - // Since get_locale() and Locale::from_name() don't always return the same items - // we need to try and parse it to match. For instance, a valid locale is de_DE - // however Locale::from_name() wants only de so we split and parse it out. - let locale_string = locale_string.replace("_", "-"); // en_AU -> en-AU - let locale = match Locale::from_name(&locale_string) { - Ok(loc) => loc, - _ => { - let all = num_format::Locale::available_names(); - let locale_prefix = &locale_string.split('-').collect::>(); - if all.contains(&locale_prefix[0]) { - // eprintln!("Found alternate: {}", &locale_prefix[0]); - Locale::from_name(locale_prefix[0]).unwrap_or(Locale::en) - } else { - // eprintln!("Unable to find matching locale. Defaulting to en-US"); - Locale::en - } - } - }; - let locale_byte = adj_byte.get_value() as u64; - let locale_byte_string = locale_byte.to_formatted_string(&locale); - if filesize_format.1 == "auto" { - let doc = (DbgDocBldr::primitive(locale_byte_string) - + DbgDocBldr::space() - + DbgDocBldr::kind("B")) - .group(); - (doc.clone(), InlineShape::render_doc(&doc)) - } else { - let doc = (DbgDocBldr::primitive(locale_byte_string)).group(); - (doc.clone(), InlineShape::render_doc(&doc)) - } - } - _ => { - let doc = DbgDocBldr::primitive(adj_byte.format(1)); - (doc.clone(), InlineShape::render_doc(&doc)) - } - } - } else { - let doc = - (DbgDocBldr::primitive(bytesize) + DbgDocBldr::space() + DbgDocBldr::kind("B")) - .group(); - (doc.clone(), InlineShape::render_doc(&doc)) - } - } - - pub fn render_doc(doc: &DebugDocBuilder) -> String { - let mut w = Vec::new(); - doc.to_doc() - .render(1000, &mut w) - .expect("Error rendering bytes"); - String::from_utf8_lossy(&w).to_string() - } -} - -impl PrettyDebug for FormatInlineShape { - fn pretty(&self) -> DebugDocBuilder { - let column = &self.column; - - match &self.shape { - InlineShape::Nothing => DbgDocBldr::blank(), - InlineShape::Int(int) => DbgDocBldr::primitive(int), - InlineShape::BigInt(int) => DbgDocBldr::primitive(int), - InlineShape::Decimal(decimal) => DbgDocBldr::description(format_primitive( - &Primitive::Decimal(decimal.clone()), - None, - )), - InlineShape::Range(range) => { - let (left, left_inclusion) = &range.from; - let (right, right_inclusion) = &range.to; - - let op = match (left_inclusion, right_inclusion) { - (RangeInclusion::Inclusive, RangeInclusion::Inclusive) => "..", - (RangeInclusion::Inclusive, RangeInclusion::Exclusive) => "..<", - _ => unimplemented!( - "No syntax for ranges that aren't inclusive on the left and exclusive \ - or inclusive on the right" - ), - }; - - left.clone().format().pretty() - + DbgDocBldr::operator(op) - + right.clone().format().pretty() - } - InlineShape::Bytesize(bytesize) => { - let bytes = InlineShape::format_bytes(*bytesize, None); - bytes.0 - } - InlineShape::String(string) => DbgDocBldr::primitive(string), - InlineShape::Line(string) => DbgDocBldr::primitive(string), - InlineShape::ColumnPath(path) => DbgDocBldr::intersperse( - path.iter().map(|member| member.pretty()), - DbgDocBldr::keyword("."), - ), - InlineShape::GlobPattern(pattern) => DbgDocBldr::primitive(pattern), - InlineShape::Boolean(boolean) => DbgDocBldr::primitive( - match (boolean, column) { - (true, None) => "true", - (false, None) => "false", - (true, Some(Column::String(s))) if !s.is_empty() => s, - (false, Some(Column::String(s))) if !s.is_empty() => "", - (true, Some(_)) => "true", - (false, Some(_)) => "false", - } - .to_owned(), - ), - InlineShape::Date(date) => DbgDocBldr::primitive(nu_protocol::format_date(date)), - InlineShape::Duration(duration) => DbgDocBldr::description(format_primitive( - &Primitive::Duration(duration.clone()), - None, - )), - InlineShape::FilePath(path) => DbgDocBldr::primitive(path.display()), - InlineShape::Binary(length) => { - DbgDocBldr::opaque(format!("", length)) - } - InlineShape::Row(row) => DbgDocBldr::delimit( - "[", - DbgDocBldr::kind("row") - + DbgDocBldr::space() - + if row.map.keys().len() <= 6 { - DbgDocBldr::intersperse( - row.map.keys().map(|key| match key { - Column::String(string) => DbgDocBldr::description(string), - Column::Value => DbgDocBldr::blank(), - }), - DbgDocBldr::space(), - ) - } else { - DbgDocBldr::description(format!("{} columns", row.map.keys().len())) - }, - "]", - ) - .group(), - InlineShape::Table(rows) => DbgDocBldr::delimit( - "[", - DbgDocBldr::kind("table") - + DbgDocBldr::space() - + DbgDocBldr::primitive(rows.len()) - + DbgDocBldr::space() - + DbgDocBldr::description("rows"), - "]", - ) - .group(), - InlineShape::Block => DbgDocBldr::opaque("block"), - InlineShape::Error => DbgDocBldr::error("error"), - #[cfg(feature = "dataframe")] - InlineShape::DataFrame(msg) => DbgDocBldr::delimit( - "[", - DbgDocBldr::kind("dataframe") + DbgDocBldr::space() + DbgDocBldr::primitive(msg), - "]", - ) - .group(), - #[cfg(feature = "dataframe")] - InlineShape::FrameStruct(msg) => { - DbgDocBldr::delimit("[", DbgDocBldr::primitive(msg), "]").group() - } - InlineShape::BeginningOfStream => DbgDocBldr::blank(), - InlineShape::EndOfStream => DbgDocBldr::blank(), - } - } -} - -pub trait GroupedValue: Debug + Clone { - type Item; - - fn new() -> Self; - fn merge(&mut self, value: Self::Item); -} - -impl GroupedValue for Vec<(usize, usize)> { - type Item = usize; - - fn new() -> Vec<(usize, usize)> { - vec![] - } - - fn merge(&mut self, new_value: usize) { - match self.last_mut() { - Some(value) if value.1 == new_value - 1 => { - value.1 += 1; - } - - _ => self.push((new_value, new_value)), - } - } -} - -#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)] -pub enum Column { - String(String), - Value, -} - -impl From for Column { - fn from(x: String) -> Self { - Column::String(x) - } -} - -impl From<&String> for Column { - fn from(x: &String) -> Self { - Column::String(x.clone()) - } -} - -impl From<&str> for Column { - fn from(x: &str) -> Self { - Column::String(x.to_string()) - } -} - -/// A shape representation of the type of a row -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct Row { - map: IndexMap, -} - -#[allow(clippy::derive_hash_xor_eq)] -impl Hash for Row { - fn hash(&self, state: &mut H) { - let mut entries = self.map.clone(); - entries.sort_keys(); - entries.keys().collect::>().hash(state); - entries.values().collect::>().hash(state); - } -} - -impl PartialOrd for Row { - fn partial_cmp(&self, other: &Row) -> Option { - let this: Vec<&Column> = self.map.keys().collect(); - let that: Vec<&Column> = other.map.keys().collect(); - - if this != that { - return this.partial_cmp(&that); - } - - let this: Vec<&InlineShape> = self.map.values().collect(); - let that: Vec<&InlineShape> = self.map.values().collect(); - - this.partial_cmp(&that) - } -} - -impl Ord for Row { - /// Compare two dictionaries for ordering - fn cmp(&self, other: &Row) -> Ordering { - let this: Vec<&Column> = self.map.keys().collect(); - let that: Vec<&Column> = other.map.keys().collect(); - - if this != that { - return this.cmp(&that); - } - - let this: Vec<&InlineShape> = self.map.values().collect(); - let that: Vec<&InlineShape> = self.map.values().collect(); - - this.cmp(&that) - } -} diff --git a/crates/nu-data/src/config.rs b/crates/nu-data/src/config.rs deleted file mode 100644 index 3181db46a6..0000000000 --- a/crates/nu-data/src/config.rs +++ /dev/null @@ -1,319 +0,0 @@ -mod conf; -mod config_trust; -mod local_config; -mod nuconfig; -pub mod path; - -pub use conf::Conf; -pub use config_trust::is_file_trusted; -pub use config_trust::read_trusted; -pub use config_trust::Trusted; -pub use local_config::loadable_cfg_exists_in_dir; -pub use local_config::LocalConfigDiff; -pub use nuconfig::NuConfig; - -use indexmap::IndexMap; -use log::trace; -use nu_errors::{CoerceInto, ShellError}; -use nu_protocol::{ - Dictionary, Primitive, ShellTypeName, TaggedDictBuilder, UnspannedPathMember, UntaggedValue, - Value, -}; -use nu_source::{SpannedItem, Tag, TaggedItem}; -use std::env::var; -use std::fs::{self, OpenOptions}; -use std::io; -use std::path::{Path, PathBuf}; - -pub fn convert_toml_value_to_nu_value(v: &toml::Value, tag: impl Into) -> Value { - let tag = tag.into(); - - match v { - toml::Value::Boolean(b) => UntaggedValue::boolean(*b).into_value(tag), - toml::Value::Integer(n) => UntaggedValue::int(*n).into_value(tag), - toml::Value::Float(n) => UntaggedValue::decimal_from_float(*n, tag.span).into_value(tag), - toml::Value::String(s) => { - UntaggedValue::Primitive(Primitive::String(String::from(s))).into_value(tag) - } - toml::Value::Array(a) => UntaggedValue::Table( - a.iter() - .map(|x| convert_toml_value_to_nu_value(x, &tag)) - .collect(), - ) - .into_value(tag), - toml::Value::Datetime(dt) => { - UntaggedValue::Primitive(Primitive::String(dt.to_string())).into_value(tag) - } - toml::Value::Table(t) => { - let mut collected = TaggedDictBuilder::new(&tag); - - for (k, v) in t { - collected.insert_value(k.clone(), convert_toml_value_to_nu_value(v, &tag)); - } - - collected.into_value() - } - } -} - -fn collect_values(input: &[Value]) -> Result, ShellError> { - let mut out = vec![]; - - for value in input { - out.push(helper(value)?); - } - - Ok(out) -} -// Helper method to recursively convert nu_protocol::Value -> toml::Value -// This shouldn't be called at the top-level -fn helper(v: &Value) -> Result { - use bigdecimal::ToPrimitive; - - Ok(match &v.value { - UntaggedValue::Primitive(Primitive::Boolean(b)) => toml::Value::Boolean(*b), - UntaggedValue::Primitive(Primitive::Filesize(b)) => { - if let Some(value) = b.to_i64() { - toml::Value::Integer(value) - } else { - return Err(ShellError::labeled_error( - "Value too large to convert to toml value", - "value too large", - v.tag.span, - )); - } - } - UntaggedValue::Primitive(Primitive::Duration(i)) => toml::Value::String(i.to_string()), - UntaggedValue::Primitive(Primitive::Date(d)) => toml::Value::String(d.to_string()), - UntaggedValue::Primitive(Primitive::EndOfStream) => { - toml::Value::String("".to_string()) - } - UntaggedValue::Primitive(Primitive::BeginningOfStream) => { - toml::Value::String("".to_string()) - } - UntaggedValue::Primitive(Primitive::Decimal(f)) => { - toml::Value::Float(f.tagged(&v.tag).coerce_into("converting to TOML float")?) - } - UntaggedValue::Primitive(Primitive::Int(i)) => toml::Value::Integer(*i), - UntaggedValue::Primitive(Primitive::BigInt(i)) => { - toml::Value::Integer(i.tagged(&v.tag).coerce_into("converting to TOML integer")?) - } - UntaggedValue::Primitive(Primitive::Nothing) => { - toml::Value::String("".to_string()) - } - UntaggedValue::Primitive(Primitive::GlobPattern(s)) => toml::Value::String(s.clone()), - UntaggedValue::Primitive(Primitive::String(s)) => toml::Value::String(s.clone()), - UntaggedValue::Primitive(Primitive::FilePath(s)) => { - toml::Value::String(s.display().to_string()) - } - UntaggedValue::Primitive(Primitive::ColumnPath(path)) => toml::Value::Array( - path.iter() - .map(|x| match &x.unspanned { - UnspannedPathMember::String(string) => Ok(toml::Value::String(string.clone())), - UnspannedPathMember::Int(int) => Ok(toml::Value::Integer(*int)), - }) - .collect::, ShellError>>()?, - ), - UntaggedValue::Table(l) => toml::Value::Array(collect_values(l)?), - UntaggedValue::Error(e) => return Err(e.clone()), - UntaggedValue::Block(_) => toml::Value::String("".to_string()), - #[cfg(feature = "dataframe")] - UntaggedValue::DataFrame(_) | UntaggedValue::FrameStruct(_) => { - toml::Value::String("".to_string()) - } - UntaggedValue::Primitive(Primitive::Range(_)) => toml::Value::String("".to_string()), - UntaggedValue::Primitive(Primitive::Binary(b)) => { - toml::Value::Array(b.iter().map(|x| toml::Value::Integer(*x as i64)).collect()) - } - UntaggedValue::Row(o) => { - let mut m = toml::map::Map::new(); - for (k, v) in &o.entries { - m.insert(k.clone(), helper(v)?); - } - toml::Value::Table(m) - } - }) -} - -/// Converts a nu_protocol::Value into a toml::Value -/// Will return a Shell Error, if the Nu Value is not a valid top-level TOML Value -pub fn value_to_toml_value(v: &Value) -> Result { - match &v.value { - UntaggedValue::Row(o) => { - let mut m = toml::map::Map::new(); - for (k, v) in &o.entries { - m.insert(k.clone(), helper(v)?); - } - Ok(toml::Value::Table(m)) - } - UntaggedValue::Primitive(Primitive::String(s)) => { - // Attempt to de-serialize the String - toml::de::from_str(s).map_err(|_| { - ShellError::labeled_error( - format!("{:?} unable to de-serialize string to TOML", s), - "invalid TOML", - v.tag(), - ) - }) - } - _ => Err(ShellError::labeled_error( - format!("{:?} is not a valid top-level TOML", v.value), - "invalid TOML", - v.tag(), - )), - } -} - -pub fn config_path() -> Result { - use directories_next::ProjectDirs; - - let dir = ProjectDirs::from("org", "nushell", "nu") - .ok_or_else(|| ShellError::untagged_runtime_error("Couldn't find project directory"))?; - let path = var("NU_CONFIG_DIR").map_or(ProjectDirs::config_dir(&dir).to_owned(), |path| { - PathBuf::from(path) - }); - std::fs::create_dir_all(&path).map_err(|err| { - ShellError::untagged_runtime_error(&format!("Couldn't create {} path:\n{}", "config", err)) - })?; - - Ok(path) -} - -pub fn default_path() -> Result { - default_path_for(&None) -} - -pub fn default_path_for(file: &Option) -> Result { - let mut filename = config_path()?; - let file: &Path = file.as_deref().unwrap_or_else(|| "config.toml".as_ref()); - filename.push(file); - - Ok(filename) -} - -pub fn user_data() -> Result { - use directories_next::ProjectDirs; - - let dir = ProjectDirs::from("org", "nushell", "nu") - .ok_or_else(|| ShellError::untagged_runtime_error("Couldn't find project directory"))?; - let path = ProjectDirs::data_local_dir(&dir).to_owned(); - std::fs::create_dir_all(&path).map_err(|err| { - ShellError::untagged_runtime_error(&format!( - "Couldn't create {} path:\n{}", - "user data", err - )) - })?; - - Ok(path) -} - -#[derive(Debug, Clone)] -pub enum Status { - LastModified(std::time::SystemTime), - Unavailable, -} - -impl Default for Status { - fn default() -> Self { - Status::Unavailable - } -} - -pub fn last_modified(at: &Option) -> Result> { - let filename = default_path()?; - - let filename = match at { - None => filename, - Some(ref file) => file.clone(), - }; - - if let Ok(time) = filename.metadata()?.modified() { - return Ok(Status::LastModified(time)); - } - - Ok(Status::Unavailable) -} - -pub fn read( - tag: impl Into, - at: &Option, -) -> Result, ShellError> { - let filename = default_path()?; - - let filename = match at { - None => filename, - Some(ref file) => file.clone(), - }; - - if !filename.exists() && touch(&filename).is_err() { - // If we can't create configs, let's just return an empty indexmap instead as we may be in - // a readonly environment - return Ok(IndexMap::new()); - } - - trace!("config file = {}", filename.display()); - - let tag = tag.into(); - let contents = fs::read_to_string(filename) - .map(|v| v.tagged(&tag)) - .map_err(|err| { - ShellError::labeled_error( - &format!("Couldn't read config file:\n{}", err), - "file name", - &tag, - ) - })?; - - let parsed: toml::Value = toml::from_str(&contents).map_err(|err| { - ShellError::labeled_error( - &format!("Couldn't parse config file:\n{}", err), - "file name", - &tag, - ) - })?; - - let value = convert_toml_value_to_nu_value(&parsed, tag); - let tag = value.tag(); - match value.value { - UntaggedValue::Row(Dictionary { entries }) => Ok(entries), - other => Err(ShellError::type_error( - "Dictionary", - other.type_name().spanned(tag.span), - )), - } -} - -pub fn config(tag: impl Into) -> Result, ShellError> { - read(tag, &None) -} - -pub fn write(config: &IndexMap, at: &Option) -> Result<(), ShellError> { - let filename = default_path()?; - - let filename = match at { - None => filename, - Some(ref file) => file.clone(), - }; - - let contents = value_to_toml_value( - &UntaggedValue::Row(Dictionary::new(config.clone())).into_untagged_value(), - )?; - - let contents = toml::to_string(&contents)?; - - fs::write(&filename, &contents)?; - - Ok(()) -} - -// A simple implementation of `% touch path` (ignores existing files) -fn touch(path: &Path) -> io::Result<()> { - match OpenOptions::new().create(true).write(true).open(path) { - Ok(_) => Ok(()), - Err(e) => Err(e), - } -} - -pub fn cfg_path_to_scope_tag(cfg_path: &Path) -> String { - cfg_path.to_string_lossy().to_string() -} diff --git a/crates/nu-data/src/config/conf.rs b/crates/nu-data/src/config/conf.rs deleted file mode 100644 index 784184eaaa..0000000000 --- a/crates/nu-data/src/config/conf.rs +++ /dev/null @@ -1,38 +0,0 @@ -use nu_errors::ShellError; -use nu_protocol::Value; -use std::{fmt::Debug, path::PathBuf}; - -pub trait Conf: Debug + Send { - fn is_modified(&self) -> Result>; - fn var(&self, key: &str) -> Option; - fn env(&self) -> Option; - fn path(&self) -> Result>, ShellError>; - fn clone_box(&self) -> Box; - fn reload(&mut self); -} - -impl Conf for Box { - fn is_modified(&self) -> Result> { - (**self).is_modified() - } - - fn var(&self, key: &str) -> Option { - (**self).var(key) - } - - fn env(&self) -> Option { - (**self).env() - } - - fn reload(&mut self) { - (**self).reload(); - } - - fn clone_box(&self) -> Box { - (**self).clone_box() - } - - fn path(&self) -> Result>, ShellError> { - (**self).path() - } -} diff --git a/crates/nu-data/src/config/config_trust.rs b/crates/nu-data/src/config/config_trust.rs deleted file mode 100644 index 98012cdf8a..0000000000 --- a/crates/nu-data/src/config/config_trust.rs +++ /dev/null @@ -1,44 +0,0 @@ -use serde::Deserialize; -use serde::Serialize; -use sha2::{Digest, Sha256}; -use std::{io::Read, path::Path, path::PathBuf}; - -use indexmap::IndexMap; -use nu_errors::ShellError; - -#[derive(Deserialize, Serialize, Debug, Default)] -pub struct Trusted { - pub files: IndexMap>, -} - -impl Trusted { - pub fn new() -> Self { - Trusted { - files: IndexMap::new(), - } - } -} - -pub fn is_file_trusted(nu_env_file: &Path, content: &[u8]) -> Result { - let contentdigest = Sha256::digest(content).as_slice().to_vec(); - let nufile = nu_path::canonicalize(nu_env_file)?; - - let trusted = read_trusted()?; - Ok(trusted.files.get(&nufile.to_string_lossy().to_string()) == Some(&contentdigest)) -} - -pub fn read_trusted() -> Result { - let config_path = crate::config::default_path_for(&Some(PathBuf::from("nu-env.toml")))?; - - let mut file = std::fs::OpenOptions::new() - .read(true) - .create(true) - .write(true) - .open(config_path) - .map_err(|_| ShellError::untagged_runtime_error("Couldn't open nu-env.toml"))?; - let mut doc = String::new(); - file.read_to_string(&mut doc)?; - - let allowed = toml::de::from_str(&doc).unwrap_or_else(|_| Trusted::new()); - Ok(allowed) -} diff --git a/crates/nu-data/src/config/local_config.rs b/crates/nu-data/src/config/local_config.rs deleted file mode 100644 index b96c1f8095..0000000000 --- a/crates/nu-data/src/config/local_config.rs +++ /dev/null @@ -1,150 +0,0 @@ -use nu_errors::ShellError; -use std::path::{Path, PathBuf}; - -static LOCAL_CFG_FILE_NAME: &str = ".nu-env"; - -pub struct LocalConfigDiff { - pub cfgs_to_load: Vec, - pub cfgs_to_unload: Vec, -} - -/// Finds all local configs between `from` up to `to`. -/// Every config seen while going up the filesystem (e.G. from `/foo` to `/foo/bar`) is returned -/// as a config to load -/// Every config seen while going down the filesystem (e.G. from `/foo/bar` to `/foo/bar`) is -/// returned as a config to unload -/// If both paths are unrelated to each other, (e.G. windows paths as: `C:/foo` and `D:/bar`) -/// this function first walks `from` completely down the filesystem and then it walks up until `to`. -/// -/// Both paths are required to be absolute. -impl LocalConfigDiff { - pub fn between(from: PathBuf, to: PathBuf) -> (LocalConfigDiff, Vec) { - let common_prefix = common_path::common_path(&from, &to); - let (cfgs_to_unload, err_down) = walk_down(&from, &common_prefix); - let (cfgs_to_load, err_up) = walk_up(&common_prefix, &to); - - ( - LocalConfigDiff { - cfgs_to_load, - cfgs_to_unload, - }, - err_down.into_iter().chain(err_up).collect(), - ) - } -} - -///Walks from the first parameter down the filesystem to the second parameter. Marking all -///configs found in directories on the way as to remove. -///If to is None, this method walks from the first parameter down to the beginning of the -///filesystem -///Returns tuple of (configs to remove, errors from io). -fn walk_down( - from_inclusive: &Path, - to_exclusive: &Option, -) -> (Vec, Vec) { - let mut all_err = vec![]; - let mut all_cfgs_to_unload = vec![]; - for dir in from_inclusive.ancestors().take_while(|cur_path| { - if let Some(until_path) = to_exclusive { - //Stop before `to_exclusive` - *cur_path != until_path - } else { - //No end, walk all the way down - true - } - }) { - match local_cfg_should_be_unloaded(dir.to_path_buf()) { - Ok(Some(cfg)) => all_cfgs_to_unload.push(cfg), - Err(e) => all_err.push(e), - _ => {} - } - } - - (all_cfgs_to_unload, all_err) -} - -///Walks from the first parameter up the filesystem to the second parameter, returns all configs -///found in directories on the way to load. -///Returns combined errors from checking directories on the way -///If from is None, this method walks from the beginning of the second parameter up to the -///second parameter -fn walk_up( - from_exclusive: &Option, - to_inclusive: &Path, -) -> (Vec, Vec) { - let mut all_err = vec![]; - let mut all_cfgs_to_load = vec![]; - - //skip all paths until (inclusive) from (or 0 if from is None) - let skip_ahead = from_exclusive - .as_ref() - .map(|p| p.ancestors().count()) - .unwrap_or(0); - //We have to traverse ancestors in reverse order (apply lower directories first) - //ancestors() does not yield iter with .rev() method. So we store all ancestors - //and then iterate over the vec - let dirs: Vec<_> = to_inclusive.ancestors().map(Path::to_path_buf).collect(); - for dir in dirs.iter().rev().skip(skip_ahead) { - match loadable_cfg_exists_in_dir(dir.clone()) { - Ok(Some(cfg)) => all_cfgs_to_load.push(cfg), - Err(e) => all_err.push(e), - _ => {} - } - } - - (all_cfgs_to_load, all_err) -} - -fn is_existent_local_cfg(cfg_file_path: &Path) -> Result { - if !cfg_file_path.exists() || cfg_file_path.parent() == super::default_path()?.parent() { - //Don't treat global cfg as local one - Ok(false) - } else { - Ok(true) - } -} - -fn is_trusted_local_cfg_content(cfg_file_path: &Path, content: &[u8]) -> Result { - //This checks whether user used `autoenv trust` to mark this cfg as secure - if !super::is_file_trusted(cfg_file_path, content)? { - //Notify user about present config, but not trusted - Err(ShellError::untagged_runtime_error( - format!("{:?} is untrusted. Run 'autoenv trust {:?}' to trust it.\nThis needs to be done after each change to the file.", - cfg_file_path, cfg_file_path.parent().unwrap_or_else(|| Path::new(""))))) - } else { - Ok(true) - } -} - -fn local_cfg_should_be_unloaded>(cfg_dir: P) -> Result, ShellError> { - let mut cfg = cfg_dir.as_ref().to_path_buf(); - cfg.push(LOCAL_CFG_FILE_NAME); - if is_existent_local_cfg(&cfg)? { - //No need to compute whether content is good. If it is not loaded before, unloading does - //nothing - Ok(Some(cfg)) - } else { - Ok(None) - } -} - -/// Checks whether a local_cfg exists in cfg_dir and returns: -/// Ok(Some(cfg_path)) if cfg exists and is good to load -/// Ok(None) if no cfg exists -/// Err(error) if cfg exits, but is not good to load -pub fn loadable_cfg_exists_in_dir(mut cfg_dir: PathBuf) -> Result, ShellError> { - cfg_dir.push(LOCAL_CFG_FILE_NAME); - let cfg_path = cfg_dir; - - if !is_existent_local_cfg(&cfg_path)? { - return Ok(None); - } - - let content = std::fs::read(&cfg_path)?; - - if !is_trusted_local_cfg_content(&cfg_path, &content)? { - return Ok(None); - } - - Ok(Some(cfg_path)) -} diff --git a/crates/nu-data/src/config/nuconfig.rs b/crates/nu-data/src/config/nuconfig.rs deleted file mode 100644 index 7cecb8e911..0000000000 --- a/crates/nu-data/src/config/nuconfig.rs +++ /dev/null @@ -1,187 +0,0 @@ -use crate::config::{last_modified, read, Conf, Status}; -use indexmap::IndexMap; -use nu_errors::ShellError; -use nu_protocol::Value; -use nu_source::Tag; -use nu_test_support::NATIVE_PATH_ENV_VAR; -use std::{fmt::Debug, path::PathBuf}; - -use super::write; - -#[derive(Debug, Clone, Default)] -pub struct NuConfig { - pub vars: IndexMap, - pub file_path: PathBuf, - pub modified_at: Status, -} - -impl Conf for NuConfig { - fn is_modified(&self) -> Result> { - self.is_modified() - } - - fn var(&self, key: &str) -> Option { - self.var(key) - } - - fn env(&self) -> Option { - self.env() - } - - fn path(&self) -> Result>, ShellError> { - self.path() - } - - fn reload(&mut self) { - if let Ok(variables) = read(Tag::unknown(), &Some(self.file_path.clone())) { - self.vars = variables; - - self.modified_at = if let Ok(status) = last_modified(&Some(self.file_path.clone())) { - status - } else { - Status::Unavailable - }; - } - } - - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } -} - -impl NuConfig { - pub fn load(cfg_file_path: Option) -> Result { - let vars = read(Tag::unknown(), &cfg_file_path)?; - let modified_at = NuConfig::get_last_modified(&cfg_file_path); - let file_path = if let Some(file_path) = cfg_file_path { - file_path - } else { - crate::config::default_path()? - }; - - Ok(NuConfig { - vars, - file_path, - modified_at, - }) - } - - /// Writes self.values under self.file_path - pub fn write(&self) -> Result<(), ShellError> { - write(&self.vars, &Some(self.file_path.clone())) - } - - pub fn new() -> NuConfig { - let vars = if let Ok(variables) = read(Tag::unknown(), &None) { - variables - } else { - IndexMap::default() - }; - let path = if let Ok(path) = crate::config::default_path() { - path - } else { - PathBuf::new() - }; - - NuConfig { - vars, - modified_at: NuConfig::get_last_modified(&None), - file_path: path, - } - } - - pub fn get_last_modified(config_file: &Option) -> Status { - if let Ok(status) = last_modified(config_file) { - status - } else { - Status::Unavailable - } - } - - pub fn is_modified(&self) -> Result> { - let modified_at = &self.modified_at; - - Ok(match (NuConfig::get_last_modified(&None), modified_at) { - (Status::LastModified(left), Status::LastModified(right)) => { - let left = left.duration_since(std::time::UNIX_EPOCH)?; - let right = (*right).duration_since(std::time::UNIX_EPOCH)?; - - left != right - } - (_, _) => false, - }) - } - - pub fn var(&self, key: &str) -> Option { - let vars = &self.vars; - - if let Some(value) = vars.get(key) { - return Some(value.clone()); - } - - None - } - - /// Return environment variables as map - pub fn env_map(&self) -> IndexMap { - let mut result = IndexMap::new(); - if let Some(variables) = self.env() { - for var in variables.row_entries() { - if let Ok(value) = var.1.as_string() { - result.insert(var.0.clone(), value); - } - } - } - result - } - - pub fn env(&self) -> Option { - let vars = &self.vars; - - if let Some(env_vars) = vars.get("env") { - return Some(env_vars.clone()); - } - - None - } - - pub fn path(&self) -> Result>, ShellError> { - let vars = &self.vars; - - if let Some(path) = vars.get("path").or_else(|| vars.get(NATIVE_PATH_ENV_VAR)) { - path - .table_entries() - .map(|p| { - p.as_string().map(PathBuf::from).map_err(|_| { - ShellError::untagged_runtime_error("Could not format path entry as string!\nPath entry from config won't be added") - }) - }) - .collect::, ShellError>>().map(Some) - } else { - Ok(None) - } - } - - fn load_scripts_if_present(&self, scripts_name: &str) -> Result, ShellError> { - if let Some(array) = self.var(scripts_name) { - if !array.is_table() { - Err(ShellError::untagged_runtime_error(format!( - "expected an array of strings as {} commands", - scripts_name - ))) - } else { - array.table_entries().map(Value::as_string).collect() - } - } else { - Ok(vec![]) - } - } - - pub fn exit_scripts(&self) -> Result, ShellError> { - self.load_scripts_if_present("on_exit") - } - - pub fn startup_scripts(&self) -> Result, ShellError> { - self.load_scripts_if_present("startup") - } -} diff --git a/crates/nu-data/src/config/path.rs b/crates/nu-data/src/config/path.rs deleted file mode 100644 index 5a9fdb031d..0000000000 --- a/crates/nu-data/src/config/path.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::path::PathBuf; - -use super::NuConfig; - -const DEFAULT_LOCATION: &str = "history.txt"; - -pub fn default_history_path() -> PathBuf { - crate::config::user_data() - .map(|mut p| { - p.push(DEFAULT_LOCATION); - p - }) - .unwrap_or_else(|_| PathBuf::from(DEFAULT_LOCATION)) -} - -/// Get history path of config, if present -pub fn history_path(config: &NuConfig) -> Option { - config - .var("history-path") - .and_then(|custom_path| custom_path.as_string().map(PathBuf::from).ok()) -} - -/// Get history path in config or default -pub fn history_path_or_default(config: &NuConfig) -> PathBuf { - history_path(config).unwrap_or_else(default_history_path) -} diff --git a/crates/nu-data/src/config/tests.rs b/crates/nu-data/src/config/tests.rs deleted file mode 100644 index 9a89355375..0000000000 --- a/crates/nu-data/src/config/tests.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::config::{Conf, NuConfig, Status}; -use nu_protocol::Value; -use std::path::{Path, PathBuf}; - -#[derive(Debug, Clone)] -pub struct FakeConfig { - pub config: NuConfig, - source_file: Option, -} - -impl Conf for FakeConfig { - fn is_modified(&self) -> Result> { - self.is_modified() - } - - fn var(&self, key: &str) -> Option { - self.config.var(key) - } - - fn env(&self) -> Option { - self.config.env() - } - - fn path(&self) -> Option { - self.config.path() - } - - fn reload(&mut self) { - self.reload() - } - - fn clone_box(&self) -> Box { - self.config.clone_box() - } -} - -impl FakeConfig { - pub fn new(config_file: &Path) -> FakeConfig { - let config_file = config_file.to_path_buf(); - - FakeConfig { - config: NuConfig::with(Some(config_file.clone().into_os_string())), - source_file: Some(config_file), - } - } - - pub fn is_modified(&self) -> Result> { - let modified_at = &self.config.modified_at; - - Ok( - match (NuConfig::get_last_modified(&self.source_file), modified_at) { - (Status::LastModified(left), Status::LastModified(right)) => { - let left = left.duration_since(std::time::UNIX_EPOCH)?; - let right = (*right).duration_since(std::time::UNIX_EPOCH)?; - - left != right - } - (_, _) => false, - }, - ) - } - - pub fn reload(&mut self) { - self.config = NuConfig::with(self.source_file.clone().map(|x| x.into_os_string())); - } -} diff --git a/crates/nu-data/src/dict.rs b/crates/nu-data/src/dict.rs deleted file mode 100644 index fa48110fc3..0000000000 --- a/crates/nu-data/src/dict.rs +++ /dev/null @@ -1,105 +0,0 @@ -use derive_new::new; -use nu_protocol::{Dictionary, MaybeOwned, Primitive, UntaggedValue, Value}; -use nu_source::{DbgDocBldr, DebugDocBuilder, PrettyDebug, Spanned, Tag}; - -#[derive(Debug, new)] -struct DebugEntry<'a> { - key: &'a str, - value: &'a Value, -} - -impl<'a> PrettyDebug for DebugEntry<'a> { - fn pretty(&self) -> DebugDocBuilder { - (DbgDocBldr::key(self.key.to_string()) - + DbgDocBldr::equals() - + self.value.pretty().into_value()) - .group() - } -} - -pub trait DictionaryExt { - fn get_data(&self, desc: &str) -> MaybeOwned<'_, Value>; - - fn keys(&self) -> indexmap::map::Keys; - fn get_data_by_key(&self, name: Spanned<&str>) -> Option; - fn get_mut_data_by_key(&mut self, name: &str) -> Option<&mut Value>; - fn insert_data_at_key(&mut self, name: &str, value: Value); -} - -impl DictionaryExt for Dictionary { - fn get_data(&self, desc: &str) -> MaybeOwned<'_, Value> { - match self.entries.get(desc) { - Some(v) => MaybeOwned::Borrowed(v), - None => MaybeOwned::Owned( - UntaggedValue::Primitive(Primitive::Nothing).into_untagged_value(), - ), - } - } - - fn keys(&self) -> indexmap::map::Keys { - self.entries.keys() - } - - fn get_data_by_key(&self, name: Spanned<&str>) -> Option { - let result = self - .entries - .iter() - .find(|&(desc_name, _)| desc_name == name.item)? - .1; - - Some( - result - .value - .clone() - .into_value(Tag::new(result.anchor(), name.span)), - ) - } - - fn get_mut_data_by_key(&mut self, name: &str) -> Option<&mut Value> { - self.entries - .iter_mut() - .find(|&(desc_name, _)| desc_name == name) - .map_or_else(|| None, |x| Some(x.1)) - } - - fn insert_data_at_key(&mut self, name: &str, value: Value) { - self.entries.insert(name.to_string(), value); - } -} - -#[derive(Debug)] -pub struct TaggedListBuilder { - tag: Tag, - pub list: Vec, -} - -impl TaggedListBuilder { - pub fn new(tag: impl Into) -> TaggedListBuilder { - TaggedListBuilder { - tag: tag.into(), - list: vec![], - } - } - - pub fn push_value(&mut self, value: impl Into) { - self.list.push(value.into()); - } - - pub fn push_untagged(&mut self, value: impl Into) { - self.list.push(value.into().into_value(self.tag.clone())); - } - - pub fn into_value(self) -> Value { - UntaggedValue::Table(self.list).into_value(self.tag) - } - - pub fn into_untagged_value(self) -> UntaggedValue { - UntaggedValue::Table(self.list).into_value(self.tag).value - } -} - -impl From for Value { - fn from(input: TaggedListBuilder) -> Value { - input.into_value() - } -} diff --git a/crates/nu-data/src/keybinding.rs b/crates/nu-data/src/keybinding.rs deleted file mode 100644 index 9c36d463cd..0000000000 --- a/crates/nu-data/src/keybinding.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub fn keybinding_path() -> Result { - crate::config::default_path_for(&Some(std::path::PathBuf::from("keybindings.yml"))) -} diff --git a/crates/nu-data/src/lib.rs b/crates/nu-data/src/lib.rs deleted file mode 100644 index 44d0862bba..0000000000 --- a/crates/nu-data/src/lib.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub mod base; -pub mod config; -pub mod dict; -pub mod keybinding; -pub mod primitive; -pub mod utils; -pub mod value; - -pub use dict::TaggedListBuilder; diff --git a/crates/nu-data/src/primitive.rs b/crates/nu-data/src/primitive.rs deleted file mode 100644 index 3d0ad340c6..0000000000 --- a/crates/nu-data/src/primitive.rs +++ /dev/null @@ -1,378 +0,0 @@ -use crate::config::NuConfig; -use nu_ansi_term::{Color, Style}; -use nu_protocol::{hir::Number, Primitive, Value}; -use nu_table::{Alignment, TextStyle}; -use std::collections::HashMap; - -pub fn number(number: impl Into) -> Primitive { - let number = number.into(); - - match number { - Number::BigInt(int) => Primitive::BigInt(int), - Number::Int(int) => Primitive::Int(int), - Number::Decimal(decimal) => Primitive::Decimal(decimal), - } -} - -pub fn lookup_ansi_color_style(s: String) -> Style { - match s.as_str() { - "g" | "green" => Color::Green.normal(), - "gb" | "green_bold" => Color::Green.bold(), - "gu" | "green_underline" => Color::Green.underline(), - "gi" | "green_italic" => Color::Green.italic(), - "gd" | "green_dimmed" => Color::Green.dimmed(), - "gr" | "green_reverse" => Color::Green.reverse(), - "gbl" | "green_blink" => Color::Green.blink(), - "gst" | "green_strike" => Color::Green.strikethrough(), - "r" | "red" => Color::Red.normal(), - "rb" | "red_bold" => Color::Red.bold(), - "ru" | "red_underline" => Color::Red.underline(), - "ri" | "red_italic" => Color::Red.italic(), - "rd" | "red_dimmed" => Color::Red.dimmed(), - "rr" | "red_reverse" => Color::Red.reverse(), - "rbl" | "red_blink" => Color::Red.blink(), - "rst" | "red_strike" => Color::Red.strikethrough(), - "u" | "blue" => Color::Blue.normal(), - "ub" | "blue_bold" => Color::Blue.bold(), - "uu" | "blue_underline" => Color::Blue.underline(), - "ui" | "blue_italic" => Color::Blue.italic(), - "ud" | "blue_dimmed" => Color::Blue.dimmed(), - "ur" | "blue_reverse" => Color::Blue.reverse(), - "ubl" | "blue_blink" => Color::Blue.blink(), - "ust" | "blue_strike" => Color::Blue.strikethrough(), - "b" | "black" => Color::Black.normal(), - "bb" | "black_bold" => Color::Black.bold(), - "bu" | "black_underline" => Color::Black.underline(), - "bi" | "black_italic" => Color::Black.italic(), - "bd" | "black_dimmed" => Color::Black.dimmed(), - "br" | "black_reverse" => Color::Black.reverse(), - "bbl" | "black_blink" => Color::Black.blink(), - "bst" | "black_strike" => Color::Black.strikethrough(), - "y" | "yellow" => Color::Yellow.normal(), - "yb" | "yellow_bold" => Color::Yellow.bold(), - "yu" | "yellow_underline" => Color::Yellow.underline(), - "yi" | "yellow_italic" => Color::Yellow.italic(), - "yd" | "yellow_dimmed" => Color::Yellow.dimmed(), - "yr" | "yellow_reverse" => Color::Yellow.reverse(), - "ybl" | "yellow_blink" => Color::Yellow.blink(), - "yst" | "yellow_strike" => Color::Yellow.strikethrough(), - "p" | "purple" => Color::Purple.normal(), - "pb" | "purple_bold" => Color::Purple.bold(), - "pu" | "purple_underline" => Color::Purple.underline(), - "pi" | "purple_italic" => Color::Purple.italic(), - "pd" | "purple_dimmed" => Color::Purple.dimmed(), - "pr" | "purple_reverse" => Color::Purple.reverse(), - "pbl" | "purple_blink" => Color::Purple.blink(), - "pst" | "purple_strike" => Color::Purple.strikethrough(), - "c" | "cyan" => Color::Cyan.normal(), - "cb" | "cyan_bold" => Color::Cyan.bold(), - "cu" | "cyan_underline" => Color::Cyan.underline(), - "ci" | "cyan_italic" => Color::Cyan.italic(), - "cd" | "cyan_dimmed" => Color::Cyan.dimmed(), - "cr" | "cyan_reverse" => Color::Cyan.reverse(), - "cbl" | "cyan_blink" => Color::Cyan.blink(), - "cst" | "cyan_strike" => Color::Cyan.strikethrough(), - "w" | "white" => Color::White.normal(), - "wb" | "white_bold" => Color::White.bold(), - "wu" | "white_underline" => Color::White.underline(), - "wi" | "white_italic" => Color::White.italic(), - "wd" | "white_dimmed" => Color::White.dimmed(), - "wr" | "white_reverse" => Color::White.reverse(), - "wbl" | "white_blink" => Color::White.blink(), - "wst" | "white_strike" => Color::White.strikethrough(), - _ => Color::White.normal(), - } -} - -pub fn string_to_lookup_value(str_prim: &str) -> String { - match str_prim { - "primitive_int" => "Primitive::Int".to_string(), - "primitive_decimal" => "Primitive::Decimal".to_string(), - "primitive_filesize" => "Primitive::Filesize".to_string(), - "primitive_string" => "Primitive::String".to_string(), - "primitive_line" => "Primitive::Line".to_string(), - "primitive_columnpath" => "Primitive::ColumnPath".to_string(), - "primitive_pattern" => "Primitive::GlobPattern".to_string(), - "primitive_boolean" => "Primitive::Boolean".to_string(), - "primitive_date" => "Primitive::Date".to_string(), - "primitive_duration" => "Primitive::Duration".to_string(), - "primitive_range" => "Primitive::Range".to_string(), - "primitive_path" => "Primitive::FilePath".to_string(), - "primitive_binary" => "Primitive::Binary".to_string(), - "separator_color" => "separator_color".to_string(), - "header_align" => "header_align".to_string(), - "header_color" => "header_color".to_string(), - "header_style" => "header_style".to_string(), - "index_color" => "index_color".to_string(), - "leading_trailing_space_bg" => "leading_trailing_space_bg".to_string(), - _ => "Primitive::Nothing".to_string(), - } -} - -fn update_hashmap(key: &str, val: &Value, hm: &mut HashMap) { - if let Ok(var) = val.as_string() { - let color = lookup_ansi_color_style(var); - let prim = string_to_lookup_value(key); - if let Some(v) = hm.get_mut(&prim) { - *v = color; - } else { - hm.insert(prim, color); - } - } -} - -pub fn get_color_config(config: &NuConfig) -> HashMap { - let config = &config.vars; - - // create the hashmap - let mut hm: HashMap = HashMap::new(); - // set some defaults - hm.insert("primitive_int".to_string(), Color::White.normal()); - hm.insert("primitive_decimal".to_string(), Color::White.normal()); - hm.insert("primitive_filesize".to_string(), Color::White.normal()); - hm.insert("primitive_string".to_string(), Color::White.normal()); - hm.insert("primitive_line".to_string(), Color::White.normal()); - hm.insert("primitive_columnpath".to_string(), Color::White.normal()); - hm.insert("primitive_pattern".to_string(), Color::White.normal()); - hm.insert("primitive_boolean".to_string(), Color::White.normal()); - hm.insert("primitive_date".to_string(), Color::White.normal()); - hm.insert("primitive_duration".to_string(), Color::White.normal()); - hm.insert("primitive_range".to_string(), Color::White.normal()); - hm.insert("primitive_path".to_string(), Color::White.normal()); - hm.insert("primitive_binary".to_string(), Color::White.normal()); - hm.insert("separator_color".to_string(), Color::White.normal()); - hm.insert("header_align".to_string(), Color::Green.bold()); - hm.insert("header_color".to_string(), Color::Green.bold()); - hm.insert("header_style".to_string(), Style::default()); - hm.insert("index_color".to_string(), Color::Green.bold()); - hm.insert( - "leading_trailing_space_bg".to_string(), - Style::default().on(Color::Rgb(128, 128, 128)), - ); - - // populate hashmap from config values - if let Some(primitive_color_vars) = config.get("color_config") { - for (key, value) in primitive_color_vars.row_entries() { - match key.as_ref() { - "primitive_int" => { - update_hashmap(key, value, &mut hm); - } - "primitive_decimal" => { - update_hashmap(key, value, &mut hm); - } - "primitive_filesize" => { - update_hashmap(key, value, &mut hm); - } - "primitive_string" => { - update_hashmap(key, value, &mut hm); - } - "primitive_line" => { - update_hashmap(key, value, &mut hm); - } - "primitive_columnpath" => { - update_hashmap(key, value, &mut hm); - } - "primitive_pattern" => { - update_hashmap(key, value, &mut hm); - } - "primitive_boolean" => { - update_hashmap(key, value, &mut hm); - } - "primitive_date" => { - update_hashmap(key, value, &mut hm); - } - "primitive_duration" => { - update_hashmap(key, value, &mut hm); - } - "primitive_range" => { - update_hashmap(key, value, &mut hm); - } - "primitive_path" => { - update_hashmap(key, value, &mut hm); - } - "primitive_binary" => { - update_hashmap(key, value, &mut hm); - } - "separator_color" => { - update_hashmap(key, value, &mut hm); - } - "header_align" => { - update_hashmap(key, value, &mut hm); - } - "header_color" => { - update_hashmap(key, value, &mut hm); - } - "header_style" => { - update_hashmap(key, value, &mut hm); - } - "index_color" => { - update_hashmap(key, value, &mut hm); - } - "leading_trailing_space_bg" => { - update_hashmap(key, value, &mut hm); - } - _ => (), - } - } - } - - hm -} - -// This function will assign a text style to a primitive, or really any string that's -// in the hashmap. The hashmap actually contains the style to be applied. -pub fn style_primitive(primitive: &str, color_hm: &HashMap) -> TextStyle { - match primitive { - "Int" => { - let style = color_hm.get("Primitive::Int"); - match style { - Some(s) => TextStyle::with_style(Alignment::Right, *s), - None => TextStyle::basic_right(), - } - } - "Decimal" => { - let style = color_hm.get("Primitive::Decimal"); - match style { - Some(s) => TextStyle::with_style(Alignment::Right, *s), - None => TextStyle::basic_right(), - } - } - "Filesize" => { - let style = color_hm.get("Primitive::Filesize"); - match style { - Some(s) => TextStyle::with_style(Alignment::Right, *s), - None => TextStyle::basic_right(), - } - } - "String" => { - let style = color_hm.get("Primitive::String"); - match style { - Some(s) => TextStyle::with_style(Alignment::Left, *s), - None => TextStyle::basic_left(), - } - } - "Line" => { - let style = color_hm.get("Primitive::Line"); - match style { - Some(s) => TextStyle::with_style(Alignment::Left, *s), - None => TextStyle::basic_left(), - } - } - "ColumnPath" => { - let style = color_hm.get("Primitive::ColumnPath"); - match style { - Some(s) => TextStyle::with_style(Alignment::Left, *s), - None => TextStyle::basic_left(), - } - } - "GlobPattern" => { - let style = color_hm.get("Primitive::GlobPattern"); - match style { - Some(s) => TextStyle::with_style(Alignment::Left, *s), - None => TextStyle::basic_left(), - } - } - "Boolean" => { - let style = color_hm.get("Primitive::Boolean"); - match style { - Some(s) => TextStyle::with_style(Alignment::Left, *s), - None => TextStyle::basic_left(), - } - } - "Date" => { - let style = color_hm.get("Primitive::Date"); - match style { - Some(s) => TextStyle::with_style(Alignment::Left, *s), - None => TextStyle::basic_left(), - } - } - "Duration" => { - let style = color_hm.get("Primitive::Duration"); - match style { - Some(s) => TextStyle::with_style(Alignment::Left, *s), - None => TextStyle::basic_left(), - } - } - "Range" => { - let style = color_hm.get("Primitive::Range"); - match style { - Some(s) => TextStyle::with_style(Alignment::Left, *s), - None => TextStyle::basic_left(), - } - } - "FilePath" => { - let style = color_hm.get("Primitive::FilePath"); - match style { - Some(s) => TextStyle::with_style(Alignment::Left, *s), - None => TextStyle::basic_left(), - } - } - "Binary" => { - let style = color_hm.get("Primitive::Binary"); - match style { - Some(s) => TextStyle::with_style(Alignment::Left, *s), - None => TextStyle::basic_left(), - } - } - "BeginningOfStream" => { - let style = color_hm.get("Primitive::BeginningOfStream"); - match style { - Some(s) => TextStyle::with_style(Alignment::Left, *s), - None => TextStyle::basic_left(), - } - } - "EndOfStream" => { - let style = color_hm.get("Primitive::EndOfStream"); - match style { - Some(s) => TextStyle::with_style(Alignment::Left, *s), - None => TextStyle::basic_left(), - } - } - "Nothing" => { - let style = color_hm.get("Primitive::Nothing"); - match style { - Some(s) => TextStyle::with_style(Alignment::Left, *s), - None => TextStyle::basic_left(), - } - } - "separator_color" => { - let style = color_hm.get("separator"); - match style { - Some(s) => TextStyle::with_style(Alignment::Left, *s), - None => TextStyle::basic_left(), - } - } - "header_align" => { - let style = color_hm.get("header_align"); - match style { - Some(s) => TextStyle::with_style(Alignment::Center, *s), - None => TextStyle::default_header(), - } - } - "header_color" => { - let style = color_hm.get("header_color"); - match style { - Some(s) => TextStyle::with_style(Alignment::Center, *s), - None => TextStyle::default_header().bold(Some(true)), - } - } - "header_style" => { - let style = color_hm.get("header_style"); - match style { - Some(s) => TextStyle::with_style(Alignment::Center, *s), - None => TextStyle::default_header(), - } - } - "index_color" => { - let style = color_hm.get("index_color"); - match style { - Some(s) => TextStyle::with_style(Alignment::Right, *s), - None => TextStyle::new() - .alignment(Alignment::Right) - .fg(Color::Green) - .bold(Some(true)), - } - } - _ => TextStyle::basic_center(), - } -} diff --git a/crates/nu-data/src/utils/group.rs b/crates/nu-data/src/utils/group.rs deleted file mode 100644 index 75c0208879..0000000000 --- a/crates/nu-data/src/utils/group.rs +++ /dev/null @@ -1,35 +0,0 @@ -use indexmap::IndexMap; -use nu_errors::ShellError; -use nu_protocol::{TaggedDictBuilder, UntaggedValue, Value}; -use nu_source::Tag; -use nu_value_ext::as_string; - -#[allow(clippy::type_complexity)] -pub fn group( - values: &Value, - grouper: &Option Result + Send>>, - tag: impl Into, -) -> Result { - let tag = tag.into(); - - let mut groups: IndexMap> = IndexMap::new(); - - for (idx, value) in values.table_entries().enumerate() { - let group_key = if let Some(ref grouper) = grouper { - grouper(idx, value) - } else { - as_string(value) - }; - - let group = groups.entry(group_key?).or_insert(vec![]); - group.push((*value).clone()); - } - - let mut out = TaggedDictBuilder::new(&tag); - - for (k, v) in &groups { - out.insert_untagged(k, UntaggedValue::table(v)); - } - - Ok(out.into_value()) -} diff --git a/crates/nu-data/src/utils/internal.rs b/crates/nu-data/src/utils/internal.rs deleted file mode 100644 index b9b05ef650..0000000000 --- a/crates/nu-data/src/utils/internal.rs +++ /dev/null @@ -1,332 +0,0 @@ -#![allow(clippy::type_complexity)] - -use crate::value::unsafe_compute_values; -use derive_new::new; -use nu_errors::ShellError; -use nu_protocol::hir::Operator; -use nu_protocol::{UntaggedValue, Value}; -use nu_source::{SpannedItem, Tag, TaggedItem}; -use nu_value_ext::ValueExt; - -#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone, new)] -pub struct Labels { - pub x: Vec, - pub y: Vec, -} - -impl Labels { - pub fn at(&self, idx: usize) -> Option<&str> { - self.x.get(idx).map(|k| &k[..]) - } - - pub fn at_split(&self, idx: usize) -> Option<&str> { - self.y.get(idx).map(|k| &k[..]) - } - - pub fn grouped(&self) -> impl Iterator { - self.x.iter() - } - - pub fn grouping_total(&self) -> Value { - UntaggedValue::int(self.x.len() as i64).into_untagged_value() - } - - pub fn splits(&self) -> impl Iterator { - self.y.iter() - } - - pub fn splits_total(&self) -> Value { - UntaggedValue::big_int(self.y.len()).into_untagged_value() - } -} - -#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone, new)] -pub struct Range { - pub start: Value, - pub end: Value, -} - -fn formula( - acc_begin: Value, - calculator: Box) -> Result + Send + Sync + 'static>, -) -> Box) -> Result + Send + Sync + 'static> { - Box::new(move |acc, datax| -> Result { - let result = match unsafe_compute_values(Operator::Multiply, acc, &acc_begin) { - Ok(v) => v.into_untagged_value(), - Err((left_type, right_type)) => { - return Err(ShellError::coerce_error( - left_type.spanned_unknown(), - right_type.spanned_unknown(), - )) - } - }; - - match calculator(datax) { - Ok(total) => Ok( - match unsafe_compute_values(Operator::Plus, &result, &total) { - Ok(v) => v.into_untagged_value(), - Err((left_type, right_type)) => { - return Err(ShellError::coerce_error( - left_type.spanned_unknown(), - right_type.spanned_unknown(), - )) - } - }, - ), - Err(reason) => Err(reason), - } - }) -} - -pub fn reducer_for( - command: &Reduction, -) -> Box) -> Result + Send + Sync + 'static> { - match command { - Reduction::Accumulate => Box::new(formula( - UntaggedValue::int(1).into_untagged_value(), - Box::new(sum), - )), - Reduction::Count => Box::new(formula( - UntaggedValue::int(0).into_untagged_value(), - Box::new(sum), - )), - } -} - -pub fn max(values: &Value, tag: impl Into) -> Result { - let tag = tag.into(); - - let mut x = UntaggedValue::int(0); - - for split in values.table_entries() { - match split.value { - UntaggedValue::Table(ref values) => { - let inner = inner_max(values)?; - - if let Ok(greater_than) = - crate::value::compare_values(Operator::GreaterThan, &inner.value, &x) - { - if greater_than { - x = inner.value.clone(); - } - } else { - return Err(ShellError::unexpected(format!( - "Could not compare\nleft: {:?}\nright: {:?}", - inner.value, x - ))); - } - } - _ => { - return Err(ShellError::labeled_error( - "Attempted to compute the sum of a value that cannot be summed.", - "value appears here", - split.tag.span, - )) - } - } - } - - Ok(x.into_value(&tag)) -} - -pub fn inner_max(data: &[Value]) -> Result { - let mut biggest = data - .first() - .ok_or_else(|| { - ShellError::unexpected("Cannot perform aggregate math operation on empty data") - })? - .value - .clone(); - - for value in data { - if let Ok(greater_than) = - crate::value::compare_values(Operator::GreaterThan, &value.value, &biggest) - { - if greater_than { - biggest = value.value.clone(); - } - } else { - return Err(ShellError::unexpected(format!( - "Could not compare\nleft: {:?}\nright: {:?}", - biggest, value.value - ))); - } - } - Ok(Value { - value: biggest, - tag: Tag::unknown(), - }) -} - -pub fn sum(data: Vec<&Value>) -> Result { - let mut acc = UntaggedValue::int(0); - - for value in data { - match value.value { - UntaggedValue::Primitive(_) => { - acc = match unsafe_compute_values(Operator::Plus, &acc, value) { - Ok(v) => v, - Err((left_type, right_type)) => { - return Err(ShellError::coerce_error( - left_type.spanned_unknown(), - right_type.spanned_unknown(), - )) - } - }; - } - _ => { - return Err(ShellError::labeled_error( - "Attempted to compute the sum of a value that cannot be summed.", - "value appears here", - value.tag.span, - )) - } - } - } - Ok(acc.into_untagged_value()) -} - -pub fn sort_columns( - values: &[String], - format: &Option Result>>, -) -> Result, ShellError> { - let mut keys = values.to_vec(); - - if format.is_none() { - keys.sort(); - } - - Ok(keys) -} - -pub fn sort(planes: &Labels, values: &Value, tag: impl Into) -> Result { - let tag = tag.into(); - - let mut x = vec![]; - for column in planes.splits() { - let key = column.clone().tagged_unknown(); - let groups = values - .get_data_by_key(key.borrow_spanned()) - .ok_or_else(|| { - ShellError::labeled_error("unknown column", "unknown column", key.span()) - })?; - - let mut y = vec![]; - for inner_column in planes.grouped() { - let key = inner_column.clone().tagged_unknown(); - let grouped = groups.get_data_by_key(key.borrow_spanned()); - - if let Some(grouped) = grouped { - y.push(grouped); - } else { - y.push(UntaggedValue::Table(vec![]).into_value(&tag)); - } - } - - x.push(UntaggedValue::table(&y).into_value(&tag)); - } - - Ok(UntaggedValue::table(&x).into_value(&tag)) -} - -pub fn evaluate( - values: &Value, - evaluator: &Option Result + Send>>, - tag: impl Into, -) -> Result { - let tag = tag.into(); - - let mut x = vec![]; - for split in values.table_entries() { - let mut y = vec![]; - - for (idx, subset) in split.table_entries().enumerate() { - if let UntaggedValue::Table(values) = &subset.value { - if let Some(ref evaluator) = evaluator { - let mut evaluations = vec![]; - - for set in values { - evaluations.push(evaluator(idx, set)?); - } - - y.push(UntaggedValue::Table(evaluations).into_value(&tag)); - } else { - y.push( - UntaggedValue::Table( - values - .iter() - .map(|_| UntaggedValue::int(1).into_value(&tag)) - .collect::>(), - ) - .into_value(&tag), - ); - } - } - } - - x.push(UntaggedValue::table(&y).into_value(&tag)); - } - - Ok(UntaggedValue::table(&x).into_value(&tag)) -} - -pub enum Reduction { - Count, - Accumulate, -} - -pub fn reduce( - values: &Value, - reduction_with: &Reduction, - tag: impl Into, -) -> Result { - let tag = tag.into(); - let reduce_with = reducer_for(reduction_with); - - let mut datasets = vec![]; - for dataset in values.table_entries() { - let mut acc = UntaggedValue::int(0).into_value(&tag); - - let mut subsets = vec![]; - for subset in dataset.table_entries() { - acc = reduce_with(&acc, subset.table_entries().collect::>())?; - subsets.push(acc.clone()); - } - datasets.push(UntaggedValue::table(&subsets).into_value(&tag)); - } - - Ok(UntaggedValue::table(&datasets).into_value(&tag)) -} - -pub fn percentages( - maxima: &Value, - values: &Value, - tag: impl Into, -) -> Result { - let tag = tag.into(); - - let mut x = vec![]; - for split in values.table_entries() { - x.push( - UntaggedValue::table( - &split - .table_entries() - .filter_map(|s| { - let hundred = UntaggedValue::decimal_from_float(100.0, tag.span); - - match unsafe_compute_values(Operator::Divide, &hundred, maxima) { - Ok(v) => match unsafe_compute_values(Operator::Multiply, s, &v) { - Ok(v) => Some(v.into_untagged_value()), - Err(_) => None, - }, - Err(_) => None, - } - }) - .collect::>(), - ) - .into_value(&tag), - ); - } - - Ok(UntaggedValue::table(&x).into_value(&tag)) -} diff --git a/crates/nu-data/src/utils/mod.rs b/crates/nu-data/src/utils/mod.rs deleted file mode 100644 index d2712b14d4..0000000000 --- a/crates/nu-data/src/utils/mod.rs +++ /dev/null @@ -1,292 +0,0 @@ -pub mod group; -pub mod split; - -mod internal; - -pub use crate::utils::group::group; -pub use crate::utils::split::split; - -pub use crate::utils::internal::Reduction; -use crate::utils::internal::*; - -use derive_new::new; -use getset::Getters; -use nu_errors::ShellError; -use nu_protocol::{UntaggedValue, Value}; -use nu_source::Tag; - -#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Getters, Clone, new)] -pub struct Model { - pub labels: Labels, - pub ranges: (Range, Range), - - pub data: Value, - pub percentages: Value, -} - -#[allow(clippy::type_complexity)] -pub struct Operation<'a> { - pub grouper: Option Result + Send>>, - pub splitter: Option Result + Send>>, - pub format: &'a Option Result>>, - pub eval: &'a Option Result + Send>>, - pub reduction: &'a Reduction, -} - -pub fn report( - values: &Value, - options: Operation, - tag: impl Into, -) -> Result { - let tag = tag.into(); - - let grouped = group(values, &options.grouper, &tag)?; - let splitted = split(&grouped, &options.splitter, &tag)?; - - let x = grouped - .row_entries() - .map(|(key, _)| key.clone()) - .collect::>(); - - let x = sort_columns(&x, options.format)?; - - let mut y = splitted - .row_entries() - .map(|(key, _)| key.clone()) - .collect::>(); - - y.sort(); - - let planes = Labels { x, y }; - - let sorted = sort(&planes, &splitted, &tag)?; - - let evaluated = evaluate( - &sorted, - if options.eval.is_some() { - options.eval - } else { - &None - }, - &tag, - )?; - - let group_labels = planes.grouping_total(); - - let reduced = reduce(&evaluated, options.reduction, &tag)?; - - let maxima = max(&reduced, &tag)?; - - let percents = percentages(&maxima, &reduced, &tag)?; - - Ok(Model { - labels: planes, - ranges: ( - Range { - start: UntaggedValue::int(0).into_untagged_value(), - end: group_labels, - }, - Range { - start: UntaggedValue::int(0).into_untagged_value(), - end: maxima, - }, - ), - data: reduced, - percentages: percents, - }) -} - -pub mod helpers { - use nu_errors::ShellError; - use nu_protocol::{row, Value}; - use nu_source::{Tag, TaggedItem}; - use nu_test_support::value::{date, int, string, table}; - use nu_value_ext::ValueExt; - - pub fn committers() -> Vec { - vec![ - row! { - "date".into() => date("2019-07-23"), - "name".into() => string("AR"), - "country".into() => string("EC"), - "chickens".into() => int(10) - }, - row! { - "date".into() => date("2019-07-23"), - "name".into() => string("JT"), - "country".into() => string("NZ"), - "chickens".into() => int(5) - }, - row! { - "date".into() => date("2019-10-10"), - "name".into() => string("YK"), - "country".into() => string("US"), - "chickens".into() => int(6) - }, - row! { - "date".into() => date("2019-09-24"), - "name".into() => string("AR"), - "country".into() => string("EC"), - "chickens".into() => int(20) - }, - row! { - "date".into() => date("2019-10-10"), - "name".into() => string("JT"), - "country".into() => string("NZ"), - "chickens".into() => int(15) - }, - row! { - "date".into() => date("2019-09-24"), - "name".into() => string("YK"), - "country".into() => string("US"), - "chickens".into() => int(4) - }, - row! { - "date".into() => date("2019-10-10"), - "name".into() => string("AR"), - "country".into() => string("EC"), - "chickens".into() => int(30) - }, - row! { - "date".into() => date("2019-09-24"), - "name".into() => string("JT"), - "country".into() => string("NZ"), - "chickens".into() => int(10) - }, - row! { - "date".into() => date("2019-07-23"), - "name".into() => string("YK"), - "country".into() => string("US"), - "chickens".into() => int(2) - }, - ] - } - - pub fn committers_grouped_by_date() -> Value { - let sample = table(&committers()); - - let grouper = Box::new(move |_, row: &Value| { - let key = String::from("date").tagged_unknown(); - let group_key = row - .get_data_by_key(key.borrow_spanned()) - .expect("get key failed"); - - group_key.format("%Y-%m-%d") - }); - - crate::utils::group(&sample, &Some(grouper), Tag::unknown()) - .expect("failed to create group") - } - - pub fn date_formatter( - fmt: String, - ) -> Box Result> { - Box::new(move |date: &Value, _: String| { - let fmt = fmt.clone(); - date.format(&fmt) - }) - } -} - -#[cfg(test)] -mod tests { - use super::helpers::{committers, date_formatter}; - use super::{report, Labels, Model, Operation, Range, Reduction}; - use nu_errors::ShellError; - use nu_protocol::Value; - use nu_source::{Tag, TaggedItem}; - use nu_test_support::value::{decimal_from_float, int, table}; - use nu_value_ext::ValueExt; - - pub fn assert_without_checking_percentages(report_a: Model, report_b: Model) { - assert_eq!(report_a.labels.x, report_b.labels.x); - assert_eq!(report_a.labels.y, report_b.labels.y); - assert_eq!(report_a.ranges, report_b.ranges); - assert_eq!(report_a.data, report_b.data); - } - - #[test] - fn prepares_report_using_counting_value() { - let committers = table(&committers()); - - let by_date = Box::new(move |_, row: &Value| { - let key = String::from("date").tagged_unknown(); - let key = row.get_data_by_key(key.borrow_spanned()).unwrap(); - - let callback = date_formatter("%Y-%m-%d".to_string()); - callback(&key, "nothing".to_string()) - }); - - let by_country = Box::new(move |_, row: &Value| { - let key = String::from("country").tagged_unknown(); - let key = row.get_data_by_key(key.borrow_spanned()).unwrap(); - nu_value_ext::as_string(&key) - }); - - let options = Operation { - grouper: Some(by_date), - splitter: Some(by_country), - format: &None, - eval: /* value to be used for accumulation */ &Some(Box::new(move |_, value: &Value| { - let chickens_key = String::from("chickens").tagged_unknown(); - - value - .get_data_by_key(chickens_key.borrow_spanned()) - .ok_or_else(|| { - ShellError::labeled_error( - "unknown column", - "unknown column", - chickens_key.span(), - ) - }) - })), - reduction: &Reduction::Count - }; - - assert_without_checking_percentages( - report(&committers, options, Tag::unknown()).unwrap(), - Model { - labels: Labels { - x: vec![ - String::from("2019-07-23"), - String::from("2019-09-24"), - String::from("2019-10-10"), - ], - y: vec![String::from("EC"), String::from("NZ"), String::from("US")], - }, - ranges: ( - Range { - start: int(0), - end: int(3), - }, - Range { - start: int(0), - end: int(30), - }, - ), - data: table(&[ - table(&[int(10), int(20), int(30)]), - table(&[int(5), int(10), int(15)]), - table(&[int(2), int(4), int(6)]), - ]), - percentages: table(&[ - table(&[ - decimal_from_float(33.33), - decimal_from_float(66.66), - decimal_from_float(99.99), - ]), - table(&[ - decimal_from_float(16.66), - decimal_from_float(33.33), - decimal_from_float(49.99), - ]), - table(&[ - decimal_from_float(6.66), - decimal_from_float(13.33), - decimal_from_float(19.99), - ]), - ]), - }, - ); - } -} diff --git a/crates/nu-data/src/utils/split.rs b/crates/nu-data/src/utils/split.rs deleted file mode 100644 index 94c43befd8..0000000000 --- a/crates/nu-data/src/utils/split.rs +++ /dev/null @@ -1,59 +0,0 @@ -use nu_errors::ShellError; -use nu_protocol::{SpannedTypeName, TaggedDictBuilder, UntaggedValue, Value}; -use nu_source::Tag; - -use crate::utils::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(); - let mut out = TaggedDictBuilder::new(&tag); - - if splitter.is_none() { - out.insert_untagged("table", value.clone()); - return Ok(out.into_value()); - } - - 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 { - out.insert_untagged(k, UntaggedValue::row(v)); - } - - Ok(out.into_value()) -} diff --git a/crates/nu-data/src/value.rs b/crates/nu-data/src/value.rs deleted file mode 100644 index c4e85016cd..0000000000 --- a/crates/nu-data/src/value.rs +++ /dev/null @@ -1,838 +0,0 @@ -use crate::base::coerce_compare; -use crate::base::shape::{Column, InlineShape}; -use crate::primitive::style_primitive; -use bigdecimal::Signed; -use chrono::{DateTime, NaiveDate, Utc}; -use nu_errors::ShellError; -use nu_protocol::hir::Operator; -use nu_protocol::ShellTypeName; -use nu_protocol::{Primitive, Type, UntaggedValue}; -use nu_source::{DebugDocBuilder, PrettyDebug, Span, Tagged}; -use nu_table::TextStyle; -use num_bigint::BigInt; -use num_bigint::ToBigInt; -use num_traits::{ToPrimitive, Zero}; -use std::collections::HashMap; - -pub struct Date; - -impl Date { - pub fn from_regular_str(s: Tagged<&str>) -> Result { - let date = DateTime::parse_from_rfc3339(s.item).map_err(|err| { - ShellError::labeled_error( - &format!("Date parse error: {}", err), - "original value", - s.tag, - ) - })?; - - let date = date.with_timezone(&chrono::offset::Utc); - - Ok(UntaggedValue::Primitive(Primitive::Date(date.into()))) - } - - pub fn naive_from_str(s: Tagged<&str>) -> Result { - let date = NaiveDate::parse_from_str(s.item, "%Y-%m-%d").map_err(|reason| { - ShellError::labeled_error( - &format!("Date parse error: {}", reason), - "original value", - s.tag, - ) - })?; - - Ok(UntaggedValue::Primitive(Primitive::Date( - DateTime::::from_utc(date.and_hms(12, 34, 56), Utc).into(), - ))) - } -} - -pub fn date_from_str(s: Tagged<&str>) -> Result { - Date::from_regular_str(s) -} - -pub fn date_naive_from_str(s: Tagged<&str>) -> Result { - Date::naive_from_str(s) -} - -pub fn merge_values( - left: &UntaggedValue, - right: &UntaggedValue, -) -> Result { - match (left, right) { - (UntaggedValue::Row(columns), UntaggedValue::Row(columns_b)) => { - Ok(UntaggedValue::Row(columns.merge_from(columns_b))) - } - (left, right) => Err((left.type_name(), right.type_name())), - } -} - -fn zero_division_error() -> UntaggedValue { - UntaggedValue::Error(ShellError::untagged_runtime_error("division by zero")) -} - -pub fn unsafe_compute_values( - operator: Operator, - left: &UntaggedValue, - right: &UntaggedValue, -) -> Result { - let computed = compute_values(operator, left, right); - - if computed.is_ok() { - return computed; - } - - match (left, right) { - (UntaggedValue::Primitive(lhs), UntaggedValue::Primitive(rhs)) => match (lhs, rhs) { - (Primitive::Filesize(x), Primitive::Int(y)) => match operator { - Operator::Multiply => { - Ok(UntaggedValue::Primitive(Primitive::Filesize(x * *y as u64))) - } - Operator::Divide => { - Ok(UntaggedValue::Primitive(Primitive::Filesize(x / *y as u64))) - } - _ => Err((left.type_name(), right.type_name())), - }, - (Primitive::Int(x), Primitive::Filesize(y)) => match operator { - Operator::Multiply => { - Ok(UntaggedValue::Primitive(Primitive::Filesize(*x as u64 * y))) - } - _ => Err((left.type_name(), right.type_name())), - }, - _ => Err((left.type_name(), right.type_name())), - }, - _ => Err((left.type_name(), right.type_name())), - } -} - -pub fn compute_values( - operator: Operator, - left: &UntaggedValue, - right: &UntaggedValue, -) -> Result { - match (left, right) { - (UntaggedValue::Primitive(lhs), UntaggedValue::Primitive(rhs)) => match (lhs, rhs) { - (Primitive::Filesize(x), Primitive::Filesize(y)) => { - let result = match operator { - Operator::Plus => Ok(x + y), - Operator::Minus => Ok(x - y), - Operator::Multiply => Ok(x * y), - Operator::Divide => { - if y.is_zero() { - Err((left.type_name(), right.type_name())) - } else { - Ok(x / y) - } - } - _ => Err((left.type_name(), right.type_name())), - }?; - Ok(UntaggedValue::Primitive(Primitive::Filesize(result))) - } - (Primitive::Filesize(x), Primitive::Int(y)) => match operator { - // Operator::Plus => Ok(UntaggedValue::Primitive(Primitive::Filesize(x + *y as u64))), - // Operator::Minus => Ok(UntaggedValue::Primitive(Primitive::Filesize(x - *y as u64))), - Operator::Multiply => { - Ok(UntaggedValue::Primitive(Primitive::Filesize(x * *y as u64))) - } - Operator::Divide => { - Ok(UntaggedValue::Primitive(Primitive::Filesize(x / *y as u64))) - } - _ => Err((left.type_name(), right.type_name())), - }, - (Primitive::Int(x), Primitive::Filesize(y)) => match operator { - // Operator::Plus => Ok(UntaggedValue::Primitive(Primitive::Filesize(*x as u64 + y))), - // Operator::Minus => Ok(UntaggedValue::Primitive(Primitive::Filesize(*x as u64 - y))), - Operator::Multiply => { - Ok(UntaggedValue::Primitive(Primitive::Filesize(*x as u64 * y))) - } - // Operator::Divide => { - // Ok(UntaggedValue::Primitive(Primitive::Filesize(*x as u64 / y))) - // } - _ => Err((left.type_name(), right.type_name())), - }, - (Primitive::Int(x), Primitive::Int(y)) => match operator { - Operator::Plus => Ok(UntaggedValue::Primitive(Primitive::Int(x + y))), - Operator::Minus => Ok(UntaggedValue::Primitive(Primitive::Int(x - y))), - Operator::Multiply => Ok(UntaggedValue::Primitive(Primitive::Int(x * y))), - Operator::Divide => { - if y.is_zero() { - Ok(zero_division_error()) - } else if x - (y * (x / y)) == 0 { - Ok(UntaggedValue::Primitive(Primitive::Int(x / y))) - } else { - Ok(UntaggedValue::Primitive(Primitive::Decimal( - bigdecimal::BigDecimal::from(*x) / bigdecimal::BigDecimal::from(*y), - ))) - } - } - Operator::Modulo => { - if y.is_zero() { - Ok(zero_division_error()) - } else { - Ok(UntaggedValue::Primitive(Primitive::Int(x % y))) - } - } - Operator::Pow => { - let prim_u32 = ToPrimitive::to_u32(y); - - let sign = match x.is_negative() { - true => -1, - false => 1, - }; - - if !y.is_negative() { - match prim_u32 { - Some(num) => Ok(UntaggedValue::Primitive(Primitive::Int( - sign * (x.pow(num)), - ))), - _ => Err((left.type_name(), right.type_name())), - } - } else { - let yp = bigdecimal::ToPrimitive::to_f64(y).unwrap_or(0.0); - let xp = bigdecimal::ToPrimitive::to_f64(x).unwrap_or(0.0); - - let pow = - bigdecimal::FromPrimitive::from_f64((sign as f64) * (xp.powf(yp))); - - match pow { - Some(p) => Ok(UntaggedValue::Primitive(Primitive::Decimal(p))), - _ => Err((left.type_name(), right.type_name())), - } - } - } - _ => Err((left.type_name(), right.type_name())), - }, - (Primitive::Int(x), Primitive::BigInt(y)) => match operator { - Operator::Plus => Ok(UntaggedValue::Primitive(Primitive::BigInt( - BigInt::from(*x) + y, - ))), - Operator::Minus => Ok(UntaggedValue::Primitive(Primitive::BigInt( - BigInt::from(*x) - y, - ))), - Operator::Multiply => Ok(UntaggedValue::Primitive(Primitive::BigInt( - BigInt::from(*x) * y, - ))), - Operator::Divide => { - if y.is_zero() { - Ok(zero_division_error()) - } else if x - (y * (x / y)) == BigInt::from(0) { - Ok(UntaggedValue::Primitive(Primitive::BigInt( - BigInt::from(*x) / y, - ))) - } else { - Ok(UntaggedValue::Primitive(Primitive::Decimal( - bigdecimal::BigDecimal::from(*x) - / bigdecimal::BigDecimal::from(y.clone()), - ))) - } - } - Operator::Modulo => { - if y.is_zero() { - Ok(zero_division_error()) - } else { - Ok(UntaggedValue::Primitive(Primitive::BigInt(x % y))) - } - } - Operator::Pow => { - let prim_u32 = ToPrimitive::to_u32(y); - - let sign = match x.is_negative() { - true => -1, - false => 1, - }; - - if !y.is_negative() { - match prim_u32 { - Some(num) => Ok(UntaggedValue::Primitive(Primitive::Int( - sign * (x.pow(num)), - ))), - _ => Err((left.type_name(), right.type_name())), - } - } else { - let yp = bigdecimal::ToPrimitive::to_f64(y).unwrap_or(0.0); - let xp = bigdecimal::ToPrimitive::to_f64(x).unwrap_or(0.0); - let pow = - bigdecimal::FromPrimitive::from_f64((sign as f64) * (xp.powf(yp))); - match pow { - Some(p) => Ok(UntaggedValue::Primitive(Primitive::Decimal(p))), - _ => Err((left.type_name(), right.type_name())), - } - } - } - _ => Err((left.type_name(), right.type_name())), - }, - (Primitive::BigInt(x), Primitive::Int(y)) => match operator { - Operator::Plus => Ok(UntaggedValue::Primitive(Primitive::BigInt( - x + BigInt::from(*y), - ))), - Operator::Minus => Ok(UntaggedValue::Primitive(Primitive::BigInt( - x - BigInt::from(*y), - ))), - Operator::Multiply => Ok(UntaggedValue::Primitive(Primitive::BigInt( - x * BigInt::from(*y), - ))), - Operator::Divide => { - if y.is_zero() { - Ok(zero_division_error()) - } else if x - (y * (x / y)) == BigInt::from(0) { - Ok(UntaggedValue::Primitive(Primitive::BigInt( - x / BigInt::from(*y), - ))) - } else { - Ok(UntaggedValue::Primitive(Primitive::Decimal( - bigdecimal::BigDecimal::from(x.clone()) - / bigdecimal::BigDecimal::from(*y), - ))) - } - } - Operator::Modulo => { - if y.is_zero() { - Ok(zero_division_error()) - } else { - Ok(UntaggedValue::Primitive(Primitive::BigInt(x % y))) - } - } - Operator::Pow => { - let prim_u32 = ToPrimitive::to_u32(y); - - let sign = match x.is_negative() { - true => -1, - false => 1, - }; - - if !y.is_negative() { - match prim_u32 { - Some(num) => Ok(UntaggedValue::Primitive(Primitive::BigInt( - (sign.to_bigint().unwrap_or_default()) * x.pow(num), - ))), - _ => Err((left.type_name(), right.type_name())), - } - } else { - let yp = bigdecimal::ToPrimitive::to_f64(y).unwrap_or(0.0); - let xp = bigdecimal::ToPrimitive::to_f64(x).unwrap_or(0.0); - let pow = bigdecimal::FromPrimitive::from_f64((sign as f64) * xp.powf(yp)); - match pow { - Some(p) => Ok(UntaggedValue::Primitive(Primitive::Decimal(p))), - _ => Err((left.type_name(), right.type_name())), - } - } - } - _ => Err((left.type_name(), right.type_name())), - }, - (Primitive::BigInt(x), Primitive::BigInt(y)) => match operator { - Operator::Plus => Ok(UntaggedValue::Primitive(Primitive::BigInt(x + y))), - Operator::Minus => Ok(UntaggedValue::Primitive(Primitive::BigInt(x - y))), - Operator::Multiply => Ok(UntaggedValue::Primitive(Primitive::BigInt(x * y))), - Operator::Divide => { - if y.is_zero() { - Ok(zero_division_error()) - } else if x - (y * (x / y)) == BigInt::from(0) { - Ok(UntaggedValue::Primitive(Primitive::BigInt(x / y))) - } else { - Ok(UntaggedValue::Primitive(Primitive::Decimal( - bigdecimal::BigDecimal::from(x.clone()) - / bigdecimal::BigDecimal::from(y.clone()), - ))) - } - } - Operator::Modulo => { - if y.is_zero() { - Ok(zero_division_error()) - } else { - Ok(UntaggedValue::Primitive(Primitive::BigInt(x % y))) - } - } - Operator::Pow => { - let prim_u32 = ToPrimitive::to_u32(y); - - let sign = match x.is_negative() { - true => -1, - false => 1, - }; - - if !y.is_negative() { - match prim_u32 { - Some(num) => Ok(UntaggedValue::Primitive(Primitive::BigInt( - (sign.to_bigint().unwrap_or_default()).pow(num), - ))), - _ => Err((left.type_name(), right.type_name())), - } - } else { - let yp = bigdecimal::ToPrimitive::to_f64(y).unwrap_or(0.0); - let xp = bigdecimal::ToPrimitive::to_f64(x).unwrap_or(0.0); - - let pow = - bigdecimal::FromPrimitive::from_f64((sign as f64) * (xp.powf(yp))); - - match pow { - Some(p) => Ok(UntaggedValue::Primitive(Primitive::Decimal(p))), - _ => Err((left.type_name(), right.type_name())), - } - } - } - _ => Err((left.type_name(), right.type_name())), - }, - (Primitive::Decimal(x), Primitive::Int(y)) => { - let result = match operator { - Operator::Plus => Ok(x + bigdecimal::BigDecimal::from(*y)), - Operator::Minus => Ok(x - bigdecimal::BigDecimal::from(*y)), - Operator::Multiply => Ok(x * bigdecimal::BigDecimal::from(*y)), - Operator::Divide => { - if y.is_zero() { - return Ok(zero_division_error()); - } - Ok(x / bigdecimal::BigDecimal::from(*y)) - } - Operator::Modulo => { - if y.is_zero() { - return Ok(zero_division_error()); - } - Ok(x % bigdecimal::BigDecimal::from(*y)) - } - - Operator::Pow => { - let sign = match x.is_negative() { - true => -1, - false => 1, - }; - - let yp = bigdecimal::ToPrimitive::to_f64(y).unwrap_or(0.0); - let xp = bigdecimal::ToPrimitive::to_f64(x).unwrap_or(0.0); - let pow = - bigdecimal::FromPrimitive::from_f64((sign as f64) * (xp.powf(yp))); - match pow { - Some(p) => Ok(p), - None => Err((left.type_name(), right.type_name())), - } - } - _ => Err((left.type_name(), right.type_name())), - }?; - Ok(UntaggedValue::Primitive(Primitive::Decimal(result))) - } - (Primitive::Int(x), Primitive::Decimal(y)) => { - let result = match operator { - Operator::Plus => Ok(bigdecimal::BigDecimal::from(*x) + y), - Operator::Minus => Ok(bigdecimal::BigDecimal::from(*x) - y), - Operator::Multiply => Ok(bigdecimal::BigDecimal::from(*x) * y), - Operator::Divide => { - if y.is_zero() { - return Ok(zero_division_error()); - } - Ok(bigdecimal::BigDecimal::from(*x) / y) - } - Operator::Modulo => { - if y.is_zero() { - return Ok(zero_division_error()); - } - Ok(bigdecimal::BigDecimal::from(*x) % y) - } - - Operator::Pow => { - let sign = match x.is_negative() { - true => -1, - false => 1, - }; - - let yp = bigdecimal::ToPrimitive::to_f64(y).unwrap_or(0.0); - let xp = bigdecimal::ToPrimitive::to_f64(x).unwrap_or(0.0); - let pow = - bigdecimal::FromPrimitive::from_f64((sign as f64) * (xp.powf(yp))); - match pow { - Some(p) => Ok(p), - None => Err((left.type_name(), right.type_name())), - } - } - _ => Err((left.type_name(), right.type_name())), - }?; - Ok(UntaggedValue::Primitive(Primitive::Decimal(result))) - } - (Primitive::Decimal(x), Primitive::BigInt(y)) => { - let result = match operator { - 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 => { - if y.is_zero() { - return Ok(zero_division_error()); - } - Ok(x / bigdecimal::BigDecimal::from(y.clone())) - } - Operator::Modulo => { - if y.is_zero() { - return Ok(zero_division_error()); - } - Ok(x % bigdecimal::BigDecimal::from(y.clone())) - } - - Operator::Pow => { - let sign = match x.is_negative() { - true => -1, - false => 1, - }; - - let yp = bigdecimal::ToPrimitive::to_f64(y).unwrap_or(0.0); - let xp = bigdecimal::ToPrimitive::to_f64(x).unwrap_or(0.0); - let pow = - bigdecimal::FromPrimitive::from_f64((sign as f64) * (xp.powf(yp))); - match pow { - Some(p) => Ok(p), - None => Err((left.type_name(), right.type_name())), - } - } - _ => Err((left.type_name(), right.type_name())), - }?; - Ok(UntaggedValue::Primitive(Primitive::Decimal(result))) - } - (Primitive::BigInt(x), Primitive::Decimal(y)) => { - let result = match operator { - 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 => { - if y.is_zero() { - return Ok(zero_division_error()); - } - Ok(bigdecimal::BigDecimal::from(x.clone()) / y) - } - Operator::Modulo => { - if y.is_zero() { - return Ok(zero_division_error()); - } - Ok(bigdecimal::BigDecimal::from(x.clone()) % y) - } - - Operator::Pow => { - let sign = match x.is_negative() { - true => -1, - false => 1, - }; - - let yp = bigdecimal::ToPrimitive::to_f64(y).unwrap_or(0.0); - let xp = bigdecimal::ToPrimitive::to_f64(x).unwrap_or(0.0); - let pow = - bigdecimal::FromPrimitive::from_f64((sign as f64) * (xp.powf(yp))); - match pow { - Some(p) => Ok(p), - None => Err((left.type_name(), right.type_name())), - } - } - _ => Err((left.type_name(), right.type_name())), - }?; - Ok(UntaggedValue::Primitive(Primitive::Decimal(result))) - } - (Primitive::Decimal(x), Primitive::Decimal(y)) => { - let result = match operator { - Operator::Plus => Ok(x + y), - Operator::Minus => Ok(x - y), - Operator::Multiply => Ok(x * y), - Operator::Divide => { - if y.is_zero() { - return Ok(zero_division_error()); - } - Ok(x / y) - } - Operator::Modulo => { - if y.is_zero() { - return Ok(zero_division_error()); - } - Ok(x % y) - } - - Operator::Pow => { - let sign = match x.is_negative() { - true => -1, - false => 1, - }; - - let yp = bigdecimal::ToPrimitive::to_f64(y).unwrap_or(0.0); - let xp = bigdecimal::ToPrimitive::to_f64(x).unwrap_or(0.0); - let pow = - bigdecimal::FromPrimitive::from_f64((sign as f64) * (xp.powf(yp))); - match pow { - Some(p) => Ok(p), - None => Err((left.type_name(), right.type_name())), - } - } - _ => Err((left.type_name(), right.type_name())), - }?; - Ok(UntaggedValue::Primitive(Primitive::Decimal(result))) - } - (Primitive::Date(x), Primitive::Date(y)) => match operator { - Operator::Minus => Ok(UntaggedValue::Primitive(Primitive::from( - x.signed_duration_since(*y), - ))), - _ => Err((left.type_name(), right.type_name())), - }, - (Primitive::Date(x), Primitive::Duration(_)) => { - let result = match operator { - Operator::Plus => { - // FIXME: Not sure if I could do something better with the Span. - match Primitive::into_chrono_duration(rhs.clone(), Span::unknown()) { - Ok(y) => match x.checked_add_signed(y) { - Some(value) => Ok(value), - None => Err(("Date", "Duration and date addition overflow")), - }, - Err(_) => Err(("Date", "Duration overflow")), - } - } - Operator::Minus => { - match Primitive::into_chrono_duration(rhs.clone(), Span::unknown()) { - Ok(y) => match x.checked_sub_signed(y) { - Some(value) => Ok(value), - None => Err(("Date", "Duration and date addition overflow")), - }, - Err(_) => Err(("Date", "Duration overflow")), - } - } - _ => Err((left.type_name(), right.type_name())), - }?; - Ok(UntaggedValue::Primitive(Primitive::Date(result))) - } - (Primitive::Duration(x), Primitive::Duration(y)) => { - let result = match operator { - Operator::Plus => Ok(x + y), - Operator::Minus => Ok(x - y), - _ => Err((left.type_name(), right.type_name())), - }?; - - Ok(UntaggedValue::Primitive(Primitive::Duration(result))) - } - (Primitive::Int(x), Primitive::Duration(y)) => { - let result = match operator { - Operator::Plus => Ok(x + y), - Operator::Minus => Ok(x - y), - _ => Err((left.type_name(), right.type_name())), - }?; - - Ok(UntaggedValue::Primitive(Primitive::Duration(result))) - } - (Primitive::Duration(x), Primitive::Decimal(y)) => { - let result = match operator { - Operator::Divide => { - if y.is_zero() { - return Ok(zero_division_error()); - } - - let y = y.as_bigint_and_exponent(); - Ok(x / y.0) - } - _ => Err((left.type_name(), right.type_name())), - }?; - - Ok(UntaggedValue::Primitive(Primitive::Duration(result))) - } - (Primitive::String(x), Primitive::String(y)) => { - let mut new_string = x.clone(); - new_string.push_str(y); - Ok(UntaggedValue::Primitive(Primitive::String(new_string))) - } - _ => Err((left.type_name(), right.type_name())), - }, - _ => Err((left.type_name(), right.type_name())), - } -} - -/// If left is {{ Operator }} right -pub fn compare_values( - operator: Operator, - left: &UntaggedValue, - right: &UntaggedValue, -) -> Result { - let coerced = coerce_compare(left, right)?; - let ordering = coerced.compare(); - - use std::cmp::Ordering; - - let result = matches!( - (operator, ordering), - (Operator::Equal, Ordering::Equal) - | (Operator::GreaterThan, Ordering::Greater) - | (Operator::GreaterThanOrEqual, Ordering::Greater) - | (Operator::GreaterThanOrEqual, Ordering::Equal) - | (Operator::LessThan, Ordering::Less) - | (Operator::LessThanOrEqual, Ordering::Less) - | (Operator::LessThanOrEqual, Ordering::Equal) - | (Operator::NotEqual, Ordering::Greater) - | (Operator::NotEqual, Ordering::Less) - ); - - Ok(result) -} - -pub fn plain_type<'a>(value: impl Into<&'a UntaggedValue>, width: usize) -> String { - Type::from_value(value.into()).plain_string(width) -} - -pub fn format_type<'a>(value: impl Into<&'a UntaggedValue>, width: usize) -> String { - Type::from_value(value.into()).colored_string(width) -} - -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>, - color_hash_map: &HashMap, -) -> TextStyle { - match value.into() { - UntaggedValue::Primitive(p) => { - // This is just to return the name of the type so that style_primitive - // can work on a string versus a type like String("some_text") - let str: &str = &p.to_string(); - let str_len = str.len(); - let paren_index = str.find('(').unwrap_or(str_len - 1); - let prim_type = str[0..paren_index].to_string(); - style_primitive(&prim_type, color_hash_map) - } - _ => TextStyle::basic_left(), - } -} - -pub fn format_for_column<'a>( - value: impl Into<&'a UntaggedValue>, - column: impl Into, -) -> DebugDocBuilder { - InlineShape::from_value(value.into()) - .format_for_column(column) - .pretty() -} - -#[cfg(test)] -mod tests { - use super::Date as d; - use super::UntaggedValue as v; - use super::{compute_values, merge_values}; - use nu_protocol::hir::Operator; - use nu_protocol::{Primitive, UntaggedValue}; - use nu_source::TaggedItem; - - use indexmap::indexmap; - - #[test] - fn merges_tables() { - let (author_1_date, author_2_date) = ( - "2020-04-29".to_string().tagged_unknown(), - "2019-10-10".to_string().tagged_unknown(), - ); - - let table_author_row = v::row(indexmap! { - "name".into() => v::string("Andrés").into_untagged_value(), - "country".into() => v::string("EC").into_untagged_value(), - "date".into() => d::naive_from_str(author_1_date.borrow_tagged()).unwrap().into_untagged_value() - }); - - let other_table_author_row = v::row(indexmap! { - "name".into() => v::string("YK").into_untagged_value(), - "country".into() => v::string("US").into_untagged_value(), - "date".into() => d::naive_from_str(author_2_date.borrow_tagged()).unwrap().into_untagged_value() - }); - - assert_eq!( - other_table_author_row, - merge_values(&table_author_row, &other_table_author_row).unwrap() - ); - } - - #[test] - fn pow_operator_negatives_and_decimals() { - // test 2 ** 2 - let result_one = compute_values( - Operator::Pow, - &UntaggedValue::Primitive(Primitive::Int(2)), - &UntaggedValue::Primitive(Primitive::Int(2)), - ); - - assert_eq!( - result_one.unwrap(), - UntaggedValue::Primitive(Primitive::Int(4)) - ); - - // test 2 ** 2.0 - let rhs_decimal = bigdecimal::FromPrimitive::from_f64(2.0).unwrap(); - - let result_two = compute_values( - Operator::Pow, - &UntaggedValue::Primitive(Primitive::Int(2)), - &UntaggedValue::Primitive(Primitive::Decimal(rhs_decimal)), - ); - - let should_equal_four_decimal = bigdecimal::FromPrimitive::from_f64(4.0).unwrap(); - - assert_eq!( - result_two.unwrap(), - UntaggedValue::Primitive(Primitive::Decimal(should_equal_four_decimal)) - ); - - // test 2.0 ** 2.0 - let rhs_decimal = bigdecimal::FromPrimitive::from_f64(2.0).unwrap(); - let lhs_decimal = bigdecimal::FromPrimitive::from_f64(2.0).unwrap(); - let should_equal_four_decimal = bigdecimal::FromPrimitive::from_f64(4.0).unwrap(); - - let result_three = compute_values( - Operator::Pow, - &UntaggedValue::Primitive(Primitive::Decimal(lhs_decimal)), - &UntaggedValue::Primitive(Primitive::Decimal(rhs_decimal)), - ); - - assert_eq!( - result_three.unwrap(), - UntaggedValue::Primitive(Primitive::Decimal(should_equal_four_decimal)) - ); - - // test 2 ** -2 - let result_four = compute_values( - Operator::Pow, - &UntaggedValue::Primitive(Primitive::Int(2)), - &UntaggedValue::Primitive(Primitive::Int(-2)), - ); - - let should_equal_zero_decimal = bigdecimal::FromPrimitive::from_f64(0.25).unwrap(); - - assert_eq!( - result_four.unwrap(), - UntaggedValue::Primitive(Primitive::Decimal(should_equal_zero_decimal)) - ); - - // test -2 ** -2 - let result_five = compute_values( - Operator::Pow, - &UntaggedValue::Primitive(Primitive::Int(-2)), - &UntaggedValue::Primitive(Primitive::Int(-2)), - ); - - let should_equal_neg_zero_decimal = bigdecimal::FromPrimitive::from_f64(-0.25).unwrap(); - - assert_eq!( - result_five.unwrap(), - UntaggedValue::Primitive(Primitive::Decimal(should_equal_neg_zero_decimal)) - ); - - // test -2 ** 2 - let result_six = compute_values( - Operator::Pow, - &UntaggedValue::Primitive(Primitive::Int(-2)), - &UntaggedValue::Primitive(Primitive::Int(2)), - ); - - assert_eq!( - result_six.unwrap(), - UntaggedValue::Primitive(Primitive::Int(-4)) - ); - - // test -2.0 ** 2 - let lhs_decimal = bigdecimal::FromPrimitive::from_f64(-2.0).unwrap(); - let should_equal_neg_four_decimal = bigdecimal::FromPrimitive::from_f64(-4.0).unwrap(); - - let result_seven = compute_values( - Operator::Pow, - &UntaggedValue::Primitive(Primitive::Decimal(lhs_decimal)), - &UntaggedValue::Primitive(Primitive::Int(2)), - ); - - // Need to validate - assert_eq!( - result_seven.unwrap(), - UntaggedValue::Primitive(Primitive::Decimal(should_equal_neg_four_decimal)) - ); - } -} diff --git a/crates/nu-engine/Cargo.toml b/crates/nu-engine/Cargo.toml index b481a0a879..17306beb45 100644 --- a/crates/nu-engine/Cargo.toml +++ b/crates/nu-engine/Cargo.toml @@ -1,59 +1,14 @@ [package] -authors = ["The Nu Project Contributors"] -description = "Core commands for nushell" -edition = "2018" -license = "MIT" name = "nu-engine" -version = "0.44.0" +version = "0.59.0" +edition = "2021" [dependencies] -nu-data = { version = "0.44.0", path="../nu-data" } -nu-errors = { version = "0.44.0", path="../nu-errors" } -nu-parser = { version = "0.44.0", path="../nu-parser" } -nu-plugin = { version = "0.44.0", path="../nu-plugin" } -nu-protocol = { version = "0.44.0", path="../nu-protocol" } -nu-source = { version = "0.44.0", path="../nu-source" } -nu-stream = { version = "0.44.0", path="../nu-stream" } -nu-value-ext = { version = "0.44.0", path="../nu-value-ext" } -nu-ansi-term = { version = "0.44.0", path="../nu-ansi-term" } -nu-test-support = { version = "0.44.0", path="../nu-test-support" } -nu-path = { version = "0.44.0", path="../nu-path" } - -trash = { version = "2.0.2", optional = true } -which = { version="4.0.2", optional=true } -codespan-reporting = "0.11.0" -bigdecimal = { package = "bigdecimal", version = "0.3.0", features = ["serde"] } -bytes = "1.1.0" +nu-protocol = { path = "../nu-protocol", features = ["plugin"], version = "0.59.0" } +nu-path = { path = "../nu-path", version = "0.59.0" } +itertools = "0.10.1" chrono = { version="0.4.19", features=["serde"] } -derive-new = "0.5.8" -dirs-next = "2.0.0" -encoding_rs = "0.8.28" -filesize = "0.2.0" -fs_extra = "1.2.0" -getset = "0.1.1" glob = "0.3.0" -indexmap = { version="1.6.1", features=["serde-1"] } -itertools = "0.10.0" -lazy_static = "1.*" -log = "0.4.14" -num-bigint = { version="0.4.3", features=["serde"] } -parking_lot = "0.11.1" -rayon = "1.5.0" -serde = { version="1.0.123", features=["derive"] } -serde_json = "1.0.61" -tempfile = "3.2.0" -term_size = "0.3.2" -termcolor = "1.1.2" - -[target.'cfg(unix)'.dependencies] -umask = "1.0.0" -users = "0.11.0" - -[dev-dependencies] -nu-test-support = { version = "0.44.0", path="../nu-test-support" } -hamcrest2 = "0.3.0" [features] -rustyline-support = [] -trash-support = ["trash"] -dataframe = ["nu-protocol/dataframe"] +plugin = [] diff --git a/crates/nu-engine/README.md b/crates/nu-engine/README.md deleted file mode 100644 index 1d4fb05881..0000000000 --- a/crates/nu-engine/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# Nu-Engine - -Nu-engine handles most of the core logic of nushell. For example, engine handles: - -- Passing of data between commands -- Evaluating a commands return values -- Loading of user configurations - -## Top level introduction -The following topics shall give the reader a top level understanding how various topics are handled in nushell. - -### How are environment variables handled? -Environment variables (or short envs) are stored in the `Scope` of the `EvaluationContext`. That means that environment variables are scoped by default and we don't use `std::env` to store envs (but make exceptions where convenient). - -Nushell handles environment variables and their lifetime the following: -- At startup all existing environment variables are read and put into `Scope`. (Nushell reads existing environment variables platform independent by asking the `Host`. They will most likely come from `std::env::*`) -- Envs can also be loaded from config files. Each loaded config produces a new `ScopeFrame` with the envs of the loaded config. -- Nu-Script files and internal commands read and write env variables from / to the `Scope`. External scripts and binaries can't interact with the `Scope`. Therefore all env variables are read from the `Scope` and put into the external binaries environment-variables-memory area. diff --git a/crates/nu-engine/src/call_ext.rs b/crates/nu-engine/src/call_ext.rs new file mode 100644 index 0000000000..3ea04e0467 --- /dev/null +++ b/crates/nu-engine/src/call_ext.rs @@ -0,0 +1,100 @@ +use nu_protocol::{ + ast::Call, + engine::{EngineState, Stack}, + FromValue, ShellError, +}; + +use crate::eval_expression; + +pub trait CallExt { + fn get_flag( + &self, + engine_state: &EngineState, + stack: &mut Stack, + name: &str, + ) -> Result, ShellError>; + + fn rest( + &self, + engine_state: &EngineState, + stack: &mut Stack, + starting_pos: usize, + ) -> Result, ShellError>; + + fn opt( + &self, + engine_state: &EngineState, + stack: &mut Stack, + pos: usize, + ) -> Result, ShellError>; + + fn req( + &self, + engine_state: &EngineState, + stack: &mut Stack, + pos: usize, + ) -> Result; +} + +impl CallExt for Call { + fn get_flag( + &self, + engine_state: &EngineState, + stack: &mut Stack, + name: &str, + ) -> Result, ShellError> { + if let Some(expr) = self.get_flag_expr(name) { + let result = eval_expression(engine_state, stack, &expr)?; + FromValue::from_value(&result).map(Some) + } else { + Ok(None) + } + } + + fn rest( + &self, + engine_state: &EngineState, + stack: &mut Stack, + starting_pos: usize, + ) -> Result, ShellError> { + let mut output = vec![]; + + for expr in self.positional.iter().skip(starting_pos) { + let result = eval_expression(engine_state, stack, expr)?; + output.push(FromValue::from_value(&result)?); + } + + Ok(output) + } + + fn opt( + &self, + engine_state: &EngineState, + stack: &mut Stack, + pos: usize, + ) -> Result, ShellError> { + if let Some(expr) = self.nth(pos) { + let result = eval_expression(engine_state, stack, &expr)?; + FromValue::from_value(&result).map(Some) + } else { + Ok(None) + } + } + + fn req( + &self, + engine_state: &EngineState, + stack: &mut Stack, + pos: usize, + ) -> Result { + if let Some(expr) = self.nth(pos) { + let result = eval_expression(engine_state, stack, &expr)?; + FromValue::from_value(&result) + } else { + Err(ShellError::AccessBeyondEnd( + self.positional.len(), + self.head, + )) + } + } +} diff --git a/crates/nu-engine/src/call_info.rs b/crates/nu-engine/src/call_info.rs deleted file mode 100644 index d6822017a0..0000000000 --- a/crates/nu-engine/src/call_info.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::evaluate::evaluate_args::evaluate_args; -use crate::evaluation_context::EvaluationContext; -use nu_errors::ShellError; -use nu_protocol::hir; -use nu_protocol::CallInfo; -use nu_source::Tag; - -#[derive(Debug, Clone)] -pub struct UnevaluatedCallInfo { - pub args: hir::Call, - pub name_tag: Tag, -} - -impl UnevaluatedCallInfo { - pub fn evaluate(self, ctx: &EvaluationContext) -> Result { - let args = evaluate_args(&self.args, ctx)?; - - Ok(CallInfo { - args, - name_tag: self.name_tag, - }) - } - - pub fn switch_present(&self, switch: &str) -> bool { - self.args.switch_preset(switch) - } -} diff --git a/crates/nu-engine/src/column.rs b/crates/nu-engine/src/column.rs new file mode 100644 index 0000000000..834ad2c86c --- /dev/null +++ b/crates/nu-engine/src/column.rs @@ -0,0 +1,38 @@ +use nu_protocol::Value; +use std::collections::HashSet; + +pub fn get_columns(input: &[Value]) -> Vec { + let mut columns = vec![]; + + for item in input { + if let Value::Record { cols, vals: _, .. } = item { + for col in cols { + if !columns.contains(col) { + columns.push(col.to_string()); + } + } + } + } + + columns +} + +/* +* Check to see if any of the columns inside the input +* does not exist in a vec of columns +*/ + +pub fn column_does_not_exist(inputs: Vec, columns: Vec) -> bool { + let mut set = HashSet::new(); + for column in columns { + set.insert(column); + } + + for input in &inputs { + if set.contains(input) { + continue; + } + return true; + } + false +} diff --git a/crates/nu-engine/src/command_args.rs b/crates/nu-engine/src/command_args.rs deleted file mode 100644 index 124ef557ef..0000000000 --- a/crates/nu-engine/src/command_args.rs +++ /dev/null @@ -1,141 +0,0 @@ -use crate::evaluate::scope::Scope; -use crate::evaluation_context::EvaluationContext; -use crate::shell::shell_manager::ShellManager; -use crate::FromValue; -use crate::{call_info::UnevaluatedCallInfo, config_holder::ConfigHolder}; -use crate::{env::host::Host, evaluate_baseline_expr}; -use getset::Getters; -use nu_errors::ShellError; -use nu_protocol::hir::SpannedExpression; -use nu_source::Tag; -use nu_stream::InputStream; -use parking_lot::Mutex; -use std::sync::atomic::AtomicBool; -use std::sync::Arc; - -#[derive(Getters)] -#[get = "pub"] -pub struct CommandArgs { - pub context: EvaluationContext, - pub call_info: UnevaluatedCallInfo, - pub input: InputStream, -} - -impl CommandArgs { - pub fn scope(&self) -> &Scope { - &self.context.scope - } - - pub fn host(&self) -> Arc>> { - self.context.host().clone() - } - - pub fn current_errors(&self) -> Arc>> { - self.context.current_errors().clone() - } - - pub fn ctrl_c(&self) -> Arc { - self.context.ctrl_c().clone() - } - - pub fn configs(&self) -> Arc> { - self.context.configs().clone() - } - - pub fn shell_manager(&self) -> ShellManager { - self.context.shell_manager().clone() - } - - pub fn nth(&self, pos: usize) -> Option<&SpannedExpression> { - if let Some(positional) = &self.call_info.args.positional { - positional.get(pos) - } else { - None - } - } - - pub fn req(&self, pos: usize) -> Result { - if let Some(expr) = self.nth(pos) { - let result = evaluate_baseline_expr(expr, &self.context)?; - FromValue::from_value(&result) - } else { - Err(ShellError::labeled_error( - "Position beyond end of command arguments", - "can't access beyond end of command arguments", - self.call_info.name_tag.span, - )) - } - } - - pub fn req_named(&self, name: &str) -> Result { - match self.get_flag(name)? { - Some(v) => Ok(v), - None => Err(ShellError::labeled_error( - "Missing flag", - format!("expected {} flag", name), - &self.call_info.name_tag, - )), - } - } - - pub fn has_flag(&self, name: &str) -> bool { - self.call_info.args.switch_preset(name) - } - - pub fn get_flag(&self, name: &str) -> Result, ShellError> { - if let Some(expr) = self.call_info.args.get_flag(name) { - let result = evaluate_baseline_expr(expr, &self.context)?; - FromValue::from_value(&result).map(Some) - } else { - Ok(None) - } - } - - pub fn opt(&self, pos: usize) -> Result, ShellError> { - if let Some(expr) = self.nth(pos) { - let result = evaluate_baseline_expr(expr, &self.context)?; - FromValue::from_value(&result).map(Some) - } else { - Ok(None) - } - } - - pub fn rest(&self, starting_pos: usize) -> Result, ShellError> { - let mut output = vec![]; - - if let Some(positional) = &self.call_info.args.positional { - for expr in positional.iter().skip(starting_pos) { - let result = evaluate_baseline_expr(expr, &self.context)?; - output.push(FromValue::from_value(&result)?); - } - } - - Ok(output) - } - - pub fn rest_with_minimum( - &self, - pos: usize, - count: usize, - ) -> Result, ShellError> { - let mut output = vec![]; - for i in pos..pos + count { - output.push(self.req(i)?); - } - output.extend(self.rest(pos + count)?); - - Ok(output) - } - - pub fn name_tag(&self) -> Tag { - self.call_info.name_tag.clone() - } -} - -pub type RunnableContext = CommandArgs; - -impl std::fmt::Debug for CommandArgs { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.call_info.fmt(f) - } -} diff --git a/crates/nu-engine/src/config_holder.rs b/crates/nu-engine/src/config_holder.rs deleted file mode 100644 index 388460859d..0000000000 --- a/crates/nu-engine/src/config_holder.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::shell::palette::ThemedPalette; -use nu_data::config::NuConfig; -use nu_protocol::ConfigPath; -use std::path::Path; - -/// ConfigHolder holds information which configs have been loaded. -#[derive(Clone)] -pub struct ConfigHolder { - pub global_config: Option, - pub local_configs: Vec, - pub syntax_config: Option, -} - -impl Default for ConfigHolder { - fn default() -> Self { - Self::new() - } -} - -impl ConfigHolder { - pub fn new() -> ConfigHolder { - ConfigHolder { - global_config: None, - local_configs: vec![], - syntax_config: None, - } - } - - pub fn global_config(&self) -> NuConfig { - match &self.global_config { - Some(config) => config.clone(), - None => NuConfig::default(), - } - } - - pub fn syntax_colors(&self) -> ThemedPalette { - match &self.syntax_config { - Some(cfg) => cfg.clone(), - None => ThemedPalette::default(), - } - } - - pub fn add_local_cfg(&mut self, cfg: NuConfig) { - self.local_configs.push(cfg); - } - - pub fn set_global_cfg(&mut self, cfg: NuConfig) { - self.global_config = Some(cfg); - } - - pub fn set_syntax_colors(&mut self, cfg: ThemedPalette) { - self.syntax_config = Some(cfg); - } - - pub fn remove_cfg(&mut self, cfg_path: &ConfigPath) { - match cfg_path { - ConfigPath::Global(_) => self.global_config = None, - ConfigPath::Local(p) => self.remove_local_cfg(p), - } - } - - fn remove_local_cfg>(&mut self, cfg_path: P) { - // Remove the first loaded local config with specified cfg_path - if let Some(index) = self - .local_configs - .iter() - .rev() - .position(|cfg| cfg.file_path == cfg_path.as_ref()) - { - self.local_configs.remove(index); - } - } -} diff --git a/crates/nu-engine/src/documentation.rs b/crates/nu-engine/src/documentation.rs index bd8f1df742..adadc7f1b3 100644 --- a/crates/nu-engine/src/documentation.rs +++ b/crates/nu-engine/src/documentation.rs @@ -1,9 +1,9 @@ -use crate::evaluate::scope::Scope; -use crate::whole_stream_command::WholeStreamCommand; -use indexmap::IndexMap; use itertools::Itertools; -use nu_protocol::{NamedType, PositionalType, Signature, UntaggedValue, Value}; -use nu_source::PrettyDebug; +use nu_protocol::{ + ast::Call, + engine::{EngineState, Stack}, + Example, IntoPipelineData, Signature, Span, Value, +}; use std::collections::HashMap; const COMMANDS_DOCS_DIR: &str = "docs/commands"; @@ -11,91 +11,122 @@ const COMMANDS_DOCS_DIR: &str = "docs/commands"; #[derive(Default)] pub struct DocumentationConfig { no_subcommands: bool, + //FIXME: add back in color support + #[allow(dead_code)] no_color: bool, brief: bool, } -fn generate_doc(name: &str, scope: &Scope) -> IndexMap { - let mut row_entries = IndexMap::new(); - let command = scope - .get_command(name) +fn generate_doc( + name: &str, + engine_state: &EngineState, + stack: &mut Stack, + head: Span, +) -> (Vec, Vec) { + let mut cols = vec![]; + let mut vals = vec![]; + + let command = engine_state + .find_decl(name.as_bytes()) + .map(|decl_id| engine_state.get_decl(decl_id)) .unwrap_or_else(|| panic!("Expected command '{}' from names to be in registry", name)); - row_entries.insert( - "name".to_owned(), - UntaggedValue::string(name).into_untagged_value(), - ); - row_entries.insert( - "usage".to_owned(), - UntaggedValue::string(command.usage()).into_untagged_value(), - ); - retrieve_doc_link(name).and_then(|link| { - row_entries.insert( - "doc_link".to_owned(), - UntaggedValue::string(link).into_untagged_value(), - ) + + cols.push("name".to_string()); + vals.push(Value::String { + val: name.into(), + span: head, }); - row_entries.insert( - "documentation".to_owned(), - UntaggedValue::string(get_documentation( - command.stream_command(), - scope, + + cols.push("usage".to_string()); + vals.push(Value::String { + val: command.usage().to_owned(), + span: head, + }); + + if let Some(link) = retrieve_doc_link(name) { + cols.push("doc_link".into()); + vals.push(Value::String { + val: link, + span: head, + }); + } + + cols.push("documentation".to_owned()); + vals.push(Value::String { + val: get_documentation( + &command.signature(), + &command.examples(), + engine_state, + stack, &DocumentationConfig { no_subcommands: true, no_color: true, brief: false, }, - )) - .into_untagged_value(), - ); - row_entries + ), + span: head, + }); + + (cols, vals) } // generate_docs gets the documentation from each command and returns a Table as output -pub fn generate_docs(scope: &Scope) -> Value { - let mut sorted_names = scope.get_command_names(); - sorted_names.sort(); +pub fn generate_docs(engine_state: &EngineState, stack: &mut Stack, head: Span) -> Value { + let signatures = engine_state.get_signatures(true); // cmap will map parent commands to it's subcommands e.g. to -> [to csv, to yaml, to bson] let mut cmap: HashMap> = HashMap::new(); - for name in &sorted_names { - if name.contains(' ') { - let split_name = name.split_whitespace().collect_vec(); - let parent_name = split_name.first().expect("Expected a parent command name"); - if cmap.contains_key(*parent_name) { + for sig in &signatures { + if sig.name.contains(' ') { + let mut split_name = sig.name.split_whitespace(); + let parent_name = split_name.next().expect("Expected a parent command name"); + if cmap.contains_key(parent_name) { let sub_names = cmap - .get_mut(*parent_name) + .get_mut(parent_name) .expect("Expected an entry for parent"); - sub_names.push(name.to_owned()); + sub_names.push(sig.name.to_owned()); } } else { - cmap.insert(name.to_owned(), Vec::new()); + cmap.insert(sig.name.to_owned(), Vec::new()); }; } // Return documentation for each command // Sub-commands are nested under their respective parent commands let mut table = Vec::new(); - for name in &sorted_names { + for sig in &signatures { // Must be a sub-command, skip since it's being handled underneath when we hit the parent command - if !cmap.contains_key(name) { + if !cmap.contains_key(&sig.name) { continue; } - let mut row_entries = generate_doc(name, scope); + let mut row_entries = generate_doc(&sig.name, engine_state, stack, head); // Iterate over all the subcommands of the parent command let mut sub_table = Vec::new(); - for sub_name in cmap.get(name).unwrap_or(&Vec::new()) { - let sub_row = generate_doc(sub_name, scope); - sub_table.push(UntaggedValue::row(sub_row).into_untagged_value()); + for sub_name in cmap.get(&sig.name).unwrap_or(&Vec::new()) { + let (cols, vals) = generate_doc(sub_name, engine_state, stack, head); + sub_table.push(Value::Record { + cols, + vals, + span: head, + }); } if !sub_table.is_empty() { - row_entries.insert( - "subcommands".to_owned(), - UntaggedValue::table(&sub_table).into_untagged_value(), - ); + row_entries.0.push("subcommands".into()); + row_entries.1.push(Value::List { + vals: sub_table, + span: head, + }); } - table.push(UntaggedValue::row(row_entries).into_untagged_value()); + table.push(Value::Record { + cols: row_entries.0, + vals: row_entries.1, + span: head, + }); + } + Value::List { + vals: table, + span: head, } - UntaggedValue::table(&table).into_untagged_value() } fn retrieve_doc_link(name: &str) -> Option { @@ -115,21 +146,22 @@ fn retrieve_doc_link(name: &str) -> Option { #[allow(clippy::cognitive_complexity)] pub fn get_documentation( - cmd: &dyn WholeStreamCommand, - scope: &Scope, + sig: &Signature, + examples: &[Example], + engine_state: &EngineState, + stack: &mut Stack, config: &DocumentationConfig, ) -> String { - let cmd_name = cmd.name(); - let signature = cmd.signature(); + let cmd_name = &sig.name; let mut long_desc = String::new(); - let usage = &cmd.usage(); + let usage = &sig.usage; if !usage.is_empty() { long_desc.push_str(usage); long_desc.push_str("\n\n"); } - let extra_usage = if config.brief { "" } else { &cmd.extra_usage() }; + let extra_usage = if config.brief { "" } else { &sig.extra_usage }; if !extra_usage.is_empty() { long_desc.push_str(extra_usage); long_desc.push_str("\n\n"); @@ -137,43 +169,15 @@ pub fn get_documentation( let mut subcommands = vec![]; if !config.no_subcommands { - for name in scope.get_command_names() { - if name.starts_with(&format!("{} ", cmd_name)) { - let subcommand = scope.get_command(&name).expect("This shouldn't happen"); - - subcommands.push(format!(" {} - {}", name, subcommand.usage())); + let signatures = engine_state.get_signatures(true); + for sig in signatures { + if sig.name.starts_with(&format!("{} ", cmd_name)) { + subcommands.push(format!(" {} - {}", sig.name, sig.usage)); } } } - let mut one_liner = String::new(); - one_liner.push_str(&signature.name); - one_liner.push(' '); - - for positional in &signature.positional { - match &positional.0 { - PositionalType::Mandatory(name, _m) => { - one_liner.push_str(&format!("<{}> ", name)); - } - PositionalType::Optional(name, _o) => { - one_liner.push_str(&format!("({}) ", name)); - } - } - } - - if signature.rest_positional.is_some() { - one_liner.push_str("...args "); - } - - if !subcommands.is_empty() { - one_liner.push_str(" "); - } - - if !signature.named.is_empty() { - one_liner.push_str("{flags} "); - } - - long_desc.push_str(&format!("Usage:\n > {}\n", one_liner)); + long_desc.push_str(&format!("Usage:\n > {}\n", sig.call_signature())); if !subcommands.is_empty() { long_desc.push_str("\nSubcommands:\n"); @@ -182,32 +186,37 @@ pub fn get_documentation( long_desc.push('\n'); } - if !signature.positional.is_empty() || signature.rest_positional.is_some() { + if !sig.named.is_empty() { + long_desc.push_str(&get_flags_section(sig)) + } + + if !sig.required_positional.is_empty() + || !sig.optional_positional.is_empty() + || sig.rest_positional.is_some() + { long_desc.push_str("\nParameters:\n"); - for positional in &signature.positional { - match &positional.0 { - PositionalType::Mandatory(name, _m) => { - long_desc.push_str(&format!(" <{}> {}\n", name, positional.1)); - } - PositionalType::Optional(name, _o) => { - long_desc.push_str(&format!(" ({}) {}\n", name, positional.1)); - } - } + for positional in &sig.required_positional { + long_desc.push_str(&format!(" {}: {}\n", positional.name, positional.desc)); + } + for positional in &sig.optional_positional { + long_desc.push_str(&format!( + " (optional) {}: {}\n", + positional.name, positional.desc + )); } - if let Some(rest_positional) = &signature.rest_positional { - long_desc.push_str(&format!(" ...args: {}\n", rest_positional.2)); + if let Some(rest_positional) = &sig.rest_positional { + long_desc.push_str(&format!( + " ...{}: {}\n", + rest_positional.name, rest_positional.desc + )); } } - if !signature.named.is_empty() { - long_desc.push_str(&get_flags_section(&signature)) - } - let palette = crate::shell::palette::DefaultPalette {}; - let examples = cmd.examples(); if !examples.is_empty() { long_desc.push_str("\nExamples:"); } + for example in examples { long_desc.push('\n'); long_desc.push_str(" "); @@ -215,10 +224,36 @@ pub fn get_documentation( if config.no_color { long_desc.push_str(&format!("\n > {}\n", example.example)); + } else if let Some(highlighter) = engine_state.find_decl(b"nu-highlight") { + let decl = engine_state.get_decl(highlighter); + + match decl.run( + engine_state, + stack, + &Call::new(Span::new(0, 0)), + Value::String { + val: example.example.to_string(), + span: Span { start: 0, end: 0 }, + } + .into_pipeline_data(), + ) { + Ok(output) => { + let result = output.into_value(Span { start: 0, end: 0 }); + match result.as_string() { + Ok(s) => { + long_desc.push_str(&format!("\n > {}\n", s)); + } + _ => { + long_desc.push_str(&format!("\n > {}\n", example.example)); + } + } + } + Err(_) => { + long_desc.push_str(&format!("\n > {}\n", example.example)); + } + } } else { - let colored_example = - crate::shell::painter::Painter::paint_string(example.example, scope, &palette); - long_desc.push_str(&format!("\n > {}\n", colored_example)); + long_desc.push_str(&format!("\n > {}\n", example.example)); } } @@ -227,79 +262,93 @@ pub fn get_documentation( long_desc } -fn get_flags_section(signature: &Signature) -> String { +pub fn get_flags_section(signature: &Signature) -> String { let mut long_desc = String::new(); long_desc.push_str("\nFlags:\n"); - for (flag, ty) in &signature.named { - let msg = match ty.0 { - NamedType::Switch(s) => { - if let Some(c) = s { + for flag in &signature.named { + let msg = if let Some(arg) = &flag.arg { + if let Some(short) = flag.short { + if flag.required { format!( - " -{}, --{}{} {}\n", - c, - flag, - if !ty.1.is_empty() { ":" } else { "" }, - ty.1 + " -{}{} (required parameter) {:?}\n {}\n", + short, + if !flag.long.is_empty() { + format!(", --{}", flag.long) + } else { + "".into() + }, + arg, + flag.desc ) } else { format!( - " --{}{} {}\n", - flag, - if !ty.1.is_empty() { ":" } else { "" }, - ty.1 + " -{}{} <{:?}>\n {}\n", + short, + if !flag.long.is_empty() { + format!(", --{}", flag.long) + } else { + "".into() + }, + arg, + flag.desc ) } + } else if flag.required { + format!( + " --{} (required parameter) <{:?}>\n {}\n", + flag.long, arg, flag.desc + ) + } else { + format!(" --{} {:?}\n {}\n", flag.long, arg, flag.desc) } - NamedType::Mandatory(s, m) => { - if let Some(c) = s { - format!( - " -{}, --{} <{}> (required parameter){} {}\n", - c, - flag, - m.display(), - if !ty.1.is_empty() { ":" } else { "" }, - ty.1 - ) - } else { - format!( - " --{} <{}> (required parameter){} {}\n", - flag, - m.display(), - if !ty.1.is_empty() { ":" } else { "" }, - ty.1 - ) - } - } - NamedType::Optional(s, o) => { - if let Some(c) = s { - format!( - " -{}, --{} <{}>{} {}\n", - c, - flag, - o.display(), - if !ty.1.is_empty() { ":" } else { "" }, - ty.1 - ) - } else { - format!( - " --{} <{}>{} {}\n", - flag, - o.display(), - if !ty.1.is_empty() { ":" } else { "" }, - ty.1 - ) - } + } else if let Some(short) = flag.short { + if flag.required { + format!( + " -{}{} (required parameter)\n {}\n", + short, + if !flag.long.is_empty() { + format!(", --{}", flag.long) + } else { + "".into() + }, + flag.desc + ) + } else { + format!( + " -{}{}\n {}\n", + short, + if !flag.long.is_empty() { + format!(", --{}", flag.long) + } else { + "".into() + }, + flag.desc + ) } + } else if flag.required { + format!( + " --{} (required parameter)\n {}\n", + flag.long, flag.desc + ) + } else { + format!(" --{}\n {}\n", flag.long, flag.desc) }; long_desc.push_str(&msg); } long_desc } -pub fn get_brief_help(cmd: &dyn WholeStreamCommand, scope: &Scope) -> String { +pub fn get_brief_help( + sig: &Signature, + examples: &[Example], + engine_state: &EngineState, + stack: &mut Stack, +) -> String { get_documentation( - cmd, - scope, + sig, + examples, + engine_state, + stack, &DocumentationConfig { no_subcommands: false, no_color: false, @@ -308,6 +357,17 @@ pub fn get_brief_help(cmd: &dyn WholeStreamCommand, scope: &Scope) -> String { ) } -pub fn get_full_help(cmd: &dyn WholeStreamCommand, scope: &Scope) -> String { - get_documentation(cmd, scope, &DocumentationConfig::default()) +pub fn get_full_help( + sig: &Signature, + examples: &[Example], + engine_state: &EngineState, + stack: &mut Stack, +) -> String { + get_documentation( + sig, + examples, + engine_state, + stack, + &DocumentationConfig::default(), + ) } diff --git a/crates/nu-engine/src/env.rs b/crates/nu-engine/src/env.rs new file mode 100644 index 0000000000..39faad40ed --- /dev/null +++ b/crates/nu-engine/src/env.rs @@ -0,0 +1,167 @@ +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +use nu_protocol::engine::{EngineState, Stack}; +use nu_protocol::{Config, PipelineData, ShellError, Value}; + +use crate::eval_block; + +#[cfg(windows)] +const ENV_SEP: &str = ";"; +#[cfg(not(windows))] +const ENV_SEP: &str = ":"; + +/// Translate environment variables from Strings to Values. Requires config to be already set up in +/// case the user defined custom env conversions in config.nu. +/// +/// It returns Option instead of Result since we do want to translate all the values we can and +/// skip errors. This function is called in the main() so we want to keep running, we cannot just +/// exit. +pub fn convert_env_values( + engine_state: &mut EngineState, + stack: &Stack, + config: &Config, +) -> Option { + let mut error = None; + + let mut new_scope = HashMap::new(); + + for (name, val) in &engine_state.env_vars { + if let Some(env_conv) = config.env_conversions.get(name) { + if let Some((block_id, from_span)) = env_conv.from_string { + let val_span = match val.span() { + Ok(sp) => sp, + Err(e) => { + error = error.or(Some(e)); + continue; + } + }; + + let block = engine_state.get_block(block_id); + + if let Some(var) = block.signature.get_positional(0) { + let mut stack = stack.gather_captures(&block.captures); + if let Some(var_id) = &var.var_id { + stack.add_var(*var_id, val.clone()); + } + + let result = + eval_block(engine_state, &mut stack, block, PipelineData::new(val_span)); + + match result { + Ok(data) => { + let val = data.into_value(val_span); + new_scope.insert(name.to_string(), val); + } + Err(e) => error = error.or(Some(e)), + } + } else { + error = error.or_else(|| { + Some(ShellError::MissingParameter( + "block input".into(), + from_span, + )) + }); + } + } else { + new_scope.insert(name.to_string(), val.clone()); + } + } else { + new_scope.insert(name.to_string(), val.clone()); + } + } + + for (k, v) in new_scope { + engine_state.env_vars.insert(k, v); + } + + error +} + +/// Translate one environment variable from Value to String +pub fn env_to_string( + env_name: &str, + value: Value, + engine_state: &EngineState, + stack: &Stack, + config: &Config, +) -> Result { + if let Some(env_conv) = config.env_conversions.get(env_name) { + if let Some((block_id, to_span)) = env_conv.to_string { + let block = engine_state.get_block(block_id); + + if let Some(var) = block.signature.get_positional(0) { + let val_span = value.span()?; + let mut stack = stack.gather_captures(&block.captures); + + if let Some(var_id) = &var.var_id { + stack.add_var(*var_id, value); + } + + Ok( + // This one is OK to fail: We want to know if custom conversion is working + eval_block(engine_state, &mut stack, block, PipelineData::new(val_span))? + .into_value(val_span) + .as_string()?, + ) + } else { + Err(ShellError::MissingParameter("block input".into(), to_span)) + } + } else { + // Do not fail here. Must succeed, otherwise setting a non-string env var would constantly + // throw errors when running externals etc. + Ok(value.into_string(ENV_SEP, config)) + } + } else { + // Do not fail here. Must succeed, otherwise setting a non-string env var would constantly + // throw errors when running externals etc. + Ok(value.into_string(ENV_SEP, config)) + } +} + +/// Translate all environment variables from Values to Strings +pub fn env_to_strings( + engine_state: &EngineState, + stack: &Stack, + config: &Config, +) -> Result, ShellError> { + let env_vars = stack.get_env_vars(engine_state); + let mut env_vars_str = HashMap::new(); + for (env_name, val) in env_vars { + let val_str = env_to_string(&env_name, val, engine_state, stack, config)?; + env_vars_str.insert(env_name, val_str); + } + + Ok(env_vars_str) +} + +/// Shorthand for env_to_string() for PWD with custom error +pub fn current_dir_str(engine_state: &EngineState, stack: &Stack) -> Result { + let config = stack.get_config()?; + if let Some(pwd) = stack.get_env_var(engine_state, "PWD") { + match env_to_string("PWD", pwd, engine_state, stack, &config) { + Ok(cwd) => { + if Path::new(&cwd).is_absolute() { + Ok(cwd) + } else { + println!("cwd is: {}", cwd); + Err(ShellError::LabeledError( + "Invalid current directory".to_string(), + format!("The 'PWD' environment variable must be set to an absolute path. Found: '{}'", cwd) + )) + } + } + Err(e) => Err(e), + } + } else { + Err(ShellError::LabeledError( + "Current directory not found".to_string(), + "The environment variable 'PWD' was not found. It is required to define the current directory.".to_string(), + )) + } +} + +/// Calls current_dir_str() and returns the current directory as a PathBuf +pub fn current_dir(engine_state: &EngineState, stack: &Stack) -> Result { + current_dir_str(engine_state, stack).map(PathBuf::from) +} diff --git a/crates/nu-engine/src/env/basic_host.rs b/crates/nu-engine/src/env/basic_host.rs deleted file mode 100644 index 177558d34c..0000000000 --- a/crates/nu-engine/src/env/basic_host.rs +++ /dev/null @@ -1,119 +0,0 @@ -use crate::Host; -use nu_errors::ShellError; -use nu_protocol::{errln, outln}; -use nu_source::Text; -use std::ffi::OsString; - -#[derive(Debug)] -pub struct BasicHost; - -impl Host for BasicHost { - fn stdout(&mut self, out: &str) { - match out { - "\n" => outln!(""), - other => outln!("{}", other), - } - } - - fn stderr(&mut self, out: &str) { - match out { - "\n" => errln!(""), - other => errln!("{}", other), - } - } - - fn print_err(&mut self, err: ShellError, source: &Text) { - let diag = err.into_diagnostic(); - let source = source.to_string(); - let mut files = codespan_reporting::files::SimpleFiles::new(); - files.add("shell", source); - - let writer = termcolor::StandardStream::stderr(termcolor::ColorChoice::Auto); - let config = codespan_reporting::term::Config::default(); - - let _ = std::panic::catch_unwind(move || { - let _ = codespan_reporting::term::emit(&mut writer.lock(), &config, &files, &diag); - }); - } - - #[allow(unused_variables)] - fn vars(&self) -> Vec<(String, String)> { - #[cfg(not(target_arch = "wasm32"))] - { - std::env::vars().collect::>() - } - - #[cfg(target_arch = "wasm32")] - { - vec![] - } - } - - #[allow(unused_variables)] - fn env_get(&mut self, key: OsString) -> Option { - #[cfg(not(target_arch = "wasm32"))] - { - std::env::var_os(key) - } - #[cfg(target_arch = "wasm32")] - { - None - } - } - - #[allow(unused_variables)] - fn env_set(&mut self, key: OsString, value: OsString) { - #[cfg(not(target_arch = "wasm32"))] - { - std::env::set_var(key, value); - } - } - - #[allow(unused_variables)] - fn env_rm(&mut self, key: OsString) { - #[cfg(not(target_arch = "wasm32"))] - { - std::env::remove_var(key); - } - } - - fn width(&self) -> usize { - let (mut term_width, _) = term_size::dimensions().unwrap_or((80, 20)); - term_width -= 1; - term_width - } - - fn height(&self) -> usize { - let (_, term_height) = term_size::dimensions().unwrap_or((80, 20)); - term_height - } - - fn is_external_cmd(&self, #[allow(unused)] cmd_name: &str) -> bool { - #[cfg(any(target_arch = "wasm32", not(feature = "which")))] - { - true - } - - #[cfg(all(unix, feature = "which"))] - { - which::which(cmd_name).is_ok() - } - - #[cfg(all(windows, feature = "which"))] - { - if which::which(cmd_name).is_ok() { - true - } else { - // Reference: https://ss64.com/nt/syntax-internal.html - let cmd_builtins = [ - "assoc", "break", "color", "copy", "date", "del", "dir", "dpath", "echo", - "erase", "for", "ftype", "md", "mkdir", "mklink", "move", "path", "ren", - "rename", "rd", "rmdir", "start", "time", "title", "type", "ver", "verify", - "vol", - ]; - - cmd_builtins.contains(&cmd_name) - } - } - } -} diff --git a/crates/nu-engine/src/env/host.rs b/crates/nu-engine/src/env/host.rs deleted file mode 100644 index 0bc4ba7108..0000000000 --- a/crates/nu-engine/src/env/host.rs +++ /dev/null @@ -1,143 +0,0 @@ -use indexmap::IndexMap; -use nu_errors::ShellError; -use nu_source::Text; -use std::ffi::OsString; -use std::fmt::Debug; - -use super::basic_host::BasicHost; - -pub trait Host: Debug + Send { - fn stdout(&mut self, out: &str); - fn stderr(&mut self, out: &str); - fn print_err(&mut self, err: ShellError, source: &Text); - - fn vars(&self) -> Vec<(String, String)>; - fn env_get(&mut self, key: OsString) -> Option; - fn env_set(&mut self, k: OsString, v: OsString); - fn env_rm(&mut self, k: OsString); - - fn width(&self) -> usize; - fn height(&self) -> usize; - - fn is_external_cmd(&self, cmd_name: &str) -> bool; -} - -impl Default for Box { - fn default() -> Self { - Box::new(BasicHost) - } -} - -impl Host for Box { - fn stdout(&mut self, out: &str) { - (**self).stdout(out) - } - - fn stderr(&mut self, out: &str) { - (**self).stderr(out) - } - - fn print_err(&mut self, err: ShellError, source: &Text) { - (**self).print_err(err, source) - } - - fn vars(&self) -> Vec<(String, String)> { - (**self).vars() - } - - fn env_get(&mut self, key: OsString) -> Option { - (**self).env_get(key) - } - - fn env_set(&mut self, key: OsString, value: OsString) { - (**self).env_set(key, value); - } - - fn env_rm(&mut self, key: OsString) { - (**self).env_rm(key) - } - - fn width(&self) -> usize { - (**self).width() - } - - fn height(&self) -> usize { - (**self).height() - } - - fn is_external_cmd(&self, name: &str) -> bool { - (**self).is_external_cmd(name) - } -} - -#[derive(Debug)] -pub struct FakeHost { - line_written: String, - env_vars: IndexMap, -} - -impl FakeHost { - pub fn new() -> FakeHost { - FakeHost { - line_written: String::from(""), - env_vars: IndexMap::default(), - } - } -} - -impl Default for FakeHost { - fn default() -> Self { - FakeHost::new() - } -} - -impl Host for FakeHost { - fn stdout(&mut self, out: &str) { - self.line_written = out.to_string(); - } - - fn stderr(&mut self, out: &str) { - self.line_written = out.to_string(); - } - - fn print_err(&mut self, err: ShellError, source: &Text) { - BasicHost {}.print_err(err, source); - } - - fn vars(&self) -> Vec<(String, String)> { - self.env_vars - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect::>() - } - - fn env_get(&mut self, key: OsString) -> Option { - let key = key.into_string().expect("Couldn't convert to string."); - - self.env_vars.get(&key).map(OsString::from) - } - - fn env_set(&mut self, key: OsString, value: OsString) { - self.env_vars.insert( - key.into_string().expect("Couldn't convert to string."), - value.into_string().expect("Couldn't convert to string."), - ); - } - - fn env_rm(&mut self, key: OsString) { - self.env_vars - .shift_remove(&key.into_string().expect("Couldn't convert to string.")); - } - - fn width(&self) -> usize { - 1 - } - - fn height(&self) -> usize { - 1 - } - - fn is_external_cmd(&self, _: &str) -> bool { - true - } -} diff --git a/crates/nu-engine/src/env/mod.rs b/crates/nu-engine/src/env/mod.rs deleted file mode 100644 index 38a48cfe53..0000000000 --- a/crates/nu-engine/src/env/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub(crate) mod basic_host; -pub(crate) mod host; diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs new file mode 100644 index 0000000000..68d0bd5abf --- /dev/null +++ b/crates/nu-engine/src/eval.rs @@ -0,0 +1,1140 @@ +use std::cmp::Ordering; +use std::collections::HashMap; +use std::io::Write; + +use nu_path::expand_path_with; +use nu_protocol::ast::{Block, Call, Expr, Expression, Operator, Statement}; +use nu_protocol::engine::{EngineState, Stack}; +use nu_protocol::{ + IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, Range, ShellError, Span, + Spanned, Unit, Value, VarId, ENV_VARIABLE_ID, +}; + +use crate::{current_dir_str, get_full_help}; + +pub fn eval_operator(op: &Expression) -> Result { + match op { + Expression { + expr: Expr::Operator(operator), + .. + } => Ok(operator.clone()), + Expression { span, expr, .. } => { + Err(ShellError::UnknownOperator(format!("{:?}", expr), *span)) + } + } +} + +fn eval_call( + engine_state: &EngineState, + caller_stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let decl = engine_state.get_decl(call.decl_id); + + if call.named.iter().any(|(flag, _)| flag.item == "help") { + let full_help = get_full_help( + &decl.signature(), + &decl.examples(), + engine_state, + caller_stack, + ); + Ok(Value::String { + val: full_help, + span: call.head, + } + .into_pipeline_data()) + } else if let Some(block_id) = decl.get_block_id() { + let block = engine_state.get_block(block_id); + + let mut callee_stack = caller_stack.gather_captures(&block.captures); + + for (param_idx, param) in decl + .signature() + .required_positional + .iter() + .chain(decl.signature().optional_positional.iter()) + .enumerate() + { + let var_id = param + .var_id + .expect("internal error: all custom parameters must have var_ids"); + + if let Some(arg) = call.positional.get(param_idx) { + let result = eval_expression(engine_state, caller_stack, arg)?; + callee_stack.add_var(var_id, result); + } else { + callee_stack.add_var(var_id, Value::nothing(call.head)); + } + } + + if let Some(rest_positional) = decl.signature().rest_positional { + let mut rest_items = vec![]; + + for arg in call.positional.iter().skip( + decl.signature().required_positional.len() + + decl.signature().optional_positional.len(), + ) { + let result = eval_expression(engine_state, caller_stack, arg)?; + rest_items.push(result); + } + + let span = if let Some(rest_item) = rest_items.first() { + rest_item.span()? + } else { + call.head + }; + + callee_stack.add_var( + rest_positional + .var_id + .expect("Internal error: rest positional parameter lacks var_id"), + Value::List { + vals: rest_items, + span, + }, + ) + } + + for named in decl.signature().named { + if let Some(var_id) = named.var_id { + let mut found = false; + for call_named in &call.named { + if call_named.0.item == named.long { + if let Some(arg) = &call_named.1 { + let result = eval_expression(engine_state, caller_stack, arg)?; + + callee_stack.add_var(var_id, result); + } else { + callee_stack.add_var( + var_id, + Value::Bool { + val: true, + span: call.head, + }, + ) + } + found = true; + } + } + + if !found { + if named.arg.is_none() { + callee_stack.add_var( + var_id, + Value::Bool { + val: false, + span: call.head, + }, + ) + } else { + callee_stack.add_var(var_id, Value::Nothing { span: call.head }) + } + } + } + } + + let result = eval_block(engine_state, &mut callee_stack, block, input); + + if block.redirect_env { + let caller_env_vars = caller_stack.get_env_var_names(engine_state); + + // remove env vars that are present in the caller but not in the callee + // (the callee hid them) + for var in caller_env_vars.iter() { + if !callee_stack.has_env_var(engine_state, var) { + caller_stack.remove_env_var(engine_state, var); + } + } + + // add new env vars from callee to caller + for env_vars in callee_stack.env_vars { + for (var, value) in env_vars { + caller_stack.add_env_var(var, value); + } + } + } + + result + } else { + // We pass caller_stack here with the knowledge that internal commands + // are going to be specifically looking for global state in the stack + // rather than any local state. + decl.run(engine_state, caller_stack, call, input) + } +} + +fn eval_external( + engine_state: &EngineState, + stack: &mut Stack, + head: &Expression, + args: &[Expression], + input: PipelineData, + last_expression: bool, +) -> Result { + let decl_id = engine_state + .find_decl("run_external".as_bytes()) + .ok_or(ShellError::ExternalNotSupported(head.span))?; + + let command = engine_state.get_decl(decl_id); + + let mut call = Call::new(head.span); + + call.positional.push(head.clone()); + + for arg in args { + call.positional.push(arg.clone()) + } + + if last_expression { + call.named.push(( + Spanned { + item: "last_expression".into(), + span: head.span, + }, + None, + )) + } + + command.run(engine_state, stack, &call, input) +} + +pub fn eval_expression( + engine_state: &EngineState, + stack: &mut Stack, + expr: &Expression, +) -> Result { + match &expr.expr { + Expr::Bool(b) => Ok(Value::Bool { + val: *b, + span: expr.span, + }), + Expr::Int(i) => Ok(Value::Int { + val: *i, + span: expr.span, + }), + Expr::Float(f) => Ok(Value::Float { + val: *f, + span: expr.span, + }), + Expr::ValueWithUnit(e, unit) => match eval_expression(engine_state, stack, e)? { + Value::Int { val, .. } => Ok(compute(val, unit.item, unit.span)), + x => Err(ShellError::CantConvert( + "unit value".into(), + x.get_type().to_string(), + e.span, + )), + }, + Expr::Range(from, next, to, operator) => { + let from = if let Some(f) = from { + eval_expression(engine_state, stack, f)? + } else { + Value::Nothing { span: expr.span } + }; + + let next = if let Some(s) = next { + eval_expression(engine_state, stack, s)? + } else { + Value::Nothing { span: expr.span } + }; + + let to = if let Some(t) = to { + eval_expression(engine_state, stack, t)? + } else { + Value::Nothing { span: expr.span } + }; + + Ok(Value::Range { + val: Box::new(Range::new(expr.span, from, next, to, operator)?), + span: expr.span, + }) + } + Expr::Var(var_id) => eval_variable(engine_state, stack, *var_id, expr.span), + Expr::VarDecl(_) => Ok(Value::Nothing { span: expr.span }), + Expr::CellPath(cell_path) => Ok(Value::CellPath { + val: cell_path.clone(), + span: expr.span, + }), + Expr::FullCellPath(cell_path) => { + let value = eval_expression(engine_state, stack, &cell_path.head)?; + + value.follow_cell_path(&cell_path.tail) + } + Expr::ImportPattern(_) => Ok(Value::Nothing { span: expr.span }), + Expr::Call(call) => { + // FIXME: protect this collect with ctrl-c + Ok( + eval_call(engine_state, stack, call, PipelineData::new(call.head))? + .into_value(call.head), + ) + } + Expr::ExternalCall(head, args) => { + let span = head.span; + // FIXME: protect this collect with ctrl-c + Ok(eval_external( + engine_state, + stack, + head, + args, + PipelineData::new(span), + false, + )? + .into_value(span)) + } + Expr::Operator(_) => Ok(Value::Nothing { span: expr.span }), + Expr::BinaryOp(lhs, op, rhs) => { + let op_span = op.span; + let lhs = eval_expression(engine_state, stack, lhs)?; + let op = eval_operator(op)?; + let rhs = eval_expression(engine_state, stack, rhs)?; + + match op { + Operator::Plus => lhs.add(op_span, &rhs), + Operator::Minus => lhs.sub(op_span, &rhs), + Operator::Multiply => lhs.mul(op_span, &rhs), + Operator::Divide => lhs.div(op_span, &rhs), + Operator::LessThan => lhs.lt(op_span, &rhs), + Operator::LessThanOrEqual => lhs.lte(op_span, &rhs), + Operator::GreaterThan => lhs.gt(op_span, &rhs), + Operator::GreaterThanOrEqual => lhs.gte(op_span, &rhs), + Operator::Equal => lhs.eq(op_span, &rhs), + Operator::NotEqual => lhs.ne(op_span, &rhs), + Operator::In => lhs.r#in(op_span, &rhs), + Operator::NotIn => lhs.not_in(op_span, &rhs), + Operator::Contains => lhs.contains(op_span, &rhs), + Operator::NotContains => lhs.not_contains(op_span, &rhs), + Operator::Modulo => lhs.modulo(op_span, &rhs), + Operator::And => lhs.and(op_span, &rhs), + Operator::Or => lhs.or(op_span, &rhs), + Operator::Pow => lhs.pow(op_span, &rhs), + } + } + Expr::Subexpression(block_id) => { + let block = engine_state.get_block(*block_id); + + // FIXME: protect this collect with ctrl-c + Ok( + eval_subexpression(engine_state, stack, block, PipelineData::new(expr.span))? + .into_value(expr.span), + ) + } + Expr::RowCondition(block_id) | Expr::Block(block_id) => { + let mut captures = HashMap::new(); + let block = engine_state.get_block(*block_id); + + for var_id in &block.captures { + captures.insert(*var_id, stack.get_var(*var_id, expr.span)?); + } + Ok(Value::Block { + val: *block_id, + captures, + span: expr.span, + }) + } + Expr::List(x) => { + let mut output = vec![]; + for expr in x { + output.push(eval_expression(engine_state, stack, expr)?); + } + Ok(Value::List { + vals: output, + span: expr.span, + }) + } + Expr::Record(fields) => { + let mut cols = vec![]; + let mut vals = vec![]; + for (col, val) in fields { + cols.push(eval_expression(engine_state, stack, col)?.as_string()?); + vals.push(eval_expression(engine_state, stack, val)?); + } + + Ok(Value::Record { + cols, + vals, + span: expr.span, + }) + } + Expr::Table(headers, vals) => { + let mut output_headers = vec![]; + for expr in headers { + output_headers.push(eval_expression(engine_state, stack, expr)?.as_string()?); + } + + let mut output_rows = vec![]; + for val in vals { + let mut row = vec![]; + for expr in val { + row.push(eval_expression(engine_state, stack, expr)?); + } + output_rows.push(Value::Record { + cols: output_headers.clone(), + vals: row, + span: expr.span, + }); + } + Ok(Value::List { + vals: output_rows, + span: expr.span, + }) + } + Expr::Keyword(_, _, expr) => eval_expression(engine_state, stack, expr), + Expr::StringInterpolation(exprs) => { + let mut parts = vec![]; + for expr in exprs { + parts.push(eval_expression(engine_state, stack, expr)?); + } + + let config = stack.get_config().unwrap_or_default(); + + parts + .into_iter() + .into_pipeline_data(None) + .collect_string("", &config) + .map(|x| Value::String { + val: x, + span: expr.span, + }) + } + Expr::String(s) => Ok(Value::String { + val: s.clone(), + span: expr.span, + }), + Expr::Filepath(s) => { + let cwd = current_dir_str(engine_state, stack)?; + let path = expand_path_with(s, cwd); + + Ok(Value::String { + val: path.to_string_lossy().to_string(), + span: expr.span, + }) + } + Expr::GlobPattern(s) => { + let cwd = current_dir_str(engine_state, stack)?; + let path = expand_path_with(s, cwd); + + Ok(Value::String { + val: path.to_string_lossy().to_string(), + span: expr.span, + }) + } + Expr::Signature(_) => Ok(Value::Nothing { span: expr.span }), + Expr::Garbage => Ok(Value::Nothing { span: expr.span }), + Expr::Nothing => Ok(Value::Nothing { span: expr.span }), + } +} + +/// Checks the expression to see if it's a internal or external call. If so, passes the input +/// into the call and gets out the result +/// Otherwise, invokes the expression +pub fn eval_expression_with_input( + engine_state: &EngineState, + stack: &mut Stack, + expr: &Expression, + mut input: PipelineData, + last_expression: bool, +) -> Result { + match expr { + Expression { + expr: Expr::Call(call), + .. + } => { + input = eval_call(engine_state, stack, call, input)?; + } + Expression { + expr: Expr::ExternalCall(head, args), + .. + } => { + input = eval_external(engine_state, stack, head, args, input, last_expression)?; + } + + Expression { + expr: Expr::Subexpression(block_id), + .. + } => { + let block = engine_state.get_block(*block_id); + + // FIXME: protect this collect with ctrl-c + input = eval_subexpression(engine_state, stack, block, input)?; + } + + elem => { + input = eval_expression(engine_state, stack, elem)?.into_pipeline_data(); + } + } + + Ok(input) +} + +pub fn eval_block( + engine_state: &EngineState, + stack: &mut Stack, + block: &Block, + mut input: PipelineData, +) -> Result { + let num_stmts = block.stmts.len(); + for (stmt_idx, stmt) in block.stmts.iter().enumerate() { + if let Statement::Pipeline(pipeline) = stmt { + for (i, elem) in pipeline.expressions.iter().enumerate() { + input = eval_expression_with_input( + engine_state, + stack, + elem, + input, + i == pipeline.expressions.len() - 1, + )? + } + } + + if stmt_idx < (num_stmts) - 1 { + match input { + PipelineData::Value(Value::Nothing { .. }, ..) => {} + _ => { + // Drain the input to the screen via tabular output + let config = stack.get_config().unwrap_or_default(); + + match engine_state.find_decl("table".as_bytes()) { + Some(decl_id) => { + let table = engine_state.get_decl(decl_id).run( + engine_state, + stack, + &Call::new(Span::new(0, 0)), + input, + )?; + + for item in table { + let stdout = std::io::stdout(); + + if let Value::Error { error } = item { + return Err(error); + } + + let mut out = item.into_string("\n", &config); + out.push('\n'); + + match stdout.lock().write_all(out.as_bytes()) { + Ok(_) => (), + Err(err) => eprintln!("{}", err), + }; + } + } + None => { + for item in input { + let stdout = std::io::stdout(); + + if let Value::Error { error } = item { + return Err(error); + } + + let mut out = item.into_string("\n", &config); + out.push('\n'); + + match stdout.lock().write_all(out.as_bytes()) { + Ok(_) => (), + Err(err) => eprintln!("{}", err), + }; + } + } + }; + } + } + + input = PipelineData::new(Span { start: 0, end: 0 }) + } + } + + Ok(input) +} + +pub fn eval_block_with_redirect( + engine_state: &EngineState, + stack: &mut Stack, + block: &Block, + mut input: PipelineData, +) -> Result { + let num_stmts = block.stmts.len(); + for (stmt_idx, stmt) in block.stmts.iter().enumerate() { + if let Statement::Pipeline(pipeline) = stmt { + for elem in pipeline.expressions.iter() { + input = eval_expression_with_input(engine_state, stack, elem, input, false)? + } + } + + if stmt_idx < (num_stmts) - 1 { + match input { + PipelineData::Value(Value::Nothing { .. }, ..) => {} + _ => { + // Drain the input to the screen via tabular output + let config = stack.get_config().unwrap_or_default(); + + match engine_state.find_decl("table".as_bytes()) { + Some(decl_id) => { + let table = engine_state.get_decl(decl_id).run( + engine_state, + stack, + &Call::new(Span::new(0, 0)), + input, + )?; + + for item in table { + let stdout = std::io::stdout(); + + if let Value::Error { error } = item { + return Err(error); + } + + let mut out = item.into_string("\n", &config); + out.push('\n'); + + match stdout.lock().write_all(out.as_bytes()) { + Ok(_) => (), + Err(err) => eprintln!("{}", err), + }; + } + } + None => { + for item in input { + let stdout = std::io::stdout(); + + if let Value::Error { error } = item { + return Err(error); + } + + let mut out = item.into_string("\n", &config); + out.push('\n'); + + match stdout.lock().write_all(out.as_bytes()) { + Ok(_) => (), + Err(err) => eprintln!("{}", err), + }; + } + } + }; + } + } + + input = PipelineData::new(Span { start: 0, end: 0 }) + } + } + + Ok(input) +} + +pub fn eval_subexpression( + engine_state: &EngineState, + stack: &mut Stack, + block: &Block, + mut input: PipelineData, +) -> Result { + for stmt in block.stmts.iter() { + if let Statement::Pipeline(pipeline) = stmt { + for expr in pipeline.expressions.iter() { + input = eval_expression_with_input(engine_state, stack, expr, input, false)? + } + } + } + + Ok(input) +} + +pub fn eval_variable( + engine_state: &EngineState, + stack: &Stack, + var_id: VarId, + span: Span, +) -> Result { + match var_id { + nu_protocol::NU_VARIABLE_ID => { + // $nu + let mut output_cols = vec![]; + let mut output_vals = vec![]; + + if let Some(mut config_path) = nu_path::config_dir() { + config_path.push("nushell"); + + let mut history_path = config_path.clone(); + let mut keybinding_path = config_path.clone(); + + history_path.push("history.txt"); + + output_cols.push("history-path".into()); + output_vals.push(Value::String { + val: history_path.to_string_lossy().to_string(), + span, + }); + + config_path.push("config.nu"); + + output_cols.push("config-path".into()); + output_vals.push(Value::String { + val: config_path.to_string_lossy().to_string(), + span, + }); + + // TODO: keybindings don't exist yet but lets add a file + // path for them to be stored in. It doesn't have to be yml. + keybinding_path.push("keybindings.yml"); + output_cols.push("keybinding-path".into()); + output_vals.push(Value::String { + val: keybinding_path.to_string_lossy().to_string(), + span, + }) + } + + #[cfg(feature = "plugin")] + if let Some(path) = &engine_state.plugin_signatures { + if let Some(path_str) = path.to_str() { + output_cols.push("plugin-path".into()); + output_vals.push(Value::String { + val: path_str.into(), + span, + }); + } + } + + // since the env var PWD doesn't exist on all platforms + // lets just get the current directory + let cwd = current_dir_str(engine_state, stack)?; + output_cols.push("cwd".into()); + output_vals.push(Value::String { val: cwd, span }); + + if let Some(home_path) = nu_path::home_dir() { + if let Some(home_path_str) = home_path.to_str() { + output_cols.push("home-path".into()); + output_vals.push(Value::String { + val: home_path_str.into(), + span, + }) + } + } + + let temp = std::env::temp_dir(); + if let Some(temp_path) = temp.to_str() { + output_cols.push("temp-path".into()); + output_vals.push(Value::String { + val: temp_path.into(), + span, + }) + } + + Ok(Value::Record { + cols: output_cols, + vals: output_vals, + span, + }) + } + nu_protocol::SCOPE_VARIABLE_ID => { + let mut output_cols = vec![]; + let mut output_vals = vec![]; + + let mut vars = vec![]; + + let mut commands = vec![]; + let mut aliases = vec![]; + let mut overlays = vec![]; + + for frame in &engine_state.scope { + for var in &frame.vars { + let var_name = Value::string(String::from_utf8_lossy(var.0).to_string(), span); + + let var_type = Value::string(engine_state.get_var(*var.1).to_string(), span); + + let var_value = if let Ok(val) = stack.get_var(*var.1, span) { + val + } else { + Value::nothing(span) + }; + + vars.push(Value::Record { + cols: vec!["name".to_string(), "type".to_string(), "value".to_string()], + vals: vec![var_name, var_type, var_value], + span, + }) + } + + for command in &frame.decls { + let mut cols = vec![]; + let mut vals = vec![]; + + cols.push("command".into()); + vals.push(Value::String { + val: String::from_utf8_lossy(command.0).to_string(), + span, + }); + + let decl = engine_state.get_decl(*command.1); + let signature = decl.signature(); + cols.push("category".to_string()); + vals.push(Value::String { + val: signature.category.to_string(), + span, + }); + + // signature + let mut sig_records = vec![]; + { + let sig_cols = vec![ + "command".to_string(), + "parameter_name".to_string(), + "parameter_type".to_string(), + "syntax_shape".to_string(), + "is_optional".to_string(), + "short_flag".to_string(), + "description".to_string(), + ]; + + // required_positional + for req in signature.required_positional { + let sig_vals = vec![ + Value::string(&signature.name, span), + Value::string(req.name, span), + Value::string("positional", span), + Value::string(req.shape.to_string(), span), + Value::boolean(false, span), + Value::nothing(span), + Value::string(req.desc, span), + ]; + + sig_records.push(Value::Record { + cols: sig_cols.clone(), + vals: sig_vals, + span, + }); + } + + // optional_positional + for opt in signature.optional_positional { + let sig_vals = vec![ + Value::string(&signature.name, span), + Value::string(opt.name, span), + Value::string("positional", span), + Value::string(opt.shape.to_string(), span), + Value::boolean(true, span), + Value::nothing(span), + Value::string(opt.desc, span), + ]; + + sig_records.push(Value::Record { + cols: sig_cols.clone(), + vals: sig_vals, + span, + }); + } + + { + // rest_positional + if let Some(rest) = signature.rest_positional { + let sig_vals = vec![ + Value::string(&signature.name, span), + Value::string(rest.name, span), + Value::string("rest", span), + Value::string(rest.shape.to_string(), span), + Value::boolean(true, span), + Value::nothing(span), + Value::string(rest.desc, span), + ]; + + sig_records.push(Value::Record { + cols: sig_cols.clone(), + vals: sig_vals, + span, + }); + } + } + + // named flags + for named in signature.named { + let flag_type; + + // Skip the help flag + if named.long == "help" { + continue; + } + + let shape = if let Some(arg) = named.arg { + flag_type = Value::string("named", span); + Value::string(arg.to_string(), span) + } else { + flag_type = Value::string("switch", span); + Value::nothing(span) + }; + + let short_flag = if let Some(c) = named.short { + Value::string(c, span) + } else { + Value::nothing(span) + }; + + let sig_vals = vec![ + Value::string(&signature.name, span), + Value::string(named.long, span), + flag_type, + shape, + Value::boolean(!named.required, span), + short_flag, + Value::string(named.desc, span), + ]; + + sig_records.push(Value::Record { + cols: sig_cols.clone(), + vals: sig_vals, + span, + }); + } + } + + cols.push("signature".to_string()); + vals.push(Value::List { + vals: sig_records, + span, + }); + + cols.push("usage".to_string()); + vals.push(Value::String { + val: decl.usage().into(), + span, + }); + + cols.push("is_binary".to_string()); + vals.push(Value::Bool { + val: decl.is_binary(), + span, + }); + + cols.push("is_private".to_string()); + vals.push(Value::Bool { + val: decl.is_private(), + span, + }); + + cols.push("is_builtin".to_string()); + vals.push(Value::Bool { + val: decl.is_builtin(), + span, + }); + + cols.push("is_sub".to_string()); + vals.push(Value::Bool { + val: decl.is_sub(), + span, + }); + + cols.push("is_plugin".to_string()); + vals.push(Value::Bool { + val: decl.is_plugin().is_some(), + span, + }); + + cols.push("is_custom".to_string()); + vals.push(Value::Bool { + val: decl.get_block_id().is_some(), + span, + }); + + cols.push("creates_scope".to_string()); + vals.push(Value::Bool { + val: signature.creates_scope, + span, + }); + + cols.push("extra_usage".to_string()); + vals.push(Value::String { + val: decl.extra_usage().into(), + span, + }); + + commands.push(Value::Record { cols, vals, span }) + } + + for alias in &frame.aliases { + let mut alias_text = String::new(); + for span in alias.1 { + let contents = engine_state.get_span_contents(span); + if !alias_text.is_empty() { + alias_text.push(' '); + } + alias_text.push_str(&String::from_utf8_lossy(contents).to_string()); + } + aliases.push(( + Value::String { + val: String::from_utf8_lossy(alias.0).to_string(), + span, + }, + Value::string(alias_text, span), + )); + } + + for overlay in &frame.overlays { + overlays.push(Value::String { + val: String::from_utf8_lossy(overlay.0).to_string(), + span, + }); + } + } + + output_cols.push("vars".to_string()); + output_vals.push(Value::List { vals: vars, span }); + + commands.sort_by(|a, b| match (a, b) { + (Value::Record { vals: rec_a, .. }, Value::Record { vals: rec_b, .. }) => { + // Comparing the first value from the record + // It is expected that the first value is the name of the column + // The names of the commands should be a value string + match (rec_a.get(0), rec_b.get(0)) { + (Some(val_a), Some(val_b)) => match (val_a, val_b) { + ( + Value::String { val: str_a, .. }, + Value::String { val: str_b, .. }, + ) => str_a.cmp(str_b), + _ => Ordering::Equal, + }, + _ => Ordering::Equal, + } + } + _ => Ordering::Equal, + }); + output_cols.push("commands".to_string()); + output_vals.push(Value::List { + vals: commands, + span, + }); + + aliases.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); + output_cols.push("aliases".to_string()); + output_vals.push(Value::List { + vals: aliases + .into_iter() + .map(|(alias, value)| Value::Record { + cols: vec!["alias".into(), "expansion".into()], + vals: vec![alias, value], + span, + }) + .collect(), + span, + }); + + overlays.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); + output_cols.push("overlays".to_string()); + output_vals.push(Value::List { + vals: overlays, + span, + }); + + Ok(Value::Record { + cols: output_cols, + vals: output_vals, + span, + }) + } + ENV_VARIABLE_ID => { + let env_vars = stack.get_env_vars(engine_state); + let env_columns = env_vars.keys(); + let env_values = env_vars.values(); + + let mut pairs = env_columns + .map(|x| x.to_string()) + .zip(env_values.cloned()) + .collect::>(); + + pairs.sort_by(|a, b| a.0.cmp(&b.0)); + + let (env_columns, env_values) = pairs.into_iter().unzip(); + + Ok(Value::Record { + cols: env_columns, + vals: env_values, + span, + }) + } + var_id => stack.get_var(var_id, span), + } +} + +fn compute(size: i64, unit: Unit, span: Span) -> Value { + match unit { + Unit::Byte => Value::Filesize { val: size, span }, + Unit::Kilobyte => Value::Filesize { + val: size * 1000, + span, + }, + Unit::Megabyte => Value::Filesize { + val: size * 1000 * 1000, + span, + }, + Unit::Gigabyte => Value::Filesize { + val: size * 1000 * 1000 * 1000, + span, + }, + Unit::Terabyte => Value::Filesize { + val: size * 1000 * 1000 * 1000 * 1000, + span, + }, + Unit::Petabyte => Value::Filesize { + val: size * 1000 * 1000 * 1000 * 1000 * 1000, + span, + }, + + Unit::Kibibyte => Value::Filesize { + val: size * 1024, + span, + }, + Unit::Mebibyte => Value::Filesize { + val: size * 1024 * 1024, + span, + }, + Unit::Gibibyte => Value::Filesize { + val: size * 1024 * 1024 * 1024, + span, + }, + Unit::Tebibyte => Value::Filesize { + val: size * 1024 * 1024 * 1024 * 1024, + span, + }, + Unit::Pebibyte => Value::Filesize { + val: size * 1024 * 1024 * 1024 * 1024 * 1024, + span, + }, + + Unit::Nanosecond => Value::Duration { val: size, span }, + Unit::Microsecond => Value::Duration { + val: size * 1000, + span, + }, + Unit::Millisecond => Value::Duration { + val: size * 1000 * 1000, + span, + }, + Unit::Second => Value::Duration { + val: size * 1000 * 1000 * 1000, + span, + }, + Unit::Minute => Value::Duration { + val: size * 1000 * 1000 * 1000 * 60, + span, + }, + Unit::Hour => Value::Duration { + val: size * 1000 * 1000 * 1000 * 60 * 60, + span, + }, + Unit::Day => match size.checked_mul(1000 * 1000 * 1000 * 60 * 60 * 24) { + Some(val) => Value::Duration { val, span }, + None => Value::Error { + error: ShellError::SpannedLabeledError( + "duration too large".into(), + "duration too large".into(), + span, + ), + }, + }, + Unit::Week => match size.checked_mul(1000 * 1000 * 1000 * 60 * 60 * 24 * 7) { + Some(val) => Value::Duration { val, span }, + None => Value::Error { + error: ShellError::SpannedLabeledError( + "duration too large".into(), + "duration too large".into(), + span, + ), + }, + }, + } +} diff --git a/crates/nu-engine/src/evaluate/block.rs b/crates/nu-engine/src/evaluate/block.rs deleted file mode 100644 index 7607a6af5e..0000000000 --- a/crates/nu-engine/src/evaluate/block.rs +++ /dev/null @@ -1,204 +0,0 @@ -use crate::evaluate::expr::run_expression_block; -use crate::evaluate::internal::run_internal_command; -use crate::evaluation_context::EvaluationContext; -use nu_errors::ShellError; -use nu_parser::ParserScope; -use nu_protocol::hir::{ - Block, Call, ClassifiedCommand, Expression, ExternalRedirection, Pipeline, SpannedExpression, - Synthetic, -}; -use nu_protocol::{UntaggedValue, Value}; -use nu_source::{Span, Tag}; -use nu_stream::{InputStream, OutputStream}; -use std::sync::atomic::Ordering; - -pub fn run_block( - block: &Block, - ctx: &EvaluationContext, - mut input: InputStream, - external_redirection: ExternalRedirection, -) -> Result { - let mut output: Result = Ok(OutputStream::empty()); - for definition in block.definitions.values() { - ctx.scope.add_definition(definition.clone()); - } - - let num_groups = block.block.len(); - for (group_num, group) in block.block.iter().enumerate() { - let num_pipelines = group.pipelines.len(); - for (pipeline_num, pipeline) in group.pipelines.iter().enumerate() { - match output { - Ok(inp) if inp.is_empty() => {} - Ok(inp) => { - // Run autoview on the values we've seen so far - // We may want to make this configurable for other kinds of hosting - if let Some(autoview) = ctx.get_command("autoview") { - let mut output_stream = match ctx.run_command( - autoview, - Tag::unknown(), - Call::new( - Box::new(SpannedExpression::new( - Expression::Synthetic(Synthetic::String("autoview".into())), - Span::unknown(), - )), - Span::unknown(), - ), - inp, - ) { - Ok(x) => x, - Err(e) => { - return Err(e); - } - }; - match output_stream.next() { - Some(Value { - value: UntaggedValue::Error(e), - .. - }) => { - return Err(e); - } - Some(_item) => { - if let Some(err) = ctx.get_errors().get(0) { - ctx.clear_errors(); - return Err(err.clone()); - } - if ctx.ctrl_c().load(Ordering::SeqCst) { - return Ok(InputStream::empty()); - } - } - None => { - if let Some(err) = ctx.get_errors().get(0) { - ctx.clear_errors(); - return Err(err.clone()); - } - } - } - } else { - let _: Vec<_> = inp.collect(); - } - } - Err(e) => { - return Err(e); - } - } - output = Ok(OutputStream::empty()); - - match output { - Ok(inp) if inp.is_empty() => {} - Ok(mut output_stream) => { - match output_stream.next() { - Some(Value { - value: UntaggedValue::Error(e), - .. - }) => { - return Err(e); - } - Some(_item) => { - if let Some(err) = ctx.get_errors().get(0) { - ctx.clear_errors(); - return Err(err.clone()); - } - if ctx.ctrl_c().load(Ordering::SeqCst) { - // This early return doesn't return the result - // we have so far, but breaking out of this loop - // causes lifetime issues. A future contribution - // could attempt to return the current output. - // https://github.com/nushell/nushell/pull/2830#discussion_r550319687 - return Ok(InputStream::empty()); - } - } - None => { - if let Some(err) = ctx.get_errors().get(0) { - ctx.clear_errors(); - return Err(err.clone()); - } - } - } - } - Err(e) => { - return Err(e); - } - } - output = if group_num == (num_groups - 1) && pipeline_num == (num_pipelines - 1) { - // we're at the end of the block, so use the given external redirection - run_pipeline(pipeline, ctx, input, external_redirection) - } else { - // otherwise, we're in the middle of the block, so use a default redirection - run_pipeline(pipeline, ctx, input, ExternalRedirection::None) - }; - - input = OutputStream::empty(); - } - } - - output -} - -fn run_pipeline( - commands: &Pipeline, - ctx: &EvaluationContext, - mut input: InputStream, - external_redirection: ExternalRedirection, -) -> Result { - let num_commands = commands.list.len(); - for (command_num, command) in commands.list.iter().enumerate() { - input = match command { - ClassifiedCommand::Dynamic(call) => { - let mut args = vec![]; - if let Some(positional) = &call.positional { - for pos in positional { - let result = run_expression_block(pos, ctx)?.into_vec(); - args.push(result); - } - } - - let block = run_expression_block(&call.head, ctx)?.into_vec(); - - if block.len() != 1 { - return Err(ShellError::labeled_error( - "Dynamic commands must start with a block", - "needs to be a block", - call.head.span, - )); - } - - match &block[0].value { - UntaggedValue::Block(captured_block) => { - ctx.scope.enter_scope(); - ctx.scope.add_vars(&captured_block.captured.entries); - for (param, value) in - captured_block.block.params.positional.iter().zip(&args) - { - ctx.scope.add_var(param.0.name(), value[0].clone()); - } - - let result = - run_block(&captured_block.block, ctx, input, external_redirection); - ctx.scope.exit_scope(); - - result? - } - _ => { - return Err(ShellError::labeled_error("Dynamic commands must start with a block (or variable pointing to a block)", "needs to be a block", call.head.span)); - } - } - } - - ClassifiedCommand::Expr(expr) => run_expression_block(expr, ctx)?, - - ClassifiedCommand::Error(err) => return Err(err.clone().into()), - - ClassifiedCommand::Internal(left) => { - if command_num == (num_commands - 1) { - let mut left = left.clone(); - left.args.external_redirection = external_redirection; - run_internal_command(&left, ctx, input)? - } else { - run_internal_command(left, ctx, input)? - } - } - }; - } - - Ok(input) -} diff --git a/crates/nu-engine/src/evaluate/envvar.rs b/crates/nu-engine/src/evaluate/envvar.rs deleted file mode 100644 index 068f204b07..0000000000 --- a/crates/nu-engine/src/evaluate/envvar.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::convert::TryFrom; - -use nu_errors::ShellError; -use nu_protocol::{SpannedTypeName, Value}; - -#[derive(Debug, Clone)] -pub enum EnvVar { - Proper(String), - Nothing, -} - -impl TryFrom for EnvVar { - type Error = ShellError; - - fn try_from(value: Value) -> Result { - if value.value.is_none() { - Ok(EnvVar::Nothing) - } else if value.is_primitive() { - Ok(EnvVar::Proper(value.convert_to_string())) - } else { - Err(ShellError::type_error( - "primitive", - value.spanned_type_name(), - )) - } - } -} - -impl TryFrom<&Value> for EnvVar { - type Error = ShellError; - - fn try_from(value: &Value) -> Result { - if value.value.is_none() { - Ok(EnvVar::Nothing) - } else if value.is_primitive() { - Ok(EnvVar::Proper(value.convert_to_string())) - } else { - Err(ShellError::type_error( - "primitive", - value.spanned_type_name(), - )) - } - } -} - -impl From for EnvVar { - fn from(string: String) -> Self { - EnvVar::Proper(string) - } -} diff --git a/crates/nu-engine/src/evaluate/evaluate_args.rs b/crates/nu-engine/src/evaluate/evaluate_args.rs deleted file mode 100644 index ecf47e2441..0000000000 --- a/crates/nu-engine/src/evaluate/evaluate_args.rs +++ /dev/null @@ -1,50 +0,0 @@ -// TODO: Temporary redirect -use crate::evaluate::evaluator::evaluate_baseline_expr; -use crate::evaluation_context::EvaluationContext; -use indexmap::IndexMap; -use nu_errors::ShellError; -use nu_protocol::{hir, EvaluatedArgs, UntaggedValue, Value}; - -pub(crate) fn evaluate_args( - call: &hir::Call, - ctx: &EvaluationContext, -) -> Result { - let mut positional_args: Vec = vec![]; - - if let Some(positional) = &call.positional { - for pos in positional { - let result = evaluate_baseline_expr(pos, ctx)?; - positional_args.push(result); - } - } - - let positional = if !positional_args.is_empty() { - Some(positional_args) - } else { - None - }; - - let mut named_args = IndexMap::new(); - - if let Some(named) = &call.named { - for (name, value) in named { - match value { - hir::NamedValue::PresentSwitch(tag) => { - named_args.insert(name.clone(), UntaggedValue::boolean(true).into_value(tag)); - } - hir::NamedValue::Value(_, expr) => { - named_args.insert(name.clone(), evaluate_baseline_expr(expr, ctx)?); - } - _ => {} - }; - } - } - - let named = if !named_args.is_empty() { - Some(named_args) - } else { - None - }; - - Ok(EvaluatedArgs::new(positional, named)) -} diff --git a/crates/nu-engine/src/evaluate/evaluator.rs b/crates/nu-engine/src/evaluate/evaluator.rs deleted file mode 100644 index 005f4d3dc9..0000000000 --- a/crates/nu-engine/src/evaluate/evaluator.rs +++ /dev/null @@ -1,328 +0,0 @@ -use crate::evaluate::block::run_block; -use crate::evaluate::operator::apply_operator; -use crate::evaluation_context::EvaluationContext; -use indexmap::IndexMap; -use log::trace; -use nu_errors::{ArgumentError, ShellError}; -use nu_protocol::did_you_mean; -use nu_protocol::{ - hir::{self, CapturedBlock, Expression, RangeOperator, SpannedExpression}, - Dictionary, -}; -use nu_protocol::{ - ColumnPath, Primitive, RangeInclusion, UnspannedPathMember, UntaggedValue, Value, -}; -use nu_source::{Span, SpannedItem, Tag}; -use nu_stream::InputStream; -use nu_value_ext::ValueExt; - -pub fn evaluate_baseline_expr( - expr: &SpannedExpression, - ctx: &EvaluationContext, -) -> Result { - let tag = Tag { - span: expr.span, - anchor: None, - }; - let span = expr.span; - match &expr.expr { - Expression::Literal(literal) => Ok(evaluate_literal(literal, span)), - Expression::ExternalWord => Err(ShellError::argument_error( - "Invalid external word".spanned(tag.span), - ArgumentError::InvalidExternalWord, - )), - Expression::FilePath(path) => Ok(UntaggedValue::filepath(path.clone()).into_value(tag)), - Expression::Synthetic(hir::Synthetic::String(s)) => { - Ok(UntaggedValue::string(s).into_untagged_value()) - } - expr @ Expression::Variable(_, _) => evaluate_reference(&Variable::from(expr), ctx, span), - Expression::Command => unimplemented!(), - Expression::Subexpression(block) => evaluate_subexpression(block, ctx), - Expression::ExternalCommand(_) => unimplemented!(), - Expression::Binary(binary) => { - // TODO: If we want to add short-circuiting, we'll need to move these down - let left = evaluate_baseline_expr(&binary.left, ctx)?; - let right = evaluate_baseline_expr(&binary.right, ctx)?; - - trace!("left={:?} right={:?}", left.value, right.value); - - match binary.op.expr { - Expression::Literal(hir::Literal::Operator(op)) => { - match apply_operator(op, &left, &right) { - 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), - )), - } - } - _ => Err(ShellError::labeled_error( - "Unknown operator", - "unknown operator", - binary.op.span, - )), - } - } - Expression::Range(range) => { - let left = if let Some(left) = &range.left { - evaluate_baseline_expr(left, ctx)? - } else { - Value::nothing() - }; - - let right = if let Some(right) = &range.right { - evaluate_baseline_expr(right, ctx)? - } else { - Value::nothing() - }; - - let left_span = left.tag.span; - let right_span = right.tag.span; - - let left = ( - left.as_primitive()?.spanned(left_span), - RangeInclusion::Inclusive, - ); - let right = ( - right.as_primitive()?.spanned(right_span), - match &range.operator.item { - RangeOperator::Inclusive => RangeInclusion::Inclusive, - RangeOperator::RightExclusive => RangeInclusion::Exclusive, - }, - ); - - Ok(UntaggedValue::range(left, right).into_value(tag)) - } - Expression::Table(headers, cells) => { - let mut output_headers = vec![]; - - for expr in headers { - let val = evaluate_baseline_expr(expr, ctx)?; - - let header = val.as_string()?; - output_headers.push(header); - } - - let mut output_table = vec![]; - - for row in cells { - if row.len() != headers.len() { - match (row.first(), row.last()) { - (Some(first), Some(last)) => { - return Err(ShellError::labeled_error( - "Cell count doesn't match header count", - format!("expected {} columns", headers.len()), - Span::new(first.span.start(), last.span.end()), - )); - } - _ => { - return Err(ShellError::untagged_runtime_error( - "Cell count doesn't match header count", - )); - } - } - } - - let mut row_output = IndexMap::new(); - for cell in output_headers.iter().zip(row) { - let val = evaluate_baseline_expr(cell.1, ctx)?; - row_output.insert(cell.0.clone(), val); - } - output_table.push(UntaggedValue::row(row_output).into_value(tag.clone())); - } - - Ok(UntaggedValue::Table(output_table).into_value(tag)) - } - Expression::List(list) => { - let mut exprs = vec![]; - - for expr in list { - let expr = evaluate_baseline_expr(expr, ctx)?; - exprs.push(expr); - } - - Ok(UntaggedValue::Table(exprs).into_value(tag)) - } - Expression::Block(block) => { - // Capture the current values of all free variables - let mut known_variables = vec![]; - let free_variables = block.get_free_variables(&mut known_variables); - - let mut captured = Dictionary::new(IndexMap::new()); - for free_variable in &free_variables { - if let Some(v) = ctx.scope.get_var(free_variable) { - captured.insert(free_variable.into(), v.clone()); - } - } - - Ok( - UntaggedValue::Block(Box::new(CapturedBlock::new(block.clone(), captured))) - .into_value(&tag), - ) - } - Expression::FullColumnPath(path) => { - let value = evaluate_baseline_expr(&path.head, ctx)?; - let mut item = value; - - for member in &path.tail { - let next = item.get_data_by_member(member); - - match next { - Err(err) => match &member.unspanned { - UnspannedPathMember::String(_name) => { - let possible_matches = did_you_mean(&item, member.as_string()); - - match possible_matches { - Some(p) => { - return Err(ShellError::labeled_error( - "Unknown column", - format!("did you mean '{}'?", p[0]), - &member.span, - )); - } - None => return Err(err), - } - } - UnspannedPathMember::Int(_row) => { - return Err(ShellError::labeled_error( - "Unknown row", - "unknown row", - &member.span, - )); - } - }, - Ok(next) => { - item = next.clone().value.into_value(&tag); - } - }; - } - - if path.tail.is_empty() { - Ok(item) - } else { - Ok(item.value.into_value(tag)) - } - } - Expression::Boolean(_boolean) => Ok(UntaggedValue::boolean(*_boolean).into_value(tag)), - Expression::Garbage => unimplemented!(), - } -} - -fn evaluate_literal(literal: &hir::Literal, span: Span) -> Value { - match &literal { - hir::Literal::ColumnPath(path) => { - let members = path.iter().map(|member| member.to_path_member()).collect(); - - UntaggedValue::Primitive(Primitive::ColumnPath(ColumnPath::new(members))) - .into_value(span) - } - hir::Literal::Number(int) => match int { - nu_protocol::hir::Number::BigInt(i) => { - UntaggedValue::big_int(i.clone()).into_value(span) - } - nu_protocol::hir::Number::Int(i) => UntaggedValue::int(*i).into_value(span), - nu_protocol::hir::Number::Decimal(d) => { - UntaggedValue::decimal(d.clone()).into_value(span) - } - }, - hir::Literal::Size(int, unit) => unit.compute(int).into_value(span), - hir::Literal::String(string) => UntaggedValue::string(string).into_value(span), - hir::Literal::GlobPattern(pattern) => UntaggedValue::glob_pattern(pattern).into_value(span), - hir::Literal::Bare(bare) => UntaggedValue::string(bare.clone()).into_value(span), - hir::Literal::Operator(_) => unimplemented!("Not sure what to do with operator yet"), - } -} - -pub enum Variable<'a> { - Nu, - Scope, - True, - False, - Nothing, - Other(&'a str), -} - -impl<'a> Variable<'a> { - pub fn list() -> Vec { - vec![ - String::from("$nu"), - String::from("$scope"), - String::from("$true"), - String::from("$false"), - String::from("$nothing"), - ] - } -} - -impl<'a> From<&'a Expression> for Variable<'a> { - fn from(expr: &'a Expression) -> Self { - match &expr { - Expression::Variable(name, _) => match name.as_str() { - "$nu" => Self::Nu, - "$scope" => Self::Scope, - "$true" => Self::True, - "$false" => Self::False, - "$nothing" => Self::Nothing, - _ => Self::Other(name), - }, - _ => unreachable!(), - } - } -} - -pub fn evaluate_reference( - variable: &Variable, - ctx: &EvaluationContext, - tag: impl Into, -) -> Result { - match variable { - Variable::Nu => crate::evaluate::variables::nu(&ctx.scope, ctx), - Variable::Scope => crate::evaluate::variables::scope( - &ctx.scope.get_aliases(), - &ctx.scope.get_commands(), - &ctx.scope.get_vars(), - ), - Variable::True => Ok(UntaggedValue::boolean(true).into_untagged_value()), - Variable::False => Ok(UntaggedValue::boolean(false).into_untagged_value()), - Variable::Nothing => Ok(UntaggedValue::nothing().into_untagged_value()), - Variable::Other(name) => match ctx.scope.get_var(name) { - Some(v) => Ok(v), - None => Err(ShellError::labeled_error( - "Variable not in scope", - format!("unknown variable: {}", name), - tag.into(), - )), - }, - } -} - -fn evaluate_subexpression( - block: &hir::Block, - ctx: &EvaluationContext, -) -> Result { - // FIXME: we should use a real context here - let input = match ctx.scope.get_var("$it") { - Some(it) => InputStream::one(it), - None => InputStream::empty(), - }; - - let result = run_block(block, ctx, input, hir::ExternalRedirection::Stdout)?; - - let output = result.into_vec(); - - if let Some(e) = ctx.get_errors().get(0) { - return Err(e.clone()); - } - - match output.len() { - x if x > 1 => { - let tag = output[0].tag.clone(); - Ok(UntaggedValue::Table(output).into_value(tag)) - } - 1 => Ok(output[0].clone()), - _ => Ok(UntaggedValue::nothing().into_value(Tag::unknown())), - } -} diff --git a/crates/nu-engine/src/evaluate/expr.rs b/crates/nu-engine/src/evaluate/expr.rs deleted file mode 100644 index 5fee9088ab..0000000000 --- a/crates/nu-engine/src/evaluate/expr.rs +++ /dev/null @@ -1,29 +0,0 @@ -use crate::evaluate_baseline_expr; - -use log::{log_enabled, trace}; - -use crate::evaluation_context::EvaluationContext; -use nu_errors::ShellError; -use nu_protocol::hir::SpannedExpression; -use nu_protocol::{UntaggedValue, Value}; -use nu_stream::{InputStream, IntoInputStream}; - -pub(crate) fn run_expression_block( - expr: &SpannedExpression, - ctx: &EvaluationContext, -) -> Result { - if log_enabled!(log::Level::Trace) { - trace!(target: "nu::run::expr", "->"); - trace!(target: "nu::run::expr", "{:?}", expr); - } - - let output = evaluate_baseline_expr(expr, ctx)?; - - match output { - Value { - value: UntaggedValue::Table(x), - .. - } => Ok(InputStream::from_stream(x.into_iter())), - output => Ok(std::iter::once(Ok(output)).into_input_stream()), - } -} diff --git a/crates/nu-engine/src/evaluate/internal.rs b/crates/nu-engine/src/evaluate/internal.rs deleted file mode 100644 index 7ec8978056..0000000000 --- a/crates/nu-engine/src/evaluate/internal.rs +++ /dev/null @@ -1,231 +0,0 @@ -use crate::call_info::UnevaluatedCallInfo; -use crate::evaluation_context::EvaluationContext; -use crate::filesystem::filesystem_shell::{FilesystemShell, FilesystemShellMode}; -use crate::shell::value_shell::ValueShell; -use crate::CommandArgs; -use log::{log_enabled, trace}; -use nu_errors::ShellError; -use nu_protocol::hir::{ - Expression, ExternalRedirection, InternalCommand, SpannedExpression, Synthetic, -}; -use nu_protocol::{CommandAction, ReturnSuccess, UntaggedValue, Value}; -use nu_source::{PrettyDebug, Span, Tag}; -use nu_stream::{ActionStream, InputStream}; - -pub(crate) fn run_internal_command( - command: &InternalCommand, - context: &EvaluationContext, - input: InputStream, -) -> Result { - if log_enabled!(log::Level::Trace) { - trace!(target: "nu::run::internal", "->"); - trace!(target: "nu::run::internal", "{}", command.name); - } - - let objects: InputStream = input; - - let internal_command = context.scope.expect_command(&command.name); - let internal_command = internal_command?; - - let result = context.run_command( - internal_command, - Tag::unknown_anchor(command.name_span), - command.args.clone(), // FIXME: this is inefficient - objects, - )?; - Ok(InputStream::from_stream(InternalIteratorSimple { - context: context.clone(), - input: result, - })) -} - -struct InternalIteratorSimple { - context: EvaluationContext, - input: InputStream, -} - -impl Iterator for InternalIteratorSimple { - type Item = Value; - - fn next(&mut self) -> Option { - match self.input.next() { - Some(Value { - value: UntaggedValue::Error(err), - .. - }) => { - self.context.error(err); - None - } - x => x, - } - } -} - -pub struct InternalIterator { - pub context: EvaluationContext, - pub leftovers: InputStream, - pub input: ActionStream, -} - -impl Iterator for InternalIterator { - type Item = Value; - - fn next(&mut self) -> Option { - if let Some(output) = self.leftovers.next() { - return Some(output); - } - - while let Some(item) = self.input.next() { - match item { - Ok(ReturnSuccess::Action(action)) => match action { - CommandAction::ChangePath(path) => { - self.context.shell_manager().set_path(path); - } - CommandAction::Exit(code) => std::process::exit(code), // TODO: save history.txt - CommandAction::Error(err) => { - self.context.error(err); - return None; - } - CommandAction::AutoConvert(tagged_contents, extension) => { - let contents_tag = tagged_contents.tag.clone(); - let command_name = format!("from {}", extension); - if let Some(converter) = self.context.scope.get_command(&command_name) { - let new_args = CommandArgs { - context: self.context.clone(), - call_info: UnevaluatedCallInfo { - args: nu_protocol::hir::Call { - head: Box::new(SpannedExpression { - expr: Expression::Synthetic(Synthetic::String( - command_name.clone(), - )), - span: tagged_contents.tag().span, - }), - positional: None, - named: None, - span: Span::unknown(), - external_redirection: ExternalRedirection::Stdout, - }, - name_tag: tagged_contents.tag(), - }, - input: InputStream::one(tagged_contents), - }; - let result = converter.run(new_args); - - match result { - Ok(mut result) => { - if let Some(x) = result.next() { - self.leftovers = - InputStream::from_stream(result.map(move |x| Value { - value: x.value, - tag: contents_tag.clone(), - })); - return Some(x); - } else { - return None; - } - } - Err(err) => { - self.leftovers = InputStream::empty(); - return Some(Value::error(err)); - } - } - } else { - return Some(tagged_contents); - } - } - CommandAction::EnterValueShell(value) => { - self.context - .shell_manager() - .insert_at_current(Box::new(ValueShell::new(value))); - } - CommandAction::EnterShell(location) => { - let mode = if self.context.shell_manager().is_interactive() { - FilesystemShellMode::Cli - } else { - FilesystemShellMode::Script - }; - self.context.shell_manager().insert_at_current(Box::new( - match FilesystemShell::with_location(location, mode) { - Ok(v) => v, - Err(err) => { - self.context.error(err.into()); - break; - } - }, - )); - } - CommandAction::AddPlugins(path) => { - match crate::plugin::build_plugin::scan(vec![std::path::PathBuf::from( - path, - )]) { - Ok(plugins) => { - self.context.add_commands( - plugins - .into_iter() - .filter(|p| !self.context.is_command_registered(p.name())) - .collect(), - ); - } - Err(reason) => { - self.context.error(reason); - } - } - } - CommandAction::PreviousShell => { - self.context.shell_manager().prev(); - } - CommandAction::NextShell => { - self.context.shell_manager().next(); - } - CommandAction::GotoShell(i) => { - self.context.shell_manager().goto(i); - } - CommandAction::LeaveShell(code) => { - self.context.shell_manager().remove_at_current(); - if self.context.shell_manager().is_empty() { - std::process::exit(code); // TODO: save history.txt - } - } - CommandAction::UnloadConfig(cfg_path) => { - self.context.unload_config(&cfg_path); - } - CommandAction::LoadConfig(cfg_path) => { - if let Err(e) = self.context.load_config(&cfg_path) { - return Some(UntaggedValue::Error(e).into_untagged_value()); - } - } - }, - - Ok(ReturnSuccess::Value(Value { - value: UntaggedValue::Error(err), - .. - })) => { - self.context.error(err); - return None; - } - - Ok(ReturnSuccess::Value(v)) => return Some(v), - - Ok(ReturnSuccess::DebugValue(v)) => { - let doc = PrettyDebug::pretty_doc(&v); - let mut buffer = termcolor::Buffer::ansi(); - - let _ = doc.render_raw( - self.context.with_host(|host| host.width() - 5), - &mut nu_source::TermColored::new(&mut buffer), - ); - - let value = String::from_utf8_lossy(buffer.as_slice()); - - return Some(UntaggedValue::string(value).into_untagged_value()); - } - - Err(err) => { - self.context.error(err); - } - } - } - - None - } -} diff --git a/crates/nu-engine/src/evaluate/lang.rs b/crates/nu-engine/src/evaluate/lang.rs deleted file mode 100644 index 8afe7ac1b1..0000000000 --- a/crates/nu-engine/src/evaluate/lang.rs +++ /dev/null @@ -1,209 +0,0 @@ -use crate::Scope; -use indexmap::IndexMap; -use nu_errors::ShellError; -use nu_protocol::{Dictionary, Signature, UntaggedValue, Value}; -use nu_source::Tag; - -pub struct Lang; - -impl Lang { - pub fn query_commands(scope: &Scope) -> Result, ShellError> { - let tag = Tag::unknown(); - let full_commands = scope.get_commands_info(); - let mut cmd_vec = Vec::new(); - for (key, cmd) in full_commands { - let mut indexmap = IndexMap::new(); - let mut sig = cmd.signature(); - // eprintln!("{}", get_signature(&sig)); - indexmap.insert( - "name".to_string(), - UntaggedValue::string(key).into_value(&tag), - ); - indexmap.insert( - "usage".to_string(), - UntaggedValue::string(cmd.usage().to_string()).into_value(&tag), - ); - // let sig_deser = serde_json::to_string(&sig).unwrap(); - // indexmap.insert( - // "signature".to_string(), - // UntaggedValue::string(sig_deser).into_value(&tag), - // ); - let signature_table = get_signature(&mut sig, tag.clone()); - indexmap.insert( - "signature".to_string(), - UntaggedValue::Table(signature_table).into_value(&tag), - ); - indexmap.insert( - "is_filter".to_string(), - UntaggedValue::boolean(sig.is_filter).into_value(&tag), - ); - indexmap.insert( - "is_builtin".to_string(), - UntaggedValue::boolean(cmd.is_builtin()).into_value(&tag), - ); - indexmap.insert( - "is_sub".to_string(), - UntaggedValue::boolean(cmd.is_sub()).into_value(&tag), - ); - indexmap.insert( - "is_plugin".to_string(), - UntaggedValue::boolean(cmd.is_plugin()).into_value(&tag), - ); - indexmap.insert( - "is_custom".to_string(), - UntaggedValue::boolean(cmd.is_custom()).into_value(&tag), - ); - indexmap.insert( - "is_private".to_string(), - UntaggedValue::boolean(cmd.is_private()).into_value(&tag), - ); - indexmap.insert( - "is_binary".to_string(), - UntaggedValue::boolean(cmd.is_binary()).into_value(&tag), - ); - indexmap.insert( - "extra_usage".to_string(), - UntaggedValue::string(cmd.extra_usage().to_string()).into_value(&tag), - ); - - cmd_vec.push(UntaggedValue::Row(Dictionary::from(indexmap)).into_value(&tag)); - } - - Ok(cmd_vec) - } -} - -fn get_signature(sig: &mut Signature, tag: Tag) -> Vec { - sig.remove_named("help"); - let p = &sig.positional; - let r = &sig.rest_positional; - let n = &sig.named; - let name = &sig.name; - let mut sig_vec: Vec = Vec::new(); - - for item in p { - let mut indexmap = IndexMap::new(); - - let (parameter, syntax_shape) = item.0.get_type_description(); - let description = &item.1; - // let output = format!( - // "Positional|{}|{}|{}|{}\n", - // name, parameter, syntax_shape, description - // ); - // eprintln!("{}", output); - - indexmap.insert( - "cmd_name".to_string(), - UntaggedValue::string(name).into_value(&tag), - ); - indexmap.insert( - "parameter_name".to_string(), - UntaggedValue::string(parameter).into_value(&tag), - ); - indexmap.insert( - "parameter_type".to_string(), - UntaggedValue::string("positional".to_string()).into_value(&tag), - ); - indexmap.insert( - "syntax_shape".to_string(), - UntaggedValue::string(syntax_shape).into_value(&tag), - ); - indexmap.insert( - "description".to_string(), - UntaggedValue::string(description).into_value(&tag), - ); - indexmap.insert( - "flag_name".to_string(), - UntaggedValue::string("".to_string()).into_value(&tag), - ); - indexmap.insert( - "flag_type".to_string(), - UntaggedValue::string("".to_string()).into_value(&tag), - ); - - sig_vec.push(UntaggedValue::Row(Dictionary::from(indexmap)).into_value(&tag)); - } - - match r { - Some((rest_name, shape, desc)) => { - let mut indexmap = IndexMap::new(); - // let output = format!("Rest|{}|{}|{}\n", name, shape.syntax_shape_name(), desc); - // eprintln!("{}", output); - - indexmap.insert( - "cmd_name".to_string(), - UntaggedValue::string(name).into_value(&tag), - ); - indexmap.insert( - "parameter_name".to_string(), - UntaggedValue::string(rest_name.to_string()).into_value(&tag), - ); - indexmap.insert( - "parameter_type".to_string(), - UntaggedValue::string("rest".to_string()).into_value(&tag), - ); - indexmap.insert( - "syntax_shape".to_string(), - UntaggedValue::string(shape.syntax_shape_name()).into_value(&tag), - ); - indexmap.insert( - "description".to_string(), - UntaggedValue::string(desc).into_value(&tag), - ); - indexmap.insert( - "flag_name".to_string(), - UntaggedValue::string("".to_string()).into_value(&tag), - ); - indexmap.insert( - "flag_type".to_string(), - UntaggedValue::string("".to_string()).into_value(&tag), - ); - - sig_vec.push(UntaggedValue::Row(Dictionary::from(indexmap)).into_value(&tag)); - } - None => {} - } - - for (parameter, (b, description)) in n { - let mut indexmap = IndexMap::new(); - - let (named_type, flag_name, shape) = b.get_type_description(); - // let output = format!( - // "Named|{}|{}|{}|{}|{}|{}\n", - // name, parameter, named_type, flag_name, shape, description - // ); - // eprint!("{}", output); - - indexmap.insert( - "cmd_name".to_string(), - UntaggedValue::string(name).into_value(&tag), - ); - indexmap.insert( - "parameter_name".to_string(), - UntaggedValue::string(parameter).into_value(&tag), - ); - indexmap.insert( - "parameter_type".to_string(), - UntaggedValue::string("named".to_string()).into_value(&tag), - ); - indexmap.insert( - "syntax_shape".to_string(), - UntaggedValue::string(shape).into_value(&tag), - ); - indexmap.insert( - "description".to_string(), - UntaggedValue::string(description).into_value(&tag), - ); - indexmap.insert( - "flag_name".to_string(), - UntaggedValue::string(flag_name).into_value(&tag), - ); - indexmap.insert( - "flag_type".to_string(), - UntaggedValue::string(named_type).into_value(&tag), - ); - sig_vec.push(UntaggedValue::Row(Dictionary::from(indexmap)).into_value(&tag)); - } - - sig_vec -} diff --git a/crates/nu-engine/src/evaluate/mod.rs b/crates/nu-engine/src/evaluate/mod.rs deleted file mode 100644 index 5bfc7fded9..0000000000 --- a/crates/nu-engine/src/evaluate/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub(crate) mod block; -pub(crate) mod envvar; -pub(crate) mod evaluate_args; -pub mod evaluator; -pub(crate) mod expr; -pub mod internal; -pub(crate) mod lang; -pub(crate) mod operator; -pub(crate) mod scope; -pub(crate) mod variables; diff --git a/crates/nu-engine/src/evaluate/operator.rs b/crates/nu-engine/src/evaluate/operator.rs deleted file mode 100644 index c6318382d0..0000000000 --- a/crates/nu-engine/src/evaluate/operator.rs +++ /dev/null @@ -1,111 +0,0 @@ -use nu_data::{value, value::compare_values}; -use nu_errors::ShellError; -use nu_protocol::hir::Operator; -use nu_protocol::{Primitive, ShellTypeName, UntaggedValue, Value}; -use std::ops::Not; - -#[cfg(feature = "dataframe")] -use nu_protocol::dataframe::{compute_between_dataframes, compute_series_single_value}; - -pub fn apply_operator( - op: Operator, - left: &Value, - right: &Value, -) -> Result { - #[cfg(feature = "dataframe")] - if let (UntaggedValue::DataFrame(_), UntaggedValue::DataFrame(_)) = (&left.value, &right.value) - { - return compute_between_dataframes(op, left, right); - } else if let (UntaggedValue::DataFrame(_), UntaggedValue::Primitive(_)) = - (&left.value, &right.value) - { - return compute_series_single_value(op, left, right); - } - - match op { - Operator::Equal - | Operator::NotEqual - | Operator::LessThan - | Operator::GreaterThan - | Operator::LessThanOrEqual - | Operator::GreaterThanOrEqual => { - value::compare_values(op, left, right).map(UntaggedValue::boolean) - } - Operator::Contains => string_contains(left, right).map(UntaggedValue::boolean), - Operator::NotContains => string_contains(left, right) - .map(Not::not) - .map(UntaggedValue::boolean), - 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::Pow => 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::Modulo => 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 => inside_of(left, right).map(UntaggedValue::boolean), - Operator::NotIn => inside_of(left, right).map(|x| UntaggedValue::boolean(!x)), - Operator::And => match (left.as_bool(), right.as_bool()) { - (Ok(left), Ok(right)) => Ok(UntaggedValue::boolean(left && right)), - _ => Err((left.type_name(), right.type_name())), - }, - Operator::Or => match (left.as_bool(), right.as_bool()) { - (Ok(left), Ok(right)) => Ok(UntaggedValue::boolean(left || right)), - _ => Err((left.type_name(), right.type_name())), - }, - } -} - -fn string_contains( - left: &UntaggedValue, - right: &UntaggedValue, -) -> Result { - match (left, right) { - ( - UntaggedValue::Primitive(Primitive::String(l)), - UntaggedValue::Primitive(Primitive::String(r)), - ) => Ok(l.contains(r)), - ( - UntaggedValue::Primitive(Primitive::FilePath(l)), - UntaggedValue::Primitive(Primitive::String(r)), - ) => Ok(l.as_path().display().to_string().contains(r)), - ( - UntaggedValue::Primitive(Primitive::String(l)), - UntaggedValue::Primitive(Primitive::FilePath(r)), - ) => Ok(l.contains(&r.as_path().display().to_string())), - _ => Err((left.type_name(), right.type_name())), - } -} - -fn inside_of( - left: &UntaggedValue, - right: &UntaggedValue, -) -> Result { - match (left, right) { - (_, UntaggedValue::Table(values)) => { - Ok(values - .iter() - .any(|x| match compare_values(Operator::Equal, left, &x.value) { - Ok(coerced) => coerced, - _ => false, - })) - } - ( - UntaggedValue::Primitive(Primitive::String(lhs)), - UntaggedValue::Primitive(Primitive::String(rhs)), - ) => Ok(rhs.contains(lhs)), - _ => Err((left.type_name(), right.type_name())), - } -} diff --git a/crates/nu-engine/src/evaluate/scope.rs b/crates/nu-engine/src/evaluate/scope.rs deleted file mode 100644 index 32b6dc472e..0000000000 --- a/crates/nu-engine/src/evaluate/scope.rs +++ /dev/null @@ -1,508 +0,0 @@ -use crate::{ - evaluate::envvar::EnvVar, - whole_stream_command::{whole_stream_command, Command}, -}; -use indexmap::IndexMap; -use nu_errors::ShellError; -use nu_parser::ParserScope; -use nu_protocol::{hir::Block, Signature, SignatureRegistry, Value}; -use nu_source::Spanned; -use std::sync::Arc; - -#[derive(Debug, Clone)] -pub struct Scope { - frames: Arc>>, -} - -impl Default for Scope { - fn default() -> Self { - Self::new() - } -} - -impl Scope { - pub fn new() -> Scope { - Scope { - frames: Arc::new(parking_lot::Mutex::new(vec![ScopeFrame::new()])), - } - } - - pub fn get_command(&self, name: &str) -> Option { - for frame in self.frames.lock().iter().rev() { - if let Some(command) = frame.get_command(name) { - return Some(command); - } - } - - None - } - - pub fn get_aliases(&self) -> IndexMap>> { - let mut output: IndexMap>> = IndexMap::new(); - - for frame in self.frames.lock().iter().rev() { - for v in &frame.aliases { - if !output.contains_key(v.0) { - output.insert(v.0.clone(), v.1.clone()); - } - } - } - - output.sorted_by(|k1, _v1, k2, _v2| k1.cmp(k2)).collect() - } - - pub fn get_commands(&self) -> IndexMap { - let mut output: IndexMap = IndexMap::new(); - - for frame in self.frames.lock().iter().rev() { - for (name, command) in &frame.commands { - if !output.contains_key(name) { - let mut sig = command.signature(); - // don't show --help and -h in the command arguments for $scope.commands - sig.remove_named("help"); - output.insert(name.clone(), sig); - } - } - } - - output.sorted_by(|k1, _v1, k2, _v2| k1.cmp(k2)).collect() - } - - pub fn get_commands_info(&self) -> IndexMap { - let mut output: IndexMap = IndexMap::new(); - - for frame in self.frames.lock().iter().rev() { - for (name, command) in &frame.commands { - if !output.contains_key(name) { - output.insert(name.clone(), command.clone()); - } - } - } - - output.sorted_by(|k1, _v1, k2, _v2| k1.cmp(k2)).collect() - } - - pub fn get_variable_names(&self) -> Vec { - self.get_vars().iter().map(|(k, _)| k.to_string()).collect() - } - - pub fn get_vars(&self) -> IndexMap { - //FIXME: should this be an iterator? - let mut output: IndexMap = IndexMap::new(); - - for frame in self.frames.lock().iter().rev() { - for v in &frame.vars { - if !output.contains_key(v.0) { - output.insert(v.0.clone(), v.1.clone()); - } - } - } - - output.sorted_by(|k1, _v1, k2, _v2| k1.cmp(k2)).collect() - } - - pub fn get_aliases_with_name(&self, name: &str) -> Option>>> { - let aliases: Vec<_> = self - .frames - .lock() - .iter() - .rev() - .filter_map(|frame| frame.aliases.get(name).cloned()) - .collect(); - if aliases.is_empty() { - None - } else { - Some(aliases) - } - } - - pub fn get_custom_commands_with_name(&self, name: &str) -> Option>> { - let custom_commands: Vec<_> = self - .frames - .lock() - .iter() - .rev() - .filter_map(|frame| frame.custom_commands.get(name).cloned()) - .collect(); - - if custom_commands.is_empty() { - None - } else { - Some(custom_commands) - } - } - - pub fn add_command(&self, name: String, command: Command) { - // Note: this is assumed to always be true, as there is always a global top frame - if let Some(frame) = self.frames.lock().last_mut() { - frame.add_command(name, command) - } - } - - pub fn get_alias_names(&self) -> Vec { - let mut names = vec![]; - - for frame in self.frames.lock().iter() { - let mut frame_command_names = frame.get_alias_names(); - names.append(&mut frame_command_names); - } - - // Sort needs to happen first because dedup works on consecutive dupes only - names.sort(); - names.dedup(); - - names - } - - pub fn get_command_names(&self) -> Vec { - let mut names = vec![]; - - for frame in self.frames.lock().iter() { - let mut frame_command_names = frame.get_command_names(); - frame_command_names.extend(frame.get_alias_names()); - frame_command_names.extend(frame.get_custom_command_names()); - names.append(&mut frame_command_names); - } - - // Sort needs to happen first because dedup works on consecutive dupes only - names.sort(); - names.dedup(); - - names - } - - pub fn len(&self) -> usize { - self.frames.lock().len() - } - - pub fn is_empty(&self) -> bool { - self.frames.lock().is_empty() - } - - fn has_cmd_helper(&self, name: &str, f: fn(&ScopeFrame, &str) -> bool) -> bool { - self.frames.lock().iter().any(|frame| f(frame, name)) - } - - pub fn has_command(&self, name: &str) -> bool { - self.has_cmd_helper(name, ScopeFrame::has_command) - } - - pub fn has_custom_command(&self, name: &str) -> bool { - self.has_cmd_helper(name, ScopeFrame::has_custom_command) - } - - pub fn has_alias(&self, name: &str) -> bool { - self.has_cmd_helper(name, ScopeFrame::has_alias) - } - - pub fn expect_command(&self, name: &str) -> Result { - if let Some(c) = self.get_command(name) { - Ok(c) - } else { - Err(ShellError::untagged_runtime_error(format!( - "Missing command '{}'", - name - ))) - } - } - - // This is used for starting processes, keep it string -> string - pub fn get_env_vars(&self) -> IndexMap { - //FIXME: should this be an iterator? - let mut output = IndexMap::new(); - - for frame in self.frames.lock().iter().rev() { - for v in &frame.env { - if !output.contains_key(v.0) { - output.insert(v.0.clone(), v.1.clone()); - } - } - } - - output - .into_iter() - .filter_map(|(k, v)| match v { - EnvVar::Proper(s) => Some((k, s)), - EnvVar::Nothing => None, - }) - .collect() - } - - pub fn get_env(&self, name: &str) -> Option { - for frame in self.frames.lock().iter().rev() { - if let Some(v) = frame.env.get(name) { - return match v { - EnvVar::Proper(string) => Some(string.clone()), - EnvVar::Nothing => None, - }; - } - } - - None - } - - pub fn get_var(&self, name: &str) -> Option { - for frame in self.frames.lock().iter().rev() { - if let Some(v) = frame.vars.get(name) { - return Some(v.clone()); - } - } - - None - } - - pub fn add_var(&self, name: impl Into, value: Value) { - if let Some(frame) = self.frames.lock().last_mut() { - frame.vars.insert(name.into(), value); - } - } - - pub fn add_vars(&self, vars: &IndexMap) { - if let Some(frame) = self.frames.lock().last_mut() { - frame - .vars - .extend(vars.iter().map(|(s, v)| (s.clone(), v.clone()))) - } - } - - pub fn add_env_var(&self, name: impl Into, value: impl Into) { - if let Some(frame) = self.frames.lock().last_mut() { - frame.env.insert(name.into(), value.into()); - } - } - - pub fn remove_env_var(&self, name: impl Into) -> Option { - if let Some(frame) = self.frames.lock().last_mut() { - if let Some(val) = frame.env.remove_entry(&name.into()) { - return Some(val.0); - } - } - None - } - - pub fn add_env(&self, env_vars: IndexMap) { - if let Some(frame) = self.frames.lock().last_mut() { - frame.env.extend(env_vars) - } - } - - pub fn add_env_to_base(&self, env_vars: IndexMap) { - if let Some(frame) = self.frames.lock().first_mut() { - frame.env.extend(env_vars) - } - } - - pub fn add_env_var_to_base(&self, name: impl Into, value: impl Into) { - if let Some(frame) = self.frames.lock().first_mut() { - frame.env.insert(name.into(), value.into()); - } - } - - pub fn set_exit_scripts(&self, scripts: Vec) { - if let Some(frame) = self.frames.lock().last_mut() { - frame.exitscripts = scripts - } - } - - pub fn enter_scope_with_tag(&self, tag: String) { - self.frames.lock().push(ScopeFrame::with_tag(tag)); - } - - //Removes the scopeframe with tag. - pub fn exit_scope_with_tag(&self, tag: &str) { - let mut frames = self.frames.lock(); - let tag = Some(tag); - if let Some(i) = frames.iter().rposition(|f| f.tag.as_deref() == tag) { - frames.remove(i); - } - } - - pub fn get_exitscripts_of_frame_with_tag(&self, tag: &str) -> Option> { - let frames = self.frames.lock(); - let tag = Some(tag); - frames.iter().find_map(|f| { - if f.tag.as_deref() == tag { - Some(f.exitscripts.clone()) - } else { - None - } - }) - } - - pub fn get_frame_with_tag(&self, tag: &str) -> Option { - let frames = self.frames.lock(); - let tag = Some(tag); - frames.iter().rev().find_map(|f| { - if f.tag.as_deref() == tag { - Some(f.clone()) - } else { - None - } - }) - } - - pub fn update_frame_with_tag(&self, frame: ScopeFrame, tag: &str) -> Result<(), ShellError> { - let mut frames = self.frames.lock(); - let tag = Some(tag); - for f in frames.iter_mut().rev() { - if f.tag.as_deref() == tag { - *f = frame; - return Ok(()); - } - } - - // Frame not found, return err - Err(ShellError::untagged_runtime_error(format!( - "Can't update frame with tag {:?}. No such frame present!", - tag - ))) - } -} - -impl SignatureRegistry for Scope { - fn names(&self) -> Vec { - self.get_command_names() - } - - fn has(&self, name: &str) -> bool { - self.get_signature(name).is_some() - } - - fn get(&self, name: &str) -> Option { - self.get_signature(name) - } - - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } -} - -impl ParserScope for Scope { - fn get_signature(&self, name: &str) -> Option { - self.get_command(name).map(|x| x.signature()) - } - - fn has_signature(&self, name: &str) -> bool { - self.get_command(name).is_some() - } - - fn add_definition(&self, block: Arc) { - if let Some(frame) = self.frames.lock().last_mut() { - let name = block.params.name.clone(); - frame.custom_commands.insert(name.clone(), block.clone()); - frame.commands.insert(name, whole_stream_command(block)); - } - } - - fn get_definitions(&self) -> Vec> { - let mut blocks = vec![]; - if let Some(frame) = self.frames.lock().last() { - for (_, custom_command) in &frame.custom_commands { - blocks.push(custom_command.clone()); - } - } - blocks - } - - fn get_alias(&self, name: &str) -> Option>> { - for frame in self.frames.lock().iter().rev() { - if let Some(x) = frame.aliases.get(name) { - return Some(x.clone()); - } - } - None - } - - fn add_alias(&self, name: &str, replacement: Vec>) { - // Note: this is assumed to always be true, as there is always a global top frame - if let Some(frame) = self.frames.lock().last_mut() { - frame.aliases.insert(name.to_string(), replacement); - } - } - - fn remove_alias(&self, name: &str) { - if let Some(frame) = self.frames.lock().last_mut() { - frame.aliases.remove(name); - } - } - - fn enter_scope(&self) { - self.frames.lock().push(ScopeFrame::new()); - } - - fn exit_scope(&self) { - self.frames.lock().pop(); - } -} - -/// An evaluation scope. Scopes map variable names to Values and aid in evaluating blocks and expressions. -#[derive(Debug, Clone)] -pub struct ScopeFrame { - pub vars: IndexMap, - pub env: IndexMap, - pub commands: IndexMap, - pub custom_commands: IndexMap>, - pub aliases: IndexMap>>, - ///Optional tag to better identify this scope frame later - pub tag: Option, - pub exitscripts: Vec, -} - -impl Default for ScopeFrame { - fn default() -> Self { - ScopeFrame::new() - } -} - -impl ScopeFrame { - pub fn has_command(&self, name: &str) -> bool { - self.commands.contains_key(name) - } - - pub fn has_custom_command(&self, name: &str) -> bool { - self.custom_commands.contains_key(name) - } - - pub fn has_alias(&self, name: &str) -> bool { - self.aliases.contains_key(name) - } - - pub fn get_alias_names(&self) -> Vec { - self.aliases.keys().map(|x| x.to_string()).collect() - } - - pub fn get_command_names(&self) -> Vec { - self.commands.keys().map(|x| x.to_string()).collect() - } - - pub fn get_custom_command_names(&self) -> Vec { - self.custom_commands.keys().map(|x| x.to_string()).collect() - } - - pub fn add_command(&mut self, name: String, command: Command) { - self.commands.insert(name, command); - } - - pub fn get_command(&self, name: &str) -> Option { - self.commands.get(name).cloned() - } - - pub fn new() -> ScopeFrame { - ScopeFrame { - vars: IndexMap::new(), - env: IndexMap::new(), - commands: IndexMap::new(), - custom_commands: IndexMap::new(), - aliases: IndexMap::new(), - tag: None, - exitscripts: Vec::new(), - } - } - - pub fn with_tag(tag: String) -> ScopeFrame { - let mut scope = ScopeFrame::new(); - scope.tag = Some(tag); - - scope - } -} diff --git a/crates/nu-engine/src/evaluate/variables.rs b/crates/nu-engine/src/evaluate/variables.rs deleted file mode 100644 index 492b4afd99..0000000000 --- a/crates/nu-engine/src/evaluate/variables.rs +++ /dev/null @@ -1,159 +0,0 @@ -use crate::{ - evaluate::{lang, scope::Scope}, - EvaluationContext, -}; -use indexmap::IndexMap; -use nu_data::config::path::{default_history_path, history_path}; -use nu_errors::ShellError; -use nu_protocol::{Dictionary, ShellTypeName, Signature, TaggedDictBuilder, UntaggedValue, Value}; -use nu_source::{Spanned, Tag}; - -pub fn nu(scope: &Scope, ctx: &EvaluationContext) -> Result { - let env = &scope.get_env_vars(); - let tag = Tag::unknown(); - - let mut nu_dict = TaggedDictBuilder::new(&tag); - - let mut dict = TaggedDictBuilder::new(&tag); - - for v in env { - if v.0 != "PATH" && v.0 != "Path" { - dict.insert_untagged(v.0, UntaggedValue::string(v.1)); - } - } - - nu_dict.insert_value("env", dict.into_value()); - - nu_dict.insert_value( - "history-path", - UntaggedValue::filepath(default_history_path()).into_value(&tag), - ); - - if let Some(global_cfg) = &ctx.configs().lock().global_config { - nu_dict.insert_value( - "config", - UntaggedValue::row(global_cfg.vars.clone()).into_value(&tag), - ); - - nu_dict.insert_value( - "config-path", - UntaggedValue::filepath(global_cfg.file_path.clone()).into_value(&tag), - ); - - // overwrite hist-path if present - if let Some(hist_path) = history_path(global_cfg) { - nu_dict.insert_value( - "history-path", - UntaggedValue::filepath(hist_path).into_value(&tag), - ); - } - } - - // A note about environment variables: - // - // Environment variables in Unix platforms are case-sensitive. On Windows, case-sensitivity is context-dependent. - // In cmd.exe, running `SET` will show you the list of environment variables and their names will be mixed case. - // In PowerShell, running `Get-ChildItem Env:` will show you a list of environment variables, and they will match - // the case in the environment variable section of the user configuration - // - // Rust currently returns the DOS-style, all-uppercase environment variables on Windows (as of 1.52) when running - // std::env::vars(), rather than the case-sensitive Environment.GetEnvironmentVariables() of .NET that PowerShell - // uses. - // - // For now, we work around the discrepancy as best we can by merging the two into what is shown to the user as the - // 'path' column of `$nu` - let mut table = vec![]; - for v in env { - if v.0 == "PATH" || v.0 == "Path" { - for path in std::env::split_paths(&v.1) { - table.push(UntaggedValue::filepath(path).into_value(&tag)); - } - } - } - nu_dict.insert_value("path", UntaggedValue::table(&table).into_value(&tag)); - - let path = std::env::current_dir()?; - nu_dict.insert_value("cwd", UntaggedValue::filepath(path).into_value(&tag)); - - if let Some(home) = crate::filesystem::filesystem_shell::homedir_if_possible() { - nu_dict.insert_value("home-dir", UntaggedValue::filepath(home).into_value(&tag)); - } - - let temp = std::env::temp_dir(); - nu_dict.insert_value("temp-dir", UntaggedValue::filepath(temp).into_value(&tag)); - - #[cfg(feature = "rustyline-support")] - { - let keybinding_path = nu_data::keybinding::keybinding_path()?; - nu_dict.insert_value( - "keybinding-path", - UntaggedValue::filepath(keybinding_path).into_value(&tag), - ); - } - - let cmd_info = lang::Lang::query_commands(scope); - match cmd_info { - Ok(cmds) => nu_dict.insert_value("lang", UntaggedValue::table(&cmds).into_value(&tag)), - Err(_) => nu_dict.insert_value("lang", UntaggedValue::string("no commands found")), - } - - Ok(nu_dict.into_value()) -} - -pub fn scope( - aliases: &IndexMap>>, - commands: &IndexMap, - variables: &IndexMap, -) -> Result { - let tag = Tag::unknown(); - - let mut scope_dict = TaggedDictBuilder::new(&tag); - - let mut aliases_dict = TaggedDictBuilder::new(&tag); - for v in aliases { - let values = v.1.clone(); - let mut vec = Vec::new(); - - for k in &values { - vec.push(k.to_string()); - } - - let alias = vec.join(" "); - - aliases_dict.insert_untagged(v.0, UntaggedValue::string(alias)); - } - - let mut commands_dict = TaggedDictBuilder::new(&tag); - for (name, signature) in commands { - commands_dict.insert_untagged(name, UntaggedValue::string(&signature.allowed().join(" "))) - } - - let var_list: Vec = variables - .iter() - .map(|var| { - let mut entries: IndexMap = IndexMap::new(); - let name = var.0.trim_start_matches('$'); - entries.insert( - "name".to_string(), - UntaggedValue::string(name).into_value(&tag), - ); - entries.insert( - "value".to_string(), - UntaggedValue::string(var.1.convert_to_string()).into_value(&tag), - ); - entries.insert( - "type".to_string(), - UntaggedValue::string(ShellTypeName::type_name(&var.1)).into_value(&tag), - ); - UntaggedValue::Row(Dictionary { entries }).into_value(&tag) - }) - .collect(); - - scope_dict.insert_value("aliases", aliases_dict.into_value()); - - scope_dict.insert_value("commands", commands_dict.into_value()); - - scope_dict.insert_value("variables", UntaggedValue::Table(var_list).into_value(&tag)); - - Ok(scope_dict.into_value()) -} diff --git a/crates/nu-engine/src/evaluation_context.rs b/crates/nu-engine/src/evaluation_context.rs deleted file mode 100644 index cab85f129b..0000000000 --- a/crates/nu-engine/src/evaluation_context.rs +++ /dev/null @@ -1,440 +0,0 @@ -use crate::evaluate::envvar::EnvVar; -use crate::evaluate::evaluator::Variable; -use crate::evaluate::scope::{Scope, ScopeFrame}; -use crate::shell::palette::ThemedPalette; -use crate::shell::shell_manager::ShellManager; -use crate::whole_stream_command::Command; -use crate::{call_info::UnevaluatedCallInfo, config_holder::ConfigHolder}; -use crate::{command_args::CommandArgs, script}; -use crate::{env::basic_host::BasicHost, Host}; - -use nu_data::config::{self, Conf, NuConfig}; -use nu_errors::ShellError; -use nu_path::expand_path; -use nu_protocol::{hir, ConfigPath, VariableRegistry}; -use nu_source::Spanned; -use nu_source::{Span, Tag}; -use nu_stream::InputStream; -use nu_test_support::NATIVE_PATH_ENV_VAR; - -use indexmap::IndexMap; -use log::trace; -use parking_lot::Mutex; -use std::fs::File; -use std::io::BufReader; -use std::sync::atomic::AtomicBool; -use std::{path::Path, sync::Arc}; - -#[derive(Clone, Default)] -pub struct EngineState { - pub host: Arc>>, - pub current_errors: Arc>>, - pub ctrl_c: Arc, - pub configs: Arc>, - pub shell_manager: ShellManager, - - /// Windows-specific: keep track of previous cwd on each drive - pub windows_drives_previous_cwd: Arc>>, -} -#[derive(Clone, Default)] -pub struct EvaluationContext { - pub scope: Scope, - pub engine_state: Arc, -} - -impl EvaluationContext { - pub fn new( - scope: Scope, - host: Arc>>, - current_errors: Arc>>, - ctrl_c: Arc, - configs: Arc>, - shell_manager: ShellManager, - windows_drives_previous_cwd: Arc>>, - ) -> Self { - Self { - scope, - engine_state: Arc::new(EngineState { - host, - current_errors, - ctrl_c, - configs, - shell_manager, - windows_drives_previous_cwd, - }), - } - } - - pub fn basic() -> EvaluationContext { - let scope = Scope::new(); - let host = BasicHost {}; - let env_vars: IndexMap = host - .vars() - .iter() - .cloned() - .map(|(k, v)| (k, v.into())) - .collect(); - scope.add_env(env_vars); - - EvaluationContext { - scope, - engine_state: Arc::new(EngineState { - host: Arc::new(parking_lot::Mutex::new(Box::new(host))), - current_errors: Arc::new(Mutex::new(vec![])), - ctrl_c: Arc::new(AtomicBool::new(false)), - configs: Arc::new(Mutex::new(ConfigHolder::new())), - shell_manager: ShellManager::basic(), - windows_drives_previous_cwd: Arc::new(Mutex::new(std::collections::HashMap::new())), - }), - } - } - - pub fn error(&self, error: ShellError) { - self.with_errors(|errors| errors.push(error)) - } - - pub fn host(&self) -> &Arc>> { - &self.engine_state.host - } - - pub fn current_errors(&self) -> &Arc>> { - &self.engine_state.current_errors - } - - pub fn ctrl_c(&self) -> &Arc { - &self.engine_state.ctrl_c - } - - pub fn configs(&self) -> &Arc> { - &self.engine_state.configs - } - - pub fn shell_manager(&self) -> &ShellManager { - &self.engine_state.shell_manager - } - - pub fn windows_drives_previous_cwd( - &self, - ) -> &Arc>> { - &self.engine_state.windows_drives_previous_cwd - } - - pub fn clear_errors(&self) { - self.engine_state.current_errors.lock().clear() - } - - pub fn get_errors(&self) -> Vec { - self.engine_state.current_errors.lock().clone() - } - - pub fn configure( - &mut self, - config: &dyn nu_data::config::Conf, - block: impl FnOnce(&dyn nu_data::config::Conf, &mut Self) -> T, - ) { - block(config, &mut *self); - } - - pub fn with_host(&self, block: impl FnOnce(&mut dyn Host) -> T) -> T { - let mut host = self.engine_state.host.lock(); - - block(&mut *host) - } - - pub fn with_errors(&self, block: impl FnOnce(&mut Vec) -> T) -> T { - let mut errors = self.engine_state.current_errors.lock(); - - block(&mut *errors) - } - - pub fn add_commands(&self, commands: Vec) { - for command in commands { - self.scope.add_command(command.name().to_string(), command); - } - } - - pub fn sync_path_to_env(&self) { - let env_vars = self.scope.get_env_vars(); - - for (var, val) in env_vars { - if var == NATIVE_PATH_ENV_VAR { - std::env::set_var(var, expand_path(val)); - break; - } - } - } - - pub fn get_command(&self, name: &str) -> Option { - self.scope.get_command(name) - } - - pub fn is_command_registered(&self, name: &str) -> bool { - self.scope.has_command(name) - } - - pub fn run_command( - &self, - command: Command, - name_tag: Tag, - args: hir::Call, - input: InputStream, - ) -> Result { - let command_args = self.command_args(args, input, name_tag); - command.run(command_args) - } - - fn call_info(&self, args: hir::Call, name_tag: Tag) -> UnevaluatedCallInfo { - UnevaluatedCallInfo { args, name_tag } - } - - fn command_args(&self, args: hir::Call, input: InputStream, name_tag: Tag) -> CommandArgs { - CommandArgs { - context: self.clone(), - call_info: self.call_info(args, name_tag), - input, - } - } - - /// Loads config under cfg_path. - /// If an error occurs while loading the config: - /// The config is not loaded - /// The error is returned - /// After successful loading of the config the startup scripts are run - /// as normal scripts (Errors are printed out, ...) - /// After executing the startup scripts, true is returned to indicate successful loading - /// of the config - // - // The rational here is that, we should not partially load any config - // that might be damaged. However, startup scripts might fail for various reasons. - // A failure there is not as crucial as wrong config files. - pub fn load_config(&self, cfg_path: &ConfigPath) -> Result<(), ShellError> { - trace!("Loading cfg {:?}", cfg_path); - - let cfg = NuConfig::load(Some(cfg_path.get_path().clone()))?; - let exit_scripts = cfg.exit_scripts()?; - let startup_scripts = cfg.startup_scripts()?; - let cfg_paths = cfg.path()?; - - let joined_paths = cfg_paths - .map(|mut cfg_paths| { - //existing paths are prepended to path - let env_paths = self.scope.get_env(NATIVE_PATH_ENV_VAR); - - if let Some(env_paths) = env_paths { - let mut env_paths = std::env::split_paths(&env_paths).collect::>(); - //No duplicates! Remove env_paths already existing in cfg_paths - env_paths.retain(|env_path| !cfg_paths.contains(env_path)); - //env_paths entries are appended at the end - //nu config paths have a higher priority - cfg_paths.extend(env_paths); - } - cfg_paths - }) - .map(|paths| { - std::env::join_paths(paths) - .map(|s| s.to_string_lossy().to_string()) - .map_err(|e| { - ShellError::labeled_error( - &format!("Error while joining paths from config: {:?}", e), - "Config path error", - Span::unknown(), - ) - }) - }) - .transpose()?; - - let tag = config::cfg_path_to_scope_tag(cfg_path.get_path()); - - self.scope.enter_scope_with_tag(tag); - let config_env = cfg.env_map(); - let env_vars = config_env - .into_iter() - .map(|(k, v)| (k, EnvVar::from(v))) - .collect(); - self.scope.add_env(env_vars); - if let Some(path) = joined_paths { - self.scope.add_env_var(NATIVE_PATH_ENV_VAR, path); - } - self.scope.set_exit_scripts(exit_scripts); - - match cfg_path { - ConfigPath::Global(_) => self.engine_state.configs.lock().set_global_cfg(cfg), - ConfigPath::Local(_) => { - self.engine_state.configs.lock().add_local_cfg(cfg); - } - } - - // The syntax_theme is really the file stem of a json file i.e. - // grape.json is the theme file and grape is the file stem and - // the syntax_theme and grape.json would be located in the same - // folder as the config.toml - - // Let's open the config - let global_config = self.engine_state.configs.lock().global_config(); - // Get the root syntax_theme value - let syntax_theme = global_config.var("syntax_theme"); - // If we have a syntax_theme let's process it - if let Some(theme_value) = syntax_theme { - // Append the .json to the syntax_theme to form the file name - let syntax_theme_filename = format!("{}.json", theme_value.convert_to_string()); - // Load the syntax config json - let config_file_path = cfg_path.get_path(); - // The syntax file should be in the same location as the config.toml - let syntax_file_path = if config_file_path.ends_with("config.toml") { - config_file_path - .display() - .to_string() - .replace("config.toml", &syntax_theme_filename) - } else { - "".to_string() - }; - // if we have a syntax_file_path use it otherwise default - if Path::new(&syntax_file_path).exists() { - // eprintln!("Loading syntax file: [{:?}]", syntax_file_path); - let syntax_theme_file = File::open(syntax_file_path)?; - let mut reader = BufReader::new(syntax_theme_file); - let theme = ThemedPalette::new(&mut reader).unwrap_or_default(); - // eprintln!("Theme: [{:?}]", theme); - self.engine_state.configs.lock().set_syntax_colors(theme); - } else { - // If the file was missing, use the default - self.engine_state - .configs - .lock() - .set_syntax_colors(ThemedPalette::default()) - } - } else { - // if there's no syntax_theme, use the default - self.engine_state - .configs - .lock() - .set_syntax_colors(ThemedPalette::default()) - }; - - if !startup_scripts.is_empty() { - self.run_scripts(startup_scripts, cfg_path.get_path().parent()); - } - - Ok(()) - } - - /// Reloads config with a path of cfg_path. - /// If an error occurs while reloading the config: - /// The config is not reloaded - /// The error is returned - pub fn reload_config(&self, cfg: &mut NuConfig) -> Result<(), ShellError> { - trace!("Reloading cfg {:?}", cfg.file_path); - - cfg.reload(); - - let exit_scripts = cfg.exit_scripts()?; - let cfg_paths = cfg.path()?; - - let joined_paths = cfg_paths - .map(|mut cfg_paths| { - //existing paths are prepended to path - let env_paths = self.scope.get_env(NATIVE_PATH_ENV_VAR); - - if let Some(env_paths) = env_paths { - let mut env_paths = std::env::split_paths(&env_paths).collect::>(); - //No duplicates! Remove env_paths already existing in cfg_paths - env_paths.retain(|env_path| !cfg_paths.contains(env_path)); - //env_paths entries are appended at the end - //nu config paths have a higher priority - cfg_paths.extend(env_paths); - } - cfg_paths - }) - .map(|paths| { - std::env::join_paths(paths) - .map(|s| s.to_string_lossy().to_string()) - .map_err(|e| { - ShellError::labeled_error( - &format!("Error while joining paths from config: {:?}", e), - "Config path error", - Span::unknown(), - ) - }) - }) - .transpose()?; - - let tag = config::cfg_path_to_scope_tag(&cfg.file_path); - let mut frame = ScopeFrame::with_tag(tag.clone()); - let config_env = cfg.env_map(); - let env_vars = config_env - .into_iter() - .map(|(k, v)| (k, EnvVar::from(v))) - .collect(); - frame.env = env_vars; - if let Some(path) = joined_paths { - frame - .env - .insert(NATIVE_PATH_ENV_VAR.to_string(), path.into()); - } - frame.exitscripts = exit_scripts; - - self.scope.update_frame_with_tag(frame, &tag)?; - - Ok(()) - } - - /// Runs all exit_scripts before unloading the config with path of cfg_path - /// If an error occurs while running exit scripts: - /// The error is added to `self.current_errors` - /// If no config with path of `cfg_path` is present, this method does nothing - pub fn unload_config(&self, cfg_path: &ConfigPath) { - trace!("UnLoading cfg {:?}", cfg_path); - - let tag = config::cfg_path_to_scope_tag(cfg_path.get_path()); - - //Run exitscripts with scope frame and cfg still applied - if let Some(scripts) = self.scope.get_exitscripts_of_frame_with_tag(&tag) { - self.run_scripts(scripts, cfg_path.get_path().parent()); - } - - //Unload config - self.engine_state.configs.lock().remove_cfg(cfg_path); - self.scope.exit_scope_with_tag(&tag); - } - - /// Runs scripts with cwd of dir. If dir is None, this method does nothing. - /// Each error is added to `self.current_errors` - pub fn run_scripts(&self, scripts: Vec, dir: Option<&Path>) { - if let Some(dir) = dir { - for script in scripts { - match script::run_script_in_dir(script.clone(), dir, self) { - Ok(_) => {} - Err(e) => { - let err = ShellError::untagged_runtime_error(format!( - "Err while executing exitscript. Err was\n{:?}", - e - )); - let text = script.into(); - self.engine_state.host.lock().print_err(err, &text); - } - } - } - } - } -} - -use itertools::Itertools; - -impl VariableRegistry for EvaluationContext { - fn get_variable(&self, name: &Spanned<&str>) -> Option { - let span = name.span; - let name = nu_protocol::hir::Expression::variable(name.item.to_string(), name.span); - - let var = Variable::from(&name); - - crate::evaluate::evaluator::evaluate_reference(&var, self, span).ok() - } - - fn variables(&self) -> Vec { - Variable::list() - .into_iter() - .chain(self.scope.get_variable_names()) - .unique() - .collect() - } -} diff --git a/crates/nu-engine/src/example.rs b/crates/nu-engine/src/example.rs deleted file mode 100644 index 41a93ea5a6..0000000000 --- a/crates/nu-engine/src/example.rs +++ /dev/null @@ -1,7 +0,0 @@ -use nu_protocol::Value; - -pub struct Example { - pub example: &'static str, - pub description: &'static str, - pub result: Option>, -} diff --git a/crates/nu-engine/src/filesystem/dir_info.rs b/crates/nu-engine/src/filesystem/dir_info.rs deleted file mode 100644 index da661b88f1..0000000000 --- a/crates/nu-engine/src/filesystem/dir_info.rs +++ /dev/null @@ -1,275 +0,0 @@ -use filesize::file_real_size_fast; -use glob::Pattern; -use indexmap::IndexMap; -use nu_errors::ShellError; -use nu_protocol::{UntaggedValue, Value}; -use nu_source::Tag; -use std::path::PathBuf; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; - -pub struct DirBuilder { - pub tag: Tag, - pub min: Option, - pub deref: bool, - pub exclude: Option, - pub all: bool, -} - -impl DirBuilder { - pub fn new( - tag: Tag, - min: Option, - deref: bool, - exclude: Option, - all: bool, - ) -> DirBuilder { - DirBuilder { - tag, - min, - deref, - exclude, - all, - } - } -} - -pub struct DirInfo { - dirs: Vec, - files: Vec, - errors: Vec, - size: u64, - blocks: u64, - path: PathBuf, - tag: Tag, -} - -pub struct FileInfo { - path: PathBuf, - size: u64, - blocks: Option, - tag: Tag, -} - -impl FileInfo { - pub fn new(path: impl Into, deref: bool, tag: Tag) -> Result { - let path = path.into(); - let m = if deref { - std::fs::metadata(&path) - } else { - std::fs::symlink_metadata(&path) - }; - - match m { - Ok(d) => { - let block_size = file_real_size_fast(&path, &d).ok(); - - Ok(FileInfo { - path, - blocks: block_size, - size: d.len(), - tag, - }) - } - Err(e) => Err(e.into()), - } - } -} - -impl DirInfo { - pub fn new( - path: impl Into, - params: &DirBuilder, - depth: Option, - ctrl_c: Arc, - ) -> Self { - let path = path.into(); - - let mut s = Self { - dirs: Vec::new(), - errors: Vec::new(), - files: Vec::new(), - size: 0, - blocks: 0, - tag: params.tag.clone(), - path, - }; - - match std::fs::metadata(&s.path) { - Ok(d) => { - s.size = d.len(); // dir entry size - s.blocks = file_real_size_fast(&s.path, &d).ok().unwrap_or(0); - } - Err(e) => s = s.add_error(e.into()), - }; - - match std::fs::read_dir(&s.path) { - Ok(d) => { - for f in d { - if ctrl_c.load(Ordering::SeqCst) { - break; - } - - match f { - Ok(i) => match i.file_type() { - Ok(t) if t.is_dir() => { - s = s.add_dir(i.path(), depth, params, ctrl_c.clone()) - } - Ok(_t) => s = s.add_file(i.path(), params), - Err(e) => s = s.add_error(e.into()), - }, - Err(e) => s = s.add_error(e.into()), - } - } - } - Err(e) => s = s.add_error(e.into()), - } - s - } - - fn add_dir( - mut self, - path: impl Into, - mut depth: Option, - params: &DirBuilder, - ctrl_c: Arc, - ) -> Self { - if let Some(current) = depth { - if let Some(new) = current.checked_sub(1) { - depth = Some(new); - } else { - return self; - } - } - - let d = DirInfo::new(path, params, depth, ctrl_c); - self.size += d.size; - self.blocks += d.blocks; - self.dirs.push(d); - self - } - - fn add_file(mut self, f: impl Into, params: &DirBuilder) -> Self { - let f = f.into(); - let include = params - .exclude - .as_ref() - .map_or(true, |x| !x.matches_path(&f)); - if include { - match FileInfo::new(f, params.deref, self.tag.clone()) { - Ok(file) => { - let inc = params.min.map_or(true, |s| file.size >= s); - if inc { - self.size += file.size; - self.blocks += file.blocks.unwrap_or(0); - if params.all { - self.files.push(file); - } - } - } - Err(e) => self = self.add_error(e), - } - } - self - } - - fn add_error(mut self, e: ShellError) -> Self { - self.errors.push(e); - self - } - - pub fn get_size(&self) -> u64 { - self.size - } -} - -impl From for Value { - fn from(d: DirInfo) -> Self { - let mut r: IndexMap = IndexMap::new(); - - r.insert( - "path".to_string(), - UntaggedValue::filepath(d.path).into_value(&d.tag), - ); - - r.insert( - "apparent".to_string(), - UntaggedValue::filesize(d.size).into_value(&d.tag), - ); - - r.insert( - "physical".to_string(), - UntaggedValue::filesize(d.blocks).into_value(&d.tag), - ); - - r.insert("directories".to_string(), value_from_vec(d.dirs, &d.tag)); - - r.insert("files".to_string(), value_from_vec(d.files, &d.tag)); - - if !d.errors.is_empty() { - let v = UntaggedValue::Table( - d.errors - .into_iter() - .map(move |e| UntaggedValue::Error(e).into_untagged_value()) - .collect::>(), - ) - .into_value(&d.tag); - - r.insert("errors".to_string(), v); - } - - Value { - value: UntaggedValue::row(r), - tag: d.tag, - } - } -} - -impl From for Value { - fn from(f: FileInfo) -> Self { - let mut r: IndexMap = IndexMap::new(); - - r.insert( - "path".to_string(), - UntaggedValue::filepath(f.path).into_value(&f.tag), - ); - - r.insert( - "apparent".to_string(), - UntaggedValue::filesize(f.size).into_value(&f.tag), - ); - - let b = f - .blocks - .map(UntaggedValue::filesize) - .unwrap_or_else(UntaggedValue::nothing) - .into_value(&f.tag); - - r.insert("physical".to_string(), b); - - r.insert( - "directories".to_string(), - UntaggedValue::nothing().into_value(&f.tag), - ); - - r.insert( - "files".to_string(), - UntaggedValue::nothing().into_value(&f.tag), - ); - - UntaggedValue::row(r).into_value(&f.tag) - } -} - -fn value_from_vec(vec: Vec, tag: &Tag) -> Value -where - V: Into, -{ - if vec.is_empty() { - UntaggedValue::nothing() - } else { - let values = vec.into_iter().map(Into::into).collect::>(); - UntaggedValue::Table(values) - } - .into_value(tag) -} diff --git a/crates/nu-engine/src/filesystem/filesystem_shell.rs b/crates/nu-engine/src/filesystem/filesystem_shell.rs deleted file mode 100644 index f221553ae3..0000000000 --- a/crates/nu-engine/src/filesystem/filesystem_shell.rs +++ /dev/null @@ -1,1234 +0,0 @@ -use crate::filesystem::utils::FileStructure; -use crate::maybe_text_codec::{MaybeTextCodec, StringOrBinary}; -use crate::shell::shell_args::{CdArgs, CopyArgs, LsArgs, MkdirArgs, MvArgs, RemoveArgs}; -use crate::shell::Shell; -use crate::BufCodecReader; -use crate::{ - filesystem::dir_info::{DirBuilder, DirInfo}, - CommandArgs, -}; -use encoding_rs::Encoding; -use nu_data::config::LocalConfigDiff; -use nu_path::{canonicalize, canonicalize_with, expand_path_with}; -use nu_protocol::{CommandAction, ConfigPath, TaggedDictBuilder, Value}; -use nu_source::{Span, Tag}; -use nu_stream::{ActionStream, Interruptible, IntoActionStream, OutputStream}; -use std::collections::VecDeque; -use std::fs::OpenOptions; -use std::io::{ErrorKind, Write}; -use std::path::{Path, PathBuf}; -use std::sync::atomic::AtomicBool; -use std::sync::Arc; -use std::{collections::HashMap, io::BufReader}; - -#[cfg(unix)] -use std::os::unix::fs::PermissionsExt; - -use nu_errors::ShellError; -use nu_protocol::{Primitive, ReturnSuccess, UntaggedValue}; -use nu_source::Tagged; - -const GLOB_PARAMS: glob::MatchOptions = glob::MatchOptions { - case_sensitive: true, - require_literal_separator: false, - require_literal_leading_dot: false, -}; - -#[derive(Eq, PartialEq, Clone, Copy)] -pub enum FilesystemShellMode { - Cli, - Script, -} - -pub struct FilesystemShell { - pub(crate) path: String, - pub(crate) last_path: String, - pub(crate) mode: FilesystemShellMode, -} - -impl std::fmt::Debug for FilesystemShell { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "FilesystemShell @ {}", self.path) - } -} - -impl Clone for FilesystemShell { - fn clone(&self) -> Self { - FilesystemShell { - path: self.path.clone(), - last_path: self.path.clone(), - mode: self.mode, - } - } -} - -impl FilesystemShell { - fn is_cli(&self) -> bool { - matches!(&self.mode, FilesystemShellMode::Cli) - } - - pub fn basic(mode: FilesystemShellMode) -> FilesystemShell { - let path = match std::env::current_dir() { - Ok(path) => path, - Err(_) => PathBuf::from("/"), - }; - - FilesystemShell { - path: path.to_string_lossy().to_string(), - last_path: path.to_string_lossy().to_string(), - mode, - } - } - - pub fn with_location( - path: String, - mode: FilesystemShellMode, - ) -> Result { - let path = canonicalize_with(&path, std::env::current_dir()?)?; - let path = path.display().to_string(); - let last_path = path.clone(); - - Ok(FilesystemShell { - path, - last_path, - mode, - }) - } -} - -pub fn homedir_if_possible() -> Option { - dirs_next::home_dir() -} - -impl Shell for FilesystemShell { - fn name(&self) -> String { - "filesystem".to_string() - } - - fn homedir(&self) -> Option { - homedir_if_possible() - } - - fn ls( - &self, - LsArgs { - path, - all, - long, - short_names, - du, - }: LsArgs, - name_tag: Tag, - ctrl_c: Arc, - ) -> Result { - let ctrl_c_copy = ctrl_c.clone(); - let (path, p_tag) = match path { - Some(p) => { - let p_tag = p.tag; - let mut p = p.item; - if p.is_dir() { - if permission_denied(&p) { - #[cfg(unix)] - let error_msg = format!( - "The permissions of {:o} do not allow access for this user", - p.metadata() - .expect( - "this shouldn't be called since we already know there is a dir" - ) - .permissions() - .mode() - & 0o0777 - ); - #[cfg(not(unix))] - let error_msg = String::from("Permission denied"); - return Err(ShellError::labeled_error( - "Permission denied", - error_msg, - &p_tag, - )); - } - if is_empty_dir(&p) { - return Ok(ActionStream::empty()); - } - p.push("*"); - } - (p, p_tag) - } - None => { - if is_empty_dir(&self.path()) { - return Ok(ActionStream::empty()); - } else { - (PathBuf::from("./*"), name_tag.clone()) - } - } - }; - - let hidden_dir_specified = is_hidden_dir(&path); - - let mut paths = glob::glob_with(&path.to_string_lossy(), GLOB_PARAMS) - .map_err(|e| ShellError::labeled_error(e.to_string(), "invalid pattern", &p_tag))? - .peekable(); - - if paths.peek().is_none() { - return Err(ShellError::labeled_error( - "No matches found", - "no matches found", - &p_tag, - )); - } - - let mut hidden_dirs = vec![]; - - // Generated stream: impl Stream - - Ok(paths - .filter_map(move |path| { - let path = match path.map_err(|e| ShellError::from(e.into_error())) { - Ok(path) => path, - Err(err) => return Some(Err(err)), - }; - - if path_contains_hidden_folder(&path, &hidden_dirs) { - return None; - } - - if !all && !hidden_dir_specified && is_hidden_dir(&path) { - if path.is_dir() { - hidden_dirs.push(path); - } - return None; - } - - let metadata = match std::fs::symlink_metadata(&path) { - Ok(metadata) => Some(metadata), - Err(e) => { - if e.kind() == ErrorKind::PermissionDenied || e.kind() == ErrorKind::Other { - None - } else { - return Some(Err(e.into())); - } - } - }; - - let entry = dir_entry_dict( - &path, - metadata.as_ref(), - name_tag.clone(), - long, - short_names, - du, - ctrl_c.clone(), - ) - .map(ReturnSuccess::Value); - - Some(entry) - }) - .interruptible(ctrl_c_copy) - .into_action_stream()) - } - - fn cd(&self, args: CdArgs, name: Tag) -> Result { - let path = match args.path { - None => match homedir_if_possible() { - Some(o) => o, - _ => { - return Err(ShellError::labeled_error( - "Cannot change to home directory", - "cannot go to home", - &name, - )) - } - }, - Some(v) => { - let Tagged { item: target, tag } = v; - if target == Path::new("-") { - PathBuf::from(&self.last_path) - } else { - // Extra expand attempt allows cd from /home/user/non-existent-dir/.. - // to /home/user - let path = match canonicalize_with(&target, self.path()) { - Ok(p) => p, - _ => expand_path_with(&target, self.path()), - }; - - if !path.exists() { - return Err(ShellError::labeled_error( - "Cannot change to directory", - "directory not found", - &tag, - )); - } - - if !path.is_dir() { - return Err(ShellError::labeled_error( - "Cannot change to directory", - "is not a directory", - &tag, - )); - } - - #[cfg(unix)] - { - let has_exec = path - .metadata() - .map(|m| { - umask::Mode::from(m.permissions().mode()).has(umask::USER_READ) - }) - .map_err(|e| { - ShellError::labeled_error( - "Cannot change to directory", - format!("cannot stat ({})", e), - &tag, - ) - })?; - - if !has_exec { - return Err(ShellError::labeled_error( - "Cannot change to directory", - "permission denied", - &tag, - )); - } - } - - path - } - } - }; - - let mut stream = VecDeque::new(); - - stream.push_back(ReturnSuccess::change_cwd( - path.to_string_lossy().to_string(), - )); - - //Loading local configs in script mode, makes scripts behave different on different - //filesystems and might therefore surprise users. That's why we only load them in cli mode. - if self.is_cli() { - match canonicalize(self.path()) { - Err(e) => { - let err = ShellError::untagged_runtime_error(format!( - "Could not get absolute path from current fs shell. The error was: {:?}", - e - )); - stream.push_back(ReturnSuccess::value( - UntaggedValue::Error(err).into_value(Tag::unknown()), - )); - } - Ok(current_pwd) => { - let (changes, errs) = LocalConfigDiff::between(current_pwd, path); - - for err in errs { - stream.push_back(ReturnSuccess::value( - UntaggedValue::Error(err).into_value(Tag::unknown()), - )); - } - - for unload_cfg in changes.cfgs_to_unload { - stream.push_back(ReturnSuccess::action(CommandAction::UnloadConfig( - ConfigPath::Local(unload_cfg), - ))); - } - - for load_cfg in changes.cfgs_to_load { - stream.push_back(ReturnSuccess::action(CommandAction::LoadConfig( - ConfigPath::Local(load_cfg), - ))); - } - } - }; - } - - Ok(stream.into()) - } - - fn cp( - &self, - CopyArgs { - src, - dst, - recursive, - }: CopyArgs, - name: Tag, - path: &str, - ) -> Result { - let name_tag = name; - - let path = Path::new(path); - let source = path.join(&src.item); - let destination = path.join(&dst.item); - - let sources: Vec<_> = match glob::glob_with(&source.to_string_lossy(), GLOB_PARAMS) { - Ok(files) => files.collect(), - Err(e) => { - return Err(ShellError::labeled_error( - e.to_string(), - "invalid pattern", - src.tag, - )) - } - }; - - if sources.is_empty() { - return Err(ShellError::labeled_error( - "No matches found", - "no matches found", - src.tag, - )); - } - - if sources.len() > 1 && !destination.is_dir() { - return Err(ShellError::labeled_error( - "Destination must be a directory when copying multiple files", - "is not a directory", - dst.tag, - )); - } - - let any_source_is_dir = sources.iter().any(|f| matches!(f, Ok(f) if f.is_dir())); - - if any_source_is_dir && !recursive { - return Err(ShellError::labeled_error( - "Directories must be copied using \"--recursive\"", - "resolves to a directory (not copied)", - src.tag, - )); - } - - for entry in sources.into_iter().flatten() { - let mut sources = FileStructure::new(); - sources.walk_decorate(&entry)?; - - if entry.is_file() { - let sources = sources.paths_applying_with(|(source_file, _depth_level)| { - if destination.is_dir() { - let mut dest = canonicalize_with(&dst.item, &path)?; - if let Some(name) = entry.file_name() { - dest.push(name); - } - Ok((source_file, dest)) - } else { - Ok((source_file, destination.clone())) - } - })?; - - for (src, dst) in sources { - if src.is_file() { - std::fs::copy(src, dst).map_err(|e| { - ShellError::labeled_error(e.to_string(), e.to_string(), &name_tag) - })?; - } - } - } else if entry.is_dir() { - let destination = if !destination.exists() { - destination.clone() - } else { - match entry.file_name() { - Some(name) => destination.join(name), - None => { - return Err(ShellError::labeled_error( - "Copy aborted. Not a valid path", - "not a valid path", - dst.tag, - )) - } - } - }; - - std::fs::create_dir_all(&destination).map_err(|e| { - ShellError::labeled_error(e.to_string(), e.to_string(), &dst.tag) - })?; - - let sources = sources.paths_applying_with(|(source_file, depth_level)| { - let mut dest = destination.clone(); - let path = canonicalize_with(&source_file, &path)?; - - let comps: Vec<_> = path - .components() - .map(|fragment| fragment.as_os_str()) - .rev() - .take(1 + depth_level) - .collect(); - - for fragment in comps.into_iter().rev() { - dest.push(fragment); - } - - Ok((PathBuf::from(&source_file), dest)) - })?; - - let dst_tag = &dst.tag; - for (src, dst) in sources { - if src.is_dir() && !dst.exists() { - std::fs::create_dir_all(&dst).map_err(|e| { - ShellError::labeled_error(e.to_string(), e.to_string(), dst_tag) - })?; - } - - if src.is_file() { - std::fs::copy(&src, &dst).map_err(|e| { - ShellError::labeled_error(e.to_string(), e.to_string(), &name_tag) - })?; - } - } - } - } - - Ok(ActionStream::empty()) - } - - fn mkdir( - &self, - MkdirArgs { - rest: directories, - show_created_paths, - }: MkdirArgs, - name: Tag, - path: &str, - ) -> Result { - let path = Path::new(path); - let mut stream = VecDeque::new(); - - if directories.is_empty() { - return Err(ShellError::labeled_error( - "mkdir requires directory paths", - "needs parameter", - name, - )); - } - - for dir in &directories { - let create_at = path.join(&dir.item); - - let dir_res = std::fs::create_dir_all(&create_at); - if let Err(reason) = dir_res { - return Err(ShellError::labeled_error( - reason.to_string(), - reason.to_string(), - dir.tag(), - )); - } - if show_created_paths { - let val = format!("{:}", create_at.to_string_lossy()).into(); - stream.push_back(val); - } - } - - Ok(stream.into()) - } - - fn mv( - &self, - MvArgs { src, dst }: MvArgs, - _name: Tag, - path: &str, - ) -> Result { - let path = Path::new(path); - let source = path.join(&src.item); - let destination = path.join(&dst.item); - - let mut sources = glob::glob_with(&source.to_string_lossy(), GLOB_PARAMS) - .map_or_else(|_| Vec::new(), Iterator::collect); - - if sources.is_empty() { - return Err(ShellError::labeled_error( - "Invalid file or pattern", - "invalid file or pattern", - src.tag, - )); - } - - // We have two possibilities. - // - // First, the destination exists. - // - If a directory, move everything into that directory, otherwise - // - if only a single source, overwrite the file, otherwise - // - error. - // - // Second, the destination doesn't exist, so we can only rename a single source. Otherwise - // it's an error. - - if (destination.exists() && !destination.is_dir() && sources.len() > 1) - || (!destination.exists() && sources.len() > 1) - { - return Err(ShellError::labeled_error( - "Can only move multiple sources if destination is a directory", - "destination must be a directory when multiple sources", - dst.tag, - )); - } - - let some_if_source_is_destination = sources - .iter() - .find(|f| matches!(f, Ok(f) if destination.starts_with(f))); - if destination.exists() && destination.is_dir() && sources.len() == 1 { - if let Some(Ok(filename)) = some_if_source_is_destination { - return Err(ShellError::labeled_error( - format!( - "Not possible to move {:?} to itself", - filename.file_name().expect("Invalid file name") - ), - "cannot move to itself", - dst.tag, - )); - } - } - - if let Some(Ok(_filename)) = some_if_source_is_destination { - sources = sources - .into_iter() - .filter(|f| matches!(f, Ok(f) if !destination.starts_with(f))) - .collect(); - } - - for entry in sources.into_iter().flatten() { - move_file( - TaggedPathBuf(&entry, &src.tag), - TaggedPathBuf(&destination, &dst.tag), - )? - } - - Ok(ActionStream::empty()) - } - - fn rm( - &self, - RemoveArgs { - rest: targets, - recursive, - trash: _trash, - permanent: _permanent, - force: _force, - }: RemoveArgs, - name: Tag, - path: &str, - ) -> Result { - let rm_always_trash = nu_data::config::config(Tag::unknown())? - .get("rm_always_trash") - .map(|val| val.is_true()) - .unwrap_or(false); - - #[cfg(not(feature = "trash-support"))] - { - if rm_always_trash { - return Err(ShellError::untagged_runtime_error( - "Cannot execute `rm`; the current configuration specifies \ - `rm_always_trash = true`, but the current nu executable was not \ - built with feature `trash_support`.", - )); - } else if _trash { - return Err(ShellError::labeled_error( - "Cannot execute `rm` with option `--trash`; feature `trash-support` not enabled", - "this option is only available if nu is built with the `trash-support` feature", - name - )); - } - } - - let name_tag = name; - - if targets.is_empty() { - return Err(ShellError::labeled_error( - "rm requires target paths", - "needs parameter", - name_tag, - )); - } - - let path = Path::new(path); - let mut all_targets: HashMap = HashMap::new(); - for target in targets { - let all_dots = target - .item - .to_str() - .map_or(false, |v| v.chars().all(|c| c == '.')); - - if all_dots { - return Err(ShellError::labeled_error( - "Cannot remove any parent directory", - "cannot remove any parent directory", - target.tag, - )); - } - - let path = path.join(&target.item); - match glob::glob_with( - &path.to_string_lossy(), - glob::MatchOptions { - require_literal_leading_dot: true, - ..GLOB_PARAMS - }, - ) { - Ok(files) => { - for file in files { - match file { - Ok(ref f) => { - // It is not appropriate to try and remove the - // current directory or its parent when using - // glob patterns. - let name = f.display().to_string(); - if name.ends_with("/.") || name.ends_with("/..") { - continue; - } - - all_targets - .entry(f.clone()) - .or_insert_with(|| target.tag.clone()); - } - Err(e) => { - return Err(ShellError::labeled_error( - format!("Could not remove {:}", path.to_string_lossy()), - e.to_string(), - &target.tag, - )); - } - } - } - } - Err(e) => { - return Err(ShellError::labeled_error( - e.to_string(), - e.to_string(), - &name_tag, - )) - } - }; - } - - if all_targets.is_empty() && !_force { - return Err(ShellError::labeled_error( - "No valid paths", - "no valid paths", - name_tag, - )); - } - - Ok(all_targets - .into_iter() - .map(move |(f, tag)| { - let is_empty = || match f.read_dir() { - Ok(mut p) => p.next().is_none(), - Err(_) => false, - }; - - if let Ok(metadata) = f.symlink_metadata() { - #[cfg(unix)] - let is_socket = metadata.file_type().is_socket(); - #[cfg(unix)] - let is_fifo = metadata.file_type().is_fifo(); - - #[cfg(not(unix))] - let is_socket = false; - #[cfg(not(unix))] - let is_fifo = false; - - if metadata.is_file() - || metadata.file_type().is_symlink() - || recursive - || is_socket - || is_fifo - || is_empty() - { - let result; - #[cfg(feature = "trash-support")] - { - use std::io::Error; - result = if _trash || (rm_always_trash && !_permanent) { - trash::delete(&f).map_err(|e: trash::Error| { - Error::new(ErrorKind::Other, format!("{:?}", e)) - }) - } else if metadata.is_file() { - std::fs::remove_file(&f) - } else { - std::fs::remove_dir_all(&f) - }; - } - #[cfg(not(feature = "trash-support"))] - { - result = if metadata.is_file() || is_socket || is_fifo { - std::fs::remove_file(&f) - } else { - std::fs::remove_dir_all(&f) - }; - } - - if let Err(e) = result { - let msg = - format!("Could not delete because: {:}\nTry '--trash' flag", e); - Err(ShellError::labeled_error(msg, e.to_string(), tag)) - } else { - let val = format!("deleted {:}", f.to_string_lossy()).into(); - Ok(ReturnSuccess::Value(val)) - } - } else { - let msg = - format!("Cannot remove {:}. try --recursive", f.to_string_lossy()); - Err(ShellError::labeled_error( - msg, - "cannot remove non-empty directory", - tag, - )) - } - } else { - let msg = format!("no such file or directory: {:}", f.to_string_lossy()); - Err(ShellError::labeled_error( - msg, - "no such file or directory", - tag, - )) - } - }) - .into_action_stream()) - } - - fn path(&self) -> String { - self.path.clone() - } - - fn pwd(&self, args: CommandArgs) -> Result { - let path = PathBuf::from(self.path()); - let p = match canonicalize(path.as_path()) { - Ok(p) => p, - Err(_) => { - return Err(ShellError::labeled_error( - "unable to show current directory", - "pwd command failed", - &args.call_info.name_tag, - )); - } - }; - - Ok(ActionStream::one(ReturnSuccess::value( - UntaggedValue::Primitive(Primitive::String(p.to_string_lossy().to_string())) - .into_value(&args.call_info.name_tag), - ))) - } - - fn set_path(&mut self, path: String) { - let pathbuf = PathBuf::from(&path); - let path = match canonicalize_with(pathbuf.as_path(), self.path()) { - Ok(path) => { - let _ = std::env::set_current_dir(&path); - std::env::set_var("PWD", &path); - path - } - _ => { - // TODO: handle the case where the path cannot be canonicalized - pathbuf - } - }; - self.last_path = self.path.clone(); - self.path = path.to_string_lossy().to_string(); - } - - fn open( - &self, - path: &Path, - name: Span, - with_encoding: Option<&'static Encoding>, - ) -> Result< - Box> + Sync + Send>, - ShellError, - > { - let metadata = std::fs::metadata(&path); - - let read_full = if let Ok(metadata) = metadata { - // Arbitrarily capping the file at 32 megs, so we don't try to read large files in all at once - metadata.is_file() && metadata.len() < (1024 * 1024 * 32) - } else { - false - }; - - if read_full { - // We should, in theory, be able to read in the whole file as one chunk - let buffer = std::fs::read(&path).map_err(|e| { - ShellError::labeled_error( - format!("Error opening file: {:?}", e), - "Error opening file", - name, - ) - })?; - - let bytes_mut = bytes::BytesMut::from(&buffer[..]); - - let mut codec = MaybeTextCodec::new(with_encoding); - - match codec.decode(&bytes_mut).map_err(|_| { - ShellError::labeled_error("Error opening file", "error opening file", name) - })? { - Some(sb) => Ok(Box::new(vec![Ok(sb)].into_iter())), - None => Ok(Box::new(vec![].into_iter())), - } - } else { - // We don't know that this is a finite file, so treat it as a stream - let f = std::fs::File::open(&path).map_err(|e| { - ShellError::labeled_error( - format!("Error opening file: {:?}", e), - "Error opening file", - name, - ) - })?; - let buf_reader = BufReader::new(f); - let buf_codec = BufCodecReader::new(buf_reader, MaybeTextCodec::new(with_encoding)); - - Ok(Box::new(buf_codec)) - } - } - - fn save( - &mut self, - full_path: &Path, - save_data: &[u8], - name: Span, - append: bool, - ) -> Result { - let mut options = OpenOptions::new(); - if append { - options.append(true).create(true) - } else { - options.write(true).create(true).truncate(true) - }; - - match options - .open(full_path) - .and_then(|ref mut file| file.write_all(save_data)) - { - Ok(_) => Ok(OutputStream::empty()), - Err(e) => Err(ShellError::labeled_error( - e.to_string(), - "IO error while saving", - name, - )), - } - } - - fn is_interactive(&self) -> bool { - self.mode == FilesystemShellMode::Cli - } -} - -struct TaggedPathBuf<'a>(&'a PathBuf, &'a Tag); - -fn move_file(from: TaggedPathBuf, to: TaggedPathBuf) -> Result<(), ShellError> { - let TaggedPathBuf(from, from_tag) = from; - let TaggedPathBuf(to, to_tag) = to; - - if to.exists() && from.is_dir() && to.is_file() { - return Err(ShellError::labeled_error( - "Cannot rename a directory to a file", - "invalid destination", - to_tag, - )); - } - - let destination_dir_exists = if to.is_dir() { - true - } else { - to.parent().map(Path::exists).unwrap_or(true) - }; - - if !destination_dir_exists { - return Err(ShellError::labeled_error( - "Destination directory does not exist", - "destination does not exist", - to_tag, - )); - } - - let mut to = to.clone(); - if to.is_dir() { - let from_file_name = match from.file_name() { - Some(name) => name, - None => { - return Err(ShellError::labeled_error( - "Not a valid entry name", - "not a valid entry name", - from_tag, - )) - } - }; - - to.push(from_file_name); - } - - move_item(from, from_tag, &to) -} - -fn move_item(from: &Path, from_tag: &Tag, to: &Path) -> Result<(), ShellError> { - // We first try a rename, which is a quick operation. If that doesn't work, we'll try a copy - // and remove the old file/folder. This is necessary if we're moving across filesystems or devices. - std::fs::rename(&from, &to).or_else(|_| { - match if from.is_file() { - let mut options = fs_extra::file::CopyOptions::new(); - options.overwrite = true; - fs_extra::file::move_file(from, to, &options) - } else { - let mut options = fs_extra::dir::CopyOptions::new(); - options.overwrite = true; - options.copy_inside = true; - fs_extra::dir::move_dir(from, to, &options) - } { - Ok(_) => Ok(()), - Err(e) => Err(ShellError::labeled_error( - format!("Could not move {:?} to {:?}. {:}", from, to, e), - "could not move", - from_tag, - )), - } - }) -} - -fn is_empty_dir(dir: impl AsRef) -> bool { - match dir.as_ref().read_dir() { - Err(_) => true, - Ok(mut s) => s.next().is_none(), - } -} - -fn permission_denied(dir: impl AsRef) -> bool { - match dir.as_ref().read_dir() { - Err(e) => matches!(e.kind(), std::io::ErrorKind::PermissionDenied), - Ok(_) => false, - } -} - -fn is_hidden_dir(dir: impl AsRef) -> bool { - #[cfg(windows)] - { - use std::os::windows::fs::MetadataExt; - - if let Ok(metadata) = dir.as_ref().metadata() { - let attributes = metadata.file_attributes(); - // https://docs.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants - (attributes & 0x2) != 0 - } else { - false - } - } - - #[cfg(not(windows))] - { - dir.as_ref() - .file_name() - .map(|name| name.to_string_lossy().starts_with('.')) - .unwrap_or(false) - } -} - -#[cfg(unix)] -use std::os::unix::fs::FileTypeExt; - -pub fn get_file_type(md: &std::fs::Metadata) -> &str { - let ft = md.file_type(); - let mut file_type = "Unknown"; - if ft.is_dir() { - file_type = "Dir"; - } else if ft.is_file() { - file_type = "File"; - } else if ft.is_symlink() { - file_type = "Symlink"; - } else { - #[cfg(unix)] - { - if ft.is_block_device() { - file_type = "Block device"; - } else if ft.is_char_device() { - file_type = "Char device"; - } else if ft.is_fifo() { - file_type = "Pipe"; - } else if ft.is_socket() { - file_type = "Socket"; - } - } - } - file_type -} - -#[allow(clippy::too_many_arguments)] -pub(crate) fn dir_entry_dict( - filename: &std::path::Path, - metadata: Option<&std::fs::Metadata>, - tag: impl Into, - long: bool, - short_name: bool, - du: bool, - ctrl_c: Arc, -) -> Result { - let tag = tag.into(); - let mut dict = TaggedDictBuilder::new(&tag); - // Insert all columns first to maintain proper table alignment if we can't find (or are not allowed to view) any information - if long { - #[cfg(windows)] - { - for column in [ - "name", "type", "target", "readonly", "size", "created", "accessed", "modified", - ] { - dict.insert_untagged(column, UntaggedValue::nothing()); - } - } - - #[cfg(unix)] - { - for column in [ - "name", - "type", - "target", - "num_links", - "inode", - "readonly", - "mode", - "uid", - "group", - "size", - "created", - "accessed", - "modified", - ] { - dict.insert_untagged(column, UntaggedValue::nothing()); - } - } - } else { - for column in ["name", "type", "target", "size", "modified"] { - if column == "target" { - continue; - } - dict.insert_untagged(column, UntaggedValue::nothing()); - } - } - - let name = if short_name { - filename.file_name().and_then(|s| s.to_str()) - } else { - filename.to_str() - } - .ok_or_else(|| { - ShellError::labeled_error( - format!("Invalid file name: {:}", filename.to_string_lossy()), - "invalid file name", - tag, - ) - })?; - - dict.insert_untagged("name", UntaggedValue::filepath(name)); - - if let Some(md) = metadata { - dict.insert_untagged("type", get_file_type(md)); - } - - if long { - if let Some(md) = metadata { - if md.file_type().is_symlink() { - let symlink_target_untagged_value: UntaggedValue; - if let Ok(path_to_link) = filename.read_link() { - symlink_target_untagged_value = - UntaggedValue::string(path_to_link.to_string_lossy()); - } else { - symlink_target_untagged_value = - UntaggedValue::string("Could not obtain target file's path"); - } - dict.insert_untagged("target", symlink_target_untagged_value); - } - } - } - - if long { - if let Some(md) = metadata { - dict.insert_untagged( - "readonly", - UntaggedValue::boolean(md.permissions().readonly()), - ); - - #[cfg(unix)] - { - use std::os::unix::fs::MetadataExt; - let mode = md.permissions().mode(); - dict.insert_untagged( - "mode", - UntaggedValue::string(umask::Mode::from(mode).to_string()), - ); - - let nlinks = md.nlink(); - dict.insert_untagged("num_links", UntaggedValue::string(nlinks.to_string())); - - let inode = md.ino(); - dict.insert_untagged("inode", UntaggedValue::string(inode.to_string())); - - if let Some(user) = users::get_user_by_uid(md.uid()) { - dict.insert_untagged( - "uid", - UntaggedValue::string(user.name().to_string_lossy()), - ); - } - - if let Some(group) = users::get_group_by_gid(md.gid()) { - dict.insert_untagged( - "group", - UntaggedValue::string(group.name().to_string_lossy()), - ); - } - } - } - } - - if let Some(md) = metadata { - let mut size_untagged_value: UntaggedValue = UntaggedValue::nothing(); - - if md.is_dir() { - let dir_size: u64 = if du { - let params = DirBuilder::new( - Tag { - anchor: None, - span: Span::new(0, 2), - }, - None, - false, - None, - false, - ); - - DirInfo::new(filename, ¶ms, None, ctrl_c).get_size() - } else { - md.len() - }; - - size_untagged_value = UntaggedValue::filesize(dir_size); - } else if md.is_file() { - size_untagged_value = UntaggedValue::filesize(md.len()); - } else if md.file_type().is_symlink() { - if let Ok(symlink_md) = filename.symlink_metadata() { - size_untagged_value = UntaggedValue::filesize(symlink_md.len() as u64); - } - } - - dict.insert_untagged("size", size_untagged_value); - } - - if let Some(md) = metadata { - if long { - if let Ok(c) = md.created() { - dict.insert_untagged("created", UntaggedValue::system_date(c)); - } - - if let Ok(a) = md.accessed() { - dict.insert_untagged("accessed", UntaggedValue::system_date(a)); - } - } - - if let Ok(m) = md.modified() { - dict.insert_untagged("modified", UntaggedValue::system_date(m)); - } - } - - Ok(dict.into_value()) -} - -fn path_contains_hidden_folder(path: &Path, folders: &[PathBuf]) -> bool { - let path_str = path.to_str().expect("failed to read path"); - if folders - .iter() - .any(|p| path_str.starts_with(&p.to_str().expect("failed to read hidden paths"))) - { - return true; - } - false -} diff --git a/crates/nu-engine/src/filesystem/mod.rs b/crates/nu-engine/src/filesystem/mod.rs deleted file mode 100644 index 8c9b64452d..0000000000 --- a/crates/nu-engine/src/filesystem/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub(crate) mod dir_info; -pub mod filesystem_shell; -pub(crate) mod utils; diff --git a/crates/nu-engine/src/filesystem/utils.rs b/crates/nu-engine/src/filesystem/utils.rs deleted file mode 100644 index dbc823e288..0000000000 --- a/crates/nu-engine/src/filesystem/utils.rs +++ /dev/null @@ -1,210 +0,0 @@ -use nu_errors::ShellError; -use nu_path::canonicalize_with; -use std::path::{Path, PathBuf}; - -#[derive(Default)] -pub struct FileStructure { - pub resources: Vec, -} - -impl FileStructure { - pub fn new() -> FileStructure { - FileStructure { - resources: Vec::::new(), - } - } - - #[allow(dead_code)] - pub fn contains_more_than_one_file(&self) -> bool { - self.resources.len() > 1 - } - - #[allow(dead_code)] - pub fn contains_files(&self) -> bool { - !self.resources.is_empty() - } - - pub fn paths_applying_with( - &mut self, - to: F, - ) -> Result, Box> - where - F: Fn((PathBuf, usize)) -> Result<(PathBuf, PathBuf), Box>, - { - self.resources - .iter() - .map(|f| (PathBuf::from(&f.loc), f.at)) - .map(to) - .collect() - } - - pub fn walk_decorate(&mut self, start_path: &Path) -> Result<(), ShellError> { - self.resources = Vec::::new(); - self.build(start_path, 0)?; - self.resources.sort(); - - Ok(()) - } - - fn build(&mut self, src: &Path, lvl: usize) -> Result<(), ShellError> { - let source = canonicalize_with(src, std::env::current_dir()?)?; - - if source.is_dir() { - for entry in std::fs::read_dir(src)? { - let entry = entry?; - let path = entry.path(); - - if path.is_dir() { - self.build(&path, lvl + 1)?; - } - - self.resources.push(Res { - loc: path.to_path_buf(), - at: lvl, - }); - } - } else { - self.resources.push(Res { - loc: source, - at: lvl, - }); - } - - Ok(()) - } -} - -#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)] -pub struct Res { - pub at: usize, - pub loc: PathBuf, -} - -impl Res {} - -#[cfg(test)] -mod tests { - use super::{FileStructure, Res}; - use nu_protocol::{TaggedDictBuilder, UntaggedValue, Value, ValueResource, ValueStructure}; - use nu_source::Tag; - use nu_test_support::{fs::Stub::EmptyFile, playground::Playground}; - use std::path::PathBuf; - - fn structured_sample_record(key: &str, value: &str) -> Value { - let mut record = TaggedDictBuilder::new(Tag::unknown()); - record.insert_untagged(key, UntaggedValue::string(value)); - record.into_value() - } - - fn sample_nushell_source_code() -> Value { - /* - src - commands - plugins => "sys.rs" - tests - helpers => "mod.rs" - */ - - let mut src = TaggedDictBuilder::new(Tag::unknown()); - let mut record = TaggedDictBuilder::new(Tag::unknown()); - - record.insert_value("commands", structured_sample_record("plugins", "sys.rs")); - record.insert_value("tests", structured_sample_record("helpers", "mod.rs")); - src.insert_value("src", record.into_value()); - - src.into_value() - } - - #[test] - fn prepares_and_decorates_value_filesystemlike_sources() { - let mut res = ValueStructure::new(); - - res.walk_decorate(&sample_nushell_source_code()) - .expect("Can not decorate values traversal."); - - assert_eq!( - res.resources, - vec![ - ValueResource { - loc: PathBuf::from("src"), - at: 0, - }, - ValueResource { - loc: PathBuf::from("commands"), - at: 1, - }, - ValueResource { - loc: PathBuf::from("tests"), - at: 1, - }, - ValueResource { - loc: PathBuf::from("helpers"), - at: 2, - }, - ValueResource { - loc: PathBuf::from("plugins"), - at: 2, - }, - ] - ); - } - - #[test] - fn recognizes_if_path_exists_in_value_filesystemlike_sources() { - let mut res = ValueStructure::new(); - - res.walk_decorate(&sample_nushell_source_code()) - .expect("Can not decorate values traversal."); - - assert!(res.exists(&PathBuf::from("/"))); - - assert!(res.exists(&PathBuf::from("src/commands/plugins"))); - assert!(res.exists(&PathBuf::from("src/commands"))); - assert!(res.exists(&PathBuf::from("src/tests"))); - assert!(res.exists(&PathBuf::from("src/tests/helpers"))); - assert!(res.exists(&PathBuf::from("src"))); - - assert!(res.exists(&PathBuf::from("/src/commands/plugins"))); - assert!(res.exists(&PathBuf::from("/src/commands"))); - assert!(res.exists(&PathBuf::from("/src/tests"))); - assert!(res.exists(&PathBuf::from("/src/tests/helpers"))); - assert!(res.exists(&PathBuf::from("/src"))); - - assert!(!res.exists(&PathBuf::from("/not_valid"))); - assert!(!res.exists(&PathBuf::from("/src/not_valid"))); - } - - #[test] - fn prepares_and_decorates_filesystem_source_files() { - Playground::setup("file_structure_test", |dirs, sandbox| { - sandbox.with_files(vec![ - EmptyFile("sample.ini"), - EmptyFile("sample.eml"), - EmptyFile("cargo_sample.toml"), - ]); - - let mut res = FileStructure::new(); - - res.walk_decorate(dirs.test()) - .expect("Can not decorate files traversal."); - - assert_eq!( - res.resources, - vec![ - Res { - loc: dirs.test().join("cargo_sample.toml"), - at: 0 - }, - Res { - loc: dirs.test().join("sample.eml"), - at: 0 - }, - Res { - loc: dirs.test().join("sample.ini"), - at: 0 - } - ] - ); - }) - } -} diff --git a/crates/nu-engine/src/from_value.rs b/crates/nu-engine/src/from_value.rs deleted file mode 100644 index 05c627bf3d..0000000000 --- a/crates/nu-engine/src/from_value.rs +++ /dev/null @@ -1,487 +0,0 @@ -use std::path::PathBuf; - -use bigdecimal::{BigDecimal, ToPrimitive}; -use chrono::{DateTime, FixedOffset}; -use nu_errors::ShellError; -use nu_path::expand_path; -use nu_protocol::{ - hir::CapturedBlock, ColumnPath, Dictionary, Primitive, Range, SpannedTypeName, UntaggedValue, - Value, -}; -use nu_source::{Tagged, TaggedItem}; -use num_bigint::BigInt; - -pub trait FromValue: Sized { - fn from_value(v: &Value) -> Result; -} - -impl FromValue for Value { - fn from_value(v: &Value) -> Result { - Ok(v.clone()) - } -} - -impl FromValue for Tagged { - fn from_value(v: &Value) -> Result { - let tag = v.tag.clone(); - - match v { - Value { - value: UntaggedValue::Primitive(Primitive::Int(i)), - .. - } => Ok(BigInt::from(*i).tagged(tag)), - Value { - value: UntaggedValue::Primitive(Primitive::Filesize(i)), - .. - } => Ok(BigInt::from(*i).tagged(tag)), - Value { - value: UntaggedValue::Primitive(Primitive::Duration(i)), - .. - } => Ok(i.clone().tagged(tag)), - Value { - value: UntaggedValue::Row(_), - .. - } => { - let mut shell_error = ShellError::type_error("integer", v.spanned_type_name()); - shell_error.notes.push( - "Note: you can access columns using dot. eg) $it.column or (ls).column".into(), - ); - Err(shell_error) - } - v => Err(ShellError::type_error("integer", v.spanned_type_name())), - } - } -} - -impl FromValue for num_bigint::BigInt { - fn from_value(v: &Value) -> Result { - match v { - Value { - value: UntaggedValue::Primitive(Primitive::Int(i)), - .. - } => Ok(BigInt::from(*i)), - Value { - value: UntaggedValue::Primitive(Primitive::Filesize(i)), - .. - } => Ok(BigInt::from(*i)), - Value { - value: UntaggedValue::Primitive(Primitive::Duration(i)), - .. - } => Ok(i.clone()), - Value { - value: UntaggedValue::Row(_), - .. - } => { - let mut shell_error = ShellError::type_error("integer", v.spanned_type_name()); - shell_error.notes.push( - "Note: you can access columns using dot. eg) $it.column or (ls).column".into(), - ); - Err(shell_error) - } - v => Err(ShellError::type_error("integer", v.spanned_type_name())), - } - } -} - -impl FromValue for Tagged { - fn from_value(v: &Value) -> Result { - let tag = v.tag.clone(); - v.as_u64().map(|s| s.tagged(tag)) - } -} - -impl FromValue for u64 { - fn from_value(v: &Value) -> Result { - v.as_u64() - } -} - -impl FromValue for i64 { - fn from_value(v: &Value) -> Result { - v.as_i64() - } -} - -impl FromValue for Tagged { - fn from_value(v: &Value) -> Result { - let tag = v.tag.clone(); - v.as_i64().map(|s| s.tagged(tag)) - } -} - -impl FromValue for Tagged { - fn from_value(v: &Value) -> Result { - let tag = v.tag.clone(); - v.as_u32().map(|s| s.tagged(tag)) - } -} - -impl FromValue for Tagged { - fn from_value(v: &Value) -> Result { - let tag = v.tag.clone(); - v.as_i16().map(|s| s.tagged(tag)) - } -} - -impl FromValue for Tagged { - fn from_value(v: &Value) -> Result { - let tag = v.tag.clone(); - v.as_usize().map(|s| s.tagged(tag)) - } -} - -impl FromValue for Tagged { - fn from_value(v: &Value) -> Result { - let tag = v.tag.clone(); - v.as_char().map(|c| c.tagged(tag)) - } -} - -impl FromValue for usize { - fn from_value(v: &Value) -> Result { - v.as_usize() - } -} - -impl FromValue for i32 { - fn from_value(v: &Value) -> Result { - v.as_i32() - } -} - -impl FromValue for bigdecimal::BigDecimal { - fn from_value(v: &Value) -> Result { - match v { - Value { - value: UntaggedValue::Primitive(Primitive::Decimal(d)), - .. - } => Ok(d.clone()), - Value { - value: UntaggedValue::Primitive(Primitive::Int(i)), - .. - } => Ok(BigDecimal::from(*i)), - Value { - value: UntaggedValue::Row(_), - .. - } => { - let mut shell_error = ShellError::type_error("decimal", v.spanned_type_name()); - shell_error.notes.push( - "Note: you can access columns using dot. eg) $it.column or (ls).column".into(), - ); - Err(shell_error) - } - v => Err(ShellError::type_error("decimal", v.spanned_type_name())), - } - } -} - -impl FromValue for Tagged { - fn from_value(v: &Value) -> Result { - let tag = v.tag.clone(); - match &v.value { - UntaggedValue::Primitive(Primitive::Decimal(d)) => Ok(d.clone().tagged(tag)), - UntaggedValue::Primitive(Primitive::Int(i)) => Ok(BigDecimal::from(*i).tagged(tag)), - _ => Err(ShellError::type_error("decimal", v.spanned_type_name())), - } - } -} - -impl FromValue for Tagged { - fn from_value(v: &Value) -> Result { - let tag = v.tag.clone(); - let decimal: bigdecimal::BigDecimal = FromValue::from_value(v)?; - - match decimal.to_f64() { - Some(d) => Ok(d.tagged(tag)), - _ => Err(ShellError::type_error("decimal", v.spanned_type_name())), - } - } -} - -impl FromValue for String { - fn from_value(v: &Value) -> Result { - match v { - Value { - value: UntaggedValue::Primitive(Primitive::String(s)), - .. - } => Ok(s.clone()), - Value { - value: UntaggedValue::Primitive(Primitive::GlobPattern(s)), - .. - } => Ok(s.clone()), - Value { - value: UntaggedValue::Primitive(Primitive::FilePath(p)), - .. - } => Ok(p.to_string_lossy().to_string()), - Value { - value: UntaggedValue::Row(_), - .. - } => { - let mut shell_error = ShellError::type_error("string", v.spanned_type_name()); - shell_error.notes.push( - "Note: you can access columns using dot. eg) $it.column or (ls).column".into(), - ); - Err(shell_error) - } - v => Err(ShellError::type_error("string", v.spanned_type_name())), - } - } -} - -impl FromValue for Tagged { - fn from_value(v: &Value) -> Result { - let tag = v.tag.clone(); - v.as_string().map(|s| s.tagged(tag)) - } -} - -impl FromValue for PathBuf { - fn from_value(v: &Value) -> Result { - match v { - Value { - value: UntaggedValue::Primitive(Primitive::String(s)), - .. - } => Ok(expand_path(s)), - Value { - value: UntaggedValue::Primitive(Primitive::FilePath(p)), - .. - } => Ok(expand_path(p)), - Value { - value: UntaggedValue::Row(_), - .. - } => { - let mut shell_error = ShellError::type_error("filepath", v.spanned_type_name()); - shell_error.notes.push( - "Note: you can access columns using dot. eg) $it.column or (ls).column".into(), - ); - Err(shell_error) - } - v => Err(ShellError::type_error("filepath", v.spanned_type_name())), - } - } -} - -impl FromValue for Tagged { - fn from_value(v: &Value) -> Result { - match v { - Value { - value: UntaggedValue::Primitive(Primitive::String(s)), - tag, - } => Ok(expand_path(s).tagged(tag)), - Value { - value: UntaggedValue::Primitive(Primitive::FilePath(p)), - tag, - } => Ok(expand_path(p).tagged(tag)), - Value { - value: UntaggedValue::Row(_), - .. - } => { - let mut shell_error = ShellError::type_error("filepath", v.spanned_type_name()); - shell_error.notes.push( - "Note: you can access columns using dot. eg) $it.column or (ls).column".into(), - ); - Err(shell_error) - } - v => Err(ShellError::type_error("filepath", v.spanned_type_name())), - } - } -} - -impl FromValue for ColumnPath { - fn from_value(v: &Value) -> Result { - match v { - Value { - value: UntaggedValue::Primitive(Primitive::ColumnPath(c)), - .. - } => Ok(c.clone()), - v => Err(ShellError::type_error("column path", v.spanned_type_name())), - } - } -} - -impl FromValue for bool { - fn from_value(v: &Value) -> Result { - match v { - Value { - value: UntaggedValue::Primitive(Primitive::Boolean(b)), - .. - } => Ok(*b), - Value { - value: UntaggedValue::Row(_), - .. - } => { - let mut shell_error = ShellError::type_error("boolean", v.spanned_type_name()); - shell_error.notes.push( - "Note: you can access columns using dot. eg) $it.column or (ls).column".into(), - ); - Err(shell_error) - } - v => Err(ShellError::type_error("boolean", v.spanned_type_name())), - } - } -} - -impl FromValue for Tagged { - fn from_value(v: &Value) -> Result { - match v { - Value { - value: UntaggedValue::Primitive(Primitive::Boolean(b)), - tag, - } => Ok((*b).tagged(tag)), - Value { - value: UntaggedValue::Row(_), - .. - } => { - let mut shell_error = ShellError::type_error("boolean", v.spanned_type_name()); - shell_error.notes.push( - "Note: you can access columns using dot. eg) $it.column or (ls).column".into(), - ); - Err(shell_error) - } - v => Err(ShellError::type_error("boolean", v.spanned_type_name())), - } - } -} - -impl FromValue for DateTime { - fn from_value(v: &Value) -> Result { - match v { - Value { - value: UntaggedValue::Primitive(Primitive::Date(d)), - .. - } => Ok(*d), - Value { - value: UntaggedValue::Row(_), - .. - } => { - let mut shell_error = ShellError::type_error("date", v.spanned_type_name()); - shell_error.notes.push( - "Note: you can access columns using dot. eg) $it.column or (ls).column".into(), - ); - Err(shell_error) - } - v => Err(ShellError::type_error("date", v.spanned_type_name())), - } - } -} - -impl FromValue for Range { - fn from_value(v: &Value) -> Result { - match v { - Value { - value: UntaggedValue::Primitive(Primitive::Range(r)), - .. - } => Ok((**r).clone()), - Value { - value: UntaggedValue::Row(_), - .. - } => { - let mut shell_error = ShellError::type_error("range", v.spanned_type_name()); - shell_error.notes.push( - "Note: you can access columns using dot. eg) $it.column or (ls).column".into(), - ); - Err(shell_error) - } - v => Err(ShellError::type_error("range", v.spanned_type_name())), - } - } -} - -impl FromValue for Tagged { - fn from_value(v: &Value) -> Result { - let tag = v.tag.clone(); - match v { - Value { - value: UntaggedValue::Primitive(Primitive::Range(ref range)), - .. - } => Ok((*range.clone()).tagged(tag)), - Value { - value: UntaggedValue::Row(_), - .. - } => { - let mut shell_error = ShellError::type_error("range", v.spanned_type_name()); - shell_error.notes.push( - "Note: you can access columns using dot. eg) $it.column or (ls).column".into(), - ); - Err(shell_error) - } - v => Err(ShellError::type_error("range", v.spanned_type_name())), - } - } -} - -impl FromValue for Vec { - fn from_value(v: &Value) -> Result { - match v { - Value { - value: UntaggedValue::Primitive(Primitive::Binary(b)), - .. - } => Ok(b.clone()), - Value { - value: UntaggedValue::Primitive(Primitive::String(s)), - .. - } => Ok(s.bytes().collect()), - Value { - value: UntaggedValue::Row(_), - .. - } => { - let mut shell_error = ShellError::type_error("binary data", v.spanned_type_name()); - shell_error.notes.push( - "Note: you can access columns using dot. eg) $it.column or (ls).column".into(), - ); - Err(shell_error) - } - v => Err(ShellError::type_error("binary data", v.spanned_type_name())), - } - } -} - -impl FromValue for Dictionary { - fn from_value(v: &Value) -> Result { - match v { - Value { - value: UntaggedValue::Row(r), - .. - } => Ok(r.clone()), - v => Err(ShellError::type_error("row", v.spanned_type_name())), - } - } -} - -impl FromValue for CapturedBlock { - fn from_value(v: &Value) -> Result { - match v { - Value { - value: UntaggedValue::Block(b), - .. - } => Ok((**b).clone()), - Value { - value: UntaggedValue::Row(_), - .. - } => { - let mut shell_error = ShellError::type_error("block", v.spanned_type_name()); - shell_error.notes.push( - "Note: you can access columns using dot. eg) $it.column or (ls).column".into(), - ); - Err(shell_error) - } - v => Err(ShellError::type_error("block", v.spanned_type_name())), - } - } -} - -impl FromValue for Vec { - fn from_value(v: &Value) -> Result { - match v { - Value { - value: UntaggedValue::Table(t), - .. - } => Ok(t.clone()), - Value { - value: UntaggedValue::Row(_), - .. - } => Ok(vec![v.clone()]), - v => Err(ShellError::type_error("table", v.spanned_type_name())), - } - } -} diff --git a/crates/nu-engine/src/glob_from.rs b/crates/nu-engine/src/glob_from.rs new file mode 100644 index 0000000000..2edf511507 --- /dev/null +++ b/crates/nu-engine/src/glob_from.rs @@ -0,0 +1,121 @@ +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; +use std::path::{Component, Path, PathBuf}; + +use nu_path::{canonicalize_with, expand_path_with}; +use nu_protocol::{ShellError, Span, Spanned}; + +/// This function is like `glob::glob` from the `glob` crate, except it is relative to a given cwd. +/// +/// It returns a tuple of two values: the first is an optional prefix that the expanded filenames share. +/// This prefix can be removed from the front of each value to give an approximation of the relative path +/// to the user +/// +/// The second of the two values is an iterator over the matching filepaths. +#[allow(clippy::type_complexity)] +pub fn glob_from( + pattern: &Spanned, + cwd: &Path, + span: Span, +) -> Result< + ( + Option, + Box> + Send>, + ), + ShellError, +> { + let path = PathBuf::from(&pattern.item); + let path = if path.is_relative() { + expand_path_with(path, cwd) + } else { + path + }; + + let (prefix, pattern) = if path.to_string_lossy().contains('*') { + // Path is a glob pattern => do not check for existence + // Select the longest prefix until the first '*' + let mut p = PathBuf::new(); + for c in path.components() { + if let Component::Normal(os) = c { + if os.to_string_lossy().contains('*') { + break; + } + } + p.push(c); + } + (Some(p), path) + } else { + let path = if let Ok(p) = canonicalize_with(path, &cwd) { + p + } else { + return Err(ShellError::DirectoryNotFound(pattern.span)); + }; + + if path.is_dir() { + if permission_denied(&path) { + #[cfg(unix)] + let error_msg = format!( + "The permissions of {:o} do not allow access for this user", + path.metadata() + .expect("this shouldn't be called since we already know there is a dir") + .permissions() + .mode() + & 0o0777 + ); + + #[cfg(not(unix))] + let error_msg = String::from("Permission denied"); + + return Err(ShellError::SpannedLabeledError( + "Permission denied".into(), + error_msg, + pattern.span, + )); + } + + if is_empty_dir(&path) { + return Ok((Some(path), Box::new(vec![].into_iter()))); + } + + (Some(path.clone()), path.join("*")) + } else { + (path.parent().map(|parent| parent.to_path_buf()), path) + } + }; + + let pattern = pattern.to_string_lossy().to_string(); + + let glob = glob::glob(&pattern).map_err(|err| { + nu_protocol::ShellError::SpannedLabeledError( + "Error extracting glob pattern".into(), + err.to_string(), + span, + ) + })?; + + Ok(( + prefix, + Box::new(glob.map(move |x| match x { + Ok(v) => Ok(v), + Err(err) => Err(nu_protocol::ShellError::SpannedLabeledError( + "Error extracting glob pattern".into(), + err.to_string(), + span, + )), + })), + )) +} + +fn permission_denied(dir: impl AsRef) -> bool { + match dir.as_ref().read_dir() { + Err(e) => matches!(e.kind(), std::io::ErrorKind::PermissionDenied), + Ok(_) => false, + } +} + +fn is_empty_dir(dir: impl AsRef) -> bool { + match dir.as_ref().read_dir() { + Err(_) => true, + Ok(mut s) => s.next().is_none(), + } +} diff --git a/crates/nu-engine/src/lib.rs b/crates/nu-engine/src/lib.rs index 607ab63e11..9de7dae381 100644 --- a/crates/nu-engine/src/lib.rs +++ b/crates/nu-engine/src/lib.rs @@ -1,40 +1,16 @@ -mod call_info; -mod command_args; -mod config_holder; +mod call_ext; +pub mod column; pub mod documentation; -mod env; -pub mod evaluate; -pub mod evaluation_context; -mod example; -pub mod filesystem; -mod from_value; -mod maybe_text_codec; -pub mod plugin; -mod print; -pub mod script; -pub mod shell; -mod types; -mod whole_stream_command; +pub mod env; +mod eval; +mod glob_from; -pub use crate::call_info::UnevaluatedCallInfo; -pub use crate::command_args::{CommandArgs, RunnableContext}; -pub use crate::config_holder::ConfigHolder; -pub use crate::documentation::{generate_docs, get_brief_help, get_documentation, get_full_help}; -pub use crate::env::host::FakeHost; -pub use crate::env::host::Host; -pub use crate::evaluate::block::run_block; -pub use crate::evaluate::envvar::EnvVar; -pub use crate::evaluate::scope::Scope; -pub use crate::evaluate::{evaluator, evaluator::evaluate_baseline_expr}; -pub use crate::evaluation_context::EvaluationContext; -pub use crate::example::Example; -pub use crate::filesystem::dir_info::{DirBuilder, DirInfo, FileInfo}; -pub use crate::filesystem::filesystem_shell::FilesystemShell; -pub use crate::from_value::FromValue; -pub use crate::maybe_text_codec::{BufCodecReader, MaybeTextCodec, StringOrBinary}; -pub use crate::print::maybe_print_errors; -pub use crate::shell::painter::Painter; -pub use crate::shell::palette::{DefaultPalette, Palette}; -pub use crate::shell::shell_manager::ShellManager; -pub use crate::shell::value_shell; -pub use crate::whole_stream_command::{whole_stream_command, Command, WholeStreamCommand}; +pub use call_ext::CallExt; +pub use column::get_columns; +pub use documentation::{generate_docs, get_brief_help, get_documentation, get_full_help}; +pub use env::*; +pub use eval::{ + eval_block, eval_block_with_redirect, eval_expression, eval_expression_with_input, + eval_operator, eval_subexpression, +}; +pub use glob_from::glob_from; diff --git a/crates/nu-engine/src/maybe_text_codec.rs b/crates/nu-engine/src/maybe_text_codec.rs deleted file mode 100644 index 60c011cf95..0000000000 --- a/crates/nu-engine/src/maybe_text_codec.rs +++ /dev/null @@ -1,113 +0,0 @@ -use std::io::{BufRead, BufReader, Read}; - -use nu_errors::ShellError; - -use encoding_rs::{CoderResult, Decoder, Encoding, UTF_8}; - -#[cfg(not(test))] -const OUTPUT_BUFFER_SIZE: usize = 8192; -#[cfg(test)] -const OUTPUT_BUFFER_SIZE: usize = 4; - -#[derive(Debug, Eq, PartialEq)] -pub enum StringOrBinary { - String(String), - Binary(Vec), -} - -pub struct MaybeTextCodec { - decoder: Decoder, -} - -pub struct BufCodecReader { - maybe_text_codec: MaybeTextCodec, - input: BufReader, -} - -impl BufCodecReader { - pub fn new(input: BufReader, maybe_text_codec: MaybeTextCodec) -> Self { - BufCodecReader { - maybe_text_codec, - input, - } - } -} - -impl Iterator for BufCodecReader { - type Item = Result; - - fn next(&mut self) -> Option { - let buffer = self.input.fill_buf(); - match buffer { - Ok(s) => { - let result = self.maybe_text_codec.decode(s).transpose(); - - let buffer_len = s.len(); - self.input.consume(buffer_len); - - result - } - Err(e) => Some(Err(ShellError::untagged_runtime_error(e.to_string()))), - } - } -} - -impl MaybeTextCodec { - // The constructor takes an Option<&'static Encoding>, because an absence of an encoding indicates that we want BOM sniffing enabled - pub fn new(encoding: Option<&'static Encoding>) -> Self { - let decoder = match encoding { - Some(e) => e.new_decoder_with_bom_removal(), - None => UTF_8.new_decoder(), - }; - MaybeTextCodec { decoder } - } -} - -impl Default for MaybeTextCodec { - fn default() -> Self { - MaybeTextCodec { - decoder: UTF_8.new_decoder(), - } - } -} - -impl MaybeTextCodec { - pub fn decode(&mut self, src: &[u8]) -> Result, ShellError> { - if src.is_empty() { - return Ok(None); - } - - let mut s = String::with_capacity(OUTPUT_BUFFER_SIZE); - - let (res, _read, replacements) = self.decoder.decode_to_string(src, &mut s, false); - - let result = if replacements { - // If we had to make replacements when converting to utf8, fall back to binary - StringOrBinary::Binary(src.to_vec()) - } else { - // If original buffer size is too small, we continue to allocate new Strings and append - // them to the result until the input buffer is smaller than the allocated String - if let CoderResult::OutputFull = res { - let mut buffer = String::with_capacity(OUTPUT_BUFFER_SIZE); - loop { - let (res, _read, _replacements) = - self.decoder - .decode_to_string(&src[s.len()..], &mut buffer, false); - s.push_str(&buffer); - - if let CoderResult::InputEmpty = res { - break; - } - - buffer.clear(); - } - } - - StringOrBinary::String(s) - }; - - // src.clear(); - - Ok(Some(result)) - } -} diff --git a/crates/nu-engine/src/plugin/build_plugin.rs b/crates/nu-engine/src/plugin/build_plugin.rs deleted file mode 100644 index 7838ed9b1f..0000000000 --- a/crates/nu-engine/src/plugin/build_plugin.rs +++ /dev/null @@ -1,172 +0,0 @@ -use crate::plugin::run_plugin::PluginCommandBuilder; -use log::trace; -use nu_errors::ShellError; -use nu_path::canonicalize; -use nu_plugin::jsonrpc::JsonRpc; -use nu_protocol::{Signature, Value}; -use std::io::{BufRead, BufReader, Write}; -use std::process::{Child, Command, Stdio}; - -use rayon::prelude::*; - -pub fn build_plugin_command( - path: &std::path::Path, -) -> Result, ShellError> { - let ext = path.extension(); - let ps1_file = match ext { - Some(ext) => ext == "ps1", - None => false, - }; - - let mut child: Child = if ps1_file { - Command::new("pwsh") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .args([ - "-NoLogo", - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - &path.to_string_lossy(), - ]) - .spawn() - .expect("Failed to spawn PowerShell process") - } else { - Command::new(path) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .expect("Failed to spawn child process") - }; - - let stdin = child.stdin.as_mut().expect("Failed to open stdin"); - let stdout = child.stdout.as_mut().expect("Failed to open stdout"); - - let mut reader = BufReader::new(stdout); - - let request = JsonRpc::new("config", Vec::::new()); - let request_raw = serde_json::to_string(&request)?; - trace!(target: "nu::load", "plugin infrastructure config -> path {:#?}, request {:?}", &path, &request_raw); - stdin.write_all(format!("{}\n", request_raw).as_bytes())?; - let path = canonicalize(path)?; - - let mut input = String::new(); - let result = match reader.read_line(&mut input) { - Ok(count) => { - trace!(target: "nu::load", "plugin infrastructure -> config response for {:#?}", &path); - 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 { - Ok(jrpc) => match jrpc.params { - Ok(params) => { - let fname = path.to_string_lossy(); - - trace!(target: "nu::load", "plugin infrastructure -> processing {:?}", params); - - let name = params.name.clone(); - - let fname = fname.to_string(); - - Ok(Some(PluginCommandBuilder::new(&name, &fname, params))) - } - Err(e) => Err(e), - }, - Err(e) => { - trace!(target: "nu::load", "plugin infrastructure -> incompatible {:?}", input); - Err(ShellError::untagged_runtime_error(format!( - "Error: {:?}", - e - ))) - } - } - } - Err(e) => Err(ShellError::untagged_runtime_error(format!( - "Error: {:?}", - e - ))), - }; - - let _ = child.wait(); - - result -} - -pub fn scan( - paths: Vec, -) -> Result, ShellError> { - let mut plugins = vec![]; - - let opts = glob::MatchOptions { - case_sensitive: false, - require_literal_separator: false, - require_literal_leading_dot: false, - }; - - for path in paths { - let mut pattern = path.to_path_buf(); - - pattern.push(std::path::Path::new("nu_plugin_[a-z0-9][a-z0-9]*")); - - let plugs: Vec<_> = glob::glob_with(&pattern.to_string_lossy(), opts)? - .filter_map(|x| x.ok()) - .collect(); - - let plugs: Vec<_> = plugs - .par_iter() - .filter_map(|path| { - let bin_name = { - if let Some(name) = path.file_name() { - name.to_str().unwrap_or("") - } else { - "" - } - }; - - // allow plugins with extensions on all platforms - let is_valid_name = { - bin_name - .chars() - .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.') - }; - - let is_executable = { - #[cfg(windows)] - { - bin_name.ends_with(".exe") - || bin_name.ends_with(".bat") - || bin_name.ends_with(".cmd") - || bin_name.ends_with(".py") - || bin_name.ends_with(".ps1") - } - - #[cfg(not(windows))] - { - !bin_name.contains('.') - || (bin_name.ends_with('.') - || bin_name.ends_with(".py") - || bin_name.ends_with(".rb") - || bin_name.ends_with(".sh") - || bin_name.ends_with(".bash") - || bin_name.ends_with(".zsh") - || bin_name.ends_with(".pl") - || bin_name.ends_with(".awk") - || bin_name.ends_with(".ps1")) - } - }; - - if is_valid_name && is_executable { - trace!(target: "nu::load", "plugin infrastructure -> Trying {:?}", path.display()); - build_plugin_command(path).unwrap_or(None) - } else { - None - } - }).map(|p| p.build()) - .collect::>(); - plugins.extend(plugs); - } - - Ok(plugins) -} diff --git a/crates/nu-engine/src/plugin/mod.rs b/crates/nu-engine/src/plugin/mod.rs deleted file mode 100644 index 1d1650c40c..0000000000 --- a/crates/nu-engine/src/plugin/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod build_plugin; -pub(crate) mod run_plugin; diff --git a/crates/nu-engine/src/plugin/run_plugin.rs b/crates/nu-engine/src/plugin/run_plugin.rs deleted file mode 100644 index 0372185cc8..0000000000 --- a/crates/nu-engine/src/plugin/run_plugin.rs +++ /dev/null @@ -1,549 +0,0 @@ -use crate::{command_args::CommandArgs, evaluate_baseline_expr, UnevaluatedCallInfo}; -use crate::{ - whole_stream_command::{whole_stream_command, WholeStreamCommand}, - EvaluationContext, -}; -use derive_new::new; - -use indexmap::IndexMap; -use log::trace; -use nu_errors::ShellError; -use nu_plugin::jsonrpc::JsonRpc; -use nu_protocol::{hir, Primitive, ReturnValue, Signature, UntaggedValue, Value}; -use nu_source::Tag; -use nu_stream::{ActionStream, InputStream, IntoActionStream}; -use serde::{self, Deserialize, Serialize}; -use std::collections::VecDeque; -use std::io::prelude::*; -use std::io::BufReader; -use std::io::Write; -use std::path::Path; -use std::process::{Child, Command, Stdio}; - -#[derive(Debug, Serialize, Deserialize)] -#[serde(tag = "method")] -#[allow(non_camel_case_types)] -pub enum NuResult { - response { - params: Result, ShellError>, - }, -} - -enum PluginCommand { - Filter(PluginFilter), - Sink(PluginSink), -} - -impl PluginCommand { - fn command(self) -> crate::whole_stream_command::Command { - match self { - PluginCommand::Filter(cmd) => whole_stream_command(cmd), - PluginCommand::Sink(cmd) => whole_stream_command(cmd), - } - } -} - -enum PluginMode { - Filter, - Sink, -} - -pub struct PluginCommandBuilder { - mode: PluginMode, - name: String, - path: String, - config: Signature, -} - -impl PluginCommandBuilder { - pub fn new( - name: impl Into, - path: impl Into, - config: impl Into, - ) -> Self { - let config = config.into(); - - PluginCommandBuilder { - mode: if config.is_filter { - PluginMode::Filter - } else { - PluginMode::Sink - }, - name: name.into(), - path: path.into(), - config, - } - } - - pub fn build(&self) -> crate::whole_stream_command::Command { - let mode = &self.mode; - - let name = self.name.clone(); - let path = self.path.clone(); - let config = self.config.clone(); - - let cmd = match mode { - PluginMode::Filter => PluginCommand::Filter(PluginFilter { name, path, config }), - PluginMode::Sink => PluginCommand::Sink(PluginSink { name, path, config }), - }; - - cmd.command() - } -} - -#[derive(new)] -pub struct PluginFilter { - name: String, - path: String, - config: Signature, -} - -impl WholeStreamCommand for PluginFilter { - fn name(&self) -> &str { - &self.name - } - - fn signature(&self) -> Signature { - self.config.clone() - } - - fn usage(&self) -> &str { - &self.config.usage - } - - fn extra_usage(&self) -> &str { - &self.config.extra_usage - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - run_filter(self.path.clone(), args) - } - - fn is_plugin(&self) -> bool { - true - } - - fn is_builtin(&self) -> bool { - false - } -} - -fn run_filter(path: String, args: CommandArgs) -> Result { - trace!("filter_plugin :: {}", path); - - let bos = vec![UntaggedValue::Primitive(Primitive::BeginningOfStream).into_untagged_value()] - .into_iter(); - let eos = [UntaggedValue::Primitive(Primitive::EndOfStream).into_untagged_value()]; - - let (call_info, input) = evaluate_once(args)?; - - let real_path = Path::new(&path); - let ext = real_path.extension(); - let ps1_file = match ext { - Some(ext) => ext == "ps1", - None => false, - }; - - let mut child: Child = if ps1_file { - Command::new("pwsh") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .args([ - "-NoLogo", - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - &real_path.to_string_lossy(), - ]) - .spawn() - .expect("Failed to spawn PowerShell process") - } else { - Command::new(path) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .expect("Failed to spawn child process") - }; - - trace!("filtering :: {:?}", call_info); - - Ok(bos - .chain(input) - .chain(eos) - .flat_map(move |item| { - match item { - Value { - value: UntaggedValue::Primitive(Primitive::BeginningOfStream), - .. - } => { - // Beginning of the stream - let stdin = child.stdin.as_mut().expect("Failed to open stdin"); - let stdout = child.stdout.as_mut().expect("Failed to open stdout"); - - let mut reader = BufReader::new(stdout); - - let request = JsonRpc::new("begin_filter", call_info.clone()); - let request_raw = serde_json::to_string(&request); - trace!("begin_filter:request {:?}", &request_raw); - - match request_raw { - Err(_) => { - return ActionStream::one(Err(ShellError::labeled_error( - "Could not load json from plugin", - "could not load json from plugin", - &call_info.name_tag, - ))); - } - Ok(request_raw) => { - match stdin.write(format!("{}\n", request_raw).as_bytes()) { - Ok(_) => {} - Err(err) => { - return ActionStream::one(Err(ShellError::unexpected( - err.to_string(), - ))); - } - } - } - } - - let mut input = String::new(); - match reader.read_line(&mut input) { - Ok(_) => { - let response = serde_json::from_str::(&input); - trace!("begin_filter:response {:?}", &response); - - match response { - Ok(NuResult::response { params }) => match params { - Ok(params) => params.into_iter().into_action_stream(), - Err(e) => { - vec![ReturnValue::Err(e)].into_iter().into_action_stream() - } - }, - - Err(e) => ActionStream::one(Err( - ShellError::untagged_runtime_error(format!( - "Error while processing begin_filter response: {:?} {}", - e, input - )), - )), - } - } - Err(e) => ActionStream::one(Err(ShellError::untagged_runtime_error( - format!("Error while reading begin_filter response: {:?}", e), - ))), - } - } - Value { - value: UntaggedValue::Primitive(Primitive::EndOfStream), - .. - } => { - // post stream contents - let stdin = child.stdin.as_mut().expect("Failed to open stdin"); - let stdout = child.stdout.as_mut().expect("Failed to open stdout"); - - let mut reader = BufReader::new(stdout); - - let request: JsonRpc> = JsonRpc::new("end_filter", vec![]); - let request_raw = serde_json::to_string(&request); - trace!("end_filter:request {:?}", &request_raw); - - match request_raw { - Err(_) => { - return ActionStream::one(Err(ShellError::labeled_error( - "Could not load json from plugin", - "could not load json from plugin", - &call_info.name_tag, - ))); - } - Ok(request_raw) => { - match stdin.write(format!("{}\n", request_raw).as_bytes()) { - Ok(_) => {} - Err(err) => { - return ActionStream::one(Err(ShellError::unexpected( - err.to_string(), - ))); - } - } - } - } - - let mut input = String::new(); - let stream = match reader.read_line(&mut input) { - Ok(_) => { - let response = serde_json::from_str::(&input); - trace!("end_filter:response {:?}", &response); - - match response { - Ok(NuResult::response { params }) => match params { - Ok(params) => params.into_iter().into_action_stream(), - Err(e) => { - vec![ReturnValue::Err(e)].into_iter().into_action_stream() - } - }, - Err(e) => vec![Err(ShellError::untagged_runtime_error(format!( - "Error while processing end_filter response: {:?} {}", - e, input - )))] - .into_iter() - .into_action_stream(), - } - } - Err(e) => vec![Err(ShellError::untagged_runtime_error(format!( - "Error while reading end_filter response: {:?}", - e - )))] - .into_iter() - .into_action_stream(), - }; - - let stdin = child.stdin.as_mut().expect("Failed to open stdin"); - - let request: JsonRpc> = JsonRpc::new("quit", vec![]); - let request_raw = serde_json::to_string(&request); - trace!("quit:request {:?}", &request_raw); - - match request_raw { - Ok(request_raw) => { - let _ = stdin.write(format!("{}\n", request_raw).as_bytes()); - // TODO: Handle error - } - Err(e) => { - return ActionStream::one(Err(ShellError::untagged_runtime_error( - format!("Error while processing quit response: {:?}", e), - ))); - } - } - let _ = child.wait(); - - stream - } - - v => { - // Stream contents - let stdin = child.stdin.as_mut().expect("Failed to open stdin"); - let stdout = child.stdout.as_mut().expect("Failed to open stdout"); - - let mut reader = BufReader::new(stdout); - - let request = JsonRpc::new("filter", v); - let request_raw = serde_json::to_string(&request); - trace!("filter:request {:?}", &request_raw); - - match request_raw { - Ok(request_raw) => { - let _ = stdin.write(format!("{}\n", request_raw).as_bytes()); - // TODO: Handle error - } - Err(e) => { - return ActionStream::one(Err(ShellError::untagged_runtime_error( - format!("Error while processing filter response: {:?}", e), - ))); - } - } - - let mut input = String::new(); - match reader.read_line(&mut input) { - Ok(_) => { - let response = serde_json::from_str::(&input); - trace!("filter:response {:?}", &response); - - match response { - Ok(NuResult::response { params }) => match params { - Ok(params) => params.into_iter().into_action_stream(), - Err(e) => { - vec![ReturnValue::Err(e)].into_iter().into_action_stream() - } - }, - Err(e) => ActionStream::one(Err( - ShellError::untagged_runtime_error(format!( - "Error while processing filter response: {:?}\n== input ==\n{}", - e, input - )), - )), - } - } - Err(e) => ActionStream::one(Err(ShellError::untagged_runtime_error( - format!("Error while reading filter response: {:?}", e), - ))), - } - } - } - }) - .into_action_stream()) -} - -#[derive(new)] -pub struct PluginSink { - name: String, - path: String, - config: Signature, -} - -impl WholeStreamCommand for PluginSink { - fn name(&self) -> &str { - &self.name - } - - fn signature(&self) -> Signature { - self.config.clone() - } - - fn usage(&self) -> &str { - &self.config.usage - } - - fn extra_usage(&self) -> &str { - &self.config.extra_usage - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - run_sink(self.path.clone(), args) - } - - fn is_plugin(&self) -> bool { - true - } - - fn is_builtin(&self) -> bool { - false - } -} - -fn run_sink(path: String, args: CommandArgs) -> Result { - let (call_info, input) = evaluate_once(args)?; - - let input: Vec = input.into_vec(); - - let request = JsonRpc::new("sink", (call_info, input)); - let request_raw = serde_json::to_string(&request); - if let Ok(request_raw) = request_raw { - if let Ok(mut tmpfile) = tempfile::NamedTempFile::new() { - let _ = writeln!(tmpfile, "{}", request_raw); - let _ = tmpfile.flush(); - - let real_path = Path::new(&path); - let ext = real_path.extension(); - let ps1_file = match ext { - Some(ext) => ext == "ps1", - None => false, - }; - - // TODO: This sink may not work in powershell, trying to find - // an example of what CallInfo would look like in this temp file - let child = if ps1_file { - Command::new("pwsh") - .args([ - "-NoLogo", - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - &real_path.to_string_lossy(), - tmpfile - .path() - .to_str() - .expect("Failed getting tmpfile path"), - ]) - .spawn() - } else { - Command::new(path).arg(&tmpfile.path()).spawn() - }; - - if let Ok(mut child) = child { - let _ = child.wait(); - - Ok(ActionStream::empty()) - } else { - Err(ShellError::untagged_runtime_error( - "Could not create process for sink command", - )) - } - } else { - Err(ShellError::untagged_runtime_error( - "Could not open file to send sink command message", - )) - } - } else { - Err(ShellError::untagged_runtime_error( - "Could not create message to sink command", - )) - } -} - -/// Associated information for the call of a command, including the args passed to the command and a tag that spans the name of the command being called -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct CallInfo { - /// The arguments associated with this call - pub args: EvaluatedArgs, - /// The tag (underline-able position) of the name of the call itself - pub name_tag: Tag, -} - -/// The set of positional and named arguments, after their values have been evaluated. -/// -/// * Positional arguments are those who are given as values, without any associated flag. For example, in `foo arg1 arg2`, both `arg1` and `arg2` are positional arguments. -/// * Named arguments are those associated with a flag. For example, `foo --given bar` the named argument would be name `given` and the value `bar`. -#[derive(Debug, Default, new, Serialize, Deserialize, Clone)] -pub struct EvaluatedArgs { - pub positional: Option>, - pub named: Option>, -} - -fn evaluate_once(args: CommandArgs) -> Result<(CallInfo, InputStream), ShellError> { - let input = args.input; - let call_info = evaluate_command(args.call_info, args.context)?; - - Ok((call_info, input)) -} - -fn evaluate_command( - args: UnevaluatedCallInfo, - ctx: EvaluationContext, -) -> Result { - let name_tag = args.name_tag.clone(); - let args = evaluate_args(&args.args, &ctx)?; - - Ok(CallInfo { args, name_tag }) -} - -fn evaluate_args(call: &hir::Call, ctx: &EvaluationContext) -> Result { - let mut positional_args: Vec = vec![]; - - if let Some(positional) = &call.positional { - for pos in positional { - let result = evaluate_baseline_expr(pos, ctx)?; - positional_args.push(result); - } - } - - let positional = if !positional_args.is_empty() { - Some(positional_args) - } else { - None - }; - - let mut named_args = IndexMap::new(); - - if let Some(named) = &call.named { - for (name, value) in named { - match value { - hir::NamedValue::PresentSwitch(tag) => { - named_args.insert(name.clone(), UntaggedValue::boolean(true).into_value(tag)); - } - hir::NamedValue::Value(_, expr) => { - named_args.insert(name.clone(), evaluate_baseline_expr(expr, ctx)?); - } - _ => {} - }; - } - } - - let named = if !named_args.is_empty() { - Some(named_args) - } else { - None - }; - - Ok(EvaluatedArgs::new(positional, named)) -} diff --git a/crates/nu-engine/src/print.rs b/crates/nu-engine/src/print.rs deleted file mode 100644 index f1072a4726..0000000000 --- a/crates/nu-engine/src/print.rs +++ /dev/null @@ -1,18 +0,0 @@ -use nu_source::Text; - -use crate::EvaluationContext; - -pub fn maybe_print_errors(context: &EvaluationContext, source: Text) -> bool { - let errors = context.current_errors().clone(); - let mut errors = errors.lock(); - - if errors.len() > 0 { - let error = errors[0].clone(); - *errors = vec![]; - - context.host().lock().print_err(error, &source); - true - } else { - false - } -} diff --git a/crates/nu-engine/src/script.rs b/crates/nu-engine/src/script.rs deleted file mode 100644 index fd9c41f0c1..0000000000 --- a/crates/nu-engine/src/script.rs +++ /dev/null @@ -1,312 +0,0 @@ -use crate::{evaluate::internal::InternalIterator, maybe_print_errors, run_block, shell::CdArgs}; -use crate::{BufCodecReader, MaybeTextCodec, StringOrBinary}; -use nu_errors::ShellError; -use nu_path::{canonicalize_with, trim_trailing_slash}; -use nu_protocol::hir::{ - Call, ClassifiedCommand, Expression, ExternalRedirection, InternalCommand, Literal, - NamedArguments, SpannedExpression, -}; -use nu_protocol::{Primitive, UntaggedValue, Value}; -use nu_stream::{InputStream, IntoInputStream}; - -use crate::EvaluationContext; -use log::{debug, trace}; -use nu_source::{AnchorLocation, Span, Tag, Tagged, Text}; -use std::path::{Path, PathBuf}; -use std::{error::Error, sync::atomic::Ordering}; -use std::{io::BufReader, iter::Iterator}; - -#[derive(Debug)] -pub enum LineResult { - Success(String), - Error(String, ShellError), - Break, - CtrlC, - CtrlD, - ClearHistory, -} - -fn chomp_newline(s: &str) -> &str { - if let Some(s) = s.strip_suffix('\n') { - s - } else { - s - } -} - -pub fn run_script_in_dir( - script: String, - dir: &Path, - ctx: &EvaluationContext, -) -> Result<(), Box> { - //Save path before to switch back to it after executing script - let path_before = ctx.shell_manager().path(); - - ctx.shell_manager() - .set_path(dir.to_string_lossy().to_string()); - run_script_standalone(script, false, ctx, false)?; - ctx.shell_manager().set_path(path_before); - - Ok(()) -} - -/// 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 -pub fn process_script( - script_text: &str, - ctx: &EvaluationContext, - redirect_stdin: bool, - span_offset: usize, - cli_mode: bool, -) -> LineResult { - if script_text.trim() == "" { - LineResult::Success(script_text.to_string()) - } else { - let line = chomp_newline(script_text); - - let (block, err) = nu_parser::parse(line, span_offset, &ctx.scope); - - debug!("{:#?}", block); - - if let Some(failure) = err { - return LineResult::Error(line.to_string(), failure.into()); - } - - // There's a special case to check before we process the pipeline: - // If we're giving a path by itself - // ...and it's not a command in the path - // ...and it doesn't have any arguments - // ...and we're in the CLI - // ...then change to this directory - if cli_mode - && block.block.len() == 1 - && block.block[0].pipelines.len() == 1 - && block.block[0].pipelines[0].list.len() == 1 - { - if let ClassifiedCommand::Internal(InternalCommand { - ref name, - ref args, - name_span, - }) = block.block[0].pipelines[0].list[0] - { - let internal_name = name; - let name = args - .positional - .as_ref() - .and_then(|positionals| { - positionals.get(0).map(|e| { - if let Expression::Literal(Literal::String(ref s)) = e.expr { - &s - } else { - "" - } - }) - }) - .unwrap_or(""); - - ctx.sync_path_to_env(); - if internal_name == "run_external" - && args - .positional - .as_ref() - .map(|v| v.len() == 1) - .unwrap_or(true) - && args - .named - .as_ref() - .map(NamedArguments::is_empty) - .unwrap_or(true) - && canonicalize_with(name, ctx.shell_manager().path()).is_ok() - && Path::new(&name).is_dir() - && !ctx.host().lock().is_external_cmd(name) - { - let tag = Tag { - anchor: Some(AnchorLocation::Source(line.into())), - span: name_span, - }; - let path = { - // Here we work differently if we're in Windows because of the expected Windows behavior - #[cfg(windows)] - { - if name.ends_with(':') { - // This looks like a drive shortcut. We need to a) switch drives and b) go back to the previous directory we were viewing on that drive - // But first, we need to save where we are now - let current_path = ctx.shell_manager().path(); - - let split_path: Vec<_> = current_path.split(':').collect(); - if split_path.len() > 1 { - ctx.windows_drives_previous_cwd() - .lock() - .insert(split_path[0].to_string(), current_path); - } - - let name = name.to_uppercase(); - let new_drive: Vec<_> = name.split(':').collect(); - - if let Some(val) = - ctx.windows_drives_previous_cwd().lock().get(new_drive[0]) - { - val.to_string() - } else { - format!("{}\\", name) - } - } else { - name.to_string() - } - } - #[cfg(not(windows))] - { - name.to_string() - } - }; - - let path = trim_trailing_slash(&path); - - let cd_args = CdArgs { - path: Some(Tagged { - item: PathBuf::from(path), - tag: tag.clone(), - }), - }; - - return match ctx.shell_manager().cd(cd_args, tag) { - Err(e) => LineResult::Error(line.to_string(), e), - Ok(stream) => { - let iter = InternalIterator { - context: ctx.clone(), - leftovers: InputStream::empty(), - input: stream, - }; - for _ in iter { - //nullopt, commands are run by iterating over iter - } - LineResult::Success(line.to_string()) - } - }; - } - } - } - - let input_stream = if redirect_stdin { - let file = std::io::stdin(); - let buf_reader = BufReader::new(file); - let buf_codec = BufCodecReader::new(buf_reader, MaybeTextCodec::default()); - let stream = buf_codec.map(|line| { - if let Ok(line) = line { - let primitive = match line { - StringOrBinary::String(s) => Primitive::String(s), - StringOrBinary::Binary(b) => Primitive::Binary(b.into_iter().collect()), - }; - - Ok(Value { - value: UntaggedValue::Primitive(primitive), - tag: Tag::unknown(), - }) - } else { - panic!("Internal error: could not read lines of text from stdin") - } - }); - stream.into_input_stream() - } else { - InputStream::empty() - }; - - trace!("{:#?}", block); - - let result = run_block(&block, ctx, input_stream, ExternalRedirection::None); - - match result { - Ok(input) => { - // Running a pipeline gives us back a stream that we can then - // work through. At the top level, we just want to pull on the - // values to compute them. - - let autoview_cmd = ctx - .get_command("autoview") - .expect("Could not find autoview command"); - - if let Ok(mut output_stream) = ctx.run_command( - autoview_cmd, - Tag::unknown(), - Call::new( - Box::new(SpannedExpression::new( - Expression::string("autoview".to_string()), - Span::unknown(), - )), - Span::unknown(), - ), - input, - ) { - loop { - match output_stream.next() { - Some(Value { - value: UntaggedValue::Error(e), - .. - }) => return LineResult::Error(line.to_string(), e), - Some(_item) => { - if ctx.ctrl_c().load(Ordering::SeqCst) { - break; - } - } - None => break, - } - } - } - - LineResult::Success(line.to_string()) - } - Err(err) => LineResult::Error(line.to_string(), err), - } - } -} - -pub fn run_script_standalone( - script_text: String, - redirect_stdin: bool, - context: &EvaluationContext, - exit_on_error: bool, -) -> Result<(), ShellError> { - context - .shell_manager() - .enter_script_mode() - .map_err(ShellError::from)?; - let line = process_script(&script_text, context, redirect_stdin, 0, false); - - match line { - LineResult::Success(line) => { - let error_code = { - let errors = context.current_errors().clone(); - let errors = errors.lock(); - - if errors.len() > 0 { - 1 - } else { - 0 - } - }; - - maybe_print_errors(context, Text::from(line)); - if error_code != 0 && exit_on_error { - std::process::exit(error_code); - } - } - - LineResult::Error(line, err) => { - context - .host() - .lock() - .print_err(err, &Text::from(line.clone())); - - maybe_print_errors(context, Text::from(line)); - if exit_on_error { - std::process::exit(1); - } - } - - _ => {} - } - - //exit script mode shell - context.shell_manager().remove_at_current(); - - Ok(()) -} diff --git a/crates/nu-engine/src/shell/mod.rs b/crates/nu-engine/src/shell/mod.rs deleted file mode 100644 index fce62a7c89..0000000000 --- a/crates/nu-engine/src/shell/mod.rs +++ /dev/null @@ -1,54 +0,0 @@ -use nu_stream::{ActionStream, OutputStream}; - -use crate::maybe_text_codec::StringOrBinary; -pub use crate::shell::shell_args::{CdArgs, CopyArgs, LsArgs, MkdirArgs, MvArgs, RemoveArgs}; -use crate::CommandArgs; -use encoding_rs::Encoding; -use nu_errors::ShellError; -use nu_source::{Span, Tag}; -use std::path::{Path, PathBuf}; -use std::sync::atomic::AtomicBool; -use std::sync::Arc; - -pub(crate) mod painter; -pub(crate) mod palette; -pub(crate) mod shell_args; -pub(crate) mod shell_manager; -pub mod value_shell; - -pub trait Shell: std::fmt::Debug { - fn is_interactive(&self) -> bool; - fn name(&self) -> String; - fn homedir(&self) -> Option; - - fn ls( - &self, - args: LsArgs, - name: Tag, - ctrl_c: Arc, - ) -> Result; - fn cd(&self, args: CdArgs, name: Tag) -> Result; - fn cp(&self, args: CopyArgs, name: Tag, path: &str) -> Result; - fn mkdir(&self, args: MkdirArgs, name: Tag, path: &str) -> Result; - fn mv(&self, args: MvArgs, name: Tag, path: &str) -> Result; - fn rm(&self, args: RemoveArgs, name: Tag, path: &str) -> Result; - fn path(&self) -> String; - fn pwd(&self, args: CommandArgs) -> Result; - fn set_path(&mut self, path: String); - fn open( - &self, - path: &Path, - name: Span, - with_encoding: Option<&'static Encoding>, - ) -> Result< - Box> + Send + Sync>, - ShellError, - >; - fn save( - &mut self, - path: &Path, - contents: &[u8], - name: Span, - append: bool, - ) -> Result; -} diff --git a/crates/nu-engine/src/shell/painter.rs b/crates/nu-engine/src/shell/painter.rs deleted file mode 100644 index bf1bcf9083..0000000000 --- a/crates/nu-engine/src/shell/painter.rs +++ /dev/null @@ -1,85 +0,0 @@ -use crate::evaluate::scope::Scope; -use crate::shell::palette::Palette; -use nu_ansi_term::{Color, Style}; -use nu_parser::ParserScope; -use nu_protocol::hir::FlatShape; -use nu_source::Spanned; -use std::borrow::Cow; - -// FIXME: find a good home, as nu-engine may be too core for styling -pub struct Painter { - original: Vec, - styles: Vec