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/.gitignore b/.gitignore index 4c234e523b..6956977c38 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +<<<<<<< HEAD /target /scratch **/*.rs.bk @@ -20,3 +21,9 @@ debian/nu/ # VSCode's IDE items .vscode/* +======= +history.txt +/target +/.vscode +.DS_Store +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce diff --git a/Cargo.lock b/Cargo.lock index 595c1f5a7e..ba99674365 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,6 +3,19 @@ version = 3 [[package]] +<<<<<<< HEAD +======= +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "addr2line" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -18,20 +31,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] +<<<<<<< HEAD name = "adler32" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" [[package]] +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "ahash" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ +<<<<<<< HEAD "getrandom 0.2.3", "once_cell", "version_check", +======= + "getrandom 0.2.4", + "once_cell", + "version_check 0.9.4", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -59,12 +81,31 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "ansi_colours" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60e2fb6138a49ad9f1cb3c6d8f8ccbdd5e62b4dab317c1b435a47ecd7da1d28f" dependencies = [ "cc", +======= +name = "ansi-cut" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe8d2994390ae20a3eb52a909f9518a89f8fd7e6990f3d25d38e51021b2c8ce" +dependencies = [ + "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", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -73,14 +114,30 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" dependencies = [ +<<<<<<< HEAD "winapi 0.3.9", +======= + "winapi", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] name = "anyhow" +<<<<<<< HEAD version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203" +======= +version = "1.0.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0" + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce [[package]] name = "arrayvec" @@ -108,16 +165,27 @@ dependencies = [ [[package]] name = "arrow2" +<<<<<<< HEAD version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d873e2775c3d87a4e8d77aa544cbd43f34a0779d5164c59e7c6a1dd0678eb395" dependencies = [ "ahash", +======= +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3452b2ae9727464a31a726c07ffec0c0da3b87831610d9ac99fc691c78b3a44" +dependencies = [ +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "arrow-format", "base64", "chrono", "csv", +<<<<<<< HEAD "futures 0.3.18", +======= + "futures", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "hash_hasher", "indexmap", "lexical-core", @@ -132,6 +200,35 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD +======= +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]] +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "async-stream" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -154,9 +251,15 @@ dependencies = [ [[package]] name = "async-trait" +<<<<<<< HEAD version = "0.1.51" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e" +======= +version = "0.1.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "proc-macro2", "quote", @@ -171,7 +274,11 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi", "libc", +<<<<<<< HEAD "winapi 0.3.9", +======= + "winapi", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -182,6 +289,7 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "backtrace" +<<<<<<< HEAD version = "0.3.63" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "321629d8ba6513061f26707241fa9bc89524ff1cd7a915a97ef0c62c666ce1b6" @@ -191,6 +299,17 @@ dependencies = [ "cfg-if 1.0.0", "libc", "miniz_oxide 0.4.4", +======= +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e121dee8023ce33ab248d9ce1493df03c3b38a659b240096fcbd7048ff9c31f" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "object", "rustc-demangle", ] @@ -202,6 +321,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] +<<<<<<< HEAD name = "bat" version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -228,6 +348,8 @@ dependencies = [ ] [[package]] +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "bigdecimal" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -240,6 +362,7 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "bincode" version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -253,6 +376,21 @@ name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +======= +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce [[package]] name = "bitpacking" @@ -264,19 +402,52 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD +======= +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]] +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "block-buffer" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ +<<<<<<< HEAD "generic-array", +======= + "generic-array 0.14.5", +] + +[[package]] +name = "block-buffer" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1d36a02058e76b040de25a4464ba1c80935655595b661505c8b39b664828b95" +dependencies = [ + "generic-array 0.14.5", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] name = "brotli" +<<<<<<< HEAD version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71cb90ade945043d3d53597b2fc359bb063db8ade2bcffe7997351d0756e9d50" +======= +version = "3.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f838e47a451d5a8fa552371f80024dd6ace9b7acdf25c4c3d0f9bc6816fb1c39" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -294,6 +465,7 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "bson" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -313,6 +485,8 @@ dependencies = [ ] [[package]] +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "bstr" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -325,6 +499,7 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "bugreport" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -340,6 +515,12 @@ name = "bumpalo" version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" +======= +name = "bumpalo" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce [[package]] name = "byte-unit" @@ -351,12 +532,15 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "bytemuck" version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72957246c41db82b8ef88a5486143830adeb8227ef9837740bdec67724cf2c5b" [[package]] +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "byteorder" version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -364,6 +548,7 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" +<<<<<<< HEAD version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" @@ -383,6 +568,17 @@ name = "bytes" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +======= +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" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce [[package]] name = "bzip2" @@ -421,10 +617,17 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "cassowary" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" +======= +name = "capnp" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16c262726f68118392269a3f7a5546baf51dcfe5cb3c3f0957b502106bf1a065" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce [[package]] name = "cc" @@ -437,12 +640,15 @@ dependencies = [ [[package]] name = "cfg-if" +<<<<<<< HEAD version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" [[package]] name = "cfg-if" +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" @@ -457,8 +663,13 @@ dependencies = [ "num-integer", "num-traits", "serde", +<<<<<<< HEAD "time 0.1.44", "winapi 0.3.9", +======= + "time", + "winapi", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -481,6 +692,7 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "clipboard-win" version = "4.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -501,6 +713,27 @@ dependencies = [ "libc", "serde", "winapi 0.3.9", +======= +name = "chrono-tz" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58549f1842da3080ce63002102d5bc954c7bc843d4f47818e642abdc36253552" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf 0.10.1", +] + +[[package]] +name = "chrono-tz-build" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db058d493fb2f65f41861bfed7e3fe6335264a9f0f92710cab5bdf01fef09069" +dependencies = [ + "parse-zoneinfo", + "phf 0.10.1", + "phf_codegen 0.10.0", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -513,6 +746,7 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "codespan-reporting" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -559,6 +793,20 @@ dependencies = [ "terminal_size", "unicode-width", "winapi 0.3.9", +======= +name = "console" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "regex", + "terminal_size", + "unicode-width", + "winapi", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -568,6 +816,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb58b6451e8c2a812ad979ed1d83378caa5e927eef2622017a45f251457c2c9d" [[package]] +<<<<<<< HEAD name = "content_inspector" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -575,6 +824,12 @@ checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38" dependencies = [ "memchr", ] +======= +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce [[package]] name = "convert_case" @@ -609,20 +864,36 @@ dependencies = [ [[package]] name = "crc32fast" +<<<<<<< HEAD version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "738c290dfaea84fc1ca15ad9c168d083b05a714e1efddd8edaab678dc28d2836" dependencies = [ "cfg-if 1.0.0", +======= +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2209c310e29876f7f0b2721e7e26b84aff178aa3da5d091f9bfbf47669e60e3" +dependencies = [ + "cfg-if", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] name = "crossbeam-channel" +<<<<<<< HEAD version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" dependencies = [ "cfg-if 1.0.0", +======= +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa" +dependencies = [ + "cfg-if", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "crossbeam-utils", ] @@ -632,18 +903,30 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" dependencies = [ +<<<<<<< HEAD "cfg-if 1.0.0", +======= + "cfg-if", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" +<<<<<<< HEAD version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec02e091aa634e2c3ada4a392989e7c3116673ef0ac5b72232439094d73b7fd" dependencies = [ "cfg-if 1.0.0", +======= +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97242a70df9b89a65d0b6df3c4bf5b9ce03c5b7309019777fbde37e7537f8762" +dependencies = [ + "cfg-if", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "crossbeam-utils", "lazy_static", "memoffset", @@ -652,16 +935,25 @@ dependencies = [ [[package]] name = "crossbeam-utils" +<<<<<<< HEAD version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" dependencies = [ "cfg-if 1.0.0", +======= +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcae03edb34f947e64acdb1c33ec169824e20657e9ecb61cef6c8c74dcb8120" +dependencies = [ + "cfg-if", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "lazy_static", ] [[package]] name = "crossterm" +<<<<<<< HEAD version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c36c10130df424b2f3552fcc2ddcd9b28a27b1e54b358b45874f88d1ca6888c" @@ -674,15 +966,38 @@ dependencies = [ "parking_lot", "signal-hook", "winapi 0.3.9", +======= +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c85525306c4291d1b73ce93c8acf9c339f9b213aef6c1d85c3830cbf1c16325c" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "serde", + "signal-hook", + "signal-hook-mio", + "winapi", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] name = "crossterm_winapi" +<<<<<<< HEAD version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0da8964ace4d3e4a044fd027919b2237000b24315a37c916f61809f1ff2140b9" dependencies = [ "winapi 0.3.9", +======= +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +dependencies = [ + "winapi", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -692,6 +1007,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] +<<<<<<< HEAD name = "crypto-mac" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -699,6 +1015,14 @@ checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" dependencies = [ "generic-array", "subtle", +======= +name = "crypto-common" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d6b536309245c849479fba3da410962a43ed8e51c26b729208ec0ac2798d0" +dependencies = [ + "generic-array 0.14.5", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -709,9 +1033,15 @@ checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" dependencies = [ "cssparser-macros", "dtoa-short", +<<<<<<< HEAD "itoa", "matches", "phf", +======= + "itoa 0.4.8", + "matches", + "phf 0.8.0", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "proc-macro2", "quote", "smallvec", @@ -730,9 +1060,15 @@ dependencies = [ [[package]] name = "cstr_core" +<<<<<<< HEAD version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "917ba9efe9e1e736671d5a03f006afc4e7e3f32503e2077e0bcaf519c0c8c1d3" +======= +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "644828c273c063ab0d39486ba42a5d1f3a499d35529c759e763a9c6cb8a0fb08" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "cty", "memchr", @@ -746,7 +1082,11 @@ checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" dependencies = [ "bstr", "csv-core", +<<<<<<< HEAD "itoa", +======= + "itoa 0.4.8", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "ryu", "serde", ] @@ -761,6 +1101,7 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "ctrlc" version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -768,6 +1109,25 @@ checksum = "377c9b002a72a0b2c1a18c62e2f3864bdfea4a015e3683a96e24aa45dd6c02d1" dependencies = [ "nix", "winapi 0.3.9", +======= +name = "ctor" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +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", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -777,6 +1137,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" [[package]] +<<<<<<< HEAD name = "deflate" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -794,10 +1155,22 @@ checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535" dependencies = [ "proc-macro2", "quote", +======= +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "syn", ] [[package]] +<<<<<<< HEAD name = "derive_more" version = "0.99.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -811,11 +1184,38 @@ dependencies = [ ] [[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]] +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "digest" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ +<<<<<<< HEAD "generic-array", ] @@ -827,13 +1227,44 @@ checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" dependencies = [ "cfg-if 1.0.0", "dirs-sys-next", +======= + "generic-array 0.14.5", +] + +[[package]] +name = "digest" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b697d66081d42af4fba142d56918a3cb21dc8eb63372c6b85d14f44fb9c5979b" +dependencies = [ + "block-buffer 0.10.0", + "crypto-common", + "generic-array 0.14.5", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] name = "dirs" +<<<<<<< HEAD version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" +======= +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +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" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "dirs-sys", ] @@ -844,7 +1275,11 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" dependencies = [ +<<<<<<< HEAD "cfg-if 1.0.0", +======= + "cfg-if", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "dirs-sys-next", ] @@ -855,8 +1290,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" dependencies = [ "libc", +<<<<<<< HEAD "redox_users", "winapi 0.3.9", +======= + "redox_users 0.4.0", + "winapi", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -866,8 +1306,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", +<<<<<<< HEAD "redox_users", "winapi 0.3.9", +======= + "redox_users 0.4.0", + "winapi", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -898,7 +1343,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13276c5dbd7f365e00efe6631242772fe6615e1899df84d1f6ce3ae7b48209f6" dependencies = [ "chrono", +<<<<<<< HEAD "chrono-tz", +======= + "chrono-tz 0.5.3", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "lazy_static", "num-traits", "rust_decimal", @@ -911,12 +1360,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453440c271cf5577fd2a40e4942540cb7d0d2f85e27c8d07dd0023c925a67541" [[package]] +<<<<<<< HEAD name = "dyn-clone" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf" [[package]] +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "ego-tree" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -944,6 +1396,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] +<<<<<<< HEAD name = "encoding" version = "0.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1106,6 +1559,92 @@ checksum = "cfc110fe50727d46a428eed832df40affe9bf74d077cac1bf3f2718e823f14c5" dependencies = [ "cfg-if 1.0.0", "libc", +======= +name = "encoding_rs" +version = "0.8.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "erased-serde" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56047058e1ab118075ca22f9ecd737bcc961aa3566a3019cb71388afa280bd8a" +dependencies = [ + "serde", +] + +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "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", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "windows-sys", ] @@ -1115,7 +1654,11 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12d741e2415d4e2e5bd1c1d00409d1a8865a57892c2d689b504365655d237d43" dependencies = [ +<<<<<<< HEAD "winapi 0.3.9", +======= + "winapi", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -1135,10 +1678,17 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" dependencies = [ +<<<<<<< HEAD "cfg-if 1.0.0", "crc32fast", "libc", "miniz_oxide 0.4.4", +======= + "cfg-if", + "crc32fast", + "libc", + "miniz_oxide", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -1173,12 +1723,15 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "fs_extra" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" [[package]] +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "futf" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1190,6 +1743,7 @@ dependencies = [ [[package]] name = "futures" +<<<<<<< HEAD version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" @@ -1199,6 +1753,11 @@ name = "futures" version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cd0210d8c325c245ff06fd95a3b13689a1a276ac8cfa8e8720cb840bfb84b9e" +======= +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28560757fe2bb34e79f907794bb6b22ae8b0e5c669b638a1132f2592b19035b4" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "futures-channel", "futures-core", @@ -1211,9 +1770,15 @@ dependencies = [ [[package]] name = "futures-channel" +<<<<<<< HEAD version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fc8cd39e3dbf865f7340dce6a2d401d24fd37c6fe6c4f0ee0de8bfca2252d27" +======= +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "futures-core", "futures-sink", @@ -1221,6 +1786,7 @@ dependencies = [ [[package]] name = "futures-core" +<<<<<<< HEAD version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445" @@ -1230,6 +1796,17 @@ name = "futures-executor" version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97" +======= +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7" + +[[package]] +name = "futures-executor" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "futures-core", "futures-task", @@ -1238,6 +1815,7 @@ dependencies = [ [[package]] name = "futures-io" +<<<<<<< HEAD version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e481354db6b5c353246ccf6a728b0c5511d752c08da7260546fc0933869daa11" @@ -1247,6 +1825,17 @@ name = "futures-macro" version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89f17b21645bc4ed773c69af9c9a0effd4a3f1a3876eadd453469f8854e7fdd" +======= +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2" + +[[package]] +name = "futures-macro" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbd947adfffb0efc70599b3ddcf7b5597bb5fa9e245eb99f62b3a5f7bb8bd3c" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "proc-macro2", "quote", @@ -1255,6 +1844,7 @@ dependencies = [ [[package]] name = "futures-sink" +<<<<<<< HEAD version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "996c6442437b62d21a32cd9906f9c41e7dc1e19a9579843fad948696769305af" @@ -1272,6 +1862,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e" dependencies = [ "futures 0.1.31", +======= +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508" + +[[package]] +name = "futures-task" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72" + +[[package]] +name = "futures-util" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164" +dependencies = [ +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "futures-channel", "futures-core", "futures-io", @@ -1282,7 +1890,10 @@ dependencies = [ "pin-project-lite", "pin-utils", "slab", +<<<<<<< HEAD "tokio-io", +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -1296,12 +1907,39 @@ dependencies = [ [[package]] name = "generic-array" +<<<<<<< HEAD version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" dependencies = [ "typenum", "version_check", +======= +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[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", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -1319,18 +1957,30 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ +<<<<<<< HEAD "cfg-if 1.0.0", +======= + "cfg-if", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "libc", "wasi 0.9.0+wasi-snapshot-preview1", ] [[package]] name = "getrandom" +<<<<<<< HEAD version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" dependencies = [ "cfg-if 1.0.0", +======= +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" +dependencies = [ + "cfg-if", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "libc", "wasi 0.10.0+wasi-snapshot-preview1", ] @@ -1348,6 +1998,7 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "gimli" version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1370,12 +2021,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe69f1cbdb6e28af2bac214e943b99ce8a0a06b447d15d3e61161b0423139f3f" dependencies = [ "proc-macro-hack", +======= +name = "ghost" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5bcf1bbeab73aa4cf2fde60a846858dc036163c7c33bec309f8d17de785479" +dependencies = [ +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "proc-macro2", "quote", "syn", ] [[package]] +<<<<<<< HEAD +======= +name = "gimli" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" + +[[package]] +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "git2" version = "0.13.25" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1385,6 +2052,11 @@ dependencies = [ "libc", "libgit2-sys", "log", +<<<<<<< HEAD +======= + "openssl-probe", + "openssl-sys", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "url", ] @@ -1401,6 +2073,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] +<<<<<<< HEAD name = "globset" version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1437,6 +2110,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fd819562fcebdac5afc5c113c3ec36f902840b70fd4fc458799c8ce4607ae55" dependencies = [ "bytes 1.1.0", +======= +name = "h2" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f1f717ddc7b2ba36df7e871fd88db79326551d3d6f1fc406fbfd28b582ff8e" +dependencies = [ + "bytes", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "fnv", "futures-core", "futures-sink", @@ -1461,6 +2142,18 @@ dependencies = [ [[package]] name = "hash32" +<<<<<<< HEAD +======= +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4041af86e63ac4298ce40e5cca669066e75b6f1aa3390fe2561ffa5e1d9f4cc" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hash32" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" @@ -1485,21 +2178,41 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "hashlink" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" dependencies = [ "hashbrown", +======= +name = "heapless" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74911a68a1658cfcfb61bc0ccfbd536e3b6e906f8c2f7883ee50157e3e2184f1" +dependencies = [ + "as-slice", + "generic-array 0.13.3", + "hash32 0.1.1", + "stable_deref_trait", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] name = "heapless" +<<<<<<< HEAD version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c1ad878e07405df82b695089e63d278244344f80e764074d0bdfe99b89460f3" dependencies = [ "hash32", +======= +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d076121838e03f862871315477528debffdb7462fb229216ecef91b1a3eb31eb" +dependencies = [ + "hash32 0.2.1", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "spin", "stable_deref_trait", ] @@ -1514,12 +2227,15 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "heck" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" [[package]] +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "hermit-abi" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1535,6 +2251,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] +<<<<<<< HEAD name = "hmac" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1554,6 +2271,8 @@ dependencies = [ ] [[package]] +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "html5ever" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1575,6 +2294,7 @@ checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" [[package]] name = "http" +<<<<<<< HEAD version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b" @@ -1582,6 +2302,15 @@ dependencies = [ "bytes 1.1.0", "fnv", "itoa", +======= +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.1", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -1590,7 +2319,11 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" dependencies = [ +<<<<<<< HEAD "bytes 1.1.0", +======= + "bytes", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "http", "pin-project-lite", ] @@ -1618,11 +2351,19 @@ dependencies = [ [[package]] name = "hyper" +<<<<<<< HEAD version = "0.14.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "436ec0091e4f20e655156a30a0df3770fe2900aa301e548e08446ec794b6953c" dependencies = [ "bytes 1.1.0", +======= +version = "0.14.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ec3e62bdc98a2f0393a5048e4c30ef659440ea6e0e572965103e72bd836f55" +dependencies = [ + "bytes", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "futures-channel", "futures-core", "futures-util", @@ -1631,7 +2372,11 @@ dependencies = [ "http-body", "httparse", "httpdate", +<<<<<<< HEAD "itoa", +======= + "itoa 0.4.8", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "pin-project-lite", "socket2", "tokio", @@ -1646,7 +2391,11 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ +<<<<<<< HEAD "bytes 1.1.0", +======= + "bytes", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "hyper", "native-tls", "tokio", @@ -1674,6 +2423,7 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "image" version = "0.23.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1687,13 +2437,32 @@ dependencies = [ "num-rational 0.3.2", "num-traits", "png", +======= +name = "im" +version = "15.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "111c1983f3c5bb72732df25cddacee9b546d08325fb584b5ebd38148be7b0246" +dependencies = [ + "bitmaps", + "rand_core 0.5.1", + "rand_xoshiro", + "sized-chunks", + "typenum", + "version_check 0.9.4", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] name = "indexmap" +<<<<<<< HEAD version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +======= +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "autocfg", "hashbrown", @@ -1701,6 +2470,7 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "insta" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1716,12 +2486,18 @@ dependencies = [ ] [[package]] +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "instant" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ +<<<<<<< HEAD "cfg-if 1.0.0", +======= + "cfg-if", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -1735,12 +2511,31 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "iovec" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" dependencies = [ "libc", +======= +name = "inventory" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6b5d8c669bfbad811d95ddd7a1c6cf9cfdbf2777e59928b6f3fa8ff54f72a0" +dependencies = [ + "ctor", + "ghost", +] + +[[package]] +name = "io-lifetimes" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ef6787e7f0faedc040f95716bdd0e62bcfcf4ba93da053b62dea2691c13864" +dependencies = [ + "winapi", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -1750,6 +2545,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" [[package]] +<<<<<<< HEAD +======= +name = "is_ci" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" + +[[package]] +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "is_debug" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1761,14 +2565,24 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa9acdc6d67b75e626ad644734e8bc6df893d9cd2a834129065d3dd6158ea9c8" dependencies = [ +<<<<<<< HEAD "winapi 0.3.9", +======= + "winapi", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] name = "itertools" +<<<<<<< HEAD version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" +======= +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "either", ] @@ -1780,6 +2594,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] +<<<<<<< HEAD +======= +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + +[[package]] +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "jobserver" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1789,6 +2612,7 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "jpeg-decoder" version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1799,6 +2623,12 @@ name = "js-sys" version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +======= +name = "js-sys" +version = "0.3.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "wasm-bindgen", ] @@ -1810,12 +2640,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] +<<<<<<< HEAD name = "lazycell" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "lexical" version = "6.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1890,9 +2723,15 @@ dependencies = [ [[package]] name = "libc" +<<<<<<< HEAD version = "0.2.112" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" +======= +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce [[package]] name = "libgit2-sys" @@ -1902,7 +2741,13 @@ checksum = "19e1c899248e606fbfe68dcb31d8b0176ebab833b103824af31bddf4b7457494" dependencies = [ "cc", "libc", +<<<<<<< HEAD "libz-sys", +======= + "libssh2-sys", + "libz-sys", + "openssl-sys", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "pkg-config", ] @@ -1913,12 +2758,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" [[package]] +<<<<<<< HEAD name = "libsqlite3-sys" version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abd5850c449b40bacb498b2bbdfaff648b1b055630073ba8db499caf2d0ea9f2" dependencies = [ "cc", +======= +name = "libproc" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +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", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "pkg-config", "vcpkg", ] @@ -1936,6 +2803,7 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "line-wrap" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1945,6 +2813,8 @@ dependencies = [ ] [[package]] +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "linked-hash-map" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1955,10 +2825,23 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "lock_api" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +======= +name = "linux-raw-sys" +version = "0.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95f5690fef754d905294c56f7ac815836f2513af966aa47f2e07ac79be07827f" + +[[package]] +name = "lock_api" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "scopeguard", ] @@ -1969,7 +2852,21 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ +<<<<<<< HEAD "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", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -2014,8 +2911,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd" dependencies = [ "log", +<<<<<<< HEAD "phf", "phf_codegen", +======= + "phf 0.8.0", + "phf_codegen 0.8.0", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "string_cache", "string_cache_codegen", "tendril", @@ -2029,6 +2931,7 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "md-5" +<<<<<<< HEAD version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" @@ -2045,6 +2948,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] +======= +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6a38fc55c8bbc10058782919516f88826e70320db6d206aebc49611d24216ae" +dependencies = [ + "digest 0.10.1", +] + +[[package]] +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "memchr" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2052,18 +2965,30 @@ checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" [[package]] name = "memmap2" +<<<<<<< HEAD version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4647a11b578fead29cdbb34d4adef8dd3dc35b876c9c6d5240d83f205abfe96e" +======= +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe3179b85e1fd8b14447cbebadb75e45a1002f541b925f0bfec366d56a81c56d" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "libc", ] [[package]] name = "memoffset" +<<<<<<< HEAD version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" +======= +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "autocfg", ] @@ -2075,6 +3000,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f79496a5651c8d57cd033c5add8ca7ee4e3d5f7587a4777484640d9cb60392d9" dependencies = [ "fnv", +<<<<<<< HEAD "nom", ] @@ -2102,6 +3028,46 @@ checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" dependencies = [ "adler32", ] +======= + "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]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce [[package]] name = "miniz_oxide" @@ -2123,7 +3089,11 @@ dependencies = [ "log", "miow", "ntapi", +<<<<<<< HEAD "winapi 0.3.9", +======= + "winapi", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -2132,6 +3102,7 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" dependencies = [ +<<<<<<< HEAD "winapi 0.3.9", ] @@ -2147,6 +3118,9 @@ dependencies = [ "serde", "serde_json", "thiserror", +======= + "winapi", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -2188,6 +3162,7 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "neso" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2202,12 +3177,15 @@ dependencies = [ ] [[package]] +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "new_debug_unreachable" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" [[package]] +<<<<<<< HEAD name = "nibble_vec" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2225,6 +3203,16 @@ dependencies = [ "bitflags", "cc", "cfg-if 1.0.0", +======= +name = "nix" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" +dependencies = [ + "bitflags", + "cc", + "cfg-if", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "libc", "memoffset", ] @@ -2242,16 +3230,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce" [[package]] +<<<<<<< HEAD +======= +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]] +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "ntapi" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" dependencies = [ +<<<<<<< HEAD "winapi 0.3.9", +======= + "winapi", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] name = "nu" +<<<<<<< HEAD version = "0.43.0" dependencies = [ "ctrlc", @@ -2288,10 +3294,48 @@ dependencies = [ "nu_plugin_xpath", "rstest", "serial_test", +======= +version = "0.1.0" +dependencies = [ + "assert_cmd", + "crossterm", + "crossterm_winapi", + "ctrlc", + "hamcrest2", + "itertools", + "log", + "miette", + "nu-ansi-term", + "nu-cli", + "nu-color-config", + "nu-command", + "nu-engine", + "nu-json", + "nu-parser", + "nu-path", + "nu-plugin", + "nu-pretty-hex", + "nu-protocol", + "nu-system", + "nu-table", + "nu-term-grid", + "nu-test-support", + "nu_plugin_example", + "nu_plugin_gstat", + "nu_plugin_inc", + "nu_plugin_query", + "pretty_assertions", + "pretty_env_logger", + "reedline", + "rstest", + "serial_test", + "tempfile", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] name = "nu-ansi-term" +<<<<<<< HEAD version = "0.43.0" dependencies = [ "doc-comment", @@ -2300,10 +3344,19 @@ dependencies = [ "serde", "serde_json", "winapi 0.3.9", +======= +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8afa9b5ba9e7ea9898e119244372cac911bea31ee7a5de42f51bbc36dc66318" +dependencies = [ + "overload", + "winapi", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] name = "nu-cli" +<<<<<<< HEAD version = "0.43.0" dependencies = [ "ctrlc", @@ -2327,10 +3380,37 @@ dependencies = [ "serde_yaml", "shadow-rs", "strip-ansi-escapes", +======= +version = "0.1.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.1.0" +dependencies = [ + "nu-ansi-term", + "nu-json", + "nu-protocol", + "nu-table", + "serde", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] name = "nu-command" +<<<<<<< HEAD version = "0.43.0" dependencies = [ "base64", @@ -2343,25 +3423,50 @@ dependencies = [ "ctrlc", "derive-new", "digest", +======= +version = "0.1.0" +dependencies = [ + "Inflector", + "base64", + "bytesize", + "calamine", + "chrono", + "chrono-humanize", + "chrono-tz 0.6.1", + "crossterm", + "csv", + "dialoguer", + "digest 0.10.1", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "dirs-next", "dtparse", "eml-parser", "encoding_rs", "filesize", +<<<<<<< HEAD "futures 0.3.18", "glob", "hamcrest2", "heck 0.4.0", +======= + "glob", + "hamcrest2", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "htmlescape", "ical", "indexmap", "itertools", "lazy_static", "log", +<<<<<<< HEAD +======= + "lscolors", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "md-5", "meval", "mime", "nu-ansi-term", +<<<<<<< HEAD "nu-data", "nu-engine", "nu-errors", @@ -2381,15 +3486,36 @@ dependencies = [ "num-format", "num-traits", "parking_lot", +======= + "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", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "polars", "quick-xml 0.22.0", "quickcheck", "quickcheck_macros", "rand 0.8.4", +<<<<<<< HEAD +======= + "rayon", + "reedline", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "regex", "reqwest", "roxmltree", "rust-embed", +<<<<<<< HEAD "rustyline", "serde", "serde_ini", @@ -2405,6 +3531,19 @@ dependencies = [ "thiserror", "titlecase", "tokio", +======= + "serde", + "serde_ini", + "serde_urlencoded", + "serde_yaml", + "sha2 0.10.1", + "shadow-rs", + "strip-ansi-escapes", + "sysinfo", + "terminal_size", + "thiserror", + "titlecase", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "toml", "trash", "umask", @@ -2417,6 +3556,7 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "nu-completion" version = "0.43.0" dependencies = [ @@ -2524,16 +3664,33 @@ dependencies = [ "serde_json", "serde_yaml", "toml", +======= +name = "nu-engine" +version = "0.1.0" +dependencies = [ + "chrono", + "glob", + "itertools", + "nu-path", + "nu-protocol", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] name = "nu-json" +<<<<<<< HEAD version = "0.43.0" +======= +version = "0.37.1" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "lazy_static", "linked-hash-map", "nu-path", +<<<<<<< HEAD "nu-test-support", +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "num-traits", "regex", "serde", @@ -2542,6 +3699,7 @@ dependencies = [ [[package]] name = "nu-parser" +<<<<<<< HEAD version = "0.43.0" dependencies = [ "bigdecimal", @@ -2557,11 +3715,26 @@ dependencies = [ "nu-test-support", "num-bigint 0.4.3", "smart-default", +======= +version = "0.1.0" +dependencies = [ + "log", + "miette", + "nu-path", + "nu-plugin", + "nu-protocol", + "serde_json", + "thiserror", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] name = "nu-path" +<<<<<<< HEAD version = "0.43.0" +======= +version = "0.37.1" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "dirs-next", "dunce", @@ -2569,6 +3742,7 @@ dependencies = [ [[package]] name = "nu-plugin" +<<<<<<< HEAD version = "0.43.0" dependencies = [ "indexmap", @@ -2577,21 +3751,35 @@ dependencies = [ "nu-source", "nu-test-support", "nu-value-ext", +======= +version = "0.1.0" +dependencies = [ + "capnp", + "nu-engine", + "nu-protocol", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "serde", "serde_json", ] [[package]] name = "nu-pretty-hex" +<<<<<<< HEAD version = "0.43.0" dependencies = [ "heapless", +======= +version = "0.41.0" +dependencies = [ + "heapless 0.7.10", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "nu-ansi-term", "rand 0.8.4", ] [[package]] name = "nu-protocol" +<<<<<<< HEAD version = "0.43.0" dependencies = [ "bigdecimal", @@ -2651,6 +3839,58 @@ dependencies = [ "atty", "nu-ansi-term", "regex", +======= +version = "0.1.0" +dependencies = [ + "byte-unit", + "chrono", + "chrono-humanize", + "im", + "indexmap", + "miette", + "nu-json", + "num-format", + "serde", + "serde_json", + "sys-locale", + "thiserror", + "typetag", +] + +[[package]] +name = "nu-system" +version = "0.60.0" +dependencies = [ + "chrono", + "errno", + "libc", + "libproc", + "ntapi", + "once_cell", + "procfs", + "users", + "which", + "winapi", +] + +[[package]] +name = "nu-table" +version = "0.36.0" +dependencies = [ + "ansi-cut", + "atty", + "nu-ansi-term", + "nu-protocol", + "regex", + "strip-ansi-escapes", + "unicode-width", +] + +[[package]] +name = "nu-term-grid" +version = "0.36.0" +dependencies = [ +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "strip-ansi-escapes", "unicode-width", ] @@ -2665,15 +3905,21 @@ dependencies = [ "glob", "hamcrest2", "indexmap", +<<<<<<< HEAD "nu-errors", "nu-path", "nu-protocol", "nu-source", +======= + "nu-path", + "nu-protocol", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "num-bigint 0.4.3", "tempfile", ] [[package]] +<<<<<<< HEAD name = "nu-value-ext" version = "0.43.0" dependencies = [ @@ -2750,10 +3996,28 @@ dependencies = [ "nu-source", "rusqlite", "tempfile", +======= +name = "nu_plugin_example" +version = "0.1.0" +dependencies = [ + "nu-plugin", + "nu-protocol", +] + +[[package]] +name = "nu_plugin_gstat" +version = "0.1.0" +dependencies = [ + "git2", + "nu-engine", + "nu-plugin", + "nu-protocol", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] name = "nu_plugin_inc" +<<<<<<< HEAD version = "0.43.0" dependencies = [ "nu-errors", @@ -2762,10 +4026,17 @@ dependencies = [ "nu-source", "nu-test-support", "nu-value-ext", +======= +version = "0.1.0" +dependencies = [ + "nu-plugin", + "nu-protocol", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "semver 0.11.0", ] [[package]] +<<<<<<< HEAD name = "nu_plugin_match" version = "0.43.0" dependencies = [ @@ -2885,6 +4156,16 @@ dependencies = [ "nu-protocol", "nu-source", "nu-test-support", +======= +name = "nu_plugin_query" +version = "0.1.0" +dependencies = [ + "gjson", + "nu-engine", + "nu-plugin", + "nu-protocol", + "scraper", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "sxd-document", "sxd-xpath", ] @@ -2919,6 +4200,7 @@ dependencies = [ [[package]] name = "num-bigint" +<<<<<<< HEAD version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" @@ -2933,6 +4215,11 @@ name = "num-bigint" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6f7833f2cbf2360a6cfd58cd41a53aa7a90bd4c202f5b1c7dd2ed73c57b2c3" +======= +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "autocfg", "num-integer", @@ -2977,8 +4264,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bafe4179722c2894288ee77a9f044f02811c86af699344c498b0840c698a2465" dependencies = [ "arrayvec 0.4.12", +<<<<<<< HEAD "itoa", "num-bigint 0.2.6", +======= + "itoa 0.4.8", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -3016,6 +4307,7 @@ dependencies = [ [[package]] name = "num-rational" +<<<<<<< HEAD version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" @@ -3029,6 +4321,8 @@ dependencies = [ [[package]] name = "num-rational" +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" @@ -3051,9 +4345,15 @@ dependencies = [ [[package]] name = "num_cpus" +<<<<<<< HEAD version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +======= +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "hermit-abi", "libc", @@ -3079,6 +4379,7 @@ dependencies = [ [[package]] name = "once_cell" +<<<<<<< HEAD version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" @@ -3104,6 +4405,11 @@ dependencies = [ "cc", "pkg-config", ] +======= +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce [[package]] name = "opaque-debug" @@ -3112,6 +4418,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] +<<<<<<< HEAD name = "open" version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3122,13 +4429,19 @@ dependencies = [ ] [[package]] +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "openssl" version = "0.10.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95" dependencies = [ "bitflags", +<<<<<<< HEAD "cfg-if 1.0.0", +======= + "cfg-if", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "foreign-types", "libc", "once_cell", @@ -3137,6 +4450,7 @@ dependencies = [ [[package]] name = "openssl-probe" +<<<<<<< HEAD version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" @@ -3146,6 +4460,17 @@ name = "openssl-sys" version = "0.9.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df13d165e607909b363a4757a6f133f8a818a74e9d3a98d09c6128e15fa4c73" +======= +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "autocfg", "cc", @@ -3164,12 +4489,33 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD +======= +name = "output_vt100" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9" +dependencies = [ + "winapi", +] + +[[package]] +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] +<<<<<<< HEAD +======= +name = "owo-colors" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20448fd678ec04e6ea15bbe0476874af65e98a01515d667aa49f1434dc44ebf4" + +[[package]] +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "parking_lot" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3186,12 +4532,21 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" dependencies = [ +<<<<<<< HEAD "cfg-if 1.0.0", "instant", "libc", "redox_syscall", "smallvec", "winapi 0.3.9", +======= + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.10", + "smallvec", + "winapi", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -3202,22 +4557,36 @@ checksum = "03abc2f9c83fe9ceec83f47c76cc071bfd56caba33794340330f35623ab1f544" dependencies = [ "async-trait", "byteorder", +<<<<<<< HEAD "futures 0.3.18", +======= + "futures", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "integer-encoding", "ordered-float", ] [[package]] name = "parquet2" +<<<<<<< HEAD version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db82df54cdd88931d29b850190915b9069bb93fba8e1aefc0d59d8ca81603d6d" +======= +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e98d7da0076cead49c49580cc5771dfe0ba8a93cadff9b47c1681a4a78e1f9" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "async-stream", "bitpacking", "brotli", "flate2", +<<<<<<< HEAD "futures 0.3.18", +======= + "futures", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "lz4", "parquet-format-async-temp", "snap", @@ -3235,6 +4604,7 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "path_abs" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3244,6 +4614,8 @@ dependencies = [ ] [[package]] +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "pathdiff" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3277,18 +4649,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ "phf_macros", +<<<<<<< HEAD "phf_shared", +======= + "phf_shared 0.8.0", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "proc-macro-hack", ] [[package]] +<<<<<<< HEAD +======= +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]] +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "phf_codegen" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" dependencies = [ +<<<<<<< HEAD "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", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -3297,18 +4700,40 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" dependencies = [ +<<<<<<< HEAD "phf_shared", +======= + "phf_shared 0.8.0", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "rand 0.7.3", ] [[package]] +<<<<<<< HEAD +======= +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]] +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "phf_macros" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" dependencies = [ +<<<<<<< HEAD "phf_generator", "phf_shared", +======= + "phf_generator 0.8.0", + "phf_shared 0.8.0", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "proc-macro-hack", "proc-macro2", "quote", @@ -3325,10 +4750,27 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "pin-project-lite" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" +======= +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +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" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce [[package]] name = "pin-utils" @@ -3338,6 +4780,7 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" +<<<<<<< HEAD version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f" @@ -3373,6 +4816,17 @@ name = "polars" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c94a25d46e93b64eac7848c028a545dc08fa01e148e4942c5442b3843c3a598" +======= +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" + +[[package]] +name = "polars" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e9211d1bb8d2d81541e4ab80ce9148a8e2a987d6412c2a48017fbbe24231ea1" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "polars-core", "polars-io", @@ -3381,9 +4835,15 @@ dependencies = [ [[package]] name = "polars-arrow" +<<<<<<< HEAD version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cc4488d2f2d6b901bb6e5728e58966013a272cae48861070b676215a79b4a99" +======= +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa5ee9c385bf6643893f98efa80ff5a07169b50f65962c7843c0a13e12f0b0cf" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "arrow2", "num 0.4.0", @@ -3392,22 +4852,36 @@ dependencies = [ [[package]] name = "polars-core" +<<<<<<< HEAD version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6771524063d742a08163d96875ca5df71dff7113f27da58db5ec5fa164165bf6" +======= +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cb1de44e479ce2764a7a3ad057e16f434efa334feb993284e1a48bb8888c6d1" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "ahash", "anyhow", "arrow2", "chrono", +<<<<<<< HEAD "comfy-table", +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "hashbrown", "itertools", "lazy_static", "num 0.4.0", "num_cpus", "polars-arrow", +<<<<<<< HEAD "rand 0.7.3", +======= + "prettytable-rs", + "rand 0.8.4", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "rand_distr", "rayon", "regex", @@ -3419,15 +4893,25 @@ dependencies = [ [[package]] name = "polars-io" +<<<<<<< HEAD version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11a5f5f51525043ee7befd49e586e6919345237826a5f17b53956f8242100957" +======= +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bcb74f52ee9ff84863ae01de6ba25db092a9880302db4bf8f351f65b3ff0d12" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "ahash", "anyhow", "arrow2", "csv-core", +<<<<<<< HEAD "dirs", +======= + "dirs 4.0.0", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "lazy_static", "lexical", "memchr", @@ -3443,9 +4927,15 @@ dependencies = [ [[package]] name = "polars-lazy" +<<<<<<< HEAD version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da3ea647e2fa59d1bbbf90929c5d10ef6a9018aac256d1c6d0e8248211804b61" +======= +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f91022ba6463df71ad6eb80ac2307884578d9959e85e1fe9dac18988291d46" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "ahash", "itertools", @@ -3457,9 +4947,15 @@ dependencies = [ [[package]] name = "ppv-lite86" +<<<<<<< HEAD version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" +======= +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce [[package]] name = "precomputed-hash" @@ -3468,12 +4964,51 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] +<<<<<<< HEAD name = "pretty" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f60c0d9f6fc88ecdd245d90c1920ff76a430ab34303fc778d33b1d0a4c3bf6d3" dependencies = [ "typed-arena", +======= +name = "predicates" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5aab5be6e4732b473071984b3164dbbfb7a3674d30ea5ff44410b6bcd960c3c" +dependencies = [ + "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", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -3487,6 +5022,23 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD +======= +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]] +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "proc-macro-error" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3496,7 +5048,11 @@ dependencies = [ "proc-macro2", "quote", "syn", +<<<<<<< HEAD "version_check", +======= + "version_check 0.9.4", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -3507,7 +5063,11 @@ checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ "proc-macro2", "quote", +<<<<<<< HEAD "version_check", +======= + "version_check 0.9.4", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -3518,20 +5078,41 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" [[package]] name = "proc-macro2" +<<<<<<< HEAD version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" +======= +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "unicode-xid", ] [[package]] +<<<<<<< HEAD name = "ptree" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0de80796b316aec75344095a6d2ef68ec9b8f573b9e7adc821149ba3598e270" dependencies = [ "serde", +======= +name = "procfs" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0941606b9934e2d98a3677759a971756eb821f75764d0e0d26946d08e74d9104" +dependencies = [ + "bitflags", + "byteorder", + "chrono", + "flate2", + "hex", + "lazy_static", + "libc", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -3583,14 +5164,21 @@ dependencies = [ [[package]] name = "quote" +<<<<<<< HEAD version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +======= +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "proc-macro2", ] [[package]] +<<<<<<< HEAD name = "radix_trie" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3601,6 +5189,8 @@ dependencies = [ ] [[package]] +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "rand" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3661,17 +5251,30 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ +<<<<<<< HEAD "getrandom 0.2.3", +======= + "getrandom 0.2.4", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] name = "rand_distr" +<<<<<<< HEAD version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9e9532ada3929fb8b2e9dbe28d1e06c9b2cc65813f074fcb6bd5fbefeff9d56" dependencies = [ "num-traits", "rand 0.7.3", +======= +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand 0.8.4", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -3702,6 +5305,7 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "rawkey" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3711,6 +5315,14 @@ dependencies = [ "user32-sys", "winapi 0.3.9", "x11", +======= +name = "rand_xoshiro" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9fcdd2e881d02f1d9390ae47ad8e5696a9e4be7b547a1da2afbc61973217004" +dependencies = [ + "rand_core 0.5.1", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -3739,10 +5351,17 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "readkey" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86d401b6d6a1725a59f1b4e813275d289dff3ad09c72b373a10a7a8217ba3146" +======= +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce [[package]] name = "redox_syscall" @@ -3755,12 +5374,48 @@ dependencies = [ [[package]] name = "redox_users" +<<<<<<< HEAD +======= +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" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" dependencies = [ +<<<<<<< HEAD "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#ca727ff4a795346d3de040f89d3f2f96bf8c627b" +dependencies = [ + "chrono", + "crossterm", + "fd-lock", + "nu-ansi-term", + "serde", + "strip-ansi-escapes", + "strum", + "strum_macros", + "unicode-segmentation", + "unicode-width", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -3792,11 +5447,16 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ +<<<<<<< HEAD "winapi 0.3.9", +======= + "winapi", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] name = "reqwest" +<<<<<<< HEAD version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bea77bc708afa10e59905c3d4af7c8fd43c9214251673095ff8b14345fcbc5" @@ -3806,6 +5466,18 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", +======= +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f242f1488a539a79bac6dbe7c8609ae43b7914b7736210f239a37cccb32525" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "http", "http-body", "hyper", @@ -3847,6 +5519,7 @@ dependencies = [ [[package]] name = "rstest" +<<<<<<< HEAD version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "041bb0202c14f6a158bbbf086afb03d0c6e975c2dec7d4912f8061ed44f290af" @@ -3855,10 +5528,21 @@ dependencies = [ "proc-macro2", "quote", "rustc_version 0.3.3", +======= +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d912f35156a3f99a66ee3e11ac2e0b3f34ac85a07e05263d05a7e2c8810d616f" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "rustc_version", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "syn", ] [[package]] +<<<<<<< HEAD name = "rusqlite" version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3871,13 +5555,30 @@ dependencies = [ "libsqlite3-sys", "memchr", "smallvec", +======= +name = "rust-argon2" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" +dependencies = [ + "base64", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] name = "rust-embed" +<<<<<<< HEAD version = "5.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fe1fe6aac5d6bb9e1ffd81002340363272a7648234ec7bdfac5ee202cb65523" +======= +version = "6.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d40377bff8cceee81e28ddb73ac97f5c2856ce5522f0b260b763f434cdfae602" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -3886,9 +5587,15 @@ dependencies = [ [[package]] name = "rust-embed-impl" +<<<<<<< HEAD version = "5.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ed91c41c42ef7bf687384439c312e75e0da9c149b0390889b94de3c7d9d9e66" +======= +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e763e24ba2bf0c72bc6be883f967f794a019fafd1b86ba1daff9c91a7edd30" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "proc-macro2", "quote", @@ -3899,10 +5606,18 @@ dependencies = [ [[package]] name = "rust-embed-utils" +<<<<<<< HEAD version = "5.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a512219132473ab0a77b52077059f1c47ce4af7fbdc94503e9862a34422876d" dependencies = [ +======= +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad22c7226e4829104deab21df575e995bfbc4adfad13a595e387477f238c1aec" +dependencies = [ + "sha2 0.9.9", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "walkdir", ] @@ -3925,6 +5640,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" [[package]] +<<<<<<< HEAD name = "rustc-serialize" version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3940,6 +5656,8 @@ dependencies = [ ] [[package]] +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "rustc_version" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3949,6 +5667,7 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "rustversion" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4021,6 +5740,32 @@ name = "safemem" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" +======= +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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" + +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce [[package]] name = "same-file" @@ -4038,7 +5783,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" dependencies = [ "lazy_static", +<<<<<<< HEAD "winapi 0.3.9", +======= + "winapi", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -4065,9 +5814,15 @@ dependencies = [ [[package]] name = "security-framework" +<<<<<<< HEAD version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467" +======= +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fed7948b6c68acbb6e20c334f55ad635dc0f75506963de4464289fbd3b051ac" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "bitflags", "core-foundation", @@ -4078,9 +5833,15 @@ dependencies = [ [[package]] name = "security-framework-sys" +<<<<<<< HEAD version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e" +======= +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a57321bf8bc2362081b2599912d2961fe899c0efadf1b4b2f8d48b3e253bb96c" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "core-foundation-sys", "libc", @@ -4098,8 +5859,13 @@ dependencies = [ "fxhash", "log", "matches", +<<<<<<< HEAD "phf", "phf_codegen", +======= + "phf 0.8.0", + "phf_codegen 0.8.0", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "precomputed-hash", "servo_arc", "smallvec", @@ -4132,14 +5898,21 @@ dependencies = [ [[package]] name = "serde" +<<<<<<< HEAD version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +======= +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "serde_derive", ] [[package]] +<<<<<<< HEAD name = "serde_bytes" version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4153,6 +5926,12 @@ name = "serde_derive" version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +======= +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "proc-macro2", "quote", @@ -4172,45 +5951,78 @@ dependencies = [ [[package]] name = "serde_json" +<<<<<<< HEAD version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527" dependencies = [ "indexmap", "itoa", +======= +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d23c1ba4cf0efd44be32017709280b32d1cea5c3f1275c3b6d9e8bc54f758085" +dependencies = [ + "indexmap", + "itoa 1.0.1", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "ryu", "serde", ] [[package]] name = "serde_test" +<<<<<<< HEAD version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d82178225dbdeae2d5d190e8649287db6a3a32c6d24da22ae3146325aa353e4c" +======= +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21675ba6f9d97711cc00eee79d8dd7d0a31e571c350fb4d8a7c78f70c0e7b0e9" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "serde", ] [[package]] name = "serde_urlencoded" +<<<<<<< HEAD version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" dependencies = [ "form_urlencoded", "itoa", +======= +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.1", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "ryu", "serde", ] [[package]] name = "serde_yaml" +<<<<<<< HEAD version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8c608a35705a5d3cdc9fbe403147647ff34b921f8e833e49306df898f9b20af" dependencies = [ "dtoa", "indexmap", +======= +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a521f2940385c165a24ee286aa8599633d162077a54bdcae2a6fd5a7bfa7a0" +dependencies = [ + "indexmap", + "ryu", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "serde", "yaml-rust", ] @@ -4248,6 +6060,7 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "sha1" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4264,6 +6077,29 @@ dependencies = [ "cpufeatures", "digest", "opaque-debug", +======= +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99c3bd8169c58782adad9290a9af5939994036b76187f7b4f0e6de91dbbfc0ec" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.1", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -4278,6 +6114,7 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "shell-escape" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4298,6 +6135,26 @@ dependencies = [ "libc", "mio", "signal-hook-registry", +======= +name = "signal-hook" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +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", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -4316,6 +6173,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c970da16e7c682fa90a261cf0724dee241c9f7831635ecc4e988ae8f3b505559" [[package]] +<<<<<<< HEAD name = "similar" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4326,6 +6184,22 @@ name = "siphasher" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b" +======= +name = "siphasher" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a86232ab60fa71287d7f2ddae4a7073f6b7aac33631c3015abb556f08c6d0a3e" + +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce [[package]] name = "slab" @@ -4335,6 +6209,7 @@ checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" [[package]] name = "smallvec" +<<<<<<< HEAD version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" @@ -4349,6 +6224,17 @@ dependencies = [ "quote", "syn", ] +======= +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" + +[[package]] +name = "smawk" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce [[package]] name = "snap" @@ -4358,12 +6244,21 @@ checksum = "45456094d1983e2ee2a18fdfebce3189fa451699d0502cb8e3b49dba5ba41451" [[package]] name = "socket2" +<<<<<<< HEAD version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" dependencies = [ "libc", "winapi 0.3.9", +======= +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +dependencies = [ + "libc", + "winapi", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -4388,6 +6283,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] +<<<<<<< HEAD name = "std_prelude" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4400,6 +6296,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a" [[package]] +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "streaming-decompression" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4429,7 +6327,11 @@ dependencies = [ "lazy_static", "new_debug_unreachable", "parking_lot", +<<<<<<< HEAD "phf_shared", +======= + "phf_shared 0.8.0", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "precomputed-hash", "serde", ] @@ -4440,8 +6342,13 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f24c8e5e19d22a726626f1a5e16fe15b132dcf21d10177fa5a45ce7962996b97" dependencies = [ +<<<<<<< HEAD "phf_generator", "phf_shared", +======= + "phf_generator 0.8.0", + "phf_shared 0.8.0", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "proc-macro2", "quote", ] @@ -4457,6 +6364,7 @@ dependencies = [ [[package]] name = "strum" +<<<<<<< HEAD version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7318c509b5ba57f18533982607f24070a55d353e90d4cae30c467cdb2ad5ac5c" @@ -4470,14 +6378,59 @@ dependencies = [ "heck 0.3.3", "proc-macro2", "quote", +======= +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cae14b91c7d11c9a851d3fbc80a963198998c2a64eec840477fa92d8ce9b70bb" + +[[package]] +name = "strum_macros" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb0dc7ee9c15cea6199cde9a127fa16a4c5819af85395457ad72d68edc85a38" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "syn", ] [[package]] +<<<<<<< HEAD name = "subtle" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +======= +name = "supports-color" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +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", +] +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce [[package]] name = "sxd-document" @@ -4502,6 +6455,7 @@ dependencies = [ [[package]] name = "syn" +<<<<<<< HEAD version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" @@ -4520,10 +6474,19 @@ dependencies = [ "proc-macro2", "quote", "syn", +======= +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" +dependencies = [ + "proc-macro2", + "quote", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "unicode-xid", ] [[package]] +<<<<<<< HEAD name = "syntect" version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4556,6 +6519,8 @@ dependencies = [ ] [[package]] +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "sys-locale" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4565,26 +6530,43 @@ dependencies = [ "cstr_core", "libc", "web-sys", +<<<<<<< HEAD "winapi 0.3.9", +======= + "winapi", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] name = "sysinfo" +<<<<<<< HEAD version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e757000a4bed2b1be9be65a3f418b9696adf30bb419214c73997422de73a591" dependencies = [ "cfg-if 1.0.0", +======= +version = "0.22.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f1bfab07306a27332451a662ca9c8156e3a9986f82660ba9c8e744fe8455d43" +dependencies = [ + "cfg-if", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "core-foundation-sys", "libc", "ntapi", "once_cell", "rayon", +<<<<<<< HEAD "winapi 0.3.9", +======= + "winapi", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] name = "tempfile" +<<<<<<< HEAD version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" @@ -4595,6 +6577,18 @@ dependencies = [ "redox_syscall", "remove_dir_all", "winapi 0.3.9", +======= +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall 0.2.10", + "remove_dir_all", + "winapi", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -4610,6 +6604,7 @@ dependencies = [ [[package]] name = "term" +<<<<<<< HEAD version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" @@ -4627,6 +6622,15 @@ checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" dependencies = [ "libc", "winapi 0.3.9", +======= +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42" +dependencies = [ + "byteorder", + "dirs 1.0.5", + "winapi", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -4645,7 +6649,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" dependencies = [ "libc", +<<<<<<< HEAD "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", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -4682,6 +6707,7 @@ checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ "libc", "wasi 0.10.0+wasi-snapshot-preview1", +<<<<<<< HEAD "winapi 0.3.9", ] @@ -4693,6 +6719,9 @@ checksum = "41effe7cfa8af36f439fac33861b66b049edc6f9a32331e2312660529c1c24ad" dependencies = [ "itoa", "libc", +======= + "winapi", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -4722,17 +6751,26 @@ dependencies = [ [[package]] name = "tokio" +<<<<<<< HEAD version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144" dependencies = [ "autocfg", "bytes 1.1.0", +======= +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c27a64b625de6d309e8c57716ba93021dccf1b3b5c97edd6d3dd2d2135afc0a" +dependencies = [ + "bytes", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "libc", "memchr", "mio", "num_cpus", "pin-project-lite", +<<<<<<< HEAD "tokio-macros", "winapi 0.3.9", ] @@ -4757,6 +6795,9 @@ dependencies = [ "proc-macro2", "quote", "syn", +======= + "winapi", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -4775,7 +6816,11 @@ version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" dependencies = [ +<<<<<<< HEAD "bytes 1.1.0", +======= + "bytes", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "futures-core", "futures-sink", "log", @@ -4804,7 +6849,11 @@ version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" dependencies = [ +<<<<<<< HEAD "cfg-if 1.0.0", +======= + "cfg-if", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "pin-project-lite", "tracing-core", ] @@ -4820,14 +6869,24 @@ dependencies = [ [[package]] name = "trash" +<<<<<<< HEAD version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3ebb6cb2db7947ab9f65dec9f7c5dbe01042b708f564242dcfb6d5cb2957cbc" +======= +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2ed4369f59214865022230fb397ad71353101fe87bfef0f0cf887c43eaa094" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "chrono", "libc", "log", "objc", +<<<<<<< HEAD +======= + "once_cell", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "scopeguard", "url", "windows", @@ -4840,6 +6899,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] +<<<<<<< HEAD name = "tui" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4853,6 +6913,8 @@ dependencies = [ ] [[package]] +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "typed-arena" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4860,9 +6922,39 @@ checksum = "a9b2228007eba4120145f785df0f6c92ea538f5a3635a612ecf4e334c8c1446d" [[package]] name = "typenum" +<<<<<<< HEAD version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" +======= +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +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", +] +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce [[package]] name = "ucd-trie" @@ -4872,6 +6964,7 @@ checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" [[package]] name = "umask" +<<<<<<< HEAD version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "982efbf70ec4d28f7862062c03dd1a4def601a5079e0faf1edc55f2ad0f6fe46" @@ -4883,6 +6976,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" dependencies = [ "version_check", +======= +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efb3f38a494193b563eb215c43cb635a4fda1dfcd885fe3906b215bc6a9fb6b8" + +[[package]] +name = "uncased" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baeed7327e25054889b9bd4f975f32e5f4c5d434042d59ab6cd4142c0a76ed0" +dependencies = [ + "version_check 0.9.4", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -4892,6 +6998,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" [[package]] +<<<<<<< HEAD +======= +name = "unicode-linebreak" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f" +dependencies = [ + "regex", +] + +[[package]] +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "unicode-normalization" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4937,6 +7055,7 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "user32-sys" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4947,6 +7066,8 @@ dependencies = [ ] [[package]] +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "users" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4980,7 +7101,11 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ +<<<<<<< HEAD "getrandom 0.2.3", +======= + "getrandom 0.2.4", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -4991,9 +7116,21 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" +<<<<<<< HEAD version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +======= +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce [[package]] name = "void" @@ -5023,13 +7160,29 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD +======= +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + +[[package]] +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "walkdir" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" dependencies = [ "same-file", +<<<<<<< HEAD "winapi 0.3.9", +======= + "winapi", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "winapi-util", ] @@ -5057,19 +7210,33 @@ checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[package]] name = "wasm-bindgen" +<<<<<<< HEAD version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" dependencies = [ "cfg-if 1.0.0", +======= +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" +dependencies = [ + "cfg-if", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" +<<<<<<< HEAD version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +======= +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "bumpalo", "lazy_static", @@ -5082,11 +7249,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" +<<<<<<< HEAD version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39" dependencies = [ "cfg-if 1.0.0", +======= +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb6ec270a31b1d3c7e266b999739109abce8b6c87e4b31fcfcd788b65267395" +dependencies = [ + "cfg-if", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "js-sys", "wasm-bindgen", "web-sys", @@ -5094,9 +7269,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" +<<<<<<< HEAD version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +======= +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5104,9 +7285,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" +<<<<<<< HEAD version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +======= +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "proc-macro2", "quote", @@ -5117,6 +7304,7 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" +<<<<<<< HEAD version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" @@ -5126,12 +7314,24 @@ name = "web-sys" version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +======= +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2" + +[[package]] +name = "web-sys" +version = "0.3.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] +<<<<<<< HEAD name = "webbrowser" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5147,6 +7347,12 @@ name = "which" version = "4.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea187a8ef279bc014ec368c27a920da2024d2a711109bfbe3440585d5cf27ad9" +======= +name = "which" +version = "4.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a5a7e487e921cf220206864a94a89b6c6905bfc19f1057fa26a4cb360e5c1d2" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "either", "lazy_static", @@ -5154,6 +7360,7 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "widestring" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5166,6 +7373,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" [[package]] +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5176,12 +7385,15 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "winapi-build" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" [[package]] +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5193,7 +7405,11 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ +<<<<<<< HEAD "winapi 0.3.9", +======= + "winapi", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] @@ -5215,9 +7431,15 @@ dependencies = [ [[package]] name = "windows-sys" +<<<<<<< HEAD version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82ca39602d5cbfa692c4b67e3bcbb2751477355141c1ed434c94da4186836ff6" +======= +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "030b7ff91626e57a05ca64a07c481973cbb2db774e4852c9c7ca342408c6a99a" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "windows_aarch64_msvc", "windows_i686_gnu", @@ -5228,9 +7450,15 @@ dependencies = [ [[package]] name = "windows_aarch64_msvc" +<<<<<<< HEAD version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52695a41e536859d5308cc613b4a022261a274390b25bd29dfff4bf08505f3c2" +======= +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29277a4435d642f775f63c7d1faeb927adba532886ce0287bd985bffb16b6bca" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce [[package]] name = "windows_gen" @@ -5243,6 +7471,7 @@ dependencies = [ [[package]] name = "windows_i686_gnu" +<<<<<<< HEAD version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f54725ac23affef038fecb177de6c9bf065787c2f432f79e3c373da92f3e1d8a" @@ -5252,6 +7481,17 @@ name = "windows_i686_msvc" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d5158a43cc43623c0729d1ad6647e62fa384a3d135fd15108d37c683461f64" +======= +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1145e1989da93956c68d1864f32fb97c8f561a8f89a5125f6a2b7ea75524e4b8" + +[[package]] +name = "windows_i686_msvc" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a09e3a0d4753b73019db171c1339cd4362c8c44baf1bcea336235e955954a6" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce [[package]] name = "windows_macros" @@ -5265,6 +7505,7 @@ dependencies = [ [[package]] name = "windows_x86_64_gnu" +<<<<<<< HEAD version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc31f409f565611535130cfe7ee8e6655d3fa99c1c61013981e491921b5ce954" @@ -5274,6 +7515,17 @@ name = "windows_x86_64_msvc" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f2b8c7cbd3bfdddd9ab98769f9746a7fad1bca236554cd032b78d768bc0e89f" +======= +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca64fcb0220d58db4c119e050e7af03c69e6f4f415ef69ec1773d9aab422d5a" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08cabc9f0066848fef4bc6a1c1668e6efce38b661d2aeec75d18d8617eebb5f1" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce [[package]] name = "winreg" @@ -5281,6 +7533,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" dependencies = [ +<<<<<<< HEAD "winapi 0.3.9", ] @@ -5301,6 +7554,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" [[package]] +======= + "winapi", +] + +[[package]] +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "xmlparser" version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5316,6 +7575,15 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD +======= +name = "zeroize" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c88870063c39ee00ec285a2f8d6a966e5b6fb2becc4e8dac77ed0d370ed6006" + +[[package]] +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce name = "zip" version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5326,23 +7594,39 @@ dependencies = [ "crc32fast", "flate2", "thiserror", +<<<<<<< HEAD "time 0.1.44", +======= + "time", +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ] [[package]] name = "zstd" +<<<<<<< HEAD version = "0.9.0+zstd.1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07749a5dc2cb6b36661290245e350f15ec3bbb304e493db54a1d354480522ccd" +======= +version = "0.9.2+zstd.1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2390ea1bf6c038c39674f22d95f0564725fc06034a47129179810b2fc58caa54" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" +<<<<<<< HEAD version = "4.1.1+zstd.1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c91c90f2c593b003603e5e0493c837088df4469da25aafff8bce42ba48caf079" +======= +version = "4.1.3+zstd.1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e99d81b99fb3c2c2c794e3fe56c305c63d5173a16a46b5850b07c935ffc7db79" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "libc", "zstd-sys", @@ -5350,9 +7634,15 @@ dependencies = [ [[package]] name = "zstd-sys" +<<<<<<< HEAD version = "1.6.1+zstd.1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "615120c7a2431d16cf1cf979e7fc31ba7a5b5e5707b29c8a99e5dbf8a8392a33" +======= +version = "1.6.2+zstd.1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2daf2f248d9ea44454bfcb2516534e8b8ad2fc91bf818a1885495fc42bc8ac9f" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce dependencies = [ "cc", "libc", diff --git a/Cargo.toml b/Cargo.toml index a831623195..169e1a0c66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [package] +<<<<<<< HEAD authors = ["The Nu Project Contributors"] default-run = "nu" description = "A new type of shell" @@ -148,10 +149,117 @@ required-features = ["textview"] [[bin]] name = "nu_plugin_core_inc" +======= +name = "nu" +version = "0.1.0" +edition = "2021" +default-run = "nu" + +# 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] +reedline = { git = "https://github.com/nushell/reedline", branch = "main" } + +crossterm = "0.22.*" +nu-cli = { path="./crates/nu-cli" } +nu-command = { path="./crates/nu-command" } +nu-engine = { path="./crates/nu-engine" } +nu-json = { path="./crates/nu-json" } +nu-parser = { path="./crates/nu-parser" } +nu-path = { path="./crates/nu-path" } +nu-pretty-hex = { path = "./crates/nu-pretty-hex" } +nu-protocol = { path = "./crates/nu-protocol" } +nu-plugin = { path = "./crates/nu-plugin", optional = true } +nu-system = { path = "./crates/nu-system"} +nu-table = { path = "./crates/nu-table" } +nu-term-grid = { path = "./crates/nu-term-grid" } + +nu-ansi-term = "0.42.0" +nu-color-config = { path = "./crates/nu-color-config" } +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.1.0", path = "./crates/nu_plugin_inc", optional = true } +nu_plugin_example = { version = "0.1.0", path = "./crates/nu_plugin_example", optional = true } +nu_plugin_gstat = { version = "0.1.0", path = "./crates/nu_plugin_gstat", optional = true } +nu_plugin_query = { version = "0.1.0", path = "./crates/nu_plugin_query", optional = true } + +[dev-dependencies] +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.12.0" +itertools = "0.10.3" + +[features] +plugin = ["nu-plugin", "nu-parser/plugin", "nu-command/plugin", "nu-protocol/plugin", "nu-engine/plugin"] +default = [ + "plugin", + "inc", + "example", + "which" + ] + +stable = ["default"] + +extra = [ + "default", + "dataframe", + "gstat", + "zip-support", + "query", +] + +wasi = ["inc"] + +# Stable (Default) +inc = ["nu_plugin_inc"] +example = ["nu_plugin_example"] +which = ["nu-command/which"] + +# Extra +gstat = ["nu_plugin_gstat"] +zip-support = ["nu-command/zip"] +query = ["nu_plugin_query"] + +# Dataframe feature for nushell +dataframe = ["nu-command/dataframe"] + +[profile.release] +opt-level = "s" # Optimize for size + +# Build plugins +[[bin]] +name = "nu_plugin_inc" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce path = "src/plugins/nu_plugin_core_inc.rs" required-features = ["inc"] [[bin]] +<<<<<<< HEAD name = "nu_plugin_core_match" path = "src/plugins/nu_plugin_core_match.rs" required-features = ["match"] @@ -222,6 +330,22 @@ required-features = ["sqlite"] name = "nu_plugin_extra_to_sqlite" path = "src/plugins/nu_plugin_extra_to_sqlite.rs" required-features = ["sqlite"] +======= +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_query" +path = "src/plugins/nu_plugin_extra_query.rs" +required-features = ["query"] +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce # Main nu binary [[bin]] diff --git a/LICENSE b/LICENSE index 70f227d78f..4f295f214d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,10 @@ MIT License +<<<<<<< HEAD Copyright (c) 2019 - 2021 Nushell Project +======= +Copyright (c) 2021 Nushell Project +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index ea2da1fb1d..4c53564418 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +<<<<<<< HEAD # README [![Crates.io](https://img.shields.io/crates/v/nu.svg)](https://crates.io/crates/nu) @@ -281,3 +282,8 @@ Thanks to all the people who already contributed! ## License The project is made available under the MIT license. See the `LICENSE` file for more information. +======= +# NOTE: Engine-q is merged into Nushell + +Please use https://github.com/nushell/nushell +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce diff --git a/crates/nu-cli/Cargo.toml b/crates/nu-cli/Cargo.toml index 305edeb457..92ae36ede7 100644 --- a/crates/nu-cli/Cargo.toml +++ b/crates/nu-cli/Cargo.toml @@ -1,4 +1,5 @@ [package] +<<<<<<< HEAD authors = ["The Nu Project Contributors"] description = "CLI for nushell" edition = "2018" @@ -41,3 +42,24 @@ shadow-rs = "0.8.1" default = ["shadow-rs"] rustyline-support = ["rustyline", "nu-engine/rustyline-support"] stable = [] +======= +name = "nu-cli" +version = "0.1.0" +edition = "2021" + +[dependencies] +nu-engine = { path = "../nu-engine" } +nu-path = { path = "../nu-path" } +nu-parser = { path = "../nu-parser" } +nu-protocol = { path = "../nu-protocol" } +# nu-ansi-term = { path = "../nu-ansi-term" } +nu-ansi-term = "0.42.0" +nu-color-config = { path = "../nu-color-config" } + +miette = { version = "3.0.0", features = ["fancy"] } +thiserror = "1.0.29" +reedline = { git = "https://github.com/nushell/reedline", branch = "main" } + +log = "0.4" +is_executable = "1.0.1" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce 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/lib.rs b/crates/nu-cli/src/lib.rs index 4b3e2fb932..8476225688 100644 --- a/crates/nu-cli/src/lib.rs +++ b/crates/nu-cli/src/lib.rs @@ -1,3 +1,4 @@ +<<<<<<< HEAD pub mod app; mod cli; #[cfg(feature = "rustyline-support")] @@ -13,3 +14,18 @@ pub use crate::app::App; pub use crate::cli::{parse_and_eval, register_plugins, run_script_file}; pub use nu_command::create_default_context; +======= +mod completions; +mod errors; +mod nu_highlight; +mod prompt; +mod syntax_highlight; +mod validation; + +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; +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce 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/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..8979c0c09a --- /dev/null +++ b/crates/nu-color-config/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "nu-color-config" +version = "0.1.0" +edition = "2021" + +[dependencies] +nu-protocol = { path = "../nu-protocol" } +# nu-ansi-term = { path = "../nu-ansi-term" } +nu-ansi-term = "0.42.0" +nu-json = { path = "../nu-json" } +nu-table = { path = "../nu-table" } + +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 943bc4480d..3f8888213f 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -1,4 +1,5 @@ [package] +<<<<<<< HEAD authors = ["The Nu Project Contributors"] build = "build.rs" description = "Commands for Nushell" @@ -46,10 +47,50 @@ eml-parser = "0.1.0" encoding_rs = "0.8.28" filesize = "0.2.0" futures = { version="0.3.12", features=["compat", "io-compat"] } +======= +name = "nu-command" +version = "0.1.0" +edition = "2021" +build = "build.rs" + + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +nu-ansi-term = "0.42.0" +nu-color-config = { path = "../nu-color-config" } +nu-engine = { path = "../nu-engine" } +nu-json = { path = "../nu-json" } +nu-parser = { path = "../nu-parser" } +nu-path = { path = "../nu-path" } +nu-pretty-hex = { path = "../nu-pretty-hex" } +nu-protocol = { path = "../nu-protocol" } +nu-system = { path = "../nu-system" } +nu-table = { path = "../nu-table" } +nu-term-grid = { path = "../nu-term-grid" } +nu-test-support = { path = "../nu-test-support" } + +# Potential dependencies for extras +base64 = "0.13.0" +bytesize = "1.1.0" +calamine = "0.18.0" +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" +dialoguer = "0.9.0" +digest = "0.10.0" +dtparse = "1.2.0" +eml-parser = "0.1.0" +encoding_rs = "0.8.30" +filesize = "0.2.0" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce glob = "0.3.0" htmlescape = "0.3.1" ical = "0.7.0" indexmap = { version="1.7", features=["serde-1"] } +<<<<<<< HEAD itertools = "0.10.0" lazy_static = "1.*" log = "0.4.14" @@ -92,21 +133,76 @@ 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"] +======= +Inflector = "0.11" +itertools = "0.10.0" +lazy_static = "1.4.0" +log = "0.4.14" +lscolors = { version = "0.8.0", features = ["crossterm"] } +md5 = { package = "md-5", version = "0.10.0" } +meval = "0.2.0" +mime = "0.3.16" +num = { version = "0.4.0", optional = true } +pathdiff = "0.2.1" +quick-xml = "0.22" +rand = "0.8" +rayon = "1.5.1" +regex = "1.5.4" +reqwest = {version = "0.11", features = ["blocking"] } +roxmltree = "0.14.0" +rust-embed = "6.3.0" +serde = { version="1.0.123", features=["derive"] } +serde_ini = "0.2.0" +serde_urlencoded = "0.7.0" +serde_yaml = "0.8.16" +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" +toml = "0.5.8" +trash = { version = "2.0.2", optional = true } +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 } +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce [target.'cfg(unix)'.dependencies] umask = "1.0.0" users = "0.11.0" +<<<<<<< HEAD # 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"] +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce [build-dependencies] shadow-rs = "0.8.1" [dev-dependencies] +<<<<<<< HEAD quickcheck = "1.0.3" quickcheck_macros = "1.0.0" hamcrest2 = "0.3.0" @@ -120,3 +216,9 @@ fetch = ["reqwest", "tokio"] post = ["reqwest", "tokio"] sys = ["sysinfo"] ps = ["sysinfo"] +======= +hamcrest2 = "0.3.0" +dirs-next = "2.0.0" +quickcheck = "1.0.3" +quickcheck_macros = "1.0.0" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce 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..8de061c1e3 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -1,3 +1,4 @@ +<<<<<<< HEAD use crate::prelude::*; use nu_engine::whole_stream_command; use std::error::Error; @@ -369,4 +370,364 @@ pub fn create_default_context(interactive: bool) -> Result) -> EngineState { + let mut engine_state = EngineState::new(); + + 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")] + add_dataframe_decls(&mut working_set); + + // 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 +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } 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/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..edcb8929a2 --- /dev/null +++ b/crates/nu-command/src/filters/drop/nth.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, PipelineIterator, ShellError, + Signature, Span, 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") + .rest("rest", SyntaxShape::Int, "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(), + }), + }, + ] + } + + 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(); + + Ok(DropNthIterator { + input: pipeline_iter, + rows, + current: 0, + } + .into_pipeline_data(engine_state.ctrlc.clone())) + } +} + +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..129658c789 100644 --- a/crates/nu-command/src/lib.rs +++ b/crates/nu-command/src/lib.rs @@ -1,3 +1,4 @@ +<<<<<<< HEAD #![recursion_limit = "2048"] #[cfg(test)] @@ -25,3 +26,55 @@ pub use num_traits::cast::ToPrimitive; // TODO: Temporary redirect pub use nu_protocol::{did_you_mean, TaggedDictBuilder}; +======= +mod conversions; +mod core_commands; +mod date; +mod default_context; +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)] +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::*; + +#[cfg(feature = "dataframe")] +mod dataframe; + +#[cfg(feature = "dataframe")] +pub use dataframe::*; +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce 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/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/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

" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ); } @@ -71,7 +75,11 @@ fn test_no_color_flag() { ); assert_eq!( actual.out, +<<<<<<< HEAD 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

" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ); } @@ -86,6 +94,10 @@ fn test_html_color_where_flag_dark_false() { ); assert_eq!( actual.out, +<<<<<<< HEAD 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

" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ); } diff --git a/crates/nu-command/tests/format_conversions/ics.rs b/crates/nu-command/tests/format_conversions/ics.rs index 7e727cb723..37b876dfa0 100644 --- a/crates/nu-command/tests/format_conversions/ics.rs +++ b/crates/nu-command/tests/format_conversions/ics.rs @@ -47,7 +47,11 @@ fn infers_types() { cwd: dirs.test(), pipeline( r#" open calendar.ics +<<<<<<< HEAD | get events +======= + | get events.0 +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce | length "# )); @@ -86,8 +90,13 @@ fn from_ics_text_to_table() { r#" open calendar.txt | from ics +<<<<<<< HEAD | get events | get properties +======= + | get events.0 + | get properties.0 +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce | 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..1a90911e08 100644 --- a/crates/nu-command/tests/format_conversions/ods.rs +++ b/crates/nu-command/tests/format_conversions/ods.rs @@ -22,7 +22,12 @@ fn from_ods_file_to_table_select_sheet() { r#" open sample_data.ods --raw | from ods -s ["SalesOrders"] +<<<<<<< HEAD | get +======= + | columns + | get 0 +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "# )); diff --git a/crates/nu-command/tests/format_conversions/vcf.rs b/crates/nu-command/tests/format_conversions/vcf.rs index 9d03dd6254..5626181042 100644 --- a/crates/nu-command/tests/format_conversions/vcf.rs +++ b/crates/nu-command/tests/format_conversions/vcf.rs @@ -70,7 +70,11 @@ fn from_vcf_text_to_table() { r#" open contacts.txt | from vcf +<<<<<<< HEAD | get properties +======= + | get properties.0 +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce | 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..9767475ef5 100644 --- a/crates/nu-command/tests/format_conversions/xlsx.rs +++ b/crates/nu-command/tests/format_conversions/xlsx.rs @@ -22,7 +22,12 @@ fn from_excel_file_to_table_select_sheet() { r#" open sample_data.xlsx --raw | from xlsx -s ["SalesOrders"] +<<<<<<< HEAD | get +======= + | columns + | get 0 +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "# )); diff --git a/crates/nu-command/tests/format_conversions/xml.rs b/crates/nu-command/tests/format_conversions/xml.rs index 068296195a..6b321f96ce 100644 --- a/crates/nu-command/tests/format_conversions/xml.rs +++ b/crates/nu-command/tests/format_conversions/xml.rs @@ -8,7 +8,11 @@ fn table_to_xml_text_and_from_xml_text_back_into_table() { open jonathan.xml | to xml | from xml +<<<<<<< HEAD | 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 +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce "# )); diff --git a/crates/nu-command/tests/main.rs b/crates/nu-command/tests/main.rs index bd02ef653b..5ed0c22d23 100644 --- a/crates/nu-command/tests/main.rs +++ b/crates/nu-command/tests/main.rs @@ -1,8 +1,14 @@ +<<<<<<< HEAD +======= +use nu_command::create_default_context; +use nu_protocol::engine::StateWorkingSet; +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce use quickcheck_macros::quickcheck; mod commands; mod format_conversions; +<<<<<<< HEAD use nu_engine::EvaluationContext; #[quickcheck] @@ -13,6 +19,24 @@ fn quickcheck_parse(data: String) -> bool { if err.is_none() && err2.is_none() { let context = EvaluationContext::basic(); let _ = nu_parser::classify_block(&lite_block, &context.scope); +======= +// use nu_engine::EvaluationContext; + +#[quickcheck] +fn quickcheck_parse(data: String) -> bool { + 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 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); + } +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } true } diff --git a/crates/nu-engine/Cargo.toml b/crates/nu-engine/Cargo.toml index 29f0adcff8..1cc0f534c9 100644 --- a/crates/nu-engine/Cargo.toml +++ b/crates/nu-engine/Cargo.toml @@ -1,4 +1,5 @@ [package] +<<<<<<< HEAD authors = ["The Nu Project Contributors"] description = "Core commands for nushell" edition = "2018" @@ -57,3 +58,18 @@ hamcrest2 = "0.3.0" rustyline-support = [] trash-support = ["trash"] dataframe = ["nu-protocol/dataframe"] +======= +name = "nu-engine" +version = "0.1.0" +edition = "2021" + +[dependencies] +nu-protocol = { path = "../nu-protocol", features = ["plugin"] } +nu-path = { path = "../nu-path" } +itertools = "0.10.1" +chrono = { version="0.4.19", features=["serde"] } +glob = "0.3.0" + +[features] +plugin = [] +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce 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/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/documentation.rs b/crates/nu-engine/src/documentation.rs index bd8f1df742..e4cc735665 100644 --- a/crates/nu-engine/src/documentation.rs +++ b/crates/nu-engine/src/documentation.rs @@ -1,9 +1,18 @@ +<<<<<<< HEAD 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 itertools::Itertools; +use nu_protocol::{ + ast::Call, + engine::{EngineState, Stack}, + Example, IntoPipelineData, Signature, Span, Value, +}; +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce use std::collections::HashMap; const COMMANDS_DOCS_DIR: &str = "docs/commands"; @@ -11,10 +20,16 @@ const COMMANDS_DOCS_DIR: &str = "docs/commands"; #[derive(Default)] pub struct DocumentationConfig { no_subcommands: bool, +<<<<<<< HEAD +======= + //FIXME: add back in color support + #[allow(dead_code)] +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce no_color: bool, brief: bool, } +<<<<<<< HEAD fn generate_doc(name: &str, scope: &Scope) -> IndexMap { let mut row_entries = IndexMap::new(); let command = scope @@ -39,11 +54,55 @@ fn generate_doc(name: &str, scope: &Scope) -> IndexMap { UntaggedValue::string(get_documentation( command.stream_command(), scope, +======= +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)); + + cols.push("name".to_string()); + vals.push(Value::String { + val: name.into(), + span: head, + }); + + 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, +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce &DocumentationConfig { no_subcommands: true, no_color: true, brief: false, }, +<<<<<<< HEAD )) .into_untagged_value(), ); @@ -69,11 +128,39 @@ pub fn generate_docs(scope: &Scope) -> Value { } } else { cmap.insert(name.to_owned(), Vec::new()); +======= + ), + span: head, + }); + + (cols, vals) +} + +// generate_docs gets the documentation from each command and returns a Table as output +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 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) + .expect("Expected an entry for parent"); + sub_names.push(sig.name.to_owned()); + } + } else { + cmap.insert(sig.name.to_owned(), Vec::new()); +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce }; } // Return documentation for each command // Sub-commands are nested under their respective parent commands let mut table = Vec::new(); +<<<<<<< HEAD for name in &sorted_names { // Must be a sub-command, skip since it's being handled underneath when we hit the parent command if !cmap.contains_key(name) { @@ -96,6 +183,42 @@ pub fn generate_docs(scope: &Scope) -> Value { table.push(UntaggedValue::row(row_entries).into_untagged_value()); } UntaggedValue::table(&table).into_untagged_value() +======= + 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(&sig.name) { + continue; + } + 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(&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.0.push("subcommands".into()); + row_entries.1.push(Value::List { + vals: sub_table, + span: head, + }); + } + table.push(Value::Record { + cols: row_entries.0, + vals: row_entries.1, + span: head, + }); + } + Value::List { + vals: table, + span: head, + } +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } fn retrieve_doc_link(name: &str) -> Option { @@ -115,6 +238,7 @@ fn retrieve_doc_link(name: &str) -> Option { #[allow(clippy::cognitive_complexity)] pub fn get_documentation( +<<<<<<< HEAD cmd: &dyn WholeStreamCommand, scope: &Scope, config: &DocumentationConfig, @@ -124,12 +248,28 @@ pub fn get_documentation( let mut long_desc = String::new(); let usage = &cmd.usage(); +======= + sig: &Signature, + examples: &[Example], + engine_state: &EngineState, + stack: &mut Stack, + config: &DocumentationConfig, +) -> String { + let cmd_name = &sig.name; + let mut long_desc = String::new(); + + let usage = &sig.usage; +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce if !usage.is_empty() { long_desc.push_str(usage); long_desc.push_str("\n\n"); } +<<<<<<< HEAD let extra_usage = if config.brief { "" } else { &cmd.extra_usage() }; +======= + let extra_usage = if config.brief { "" } else { &sig.extra_usage }; +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce if !extra_usage.is_empty() { long_desc.push_str(extra_usage); long_desc.push_str("\n\n"); @@ -137,15 +277,23 @@ pub fn get_documentation( let mut subcommands = vec![]; if !config.no_subcommands { +<<<<<<< HEAD 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)); +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } } } +<<<<<<< HEAD let mut one_liner = String::new(); one_liner.push_str(&signature.name); one_liner.push(' '); @@ -174,6 +322,9 @@ pub fn get_documentation( } long_desc.push_str(&format!("Usage:\n > {}\n", one_liner)); +======= + long_desc.push_str(&format!("Usage:\n > {}\n", sig.call_signature())); +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce if !subcommands.is_empty() { long_desc.push_str("\nSubcommands:\n"); @@ -182,6 +333,7 @@ pub fn get_documentation( long_desc.push('\n'); } +<<<<<<< HEAD if !signature.positional.is_empty() || signature.rest_positional.is_some() { long_desc.push_str("\nParameters:\n"); for positional in &signature.positional { @@ -208,6 +360,39 @@ pub fn get_documentation( if !examples.is_empty() { long_desc.push_str("\nExamples:"); } +======= + 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 &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) = &sig.rest_positional { + long_desc.push_str(&format!( + " ...{}: {}\n", + rest_positional.name, rest_positional.desc + )); + } + } + + if !examples.is_empty() { + long_desc.push_str("\nExamples:"); + } + +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce for example in examples { long_desc.push('\n'); long_desc.push_str(" "); @@ -215,10 +400,43 @@ pub fn get_documentation( if config.no_color { long_desc.push_str(&format!("\n > {}\n", example.example)); +<<<<<<< HEAD } else { let colored_example = crate::shell::painter::Painter::paint_string(example.example, scope, &palette); long_desc.push_str(&format!("\n > {}\n", colored_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 { + long_desc.push_str(&format!("\n > {}\n", example.example)); +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } } @@ -227,6 +445,7 @@ pub fn get_documentation( long_desc } +<<<<<<< HEAD fn get_flags_section(signature: &Signature) -> String { let mut long_desc = String::new(); long_desc.push_str("\nFlags:\n"); @@ -290,16 +509,102 @@ 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 in &signature.named { + let msg = if let Some(arg) = &flag.arg { + 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() + }, + arg, + flag.desc + ) + } else { + format!( + " -{}{} <{:?}>\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) + } + } 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) +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce }; long_desc.push_str(&msg); } long_desc } +<<<<<<< HEAD pub fn get_brief_help(cmd: &dyn WholeStreamCommand, scope: &Scope) -> String { get_documentation( cmd, scope, +======= +pub fn get_brief_help( + sig: &Signature, + examples: &[Example], + engine_state: &EngineState, + stack: &mut Stack, +) -> String { + get_documentation( + sig, + examples, + engine_state, + stack, +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce &DocumentationConfig { no_subcommands: false, no_color: false, @@ -308,6 +613,22 @@ pub fn get_brief_help(cmd: &dyn WholeStreamCommand, scope: &Scope) -> String { ) } +<<<<<<< HEAD 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(), + ) +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } 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/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/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..359746fcb7 100644 --- a/crates/nu-engine/src/lib.rs +++ b/crates/nu-engine/src/lib.rs @@ -1,3 +1,4 @@ +<<<<<<< HEAD mod call_info; mod command_args; mod config_holder; @@ -38,3 +39,21 @@ 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}; +======= +mod call_ext; +pub mod column; +pub mod documentation; +pub mod env; +mod eval; +mod glob_from; + +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; +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce diff --git a/crates/nu-json/Cargo.toml b/crates/nu-json/Cargo.toml index 739e8f8b8a..6e476265b2 100644 --- a/crates/nu-json/Cargo.toml +++ b/crates/nu-json/Cargo.toml @@ -1,10 +1,17 @@ [package] authors = ["The Nu Project Contributors", "Christian Zangl "] description = "Fork of serde-hjson" +<<<<<<< HEAD edition = "2018" license = "MIT" name = "nu-json" version = "0.43.0" +======= +edition = "2021" +license = "MIT" +name = "nu-json" +version = "0.37.1" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -20,6 +27,10 @@ lazy_static = "1" linked-hash-map = { version="0.5", optional=true } [dev-dependencies] +<<<<<<< HEAD nu-path = { version = "0.43.0", path="../nu-path" } nu-test-support = { version = "0.43.0", path="../nu-test-support" } +======= +nu-path = { version = "0.37.1", path="../nu-path" } +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce serde_json = "1.0.39" diff --git a/crates/nu-json/src/lib.rs b/crates/nu-json/src/lib.rs index 6a196c5273..98f58f64ff 100644 --- a/crates/nu-json/src/lib.rs +++ b/crates/nu-json/src/lib.rs @@ -2,7 +2,11 @@ pub use self::de::{ from_iter, from_reader, from_slice, from_str, Deserializer, StreamDeserializer, }; pub use self::error::{Error, ErrorCode, Result}; +<<<<<<< HEAD pub use self::ser::{to_string, to_vec, to_writer, Serializer}; +======= +pub use self::ser::{to_string, to_string_raw, to_vec, to_writer, Serializer}; +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce pub use self::value::{from_value, to_value, Map, Value}; pub mod builder; diff --git a/crates/nu-json/src/ser.rs b/crates/nu-json/src/ser.rs index 200b90f1e4..96ae4238b7 100644 --- a/crates/nu-json/src/ser.rs +++ b/crates/nu-json/src/ser.rs @@ -4,12 +4,20 @@ use std::fmt::{Display, LowerExp}; use std::io; +<<<<<<< HEAD +======= +use std::io::{BufRead, BufReader}; +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce use std::num::FpCategory; use super::error::{Error, ErrorCode, Result}; use serde::ser; +<<<<<<< HEAD use super::util::ParseNumber; +======= +//use super::util::ParseNumber; +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce use regex::Regex; @@ -730,11 +738,23 @@ impl<'a> Formatter for HjsonFormatter<'a> { writer.write_all(&[ch]).map_err(From::from) } +<<<<<<< HEAD fn comma(&mut self, writer: &mut W, _: bool) -> Result<()> where W: io::Write, { writer.write_all(b"\n")?; +======= + fn comma(&mut self, writer: &mut W, first: bool) -> Result<()> + where + W: io::Write, + { + if !first { + writer.write_all(b",\n")?; + } else { + writer.write_all(b"\n")?; + } +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce indent(writer, self.current_indent, self.indent) } @@ -848,10 +868,18 @@ where // Check if we can insert this string without quotes // see hjson syntax (must not parse as true, false, null or number) +<<<<<<< HEAD let mut pn = ParseNumber::new(value.bytes()); let is_number = pn.parse(true).is_ok(); if is_number || NEEDS_QUOTES.is_match(value) || STARTS_WITH_KEYWORD.is_match(value) { +======= + //let mut pn = ParseNumber::new(value.bytes()); + //let is_number = pn.parse(true).is_ok(); + + if true { + // is_number || NEEDS_QUOTES.is_match(value) || STARTS_WITH_KEYWORD.is_match(value) { +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce // First check if the string can be expressed in multiline format or // we must replace the offending characters with safe escape sequences. @@ -913,11 +941,19 @@ where } // Check if we can insert this name without quotes +<<<<<<< HEAD if NEEDS_ESCAPE_NAME.is_match(value) { escape_bytes(wr, value.as_bytes()).map_err(From::from) } else { wr.write_all(value.as_bytes()).map_err(From::from) } +======= + //if NEEDS_ESCAPE_NAME.is_match(value) { + escape_bytes(wr, value.as_bytes()).map_err(From::from) + // } else { + // wr.write_all(value.as_bytes()).map_err(From::from) + // } +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } #[inline] @@ -925,8 +961,16 @@ fn escape_char(wr: &mut W, value: char) -> Result<()> where W: io::Write, { +<<<<<<< HEAD let mut scratch = [0_u8; 4]; escape_bytes(wr, value.encode_utf8(&mut scratch).as_bytes()) +======= + // FIXME: this allocation is required in order to be compatible with stable + // rust, which doesn't support encoding a `char` into a stack buffer. + let mut s = String::new(); + s.push(value); + escape_bytes(wr, s.as_bytes()) +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } fn fmt_f32_or_null(wr: &mut W, value: f32) -> Result<()> @@ -1015,3 +1059,32 @@ where let string = String::from_utf8(vec)?; Ok(string) } +<<<<<<< HEAD +======= + +/// Encode the specified struct into a Hjson `String` buffer. +/// And remove all whitespace +#[inline] +pub fn to_string_raw(value: &T) -> Result +where + T: ser::Serialize, +{ + let vec = to_vec(value)?; + let string = remove_json_whitespace(vec); + Ok(string) +} + +fn remove_json_whitespace(v: Vec) -> String { + let reader = BufReader::new(&v[..]); + let mut output = String::new(); + for line in reader.lines() { + match line { + Ok(line) => output.push_str(line.trim().trim_end()), + _ => { + eprintln!("Error removing JSON whitespace"); + } + } + } + output +} +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce diff --git a/crates/nu-json/tests/main.rs b/crates/nu-json/tests/main.rs index 9b20a6b49c..92a7fd2a2d 100644 --- a/crates/nu-json/tests/main.rs +++ b/crates/nu-json/tests/main.rs @@ -1,8 +1,13 @@ +<<<<<<< HEAD extern crate nu_json; extern crate nu_test_support; extern crate serde; extern crate serde_json; +======= +// FIXME: re-enable tests +/* +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce use nu_json::Value; use regex::Regex; use std::fs; @@ -211,3 +216,8 @@ fn test_hjson() { panic!(); } } +<<<<<<< HEAD +======= + +*/ +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce diff --git a/crates/nu-parser/Cargo.toml b/crates/nu-parser/Cargo.toml index eb937612ae..f8774c2f71 100644 --- a/crates/nu-parser/Cargo.toml +++ b/crates/nu-parser/Cargo.toml @@ -1,4 +1,5 @@ [package] +<<<<<<< HEAD authors = ["The Nu Project Contributors"] description = "Nushell parser" edition = "2018" @@ -24,3 +25,20 @@ nu-test-support = { version = "0.43.0", path="../nu-test-support" } [features] stable = [] +======= +name = "nu-parser" +version = "0.1.0" +edition = "2021" + +[dependencies] +miette = "3.0.0" +thiserror = "1.0.29" +serde_json = "1.0" +nu-path = {path = "../nu-path"} +nu-protocol = { path = "../nu-protocol"} +nu-plugin = { path = "../nu-plugin", optional = true } +log = "0.4" + +[features] +plugin = ["nu-plugin"] +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce diff --git a/crates/nu-parser/README.md b/crates/nu-parser/README.md new file mode 100644 index 0000000000..58681f88ba --- /dev/null +++ b/crates/nu-parser/README.md @@ -0,0 +1,99 @@ +# nu-parser, the Nushell parser + +Nushell's parser is a type-directed parser, meaning that the parser will use type information available during parse time to configure the parser. This allows it to handle a broader range of techniques to handle the arguments of a command. + +Nushell's base language is whitespace-separated tokens with the command (Nushell's term for a function) name in the head position: + +``` +head1 arg1 arg2 | head2 +``` + +## Lexing + +The first job of the parser is to a lexical analysis to find where the tokens start and end in the input. This turns the above into: + +``` +, , , , +``` + +At this point, the parser has little to no understanding of the shape of the command or how to parse its arguments. + +## Lite parsing + +As nushell is a language of pipelines, pipes form a key role in both separating commands from each other as well as denoting the flow of information between commands. The lite parse phase, as the name suggests, helps to group the lexed tokens into units. + +The above tokens are converted the following during the lite parse phase: + +``` +Pipeline: + Command #1: + , , + Command #2: + +``` + +## Parsing + +The real magic begins to happen when the parse moves on to the parsing stage. At this point, it traverses the lite parse tree and for each command makes a decision: + +* If the command looks like an internal/external command literal: eg) `foo` or `/usr/bin/ls`, it parses it as an internal or external command +* Otherwise, it parses the command as part of a mathematical expression + +### Types/shapes + +Each command has a shape assigned to each of the arguments in reads in. These shapes help define how the parser will handle the parse. + +For example, if the command is written as: + +```sql +where $x > 10 +``` + +When the parsing happens, the parser will look up the `where` command and find its Signature. The Signature states what flags are allowed and what positional arguments are allowed (both required and optional). Each argument comes with it a Shape that defines how to parse values to get that position. + +In the above example, if the Signature of `where` said that it took three String values, the result would be: + +``` +CallInfo: + Name: `where` + Args: + Expression($x), a String + Expression(>), a String + Expression(10), a String +``` + +Or, the Signature could state that it takes in three positional arguments: a Variable, an Operator, and a Number, which would give: + +``` +CallInfo: + Name: `where` + Args: + Expression($x), a Variable + Expression(>), an Operator + Expression(10), a Number +``` + +Note that in this case, each would be checked at compile time to confirm that the expression has the shape requested. For example, `"foo"` would fail to parse as a Number. + +Finally, some Shapes can consume more than one token. In the above, if the `where` command stated it took in a single required argument, and that the Shape of this argument was a MathExpression, then the parser would treat the remaining tokens as part of the math expression. + +``` +CallInfo: + Name: `where` + Args: + MathExpression: + Op: > + LHS: Expression($x) + RHS: Expression(10) +``` + +When the command runs, it will now be able to evaluate the whole math expression as a single step rather than doing any additional parsing to understand the relationship between the parameters. + +## Making space + +As some Shapes can consume multiple tokens, it's important that the parser allow for multiple Shapes to coexist as peacefully as possible. + +The simplest way it does this is to ensure there is at least one token for each required parameter. If the Signature of the command says that it takes a MathExpression and a Number as two required arguments, then the parser will stop the math parser one token short. This allows the second Shape to consume the final token. + +Another way that the parser makes space is to look for Keyword shapes in the Signature. A Keyword is a word that's special to this command. For example in the `if` command, `else` is a keyword. When it is found in the arguments, the parser will use it as a signpost for where to make space for each Shape. The tokens leading up to the `else` will then feed into the parts of the Signature before the `else`, and the tokens following are consumed by the `else` and the Shapes that follow. + diff --git a/crates/nu-parser/src/errors.rs b/crates/nu-parser/src/errors.rs index cb576ebcf7..ddda90f023 100644 --- a/crates/nu-parser/src/errors.rs +++ b/crates/nu-parser/src/errors.rs @@ -1,3 +1,4 @@ +<<<<<<< HEAD // use std::fmt::Debug; // A combination of an informative parse error, and what has been successfully parsed so far @@ -16,3 +17,224 @@ // e.cause.into() // } // } +======= +use miette::Diagnostic; +use nu_protocol::{Span, Type}; +use thiserror::Error; + +#[derive(Clone, Debug, Error, Diagnostic)] +pub enum ParseError { + /// The parser encountered unexpected tokens, when the code should have + /// finished. You should remove these or finish adding what you intended + /// to add. + #[error("Extra tokens in code.")] + #[diagnostic( + code(nu::parser::extra_tokens), + url(docsrs), + help("Try removing them.") + )] + ExtraTokens(#[label = "extra tokens"] Span), + + #[error("Extra positional argument.")] + #[diagnostic(code(nu::parser::extra_positional), url(docsrs), help("Usage: {0}"))] + ExtraPositional(String, #[label = "extra positional argument"] Span), + + #[error("Unexpected end of code.")] + #[diagnostic(code(nu::parser::unexpected_eof), url(docsrs))] + UnexpectedEof(String, #[label("expected closing {0}")] Span), + + #[error("Unclosed delimiter.")] + #[diagnostic(code(nu::parser::unclosed_delimiter), url(docsrs))] + Unclosed(String, #[label("unclosed {0}")] Span), + + #[error("Unknown statement.")] + #[diagnostic(code(nu::parser::unknown_statement), url(docsrs))] + UnknownStatement(#[label("unknown statement")] Span), + + #[error("Parse mismatch during operation.")] + #[diagnostic(code(nu::parser::parse_mismatch), url(docsrs))] + Expected(String, #[label("expected {0}")] Span), + + #[error("Type mismatch during operation.")] + #[diagnostic(code(nu::parser::type_mismatch), url(docsrs))] + Mismatch(String, String, #[label("expected {0}, found {1}")] Span), // expected, found, span + + #[error("Types mismatched for operation.")] + #[diagnostic( + code(nu::parser::unsupported_operation), + url(docsrs), + help("Change {2} or {4} to be the right types and try again.") + )] + UnsupportedOperation( + #[label = "doesn't support these values."] Span, + #[label("{2}")] Span, + Type, + #[label("{4}")] Span, + Type, + ), + + #[error("Expected keyword.")] + #[diagnostic(code(nu::parser::expected_keyword), url(docsrs))] + ExpectedKeyword(String, #[label("expected {0}")] Span), + + #[error("Unexpected keyword.")] + #[diagnostic( + code(nu::parser::unexpected_keyword), + url(docsrs), + help("'{0}' keyword is allowed only in a module.") + )] + UnexpectedKeyword(String, #[label("unexpected {0}")] Span), + + #[error("Statement used in pipeline.")] + #[diagnostic( + code(nu::parser::unexpected_keyword), + url(docsrs), + help( + "'{0}' keyword is not allowed in pipeline. Use '{0}' by itself, outside of a pipeline." + ) + )] + StatementInPipeline(String, #[label("not allowed in pipeline")] Span), + + #[error("Incorrect value")] + #[diagnostic(code(nu::parser::incorrect_value), url(docsrs), help("{2}"))] + IncorrectValue(String, #[label("unexpected {0}")] Span, String), + + #[error("Multiple rest params.")] + #[diagnostic(code(nu::parser::multiple_rest_params), url(docsrs))] + MultipleRestParams(#[label = "multiple rest params"] Span), + + #[error("Variable not found.")] + #[diagnostic(code(nu::parser::variable_not_found), url(docsrs))] + VariableNotFound(#[label = "variable not found"] Span), + + #[error("Variable name not supported.")] + #[diagnostic(code(nu::parser::variable_not_valid), url(docsrs))] + VariableNotValid(#[label = "variable name can't contain spaces or quotes"] Span), + + #[error("Module not found.")] + #[diagnostic(code(nu::parser::module_not_found), url(docsrs))] + ModuleNotFound(#[label = "module not found"] Span), + + #[error("Not found.")] + #[diagnostic(code(nu::parser::not_found), url(docsrs))] + NotFound(#[label = "did not find anything under this name"] Span), + + #[error("Duplicate command definition within a block.")] + #[diagnostic(code(nu::parser::duplicate_command_def), url(docsrs))] + DuplicateCommandDef(#[label = "defined more than once"] Span), + + #[error("Unknown command.")] + #[diagnostic( + code(nu::parser::unknown_command), + url(docsrs), + // TODO: actual suggestions like "Did you mean `foo`?" + )] + UnknownCommand(#[label = "unknown command"] Span), + + #[error("Non-UTF8 string.")] + #[diagnostic(code(nu::parser::non_utf8), url(docsrs))] + NonUtf8(#[label = "non-UTF8 string"] Span), + + #[error("The `{0}` command doesn't have flag `{1}`.")] + #[diagnostic( + code(nu::parser::unknown_flag), + url(docsrs), + help("use {0} --help for a list of flags") + )] + UnknownFlag(String, String, #[label = "unknown flag"] Span), + + #[error("Unknown type.")] + #[diagnostic(code(nu::parser::unknown_type), url(docsrs))] + UnknownType(#[label = "unknown type"] Span), + + #[error("Missing flag argument.")] + #[diagnostic(code(nu::parser::missing_flag_param), url(docsrs))] + MissingFlagParam(String, #[label = "flag missing {0} argument"] Span), + + #[error("Batches of short flags can't take arguments.")] + #[diagnostic(code(nu::parser::short_flag_arg_cant_take_arg), url(docsrs))] + ShortFlagBatchCantTakeArg(#[label = "short flag batches can't take args"] Span), + + #[error("Missing required positional argument.")] + #[diagnostic(code(nu::parser::missing_positional), url(docsrs), help("Usage: {2}"))] + MissingPositional(String, #[label("missing {0}")] Span, String), + + #[error("Missing argument to `{1}`.")] + #[diagnostic(code(nu::parser::keyword_missing_arg), url(docsrs))] + KeywordMissingArgument( + String, + String, + #[label("missing {0} value that follows {1}")] Span, + ), + + #[error("Missing type.")] + #[diagnostic(code(nu::parser::missing_type), url(docsrs))] + MissingType(#[label = "expected type"] Span), + + #[error("Type mismatch.")] + #[diagnostic(code(nu::parser::type_mismatch), url(docsrs))] + TypeMismatch(Type, Type, #[label("expected {0:?}, found {1:?}")] Span), // expected, found, span + + #[error("Missing required flag.")] + #[diagnostic(code(nu::parser::missing_required_flag), url(docsrs))] + MissingRequiredFlag(String, #[label("missing required flag {0}")] Span), + + #[error("Incomplete math expression.")] + #[diagnostic(code(nu::parser::incomplete_math_expression), url(docsrs))] + IncompleteMathExpression(#[label = "incomplete math expression"] Span), + + #[error("Unknown state.")] + #[diagnostic(code(nu::parser::unknown_state), url(docsrs))] + UnknownState(String, #[label("{0}")] Span), + + #[error("Internal error.")] + #[diagnostic(code(nu::parser::unknown_state), url(docsrs))] + InternalError(String, #[label("{0}")] Span), + + #[error("Parser incomplete.")] + #[diagnostic(code(nu::parser::parser_incomplete), url(docsrs))] + IncompleteParser(#[label = "parser support missing for this expression"] Span), + + #[error("Rest parameter needs a name.")] + #[diagnostic(code(nu::parser::rest_needs_name), url(docsrs))] + RestNeedsName(#[label = "needs a parameter name"] Span), + + #[error("Extra columns.")] + #[diagnostic(code(nu::parser::extra_columns), url(docsrs))] + ExtraColumns( + usize, + #[label("expected {0} column{}", if *.0 == 1 { "" } else { "s" })] Span, + ), + + #[error("Missing columns.")] + #[diagnostic(code(nu::parser::missing_columns), url(docsrs))] + MissingColumns( + usize, + #[label("expected {0} column{}", if *.0 == 1 { "" } else { "s" })] Span, + ), + + #[error("{0}")] + #[diagnostic(code(nu::parser::assignment_mismatch), url(docsrs))] + AssignmentMismatch(String, String, #[label("{1}")] Span), + + #[error("Missing import pattern.")] + #[diagnostic(code(nu::parser::missing_import_pattern), url(docsrs))] + MissingImportPattern(#[label = "needs an import pattern"] Span), + + #[error("Wrong import pattern structure.")] + #[diagnostic(code(nu::parser::missing_import_pattern), url(docsrs))] + WrongImportPattern(#[label = "invalid import pattern structure"] Span), + + #[error("Export not found.")] + #[diagnostic(code(nu::parser::export_not_found), url(docsrs))] + ExportNotFound(#[label = "could not find imports"] Span), + + #[error("File not found")] + #[diagnostic(code(nu::parser::file_not_found), url(docsrs))] + FileNotFound(String, #[label("File not found: {0}")] Span), + + #[error("{0}")] + #[diagnostic()] + LabeledError(String, String, #[label("{1}")] Span), +} +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce diff --git a/crates/nu-parser/src/flatten.rs b/crates/nu-parser/src/flatten.rs new file mode 100644 index 0000000000..7971bf3a77 --- /dev/null +++ b/crates/nu-parser/src/flatten.rs @@ -0,0 +1,476 @@ +use nu_protocol::ast::{ + Block, Expr, Expression, ImportPatternMember, PathMember, Pipeline, Statement, +}; +use nu_protocol::{engine::StateWorkingSet, Span}; +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)] +pub enum FlatShape { + Garbage, + Nothing, + Bool, + Int, + Float, + Range, + InternalCall, + External, + ExternalArg, + Literal, + Operator, + Signature, + String, + StringInterpolation, + List, + Table, + Record, + Block, + Filepath, + GlobPattern, + Variable, + Flag, + Custom(String), +} + +impl Display for FlatShape { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + FlatShape::Garbage => write!(f, "flatshape_garbage"), + FlatShape::Nothing => write!(f, "flatshape_nothing"), + FlatShape::Bool => write!(f, "flatshape_bool"), + FlatShape::Int => write!(f, "flatshape_int"), + FlatShape::Float => write!(f, "flatshape_float"), + FlatShape::Range => write!(f, "flatshape_range"), + FlatShape::InternalCall => write!(f, "flatshape_internalcall"), + FlatShape::External => write!(f, "flatshape_external"), + FlatShape::ExternalArg => write!(f, "flatshape_externalarg"), + FlatShape::Literal => write!(f, "flatshape_literal"), + FlatShape::Operator => write!(f, "flatshape_operator"), + FlatShape::Signature => write!(f, "flatshape_signature"), + FlatShape::String => write!(f, "flatshape_string"), + FlatShape::StringInterpolation => write!(f, "flatshape_string_interpolation"), + FlatShape::List => write!(f, "flatshape_string_interpolation"), + FlatShape::Table => write!(f, "flatshape_table"), + FlatShape::Record => write!(f, "flatshape_record"), + FlatShape::Block => write!(f, "flatshape_block"), + FlatShape::Filepath => write!(f, "flatshape_filepath"), + FlatShape::GlobPattern => write!(f, "flatshape_globpattern"), + FlatShape::Variable => write!(f, "flatshape_variable"), + FlatShape::Flag => write!(f, "flatshape_flag"), + FlatShape::Custom(_) => write!(f, "flatshape_custom"), + } + } +} + +pub fn flatten_block(working_set: &StateWorkingSet, block: &Block) -> Vec<(Span, FlatShape)> { + let mut output = vec![]; + for stmt in &block.stmts { + output.extend(flatten_statement(working_set, stmt)); + } + output +} + +pub fn flatten_statement( + working_set: &StateWorkingSet, + stmt: &Statement, +) -> Vec<(Span, FlatShape)> { + match stmt { + Statement::Pipeline(pipeline) => flatten_pipeline(working_set, pipeline), + _ => vec![], + } +} + +pub fn flatten_expression( + working_set: &StateWorkingSet, + expr: &Expression, +) -> Vec<(Span, FlatShape)> { + if let Some(custom_completion) = &expr.custom_completion { + return vec![(expr.span, FlatShape::Custom(custom_completion.clone()))]; + } + + match &expr.expr { + Expr::BinaryOp(lhs, op, rhs) => { + let mut output = vec![]; + output.extend(flatten_expression(working_set, lhs)); + output.extend(flatten_expression(working_set, op)); + output.extend(flatten_expression(working_set, rhs)); + output + } + Expr::Block(block_id) | Expr::RowCondition(block_id) | Expr::Subexpression(block_id) => { + let outer_span = expr.span; + + let mut output = vec![]; + + let flattened = flatten_block(working_set, working_set.get_block(*block_id)); + + if let Some(first) = flattened.first() { + if first.0.start > outer_span.start { + output.push(( + Span { + start: outer_span.start, + end: first.0.start, + }, + FlatShape::Block, + )); + } + } + + let last = if let Some(last) = flattened.last() { + if last.0.end < outer_span.end { + Some(( + Span { + start: last.0.end, + end: outer_span.end, + }, + FlatShape::Table, + )) + } else { + None + } + } else { + None + }; + + output.extend(flattened); + if let Some(last) = last { + output.push(last) + } + + output + } + Expr::Call(call) => { + let mut output = vec![(call.head, FlatShape::InternalCall)]; + + let mut args = vec![]; + for positional in &call.positional { + args.extend(flatten_expression(working_set, positional)); + } + for named in &call.named { + args.push((named.0.span, FlatShape::Flag)); + if let Some(expr) = &named.1 { + args.extend(flatten_expression(working_set, expr)); + } + } + // sort these since flags and positional args can be intermixed + args.sort(); + + output.extend(args); + output + } + Expr::ExternalCall(head, args) => { + let mut output = vec![]; + + match **head { + Expression { + expr: Expr::String(..), + span, + .. + } => { + output.push((span, FlatShape::External)); + } + _ => { + output.extend(flatten_expression(working_set, head)); + } + } + + for arg in args { + //output.push((*arg, FlatShape::ExternalArg)); + match arg { + Expression { + expr: Expr::String(..), + span, + .. + } => { + output.push((*span, FlatShape::ExternalArg)); + } + _ => { + output.extend(flatten_expression(working_set, arg)); + } + } + } + + output + } + Expr::Garbage => { + vec![(expr.span, FlatShape::Garbage)] + } + Expr::Nothing => { + vec![(expr.span, FlatShape::Nothing)] + } + Expr::Int(_) => { + vec![(expr.span, FlatShape::Int)] + } + Expr::Float(_) => { + vec![(expr.span, FlatShape::Float)] + } + Expr::ValueWithUnit(x, unit) => { + let mut output = flatten_expression(working_set, x); + output.push((unit.span, FlatShape::String)); + + output + } + Expr::CellPath(cell_path) => { + let mut output = vec![]; + for path_element in &cell_path.members { + match path_element { + PathMember::String { span, .. } => output.push((*span, FlatShape::String)), + PathMember::Int { span, .. } => output.push((*span, FlatShape::Int)), + } + } + output + } + Expr::FullCellPath(cell_path) => { + let mut output = vec![]; + output.extend(flatten_expression(working_set, &cell_path.head)); + for path_element in &cell_path.tail { + match path_element { + PathMember::String { span, .. } => output.push((*span, FlatShape::String)), + PathMember::Int { span, .. } => output.push((*span, FlatShape::Int)), + } + } + output + } + Expr::ImportPattern(import_pattern) => { + let mut output = vec![(import_pattern.head.span, FlatShape::String)]; + + for member in &import_pattern.members { + match member { + ImportPatternMember::Glob { span } => output.push((*span, FlatShape::String)), + ImportPatternMember::Name { span, .. } => { + output.push((*span, FlatShape::String)) + } + ImportPatternMember::List { names } => { + for (_, span) in names { + output.push((*span, FlatShape::String)); + } + } + } + } + + output + } + Expr::Range(from, next, to, op) => { + let mut output = vec![]; + if let Some(f) = from { + output.extend(flatten_expression(working_set, f)); + } + if let Some(s) = next { + output.extend(vec![(op.next_op_span, FlatShape::Operator)]); + output.extend(flatten_expression(working_set, s)); + } + output.extend(vec![(op.span, FlatShape::Operator)]); + if let Some(t) = to { + output.extend(flatten_expression(working_set, t)); + } + output + } + Expr::Bool(_) => { + vec![(expr.span, FlatShape::Bool)] + } + Expr::Filepath(_) => { + vec![(expr.span, FlatShape::Filepath)] + } + Expr::GlobPattern(_) => { + vec![(expr.span, FlatShape::GlobPattern)] + } + Expr::List(list) => { + let outer_span = expr.span; + let mut last_end = outer_span.start; + + let mut output = vec![]; + for l in list { + let flattened = flatten_expression(working_set, l); + + if let Some(first) = flattened.first() { + if first.0.start > last_end { + output.push(( + Span { + start: last_end, + end: first.0.start, + }, + FlatShape::List, + )); + } + } + + if let Some(last) = flattened.last() { + last_end = last.0.end; + } + + output.extend(flattened); + } + + if last_end < outer_span.end { + output.push(( + Span { + start: last_end, + end: outer_span.end, + }, + FlatShape::List, + )); + } + output + } + Expr::StringInterpolation(exprs) => { + let mut output = vec![( + Span { + start: expr.span.start, + end: expr.span.start + 2, + }, + FlatShape::StringInterpolation, + )]; + for expr in exprs { + output.extend(flatten_expression(working_set, expr)); + } + output.push(( + Span { + start: expr.span.end - 1, + end: expr.span.end, + }, + FlatShape::StringInterpolation, + )); + output + } + Expr::Record(list) => { + let outer_span = expr.span; + let mut last_end = outer_span.start; + + let mut output = vec![]; + for l in list { + let flattened_lhs = flatten_expression(working_set, &l.0); + let flattened_rhs = flatten_expression(working_set, &l.1); + + if let Some(first) = flattened_lhs.first() { + if first.0.start > last_end { + output.push(( + Span { + start: last_end, + end: first.0.start, + }, + FlatShape::Record, + )); + } + } + if let Some(last) = flattened_lhs.last() { + last_end = last.0.end; + } + output.extend(flattened_lhs); + + if let Some(first) = flattened_rhs.first() { + if first.0.start > last_end { + output.push(( + Span { + start: last_end, + end: first.0.start, + }, + FlatShape::Record, + )); + } + } + if let Some(last) = flattened_rhs.last() { + last_end = last.0.end; + } + + output.extend(flattened_rhs); + } + if last_end < outer_span.end { + output.push(( + Span { + start: last_end, + end: outer_span.end, + }, + FlatShape::Record, + )); + } + + output + } + Expr::Keyword(_, span, expr) => { + let mut output = vec![(*span, FlatShape::InternalCall)]; + output.extend(flatten_expression(working_set, expr)); + output + } + Expr::Operator(_) => { + vec![(expr.span, FlatShape::Operator)] + } + Expr::Signature(_) => { + vec![(expr.span, FlatShape::Signature)] + } + Expr::String(_) => { + vec![(expr.span, FlatShape::String)] + } + Expr::Table(headers, cells) => { + let outer_span = expr.span; + let mut last_end = outer_span.start; + + let mut output = vec![]; + for e in headers { + let flattened = flatten_expression(working_set, e); + if let Some(first) = flattened.first() { + if first.0.start > last_end { + output.push(( + Span { + start: last_end, + end: first.0.start, + }, + FlatShape::Table, + )); + } + } + + if let Some(last) = flattened.last() { + last_end = last.0.end; + } + + output.extend(flattened); + } + for row in cells { + for expr in row { + let flattened = flatten_expression(working_set, expr); + if let Some(first) = flattened.first() { + if first.0.start > last_end { + output.push(( + Span { + start: last_end, + end: first.0.start, + }, + FlatShape::Table, + )); + } + } + + if let Some(last) = flattened.last() { + last_end = last.0.end; + } + + output.extend(flattened); + } + } + + if last_end < outer_span.end { + output.push(( + Span { + start: last_end, + end: outer_span.end, + }, + FlatShape::Table, + )); + } + + output + } + Expr::Var(_) | Expr::VarDecl(_) => { + vec![(expr.span, FlatShape::Variable)] + } + } +} + +pub fn flatten_pipeline( + working_set: &StateWorkingSet, + pipeline: &Pipeline, +) -> Vec<(Span, FlatShape)> { + let mut output = vec![]; + for expr in &pipeline.expressions { + output.extend(flatten_expression(working_set, expr)) + } + output +} diff --git a/crates/nu-parser/src/lex.rs b/crates/nu-parser/src/lex.rs new file mode 100644 index 0000000000..fb807e35da --- /dev/null +++ b/crates/nu-parser/src/lex.rs @@ -0,0 +1,320 @@ +use crate::ParseError; +use nu_protocol::Span; + +#[derive(Debug, PartialEq, Eq)] +pub enum TokenContents { + Item, + Comment, + Pipe, + Semicolon, + Eol, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Token { + pub contents: TokenContents, + pub span: Span, +} + +impl Token { + pub fn new(contents: TokenContents, span: Span) -> Token { + Token { contents, span } + } +} + +#[derive(Clone, Copy, Debug)] +pub enum BlockKind { + Paren, + CurlyBracket, + SquareBracket, +} + +impl BlockKind { + fn closing(self) -> u8 { + match self { + BlockKind::Paren => b')', + BlockKind::SquareBracket => b']', + BlockKind::CurlyBracket => b'}', + } + } +} + +// 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_item_terminator( + block_level: &[BlockKind], + c: u8, + additional_whitespace: &[u8], + special_tokens: &[u8], +) -> bool { + block_level.is_empty() + && (c == b' ' + || c == b'\t' + || c == b'\n' + || c == b'\r' + || c == b'|' + || c == b';' + || c == b'#' + || additional_whitespace.contains(&c) + || special_tokens.contains(&c)) +} + +// A special token is one that is a byte that stands alone as its own token. For example +// when parsing a signature you may want to have `:` be able to separate tokens and also +// to be handled as its own token to notify you you're about to parse a type in the example +// `foo:bar` +fn is_special_item(block_level: &[BlockKind], c: u8, special_tokens: &[u8]) -> bool { + block_level.is_empty() && special_tokens.contains(&c) +} + +pub fn lex_item( + input: &[u8], + curr_offset: &mut usize, + span_offset: usize, + additional_whitespace: &[u8], + special_tokens: &[u8], +) -> (Span, Option) { + // 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; + + let mut in_comment = false; + + let token_start = *curr_offset; + + // This Vec tracks paired delimiters + let mut block_level: Vec = vec![]; + + // 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) = input.get(*curr_offset) { + 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 == b'#' { + if is_item_terminator(&block_level, c, additional_whitespace, special_tokens) { + break; + } + in_comment = true; + } else if c == b'\n' || c == b'\r' { + in_comment = false; + if is_item_terminator(&block_level, c, additional_whitespace, special_tokens) { + break; + } + } else if in_comment { + if is_item_terminator(&block_level, c, additional_whitespace, special_tokens) { + break; + } + } else if is_special_item(&block_level, c, special_tokens) && token_start == *curr_offset { + *curr_offset += 1; + break; + } else if c == b'\'' || c == b'"' { + // We encountered the opening quote of a string literal. + quote_start = Some(c); + } else if c == b'[' { + // We encountered an opening `[` delimiter. + block_level.push(BlockKind::SquareBracket); + } else if c == b']' { + // 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 == b'{' { + // We encountered an opening `{` delimiter. + block_level.push(BlockKind::CurlyBracket); + } else if c == b'}' { + // We encountered a closing `}` delimiter. Pop off the opening `{`. + if let Some(BlockKind::CurlyBracket) = block_level.last() { + let _ = block_level.pop(); + } + } else if c == b'(' { + // We encountered an opening `(` delimiter. + block_level.push(BlockKind::Paren); + } else if c == b')' { + // We encountered a closing `)` delimiter. Pop off the opening `(`. + if let Some(BlockKind::Paren) = block_level.last() { + let _ = block_level.pop(); + } + } else if is_item_terminator(&block_level, c, additional_whitespace, special_tokens) { + break; + } + + *curr_offset += 1; + } + + let span = Span::new(span_offset + token_start, span_offset + *curr_offset); + + // If there is still unclosed opening delimiters, remember they were missing + if let Some(block) = block_level.last() { + let delim = block.closing(); + let cause = ParseError::UnexpectedEof( + (delim as char).to_string(), + Span { + start: span.end, + end: span.end, + }, + ); + + return (span, Some(cause)); + } + + if let Some(delim) = quote_start { + // 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. + return ( + span, + Some(ParseError::UnexpectedEof( + (delim as char).to_string(), + Span { + start: span.end, + end: span.end, + }, + )), + ); + } + + // If we didn't accumulate any characters, it's an unexpected error. + if *curr_offset - token_start == 0 { + return ( + span, + Some(ParseError::UnexpectedEof("command".to_string(), span)), + ); + } + + (span, None) +} + +pub fn lex( + input: &[u8], + span_offset: usize, + additional_whitespace: &[u8], + special_tokens: &[u8], + skip_comment: bool, +) -> (Vec, Option) { + let mut error = None; + + let mut curr_offset = 0; + + let mut output = vec![]; + let mut is_complete = true; + + while let Some(c) = input.get(curr_offset) { + let c = *c; + if c == b'|' { + // If the next character is `|`, it's either `|` or `||`. + + let idx = curr_offset; + let prev_idx = idx; + curr_offset += 1; + + // If the next character is `|`, we're looking at a `||`. + if let Some(c) = input.get(curr_offset) { + if *c == b'|' { + let idx = curr_offset; + curr_offset += 1; + output.push(Token::new( + TokenContents::Item, + Span::new(span_offset + prev_idx, span_offset + idx + 1), + )); + continue; + } + } + + // Otherwise, it's just a regular `|` token. + output.push(Token::new( + TokenContents::Pipe, + Span::new(span_offset + idx, span_offset + idx + 1), + )); + is_complete = false; + } else if c == b';' { + // If the next character is a `;`, we're looking at a semicolon token. + + if !is_complete && error.is_none() { + error = Some(ParseError::ExtraTokens(Span::new( + curr_offset, + curr_offset + 1, + ))); + } + let idx = curr_offset; + curr_offset += 1; + output.push(Token::new( + TokenContents::Semicolon, + Span::new(span_offset + idx, span_offset + idx + 1), + )); + } else if c == b'\n' || c == b'\r' { + // If the next character is a newline, we're looking at an EOL (end of line) token. + + let idx = curr_offset; + curr_offset += 1; + if !additional_whitespace.contains(&c) { + output.push(Token::new( + TokenContents::Eol, + Span::new(span_offset + idx, span_offset + idx + 1), + )); + } + } else if c == b'#' { + // If the next character is `#`, we're at the beginning of a line + // comment. The comment continues until the next newline. + let mut start = curr_offset; + + while let Some(input) = input.get(curr_offset) { + if *input == b'\n' || *input == b'\r' { + if !skip_comment { + output.push(Token::new( + TokenContents::Comment, + Span::new(span_offset + start, span_offset + curr_offset), + )); + } + start = curr_offset; + + break; + } else { + curr_offset += 1; + } + } + if start != curr_offset && !skip_comment { + output.push(Token::new( + TokenContents::Comment, + Span::new(span_offset + start, span_offset + curr_offset), + )); + } + } else if c == b' ' || c == b'\t' || additional_whitespace.contains(&c) { + // If the next character is non-newline whitespace, skip it. + curr_offset += 1; + } else { + // Otherwise, try to consume an unclassified token. + + let (span, err) = lex_item( + input, + &mut curr_offset, + span_offset, + additional_whitespace, + special_tokens, + ); + if error.is_none() { + error = err; + } + is_complete = true; + output.push(Token::new(TokenContents::Item, span)); + } + } + (output, error) +} diff --git a/crates/nu-parser/src/lib.rs b/crates/nu-parser/src/lib.rs index ee120446ca..8d37af0051 100644 --- a/crates/nu-parser/src/lib.rs +++ b/crates/nu-parser/src/lib.rs @@ -1,3 +1,4 @@ +<<<<<<< HEAD #[macro_use] extern crate derive_new; @@ -13,3 +14,24 @@ pub use lex::tokens::{LiteBlock, LiteCommand, LiteGroup, LitePipeline}; pub use parse::{classify_block, garbage, parse, parse_full_column_path, parse_math_expression}; pub use scope::ParserScope; pub use shapes::shapes; +======= +mod errors; +mod flatten; +mod lex; +mod lite_parse; +mod parse_keywords; +mod parser; +mod type_check; + +pub use errors::ParseError; +pub use flatten::{ + flatten_block, flatten_expression, flatten_pipeline, flatten_statement, FlatShape, +}; +pub use lex::{lex, Token, TokenContents}; +pub use lite_parse::{lite_parse, LiteBlock}; + +pub use parser::{find_captures_in_expr, parse, parse_block, trim_quotes, Import}; + +#[cfg(feature = "plugin")] +pub use parse_keywords::parse_register; +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce diff --git a/crates/nu-parser/src/lite_parse.rs b/crates/nu-parser/src/lite_parse.rs new file mode 100644 index 0000000000..922ddd84de --- /dev/null +++ b/crates/nu-parser/src/lite_parse.rs @@ -0,0 +1,184 @@ +use crate::{ParseError, Token, TokenContents}; +use nu_protocol::Span; + +#[derive(Debug)] +pub struct LiteCommand { + pub comments: Vec, + pub parts: Vec, +} + +impl Default for LiteCommand { + fn default() -> Self { + Self::new() + } +} + +impl LiteCommand { + pub fn new() -> Self { + Self { + comments: vec![], + parts: vec![], + } + } + + pub fn push(&mut self, span: Span) { + self.parts.push(span); + } + + pub fn is_empty(&self) -> bool { + self.parts.is_empty() + } +} + +#[derive(Debug)] +pub struct LiteStatement { + pub commands: Vec, +} + +impl Default for LiteStatement { + fn default() -> Self { + Self::new() + } +} + +impl LiteStatement { + pub fn new() -> Self { + Self { commands: vec![] } + } + + pub fn push(&mut self, command: LiteCommand) { + self.commands.push(command); + } + + pub fn is_empty(&self) -> bool { + self.commands.is_empty() + } +} + +#[derive(Debug)] +pub struct LiteBlock { + pub block: Vec, +} + +impl Default for LiteBlock { + fn default() -> Self { + Self::new() + } +} + +impl LiteBlock { + pub fn new() -> Self { + Self { block: vec![] } + } + + pub fn push(&mut self, pipeline: LiteStatement) { + self.block.push(pipeline); + } + + pub fn is_empty(&self) -> bool { + self.block.is_empty() + } +} + +pub fn lite_parse(tokens: &[Token]) -> (LiteBlock, Option) { + let mut block = LiteBlock::new(); + let mut curr_pipeline = LiteStatement::new(); + let mut curr_command = LiteCommand::new(); + + let mut last_token = TokenContents::Eol; + + let mut curr_comment: Option> = None; + + for token in tokens.iter() { + match &token.contents { + TokenContents::Item => { + // If we have a comment, go ahead and attach it + if let Some(curr_comment) = curr_comment.take() { + curr_command.comments = curr_comment; + } + curr_command.push(token.span); + last_token = TokenContents::Item; + } + TokenContents::Pipe => { + if !curr_command.is_empty() { + curr_pipeline.push(curr_command); + curr_command = LiteCommand::new(); + } + last_token = TokenContents::Pipe; + } + TokenContents::Eol => { + if last_token != TokenContents::Pipe { + if !curr_command.is_empty() { + curr_pipeline.push(curr_command); + + curr_command = LiteCommand::new(); + } + + if !curr_pipeline.is_empty() { + block.push(curr_pipeline); + + curr_pipeline = LiteStatement::new(); + } + } + + if last_token == TokenContents::Eol { + // Clear out the comment as we're entering a new comment + curr_comment = None; + } + + last_token = TokenContents::Eol; + } + TokenContents::Semicolon => { + if !curr_command.is_empty() { + curr_pipeline.push(curr_command); + + curr_command = LiteCommand::new(); + } + + if !curr_pipeline.is_empty() { + block.push(curr_pipeline); + + curr_pipeline = LiteStatement::new(); + } + + last_token = TokenContents::Semicolon; + } + TokenContents::Comment => { + // Comment is beside something + if last_token != TokenContents::Eol { + curr_command.comments.push(token.span); + curr_comment = None; + } else { + // Comment precedes something + if let Some(curr_comment) = &mut curr_comment { + curr_comment.push(token.span); + } else { + curr_comment = Some(vec![token.span]); + } + } + + last_token = TokenContents::Comment; + } + } + } + + if !curr_command.is_empty() { + curr_pipeline.push(curr_command); + } + + if !curr_pipeline.is_empty() { + block.push(curr_pipeline); + } + + if last_token == TokenContents::Pipe { + ( + block, + Some(ParseError::UnexpectedEof( + "pipeline missing end".into(), + tokens[tokens.len() - 1].span, + )), + ) + } else { + (block, None) + } +} diff --git a/crates/nu-parser/src/parse_keywords.rs b/crates/nu-parser/src/parse_keywords.rs new file mode 100644 index 0000000000..aff3e2251e --- /dev/null +++ b/crates/nu-parser/src/parse_keywords.rs @@ -0,0 +1,1700 @@ +use nu_path::canonicalize_with; +use nu_protocol::{ + ast::{ + Block, Call, Expr, Expression, ImportPattern, ImportPatternHead, ImportPatternMember, + Pipeline, Statement, + }, + engine::StateWorkingSet, + span, Exportable, Overlay, PositionalArg, Span, SyntaxShape, Type, CONFIG_VARIABLE_ID, +}; +use std::collections::HashSet; + +use crate::{ + lex, lite_parse, + lite_parse::LiteCommand, + parser::{ + check_call, check_name, find_captures_in_block, garbage, garbage_statement, parse, + parse_block_expression, parse_internal_call, parse_multispan_value, parse_signature, + parse_string, parse_var_with_opt_type, trim_quotes, + }, + ParseError, +}; + +pub fn parse_def_predecl(working_set: &mut StateWorkingSet, spans: &[Span]) -> Option { + let name = working_set.get_span_contents(spans[0]); + + // handle "export def" same as "def" + let (name, spans) = if name == b"export" && spans.len() >= 2 { + (working_set.get_span_contents(spans[1]), &spans[1..]) + } else { + (name, spans) + }; + + if (name == b"def" || name == b"def-env") && spans.len() >= 4 { + let (name_expr, ..) = parse_string(working_set, spans[1]); + let name = name_expr.as_string(); + + working_set.enter_scope(); + // FIXME: because parse_signature will update the scope with the variables it sees + // we end up parsing the signature twice per def. The first time is during the predecl + // so that we can see the types that are part of the signature, which we need for parsing. + // The second time is when we actually parse the body itworking_set. + // We can't reuse the first time because the variables that are created during parse_signature + // are lost when we exit the scope below. + let (sig, ..) = parse_signature(working_set, spans[2]); + let signature = sig.as_signature(); + working_set.exit_scope(); + + if let (Some(name), Some(mut signature)) = (name, signature) { + signature.name = name; + let decl = signature.predeclare(); + + if working_set.add_predecl(decl).is_some() { + return Some(ParseError::DuplicateCommandDef(spans[1])); + } + } + } + + None +} + +pub fn parse_for( + working_set: &mut StateWorkingSet, + spans: &[Span], +) -> (Expression, Option) { + // Checking that the function is used with the correct name + // Maybe this is not necessary but it is a sanity check + if working_set.get_span_contents(spans[0]) != b"for" { + return ( + garbage(spans[0]), + Some(ParseError::UnknownState( + "internal error: Wrong call name for 'for' function".into(), + span(spans), + )), + ); + } + + // Parsing the spans and checking that they match the register signature + // Using a parsed call makes more sense than checking for how many spans are in the call + // Also, by creating a call, it can be checked if it matches the declaration signature + let (call, call_span) = match working_set.find_decl(b"for") { + None => { + return ( + garbage(spans[0]), + Some(ParseError::UnknownState( + "internal error: def declaration not found".into(), + span(spans), + )), + ) + } + Some(decl_id) => { + working_set.enter_scope(); + let (call, mut err) = parse_internal_call(working_set, spans[0], &spans[1..], decl_id); + working_set.exit_scope(); + + let call_span = span(spans); + let decl = working_set.get_decl(decl_id); + let sig = decl.signature(); + + // Let's get our block and make sure it has the right signature + if let Some(arg) = call.positional.get(2) { + match arg { + Expression { + expr: Expr::Block(block_id), + .. + } + | Expression { + expr: Expr::RowCondition(block_id), + .. + } => { + let block = working_set.get_block_mut(*block_id); + + block.signature = Box::new(sig.clone()); + } + _ => {} + } + } + + err = check_call(call_span, &sig, &call).or(err); + if err.is_some() || call.has_flag("help") { + return ( + Expression { + expr: Expr::Call(call), + span: call_span, + ty: Type::Unknown, + custom_completion: None, + }, + err, + ); + } + + (call, call_span) + } + }; + + // All positional arguments must be in the call positional vector by this point + let var_decl = call.positional.get(0).expect("for call already checked"); + let block = call.positional.get(2).expect("for call already checked"); + + let error = None; + if let (Some(var_id), Some(block_id)) = (&var_decl.as_var(), block.as_block()) { + let block = working_set.get_block_mut(block_id); + + block.signature.required_positional.insert( + 0, + PositionalArg { + name: String::new(), + desc: String::new(), + shape: SyntaxShape::Any, + var_id: Some(*var_id), + }, + ); + + let block = working_set.get_block(block_id); + + // Now that we have a signature for the block, we know more about what variables + // will come into scope as params. Because of this, we need to recalculated what + // variables this block will capture from the outside. + let mut seen = vec![]; + let mut seen_decls = vec![]; + let captures = find_captures_in_block(working_set, block, &mut seen, &mut seen_decls); + + let mut block = working_set.get_block_mut(block_id); + block.captures = captures; + } + + ( + Expression { + expr: Expr::Call(call), + span: call_span, + ty: Type::Unknown, + custom_completion: None, + }, + error, + ) +} + +fn build_usage(working_set: &StateWorkingSet, spans: &[Span]) -> String { + let mut usage = String::new(); + + let mut num_spaces = 0; + let mut first = true; + + // Use the comments to build the usage + for comment_part in spans { + let contents = working_set.get_span_contents(*comment_part); + + let comment_line = if first { + // Count the number of spaces still at the front, skipping the '#' + let mut pos = 1; + while pos < contents.len() { + if let Some(b' ') = contents.get(pos) { + // continue + } else { + break; + } + pos += 1; + } + + num_spaces = pos; + + first = false; + + String::from_utf8_lossy(&contents[pos..]).to_string() + } else { + let mut pos = 1; + + while pos < contents.len() && pos < num_spaces { + if let Some(b' ') = contents.get(pos) { + // continue + } else { + break; + } + pos += 1; + } + + String::from_utf8_lossy(&contents[pos..]).to_string() + }; + + if !usage.is_empty() { + usage.push('\n'); + } + usage.push_str(&comment_line); + } + + usage +} + +pub fn parse_def( + working_set: &mut StateWorkingSet, + lite_command: &LiteCommand, +) -> (Statement, Option) { + let spans = &lite_command.parts[..]; + + let usage = build_usage(working_set, &lite_command.comments); + + // Checking that the function is used with the correct name + // Maybe this is not necessary but it is a sanity check + + let def_call = working_set.get_span_contents(spans[0]).to_vec(); + if def_call != b"def" && def_call != b"def-env" { + return ( + garbage_statement(spans), + Some(ParseError::UnknownState( + "internal error: Wrong call name for def function".into(), + span(spans), + )), + ); + } + + // Parsing the spans and checking that they match the register signature + // Using a parsed call makes more sense than checking for how many spans are in the call + // Also, by creating a call, it can be checked if it matches the declaration signature + let (call, call_span) = match working_set.find_decl(&def_call) { + None => { + return ( + garbage_statement(spans), + Some(ParseError::UnknownState( + "internal error: def declaration not found".into(), + span(spans), + )), + ) + } + Some(decl_id) => { + working_set.enter_scope(); + let (call, mut err) = parse_internal_call(working_set, spans[0], &spans[1..], decl_id); + working_set.exit_scope(); + + let call_span = span(spans); + let decl = working_set.get_decl(decl_id); + let sig = decl.signature(); + + // Let's get our block and make sure it has the right signature + if let Some(arg) = call.positional.get(2) { + match arg { + Expression { + expr: Expr::Block(block_id), + .. + } + | Expression { + expr: Expr::RowCondition(block_id), + .. + } => { + let block = working_set.get_block_mut(*block_id); + + block.signature = Box::new(sig.clone()); + } + _ => {} + } + } + + err = check_call(call_span, &sig, &call).or(err); + if err.is_some() || call.has_flag("help") { + return ( + Statement::Pipeline(Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: call_span, + ty: Type::Unknown, + custom_completion: None, + }])), + err, + ); + } + + (call, call_span) + } + }; + + // All positional arguments must be in the call positional vector by this point + let name_expr = call.positional.get(0).expect("def call already checked"); + let sig = call.positional.get(1).expect("def call already checked"); + let block = call.positional.get(2).expect("def call already checked"); + + let mut error = None; + if let (Some(name), Some(mut signature), Some(block_id)) = + (&name_expr.as_string(), sig.as_signature(), block.as_block()) + { + if let Some(decl_id) = working_set.find_decl(name.as_bytes()) { + let declaration = working_set.get_decl_mut(decl_id); + + signature.name = name.clone(); + signature.usage = usage; + + *declaration = signature.clone().into_block_command(block_id); + + let mut block = working_set.get_block_mut(block_id); + block.signature = signature; + + let block = working_set.get_block(block_id); + + // Now that we have a signature for the block, we know more about what variables + // will come into scope as params. Because of this, we need to recalculated what + // variables this block will capture from the outside. + let mut seen = vec![]; + let mut seen_decls = vec![]; + let captures = find_captures_in_block(working_set, block, &mut seen, &mut seen_decls); + + let mut block = working_set.get_block_mut(block_id); + block.redirect_env = def_call == b"def-env"; + block.captures = captures; + } else { + error = error.or_else(|| { + Some(ParseError::InternalError( + "Predeclaration failed to add declaration".into(), + spans[1], + )) + }); + }; + } + + if let Some(name) = name_expr.as_string() { + // It's OK if it returns None: The decl was already merged in previous parse pass. + working_set.merge_predecl(name.as_bytes()); + } else { + error = error.or_else(|| { + Some(ParseError::UnknownState( + "Could not get string from string expression".into(), + name_expr.span, + )) + }); + } + + ( + Statement::Pipeline(Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: call_span, + ty: Type::Unknown, + custom_completion: None, + }])), + error, + ) +} + +pub fn parse_alias( + working_set: &mut StateWorkingSet, + spans: &[Span], +) -> (Statement, Option) { + let name = working_set.get_span_contents(spans[0]); + + if name == b"alias" { + if let Some((span, err)) = check_name(working_set, spans) { + return ( + Statement::Pipeline(Pipeline::from_vec(vec![garbage(*span)])), + Some(err), + ); + } + + if let Some(decl_id) = working_set.find_decl(b"alias") { + let (call, _) = parse_internal_call(working_set, spans[0], &spans[1..], decl_id); + + if spans.len() >= 4 { + let alias_name = working_set.get_span_contents(spans[1]); + + let alias_name = if alias_name.starts_with(b"\"") + && alias_name.ends_with(b"\"") + && alias_name.len() > 1 + { + alias_name[1..(alias_name.len() - 1)].to_vec() + } else { + alias_name.to_vec() + }; + let _equals = working_set.get_span_contents(spans[2]); + + let replacement = spans[3..].to_vec(); + + working_set.add_alias(alias_name, replacement); + } + + return ( + Statement::Pipeline(Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: span(spans), + ty: Type::Unknown, + custom_completion: None, + }])), + None, + ); + } + } + + ( + garbage_statement(spans), + Some(ParseError::InternalError( + "Alias statement unparseable".into(), + span(spans), + )), + ) +} + +pub fn parse_export( + working_set: &mut StateWorkingSet, + lite_command: &LiteCommand, +) -> (Statement, Option, Option) { + let spans = &lite_command.parts[..]; + let mut error = None; + + let export_span = if let Some(sp) = spans.get(0) { + if working_set.get_span_contents(*sp) != b"export" { + return ( + garbage_statement(spans), + None, + Some(ParseError::UnknownState( + "expected export statement".into(), + span(spans), + )), + ); + } + + *sp + } else { + return ( + garbage_statement(spans), + None, + Some(ParseError::UnknownState( + "got empty input for parsing export statement".into(), + span(spans), + )), + ); + }; + + let export_decl_id = if let Some(id) = working_set.find_decl(b"export") { + id + } else { + return ( + garbage_statement(spans), + None, + Some(ParseError::InternalError( + "missing export command".into(), + export_span, + )), + ); + }; + + let mut call = Box::new(Call { + head: spans[0], + decl_id: export_decl_id, + positional: vec![], + named: vec![], + }); + + let exportable = if let Some(kw_span) = spans.get(1) { + let kw_name = working_set.get_span_contents(*kw_span); + match kw_name { + b"def" => { + let lite_command = LiteCommand { + comments: lite_command.comments.clone(), + parts: spans[1..].to_vec(), + }; + let (stmt, err) = parse_def(working_set, &lite_command); + error = error.or(err); + + let export_def_decl_id = if let Some(id) = working_set.find_decl(b"export def") { + id + } else { + return ( + garbage_statement(spans), + None, + Some(ParseError::InternalError( + "missing 'export def' command".into(), + export_span, + )), + ); + }; + + // Trying to warp the 'def' call into the 'export def' in a very clumsy way + if let Statement::Pipeline(ref pipe) = stmt { + if let Some(Expression { + expr: Expr::Call(ref def_call), + .. + }) = pipe.expressions.get(0) + { + call = def_call.clone(); + + call.head = span(&spans[0..=1]); + call.decl_id = export_def_decl_id; + } else { + error = error.or_else(|| { + Some(ParseError::InternalError( + "unexpected output from parsing a definition".into(), + span(&spans[1..]), + )) + }); + } + } else { + error = error.or_else(|| { + Some(ParseError::InternalError( + "unexpected output from parsing a definition".into(), + span(&spans[1..]), + )) + }); + }; + + if error.is_none() { + let decl_name = working_set.get_span_contents(spans[2]); + let decl_name = trim_quotes(decl_name); + if let Some(decl_id) = working_set.find_decl(decl_name) { + Some(Exportable::Decl(decl_id)) + } else { + error = error.or_else(|| { + Some(ParseError::InternalError( + "failed to find added declaration".into(), + span(&spans[1..]), + )) + }); + None + } + } else { + None + } + } + b"def-env" => { + let lite_command = LiteCommand { + comments: lite_command.comments.clone(), + parts: spans[1..].to_vec(), + }; + let (stmt, err) = parse_def(working_set, &lite_command); + error = error.or(err); + + let export_def_decl_id = if let Some(id) = working_set.find_decl(b"export def-env") + { + id + } else { + return ( + garbage_statement(spans), + None, + Some(ParseError::InternalError( + "missing 'export def-env' command".into(), + export_span, + )), + ); + }; + + // Trying to warp the 'def' call into the 'export def' in a very clumsy way + if let Statement::Pipeline(ref pipe) = stmt { + if let Some(Expression { + expr: Expr::Call(ref def_call), + .. + }) = pipe.expressions.get(0) + { + call = def_call.clone(); + + call.head = span(&spans[0..=1]); + call.decl_id = export_def_decl_id; + } else { + error = error.or_else(|| { + Some(ParseError::InternalError( + "unexpected output from parsing a definition".into(), + span(&spans[1..]), + )) + }); + } + } else { + error = error.or_else(|| { + Some(ParseError::InternalError( + "unexpected output from parsing a definition".into(), + span(&spans[1..]), + )) + }); + }; + + if error.is_none() { + let decl_name = working_set.get_span_contents(spans[2]); + let decl_name = trim_quotes(decl_name); + if let Some(decl_id) = working_set.find_decl(decl_name) { + Some(Exportable::Decl(decl_id)) + } else { + error = error.or_else(|| { + Some(ParseError::InternalError( + "failed to find added declaration".into(), + span(&spans[1..]), + )) + }); + None + } + } else { + None + } + } + b"env" => { + if let Some(id) = working_set.find_decl(b"export env") { + call.decl_id = id; + } else { + return ( + garbage_statement(spans), + None, + Some(ParseError::InternalError( + "missing 'export env' command".into(), + export_span, + )), + ); + } + + let sig = working_set.get_decl(call.decl_id); + let call_signature = sig.signature().call_signature(); + + call.head = span(&spans[0..=1]); + + if let Some(name_span) = spans.get(2) { + let (name_expr, err) = parse_string(working_set, *name_span); + error = error.or(err); + call.positional.push(name_expr); + + if let Some(block_span) = spans.get(3) { + let (block_expr, err) = parse_block_expression( + working_set, + &SyntaxShape::Block(None), + *block_span, + ); + error = error.or(err); + + let exportable = if let Expression { + expr: Expr::Block(block_id), + .. + } = block_expr + { + Some(Exportable::EnvVar(block_id)) + } else { + error = error.or_else(|| { + Some(ParseError::InternalError( + "block was not parsed as a block".into(), + *block_span, + )) + }); + None + }; + + call.positional.push(block_expr); + + exportable + } else { + let err_span = Span { + start: name_span.end, + end: name_span.end, + }; + + error = error.or_else(|| { + Some(ParseError::MissingPositional( + "block".into(), + err_span, + call_signature, + )) + }); + + None + } + } else { + let err_span = Span { + start: kw_span.end, + end: kw_span.end, + }; + + error = error.or_else(|| { + Some(ParseError::MissingPositional( + "environment variable name".into(), + err_span, + call_signature, + )) + }); + + None + } + } + _ => { + error = error.or_else(|| { + Some(ParseError::Expected( + // TODO: Fill in more keywords as they come + "def or env keyword".into(), + spans[1], + )) + }); + + None + } + } + } else { + error = error.or_else(|| { + Some(ParseError::MissingPositional( + "def or env keyword".into(), // TODO: keep filling more keywords as they come + Span { + start: export_span.end, + end: export_span.end, + }, + "'def' or 'env' keyword.".to_string(), + )) + }); + + None + }; + + ( + Statement::Pipeline(Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: span(spans), + ty: Type::Unknown, + custom_completion: None, + }])), + exportable, + error, + ) +} + +pub fn parse_module_block( + working_set: &mut StateWorkingSet, + span: Span, +) -> (Block, Overlay, Option) { + let mut error = None; + + working_set.enter_scope(); + + let source = working_set.get_span_contents(span); + + let (output, err) = lex(source, span.start, &[], &[], false); + error = error.or(err); + + let (output, err) = lite_parse(&output); + error = error.or(err); + + for pipeline in &output.block { + // TODO: Should we add export env predecls as well? + if pipeline.commands.len() == 1 { + parse_def_predecl(working_set, &pipeline.commands[0].parts); + } + } + + let mut overlay = Overlay::from_span(span); + + let block: Block = output + .block + .iter() + .map(|pipeline| { + if pipeline.commands.len() == 1 { + let name = working_set.get_span_contents(pipeline.commands[0].parts[0]); + + let (stmt, err) = match name { + b"def" | b"def-env" => { + let (stmt, err) = parse_def(working_set, &pipeline.commands[0]); + + (stmt, err) + } + // TODO: Currently, it is not possible to define a private env var. + // TODO: Exported env vars are usable iside the module only if correctly + // exported by the user. For example: + // + // > module foo { export env a { "2" }; export def b [] { $env.a } } + // + // will work only if you call `use foo *; b` but not with `use foo; foo b` + // since in the second case, the name of the env var would be $env."foo a". + b"export" => { + let (stmt, exportable, err) = + parse_export(working_set, &pipeline.commands[0]); + + if err.is_none() { + let name_span = pipeline.commands[0].parts[2]; + let name = working_set.get_span_contents(name_span); + let name = trim_quotes(name); + + match exportable { + Some(Exportable::Decl(decl_id)) => { + overlay.add_decl(name, decl_id); + } + Some(Exportable::EnvVar(block_id)) => { + overlay.add_env_var(name, block_id); + } + None => {} // None should always come with error from parse_export() + } + } + + (stmt, err) + } + _ => ( + garbage_statement(&pipeline.commands[0].parts), + Some(ParseError::UnexpectedKeyword( + "expected def or export keyword".into(), + pipeline.commands[0].parts[0], + )), + ), + }; + + if error.is_none() { + error = err; + } + + stmt + } else { + error = Some(ParseError::Expected("not a pipeline".into(), span)); + garbage_statement(&[span]) + } + }) + .into(); + + working_set.exit_scope(); + + (block, overlay, error) +} + +pub fn parse_module( + working_set: &mut StateWorkingSet, + spans: &[Span], +) -> (Statement, Option) { + // TODO: Currently, module is closing over its parent scope (i.e., defs in the parent scope are + // visible and usable in this module's scope). We want to disable that for files. + + let mut error = None; + let bytes = working_set.get_span_contents(spans[0]); + + if bytes == b"module" && spans.len() >= 3 { + let (module_name_expr, err) = parse_string(working_set, spans[1]); + error = error.or(err); + + let module_name = module_name_expr + .as_string() + .expect("internal error: module name is not a string"); + + let block_span = spans[2]; + let block_bytes = working_set.get_span_contents(block_span); + let mut start = block_span.start; + let mut end = block_span.end; + + if block_bytes.starts_with(b"{") { + start += 1; + } else { + return ( + garbage_statement(spans), + Some(ParseError::Expected("block".into(), block_span)), + ); + } + + if block_bytes.ends_with(b"}") { + end -= 1; + } else { + error = + error.or_else(|| Some(ParseError::Unclosed("}".into(), Span { start: end, end }))); + } + + let block_span = Span { start, end }; + + let (block, overlay, err) = parse_module_block(working_set, block_span); + error = error.or(err); + + let block_id = working_set.add_block(block); + let _ = working_set.add_overlay(&module_name, overlay); + + let block_expr = Expression { + expr: Expr::Block(block_id), + span: block_span, + ty: Type::Block, + custom_completion: None, + }; + + let module_decl_id = working_set + .find_decl(b"module") + .expect("internal error: missing module command"); + + let call = Box::new(Call { + head: spans[0], + decl_id: module_decl_id, + positional: vec![module_name_expr, block_expr], + named: vec![], + }); + + ( + Statement::Pipeline(Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: span(spans), + ty: Type::Unknown, + custom_completion: None, + }])), + error, + ) + } else { + ( + garbage_statement(spans), + Some(ParseError::UnknownState( + "Expected structure: module {}".into(), + span(spans), + )), + ) + } +} + +pub fn parse_use( + working_set: &mut StateWorkingSet, + spans: &[Span], +) -> (Statement, Option) { + if working_set.get_span_contents(spans[0]) != b"use" { + return ( + garbage_statement(spans), + Some(ParseError::UnknownState( + "internal error: Wrong call name for 'use' command".into(), + span(spans), + )), + ); + } + + let (call, call_span, use_decl_id) = match working_set.find_decl(b"use") { + Some(decl_id) => { + let (call, mut err) = parse_internal_call(working_set, spans[0], &spans[1..], decl_id); + let decl = working_set.get_decl(decl_id); + + let call_span = span(spans); + + err = check_call(call_span, &decl.signature(), &call).or(err); + if err.is_some() || call.has_flag("help") { + return ( + Statement::Pipeline(Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: call_span, + ty: Type::Unknown, + custom_completion: None, + }])), + err, + ); + } + + (call, call_span, decl_id) + } + None => { + return ( + garbage_statement(spans), + Some(ParseError::UnknownState( + "internal error: 'use' declaration not found".into(), + span(spans), + )), + ) + } + }; + + let import_pattern = if let Some(expr) = call.nth(0) { + if let Some(pattern) = expr.as_import_pattern() { + pattern + } else { + return ( + garbage_statement(spans), + Some(ParseError::UnknownState( + "internal error: Import pattern positional is not import pattern".into(), + call_span, + )), + ); + } + } else { + return ( + garbage_statement(spans), + Some(ParseError::UnknownState( + "internal error: Missing required positional after call parsing".into(), + call_span, + )), + ); + }; + + let cwd = working_set.get_cwd(); + + let mut error = None; + + // TODO: Add checking for importing too long import patterns, e.g.: + // > use spam foo non existent names here do not throw error + let (import_pattern, overlay) = + if let Some(overlay_id) = working_set.find_overlay(&import_pattern.head.name) { + (import_pattern, working_set.get_overlay(overlay_id).clone()) + } else { + // TODO: Do not close over when loading module from file + // It could be a file + if let Ok(module_filename) = + String::from_utf8(trim_quotes(&import_pattern.head.name).to_vec()) + { + if let Ok(module_path) = canonicalize_with(&module_filename, cwd) { + let module_name = if let Some(stem) = module_path.file_stem() { + stem.to_string_lossy().to_string() + } else { + return ( + Statement::Pipeline(Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: call_span, + ty: Type::Unknown, + custom_completion: None, + }])), + Some(ParseError::ModuleNotFound(spans[1])), + ); + }; + + if let Ok(contents) = std::fs::read(module_path) { + let span_start = working_set.next_span_start(); + working_set.add_file(module_filename, &contents); + let span_end = working_set.next_span_start(); + + let (block, overlay, err) = + parse_module_block(working_set, Span::new(span_start, span_end)); + error = error.or(err); + + let _ = working_set.add_block(block); + let _ = working_set.add_overlay(&module_name, overlay.clone()); + + ( + ImportPattern { + head: ImportPatternHead { + name: module_name.into(), + span: spans[1], + }, + members: import_pattern.members, + hidden: HashSet::new(), + }, + overlay, + ) + } else { + return ( + Statement::Pipeline(Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: call_span, + ty: Type::Unknown, + custom_completion: None, + }])), + Some(ParseError::ModuleNotFound(spans[1])), + ); + } + } else { + error = error.or(Some(ParseError::FileNotFound( + module_filename, + import_pattern.head.span, + ))); + (ImportPattern::new(), Overlay::new()) + } + } else { + return ( + garbage_statement(spans), + Some(ParseError::NonUtf8(spans[1])), + ); + } + }; + + let decls_to_use = if import_pattern.members.is_empty() { + overlay.decls_with_head(&import_pattern.head.name) + } else { + match &import_pattern.members[0] { + ImportPatternMember::Glob { .. } => overlay.decls(), + ImportPatternMember::Name { name, span } => { + let mut output = vec![]; + + if let Some(id) = overlay.get_decl_id(name) { + output.push((name.clone(), id)); + } else if !overlay.has_env_var(name) { + error = error.or(Some(ParseError::ExportNotFound(*span))) + } + + output + } + ImportPatternMember::List { names } => { + let mut output = vec![]; + + for (name, span) in names { + if let Some(id) = overlay.get_decl_id(name) { + output.push((name.clone(), id)); + } else if !overlay.has_env_var(name) { + error = error.or(Some(ParseError::ExportNotFound(*span))); + break; + } + } + + output + } + } + }; + + // Extend the current scope with the module's overlay + working_set.use_decls(decls_to_use); + + // Create a new Use command call to pass the new import pattern + let import_pattern_expr = Expression { + expr: Expr::ImportPattern(import_pattern), + span: span(&spans[1..]), + ty: Type::List(Box::new(Type::String)), + custom_completion: None, + }; + + let call = Box::new(Call { + head: spans[0], + decl_id: use_decl_id, + positional: vec![import_pattern_expr], + named: vec![], + }); + + ( + Statement::Pipeline(Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: span(spans), + ty: Type::Unknown, + custom_completion: None, + }])), + error, + ) +} + +pub fn parse_hide( + working_set: &mut StateWorkingSet, + spans: &[Span], +) -> (Statement, Option) { + if working_set.get_span_contents(spans[0]) != b"hide" { + return ( + garbage_statement(spans), + Some(ParseError::UnknownState( + "internal error: Wrong call name for 'hide' command".into(), + span(spans), + )), + ); + } + + let (call, call_span, hide_decl_id) = match working_set.find_decl(b"hide") { + Some(decl_id) => { + let (call, mut err) = parse_internal_call(working_set, spans[0], &spans[1..], decl_id); + let decl = working_set.get_decl(decl_id); + + let call_span = span(spans); + + err = check_call(call_span, &decl.signature(), &call).or(err); + if err.is_some() || call.has_flag("help") { + return ( + Statement::Pipeline(Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: call_span, + ty: Type::Unknown, + custom_completion: None, + }])), + err, + ); + } + + (call, call_span, decl_id) + } + None => { + return ( + garbage_statement(spans), + Some(ParseError::UnknownState( + "internal error: 'hide' declaration not found".into(), + span(spans), + )), + ) + } + }; + + let import_pattern = if let Some(expr) = call.nth(0) { + if let Some(pattern) = expr.as_import_pattern() { + pattern + } else { + return ( + garbage_statement(spans), + Some(ParseError::UnknownState( + "internal error: Import pattern positional is not import pattern".into(), + call_span, + )), + ); + } + } else { + return ( + garbage_statement(spans), + Some(ParseError::UnknownState( + "internal error: Missing required positional after call parsing".into(), + call_span, + )), + ); + }; + + let mut error = None; + let bytes = working_set.get_span_contents(spans[0]); + + if bytes == b"hide" && spans.len() >= 2 { + for span in spans[1..].iter() { + let (_, err) = parse_string(working_set, *span); + error = error.or(err); + } + + let (is_module, overlay) = + if let Some(overlay_id) = working_set.find_overlay(&import_pattern.head.name) { + (true, working_set.get_overlay(overlay_id).clone()) + } else if import_pattern.members.is_empty() { + // The pattern head can be e.g. a function name, not just a module + if let Some(id) = working_set.find_decl(&import_pattern.head.name) { + let mut overlay = Overlay::new(); + overlay.add_decl(&import_pattern.head.name, id); + + (false, overlay) + } else { + // Or it could be an env var + (false, Overlay::new()) + } + } else { + return ( + garbage_statement(spans), + Some(ParseError::ModuleNotFound(spans[1])), + ); + }; + + // This kind of inverts the import pattern matching found in parse_use() + let decls_to_hide = if import_pattern.members.is_empty() { + if is_module { + overlay.decls_with_head(&import_pattern.head.name) + } else { + overlay.decls() + } + } else { + match &import_pattern.members[0] { + ImportPatternMember::Glob { .. } => overlay.decls(), + ImportPatternMember::Name { name, span } => { + let mut output = vec![]; + + if let Some(item) = overlay.decl_with_head(name, &import_pattern.head.name) { + output.push(item); + } else if !overlay.has_env_var(name) { + error = error.or(Some(ParseError::ExportNotFound(*span))); + } + + output + } + ImportPatternMember::List { names } => { + let mut output = vec![]; + + for (name, span) in names { + if let Some(item) = overlay.decl_with_head(name, &import_pattern.head.name) + { + output.push(item); + } else if !overlay.has_env_var(name) { + error = error.or(Some(ParseError::ExportNotFound(*span))); + break; + } + } + + output + } + } + }; + + // TODO: `use spam; use spam foo; hide foo` will hide both `foo` and `spam foo` since + // they point to the same DeclId. Do we want to keep it that way? + working_set.hide_decls(&decls_to_hide); + let import_pattern = import_pattern + .with_hidden(decls_to_hide.iter().map(|(name, _)| name.clone()).collect()); + + // Create a new Use command call to pass the new import pattern + let import_pattern_expr = Expression { + expr: Expr::ImportPattern(import_pattern), + span: span(&spans[1..]), + ty: Type::List(Box::new(Type::String)), + custom_completion: None, + }; + + let call = Box::new(Call { + head: spans[0], + decl_id: hide_decl_id, + positional: vec![import_pattern_expr], + named: vec![], + }); + + ( + Statement::Pipeline(Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: span(spans), + ty: Type::Unknown, + custom_completion: None, + }])), + error, + ) + } else { + ( + garbage_statement(spans), + Some(ParseError::UnknownState( + "Expected structure: hide ".into(), + span(spans), + )), + ) + } +} + +pub fn parse_let( + working_set: &mut StateWorkingSet, + spans: &[Span], +) -> (Statement, Option) { + let name = working_set.get_span_contents(spans[0]); + + if name == b"let" { + if let Some((span, err)) = check_name(working_set, spans) { + return ( + Statement::Pipeline(Pipeline::from_vec(vec![garbage(*span)])), + Some(err), + ); + } + + if let Some(decl_id) = working_set.find_decl(b"let") { + let cmd = working_set.get_decl(decl_id); + let call_signature = cmd.signature().call_signature(); + + if spans.len() >= 4 { + // This is a bit of by-hand parsing to get around the issue where we want to parse in the reverse order + // so that the var-id created by the variable isn't visible in the expression that init it + for span in spans.iter().enumerate() { + let item = working_set.get_span_contents(*span.1); + if item == b"=" && spans.len() > (span.0 + 1) { + let mut error = None; + + let mut idx = span.0; + let (rvalue, err) = parse_multispan_value( + working_set, + spans, + &mut idx, + &SyntaxShape::Keyword(b"=".to_vec(), Box::new(SyntaxShape::Expression)), + ); + error = error.or(err); + + if idx < (spans.len() - 1) { + error = error.or(Some(ParseError::ExtraPositional( + call_signature, + spans[idx + 1], + ))); + } + + let mut idx = 0; + let (lvalue, err) = + parse_var_with_opt_type(working_set, &spans[1..(span.0)], &mut idx); + error = error.or(err); + + let var_id = lvalue.as_var(); + + let rhs_type = rvalue.ty.clone(); + + if let Some(var_id) = var_id { + if var_id != CONFIG_VARIABLE_ID { + working_set.set_variable_type(var_id, rhs_type); + } + } + + let call = Box::new(Call { + decl_id, + head: spans[0], + positional: vec![lvalue, rvalue], + named: vec![], + }); + + return ( + Statement::Pipeline(Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: nu_protocol::span(spans), + ty: Type::Unknown, + custom_completion: None, + }])), + error, + ); + } + } + } + let (call, err) = parse_internal_call(working_set, spans[0], &spans[1..], decl_id); + + return ( + Statement::Pipeline(Pipeline { + expressions: vec![Expression { + expr: Expr::Call(call), + span: nu_protocol::span(spans), + ty: Type::Unknown, + custom_completion: None, + }], + }), + err, + ); + } + } + ( + garbage_statement(spans), + Some(ParseError::UnknownState( + "internal error: let statement unparseable".into(), + span(spans), + )), + ) +} + +pub fn parse_source( + working_set: &mut StateWorkingSet, + spans: &[Span], +) -> (Statement, Option) { + let mut error = None; + let name = working_set.get_span_contents(spans[0]); + + if name == b"source" { + if let Some(decl_id) = working_set.find_decl(b"source") { + let cwd = working_set.get_cwd(); + // Is this the right call to be using here? + // Some of the others (`parse_let`) use it, some of them (`parse_hide`) don't. + let (call, err) = parse_internal_call(working_set, spans[0], &spans[1..], decl_id); + error = error.or(err); + + // Command and one file name + if spans.len() >= 2 { + let name_expr = working_set.get_span_contents(spans[1]); + let name_expr = trim_quotes(name_expr); + if let Ok(filename) = String::from_utf8(name_expr.to_vec()) { + if let Ok(path) = canonicalize_with(&filename, cwd) { + if let Ok(contents) = std::fs::read(&path) { + // This will load the defs from the file into the + // working set, if it was a successful parse. + let (block, err) = parse( + working_set, + path.file_name().and_then(|x| x.to_str()), + &contents, + false, + ); + + if err.is_some() { + // Unsuccessful parse of file + return ( + Statement::Pipeline(Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: span(&spans[1..]), + ty: Type::Unknown, + custom_completion: None, + }])), + // Return the file parse error + err, + ); + } else { + // Save the block into the working set + let block_id = working_set.add_block(block); + + let mut call_with_block = call; + + // Adding this expression to the positional creates a syntax highlighting error + // after writing `source example.nu` + call_with_block.positional.push(Expression { + expr: Expr::Int(block_id as i64), + span: spans[1], + ty: Type::Unknown, + custom_completion: None, + }); + + return ( + Statement::Pipeline(Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call_with_block), + span: span(spans), + ty: Type::Unknown, + custom_completion: None, + }])), + None, + ); + } + } + } else { + error = error.or(Some(ParseError::FileNotFound(filename, spans[1]))); + } + } else { + return ( + garbage_statement(spans), + Some(ParseError::NonUtf8(spans[1])), + ); + } + } + return ( + Statement::Pipeline(Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: span(spans), + ty: Type::Unknown, + custom_completion: None, + }])), + error, + ); + } + } + ( + garbage_statement(spans), + Some(ParseError::UnknownState( + "internal error: source statement unparseable".into(), + span(spans), + )), + ) +} + +#[cfg(feature = "plugin")] +pub fn parse_register( + working_set: &mut StateWorkingSet, + spans: &[Span], +) -> (Statement, Option) { + use nu_plugin::{get_signature, EncodingType, PluginDeclaration}; + use nu_protocol::Signature; + let cwd = working_set.get_cwd(); + + // Checking that the function is used with the correct name + // Maybe this is not necessary but it is a sanity check + if working_set.get_span_contents(spans[0]) != b"register" { + return ( + garbage_statement(spans), + Some(ParseError::UnknownState( + "internal error: Wrong call name for parse plugin function".into(), + span(spans), + )), + ); + } + + // Parsing the spans and checking that they match the register signature + // Using a parsed call makes more sense than checking for how many spans are in the call + // Also, by creating a call, it can be checked if it matches the declaration signature + let (call, call_span) = match working_set.find_decl(b"register") { + None => { + return ( + garbage_statement(spans), + Some(ParseError::UnknownState( + "internal error: Register declaration not found".into(), + span(spans), + )), + ) + } + Some(decl_id) => { + let (call, mut err) = parse_internal_call(working_set, spans[0], &spans[1..], decl_id); + let decl = working_set.get_decl(decl_id); + + let call_span = span(spans); + + err = check_call(call_span, &decl.signature(), &call).or(err); + if err.is_some() || call.has_flag("help") { + return ( + Statement::Pipeline(Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: call_span, + ty: Type::Unknown, + custom_completion: None, + }])), + err, + ); + } + + (call, call_span) + } + }; + + // Extracting the required arguments from the call and keeping them together in a tuple + // The ? operator is not used because the error has to be kept to be printed in the shell + // For that reason the values are kept in a result that will be passed at the end of this call + let cwd_clone = cwd.clone(); + let arguments = call + .positional + .get(0) + .map(|expr| { + let name_expr = working_set.get_span_contents(expr.span); + String::from_utf8(name_expr.to_vec()) + .map_err(|_| ParseError::NonUtf8(expr.span)) + .and_then(move |name| { + canonicalize_with(&name, cwd_clone) + .map_err(|_| ParseError::FileNotFound(name, expr.span)) + }) + .and_then(|path| { + if path.exists() & path.is_file() { + Ok(path) + } else { + Err(ParseError::FileNotFound(format!("{:?}", path), expr.span)) + } + }) + }) + .expect("required positional has being checked") + .and_then(|path| { + call.get_flag_expr("encoding") + .map(|expr| { + EncodingType::try_from_bytes(working_set.get_span_contents(expr.span)) + .ok_or_else(|| { + ParseError::IncorrectValue( + "wrong encoding".into(), + expr.span, + "Encodings available: capnp and json".into(), + ) + }) + }) + .expect("required named has being checked") + .map(|encoding| (path, encoding)) + }); + + // Signature is an optional value from the call and will be used to decide if + // the plugin is called to get the signatures or to use the given signature + let signature = call.positional.get(1).map(|expr| { + let signature = working_set.get_span_contents(expr.span); + serde_json::from_slice::(signature).map_err(|_| { + ParseError::LabeledError( + "Signature deserialization error".into(), + "unable to deserialize signature".into(), + spans[0], + ) + }) + }); + + // Shell is another optional value used as base to call shell to plugins + let shell = call.get_flag_expr("shell").map(|expr| { + let shell_expr = working_set.get_span_contents(expr.span); + + String::from_utf8(shell_expr.to_vec()) + .map_err(|_| ParseError::NonUtf8(expr.span)) + .and_then(|name| { + canonicalize_with(&name, cwd).map_err(|_| ParseError::FileNotFound(name, expr.span)) + }) + .and_then(|path| { + if path.exists() & path.is_file() { + Ok(path) + } else { + Err(ParseError::FileNotFound(format!("{:?}", path), expr.span)) + } + }) + }); + + let shell = match shell { + None => None, + Some(path) => match path { + Ok(path) => Some(path), + Err(err) => { + return ( + Statement::Pipeline(Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: call_span, + ty: Type::Unknown, + custom_completion: None, + }])), + Some(err), + ); + } + }, + }; + + let error = match signature { + Some(signature) => arguments.and_then(|(path, encoding)| { + signature.map(|signature| { + let plugin_decl = PluginDeclaration::new(path, signature, encoding, shell); + working_set.add_decl(Box::new(plugin_decl)); + working_set.mark_plugins_file_dirty(); + }) + }), + None => arguments.and_then(|(path, encoding)| { + get_signature(path.as_path(), &encoding, &shell) + .map_err(|err| { + ParseError::LabeledError( + "Error getting signatures".into(), + err.to_string(), + spans[0], + ) + }) + .map(|signatures| { + for signature in signatures { + // create plugin command declaration (need struct impl Command) + // store declaration in working set + let plugin_decl = PluginDeclaration::new( + path.clone(), + signature, + encoding.clone(), + shell.clone(), + ); + + working_set.add_decl(Box::new(plugin_decl)); + } + + working_set.mark_plugins_file_dirty(); + }) + }), + } + .err(); + + ( + Statement::Pipeline(Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: call_span, + ty: Type::Nothing, + custom_completion: None, + }])), + error, + ) +} diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs new file mode 100644 index 0000000000..3bc9340fbb --- /dev/null +++ b/crates/nu-parser/src/parser.rs @@ -0,0 +1,4055 @@ +use crate::{ + lex, lite_parse, + lite_parse::LiteCommand, + parse_keywords::{parse_for, parse_source}, + type_check::{math_result_type, type_compatible}, + LiteBlock, ParseError, Token, TokenContents, +}; + +use nu_protocol::{ + ast::{ + Block, Call, CellPath, Expr, Expression, FullCellPath, ImportPattern, ImportPatternHead, + ImportPatternMember, Operator, PathMember, Pipeline, RangeInclusion, RangeOperator, + Statement, + }, + engine::StateWorkingSet, + span, DeclId, Flag, PositionalArg, Signature, Span, Spanned, SyntaxShape, Type, Unit, VarId, + CONFIG_VARIABLE_ID, ENV_VARIABLE_ID, IN_VARIABLE_ID, +}; + +use crate::parse_keywords::{ + parse_alias, parse_def, parse_def_predecl, parse_hide, parse_let, parse_module, parse_use, +}; + +use log::trace; +use std::collections::HashSet; + +#[cfg(feature = "plugin")] +use crate::parse_keywords::parse_register; + +#[derive(Debug, Clone)] +pub enum Import {} + +pub fn garbage(span: Span) -> Expression { + Expression::garbage(span) +} + +pub fn garbage_statement(spans: &[Span]) -> Statement { + Statement::Pipeline(Pipeline::from_vec(vec![garbage(span(spans))])) +} + +fn is_identifier_byte(b: u8) -> bool { + b != b'.' && b != b'[' && b != b'(' && b != b'{' +} + +fn is_math_expression_byte(b: u8) -> bool { + b == b'0' + || b == b'1' + || b == b'2' + || b == b'3' + || b == b'4' + || b == b'5' + || b == b'6' + || b == b'7' + || b == b'8' + || b == b'9' + || b == b'(' + || b == b'{' + || b == b'[' + || b == b'$' + || b == b'"' + || b == b'\'' + || b == b'-' +} + +fn is_identifier(bytes: &[u8]) -> bool { + bytes.iter().all(|x| is_identifier_byte(*x)) +} + +fn is_variable(bytes: &[u8]) -> bool { + if bytes.len() > 1 && bytes[0] == b'$' { + is_identifier(&bytes[1..]) + } else { + is_identifier(bytes) + } +} + +pub fn trim_quotes(bytes: &[u8]) -> &[u8] { + if (bytes.starts_with(b"\"") && bytes.ends_with(b"\"") && bytes.len() > 1) + || (bytes.starts_with(b"\'") && bytes.ends_with(b"\'") && bytes.len() > 1) + { + &bytes[1..(bytes.len() - 1)] + } else { + bytes + } +} + +pub fn check_call(command: Span, sig: &Signature, call: &Call) -> Option { + // Allow the call to pass if they pass in the help flag + if call.named.iter().any(|(n, _)| n.item == "help") { + return None; + } + + if call.positional.len() < sig.required_positional.len() { + // Comparing the types of all signature positional arguments against the parsed + // expressions found in the call. If one type is not found then it could be assumed + // that that positional argument is missing from the parsed call + for argument in &sig.required_positional { + let found = call.positional.iter().fold(false, |ac, expr| { + if argument.shape.to_type() == expr.ty { + true + } else { + ac + } + }); + if !found { + if let Some(last) = call.positional.last() { + return Some(ParseError::MissingPositional( + argument.name.clone(), + Span { + start: last.span.end, + end: last.span.end, + }, + sig.call_signature(), + )); + } else { + return Some(ParseError::MissingPositional( + argument.name.clone(), + command, + sig.call_signature(), + )); + } + } + } + + let missing = &sig.required_positional[call.positional.len()]; + if let Some(last) = call.positional.last() { + Some(ParseError::MissingPositional( + missing.name.clone(), + Span { + start: last.span.end, + end: last.span.end, + }, + sig.call_signature(), + )) + } else { + Some(ParseError::MissingPositional( + missing.name.clone(), + command, + sig.call_signature(), + )) + } + } else { + for req_flag in sig.named.iter().filter(|x| x.required) { + if call.named.iter().all(|(n, _)| n.item != req_flag.long) { + return Some(ParseError::MissingRequiredFlag( + req_flag.long.clone(), + command, + )); + } + } + None + } +} + +pub fn check_name<'a>( + working_set: &mut StateWorkingSet, + spans: &'a [Span], +) -> Option<(&'a Span, ParseError)> { + if spans.len() == 1 { + None + } else if spans.len() < 4 { + if working_set.get_span_contents(spans[1]) == b"=" { + let name = String::from_utf8_lossy(working_set.get_span_contents(spans[0])); + Some(( + &spans[1], + ParseError::AssignmentMismatch( + format!("{} missing name", name), + "missing name".into(), + spans[1], + ), + )) + } else { + None + } + } else if working_set.get_span_contents(spans[2]) != b"=" { + let name = String::from_utf8_lossy(working_set.get_span_contents(spans[0])); + Some(( + &spans[2], + ParseError::AssignmentMismatch( + format!("{} missing sign", name), + "missing equal sign".into(), + spans[2], + ), + )) + } else { + None + } +} + +pub fn parse_external_call( + working_set: &mut StateWorkingSet, + spans: &[Span], +) -> (Expression, Option) { + let mut args = vec![]; + + let head_contents = working_set.get_span_contents(spans[0]); + + let head_span = if head_contents.starts_with(b"^") { + Span { + start: spans[0].start + 1, + end: spans[0].end, + } + } else { + spans[0] + }; + + let head_contents = working_set.get_span_contents(head_span); + + let mut error = None; + + let head = if head_contents.starts_with(b"$") || head_contents.starts_with(b"(") { + let (arg, err) = parse_expression(working_set, &[head_span], true); + error = error.or(err); + Box::new(arg) + } else { + Box::new(Expression { + expr: Expr::String(String::from_utf8_lossy(head_contents).to_string()), + span: head_span, + ty: Type::String, + custom_completion: None, + }) + }; + + for span in &spans[1..] { + let contents = working_set.get_span_contents(*span); + + if contents.starts_with(b"$") || contents.starts_with(b"(") { + let (arg, err) = parse_expression(working_set, &[*span], true); + error = error.or(err); + args.push(arg); + } else { + args.push(Expression { + expr: Expr::String(String::from_utf8_lossy(contents).to_string()), + span: *span, + ty: Type::String, + custom_completion: None, + }) + } + } + ( + Expression { + expr: Expr::ExternalCall(head, args), + span: span(spans), + ty: Type::Unknown, + custom_completion: None, + }, + error, + ) +} + +fn parse_long_flag( + working_set: &mut StateWorkingSet, + spans: &[Span], + spans_idx: &mut usize, + sig: &Signature, +) -> ( + Option>, + Option, + Option, +) { + let arg_span = spans[*spans_idx]; + let arg_contents = working_set.get_span_contents(arg_span); + + if arg_contents.starts_with(b"--") { + // FIXME: only use the first flag you find? + let split: Vec<_> = arg_contents.split(|x| *x == b'=').collect(); + let long_name = String::from_utf8(split[0].into()); + if let Ok(long_name) = long_name { + let long_name = long_name[2..].to_string(); + if let Some(flag) = sig.get_long_flag(&long_name) { + if let Some(arg_shape) = &flag.arg { + if split.len() > 1 { + // and we also have the argument + let long_name_len = long_name.len(); + let mut span = arg_span; + span.start += long_name_len + 3; //offset by long flag and '=' + + let (arg, err) = parse_value(working_set, span, arg_shape); + + ( + Some(Spanned { + item: long_name, + span: Span { + start: arg_span.start, + end: arg_span.start + long_name_len + 2, + }, + }), + Some(arg), + err, + ) + } else if let Some(arg) = spans.get(*spans_idx + 1) { + let (arg, err) = parse_value(working_set, *arg, arg_shape); + + *spans_idx += 1; + ( + Some(Spanned { + item: long_name, + span: arg_span, + }), + Some(arg), + err, + ) + } else { + ( + Some(Spanned { + item: long_name, + span: arg_span, + }), + None, + Some(ParseError::MissingFlagParam( + arg_shape.to_string(), + arg_span, + )), + ) + } + } else { + // A flag with no argument + ( + Some(Spanned { + item: long_name, + span: arg_span, + }), + None, + None, + ) + } + } else { + ( + Some(Spanned { + item: long_name.clone(), + span: arg_span, + }), + None, + Some(ParseError::UnknownFlag( + sig.name.clone(), + long_name.clone(), + arg_span, + )), + ) + } + } else { + ( + Some(Spanned { + item: "--".into(), + span: arg_span, + }), + None, + Some(ParseError::NonUtf8(arg_span)), + ) + } + } else { + (None, None, None) + } +} + +fn parse_short_flags( + working_set: &mut StateWorkingSet, + spans: &[Span], + spans_idx: &mut usize, + positional_idx: usize, + sig: &Signature, +) -> (Option>, Option) { + let mut error = None; + let arg_span = spans[*spans_idx]; + + let arg_contents = working_set.get_span_contents(arg_span); + + if arg_contents.starts_with(b"-") && arg_contents.len() > 1 { + let short_flags = &arg_contents[1..]; + let mut found_short_flags = vec![]; + let mut unmatched_short_flags = vec![]; + for short_flag in short_flags.iter().enumerate() { + let short_flag_char = char::from(*short_flag.1); + let orig = arg_span; + let short_flag_span = Span { + start: orig.start + 1 + short_flag.0, + end: orig.start + 1 + short_flag.0 + 1, + }; + if let Some(flag) = sig.get_short_flag(short_flag_char) { + // If we require an arg and are in a batch of short flags, error + if !found_short_flags.is_empty() && flag.arg.is_some() { + error = error.or(Some(ParseError::ShortFlagBatchCantTakeArg(short_flag_span))) + } + found_short_flags.push(flag); + } else { + unmatched_short_flags.push(short_flag_span); + } + } + + if found_short_flags.is_empty() { + // check to see if we have a negative number + if let Some(positional) = sig.get_positional(positional_idx) { + if positional.shape == SyntaxShape::Int || positional.shape == SyntaxShape::Number { + if String::from_utf8_lossy(arg_contents).parse::().is_ok() { + return (None, None); + } else if let Some(first) = unmatched_short_flags.first() { + let contents = working_set.get_span_contents(*first); + error = error.or_else(|| { + Some(ParseError::UnknownFlag( + sig.name.clone(), + format!("-{}", String::from_utf8_lossy(contents)), + *first, + )) + }); + } + } else if let Some(first) = unmatched_short_flags.first() { + let contents = working_set.get_span_contents(*first); + error = error.or_else(|| { + Some(ParseError::UnknownFlag( + sig.name.clone(), + format!("-{}", String::from_utf8_lossy(contents)), + *first, + )) + }); + } + } else if let Some(first) = unmatched_short_flags.first() { + let contents = working_set.get_span_contents(*first); + error = error.or_else(|| { + Some(ParseError::UnknownFlag( + sig.name.clone(), + format!("-{}", String::from_utf8_lossy(contents)), + *first, + )) + }); + } + } else if !unmatched_short_flags.is_empty() { + if let Some(first) = unmatched_short_flags.first() { + let contents = working_set.get_span_contents(*first); + error = error.or_else(|| { + Some(ParseError::UnknownFlag( + sig.name.clone(), + format!("-{}", String::from_utf8_lossy(contents)), + *first, + )) + }); + } + } + + (Some(found_short_flags), error) + } else { + (None, None) + } +} + +fn first_kw_idx( + working_set: &StateWorkingSet, + signature: &Signature, + spans: &[Span], + spans_idx: usize, + positional_idx: usize, +) -> (Option, usize) { + for idx in (positional_idx + 1)..signature.num_positionals() { + if let Some(PositionalArg { + shape: SyntaxShape::Keyword(kw, ..), + .. + }) = signature.get_positional(idx) + { + #[allow(clippy::needless_range_loop)] + for span_idx in spans_idx..spans.len() { + let contents = working_set.get_span_contents(spans[span_idx]); + + if contents == kw { + return (Some(idx), span_idx); + } + } + } + } + (None, spans.len()) +} + +fn calculate_end_span( + working_set: &StateWorkingSet, + signature: &Signature, + spans: &[Span], + spans_idx: usize, + positional_idx: usize, +) -> usize { + if signature.rest_positional.is_some() { + spans.len() + } else { + let (kw_pos, kw_idx) = + first_kw_idx(working_set, signature, spans, spans_idx, positional_idx); + + if let Some(kw_pos) = kw_pos { + // We found a keyword. Keywords, once found, create a guidepost to + // show us where the positionals will lay into the arguments. Because they're + // keywords, they get to set this by being present + + let positionals_between = kw_pos - positional_idx - 1; + if positionals_between > (kw_idx - spans_idx) { + kw_idx + } else { + kw_idx - positionals_between + } + } else { + // Make space for the remaining require positionals, if we can + if positional_idx < signature.required_positional.len() + && spans.len() > (signature.required_positional.len() - positional_idx) + { + spans.len() - (signature.required_positional.len() - positional_idx - 1) + } else if signature.num_positionals_after(positional_idx) == 0 { + spans.len() + } else { + spans_idx + 1 + } + } + } +} + +pub fn parse_multispan_value( + working_set: &mut StateWorkingSet, + spans: &[Span], + spans_idx: &mut usize, + shape: &SyntaxShape, +) -> (Expression, Option) { + let mut error = None; + + match shape { + SyntaxShape::VarWithOptType => { + trace!("parsing: var with opt type"); + + let (arg, err) = parse_var_with_opt_type(working_set, spans, spans_idx); + error = error.or(err); + + (arg, error) + } + SyntaxShape::RowCondition => { + trace!("parsing: row condition"); + let (arg, err) = parse_row_condition(working_set, &spans[*spans_idx..]); + error = error.or(err); + *spans_idx = spans.len() - 1; + + (arg, error) + } + SyntaxShape::MathExpression => { + trace!("parsing: math expression"); + + let (arg, err) = parse_math_expression(working_set, &spans[*spans_idx..], None); + error = error.or(err); + *spans_idx = spans.len() - 1; + + (arg, error) + } + SyntaxShape::Expression => { + trace!("parsing: expression"); + + let (arg, err) = parse_expression(working_set, &spans[*spans_idx..], true); + error = error.or(err); + *spans_idx = spans.len() - 1; + + (arg, error) + } + SyntaxShape::ImportPattern => { + trace!("parsing: import pattern"); + + let (arg, err) = parse_import_pattern(working_set, &spans[*spans_idx..]); + error = error.or(err); + *spans_idx = spans.len() - 1; + + (arg, error) + } + SyntaxShape::Keyword(keyword, arg) => { + trace!( + "parsing: keyword({}) {:?}", + String::from_utf8_lossy(keyword), + arg + ); + let arg_span = spans[*spans_idx]; + + let arg_contents = working_set.get_span_contents(arg_span); + + if arg_contents != keyword { + // When keywords mismatch, this is a strong indicator of something going wrong. + // We won't often override the current error, but as this is a strong indicator + // go ahead and override the current error and tell the user about the missing + // keyword/literal. + error = Some(ParseError::ExpectedKeyword( + String::from_utf8_lossy(keyword).into(), + arg_span, + )) + } + + *spans_idx += 1; + if *spans_idx >= spans.len() { + error = error.or_else(|| { + Some(ParseError::KeywordMissingArgument( + arg.to_string(), + String::from_utf8_lossy(keyword).into(), + Span { + start: spans[*spans_idx - 1].end, + end: spans[*spans_idx - 1].end, + }, + )) + }); + return ( + Expression { + expr: Expr::Keyword( + keyword.clone(), + spans[*spans_idx - 1], + Box::new(Expression::garbage(arg_span)), + ), + span: arg_span, + ty: Type::Unknown, + custom_completion: None, + }, + error, + ); + } + let keyword_span = spans[*spans_idx - 1]; + let (expr, err) = parse_multispan_value(working_set, spans, spans_idx, arg); + error = error.or(err); + let ty = expr.ty.clone(); + + ( + Expression { + expr: Expr::Keyword(keyword.clone(), keyword_span, Box::new(expr)), + span: arg_span, + ty, + custom_completion: None, + }, + error, + ) + } + _ => { + // All other cases are single-span values + let arg_span = spans[*spans_idx]; + + let (arg, err) = parse_value(working_set, arg_span, shape); + error = error.or(err); + + (arg, error) + } + } +} + +pub fn parse_internal_call( + working_set: &mut StateWorkingSet, + command_span: Span, + spans: &[Span], + decl_id: usize, +) -> (Box, Option) { + trace!("parsing: internal call (decl id: {})", decl_id); + + let mut error = None; + + let mut call = Call::new(command_span); + call.decl_id = decl_id; + call.head = command_span; + + let signature = working_set.get_decl(decl_id).signature(); + + if signature.creates_scope { + working_set.enter_scope(); + } + + // The index into the positional parameter in the definition + let mut positional_idx = 0; + + // The index into the spans of argument data given to parse + // Starting at the first argument + let mut spans_idx = 0; + + while spans_idx < spans.len() { + let arg_span = spans[spans_idx]; + + // Check if we're on a long flag, if so, parse + let (long_name, arg, err) = parse_long_flag(working_set, spans, &mut spans_idx, &signature); + if let Some(long_name) = long_name { + // We found a long flag, like --bar + error = error.or(err); + call.named.push((long_name, arg)); + spans_idx += 1; + continue; + } + + // Check if we're on a short flag or group of short flags, if so, parse + let (short_flags, err) = parse_short_flags( + working_set, + spans, + &mut spans_idx, + positional_idx, + &signature, + ); + + if let Some(short_flags) = short_flags { + error = error.or(err); + for flag in short_flags { + if let Some(arg_shape) = flag.arg { + if let Some(arg) = spans.get(spans_idx + 1) { + let (arg, err) = parse_value(working_set, *arg, &arg_shape); + error = error.or(err); + + call.named.push(( + Spanned { + item: flag.long.clone(), + span: spans[spans_idx], + }, + Some(arg), + )); + spans_idx += 1; + } else { + error = error.or_else(|| { + Some(ParseError::MissingFlagParam( + arg_shape.to_string(), + arg_span, + )) + }) + } + } else { + call.named.push(( + Spanned { + item: flag.long.clone(), + span: spans[spans_idx], + }, + None, + )); + } + } + spans_idx += 1; + continue; + } + + // Parse a positional arg if there is one + if let Some(positional) = signature.get_positional(positional_idx) { + let end = calculate_end_span(working_set, &signature, spans, spans_idx, positional_idx); + + // println!( + // "start: {} end: {} positional_idx: {}", + // spans_idx, end, positional_idx + // ); + + if spans[..end].is_empty() { + error = error.or_else(|| { + Some(ParseError::MissingPositional( + positional.name.clone(), + spans[spans_idx], + signature.call_signature(), + )) + }); + positional_idx += 1; + continue; + } + + let orig_idx = spans_idx; + let (arg, err) = parse_multispan_value( + working_set, + &spans[..end], + &mut spans_idx, + &positional.shape, + ); + error = error.or(err); + + let arg = if !type_compatible(&positional.shape.to_type(), &arg.ty) { + let span = span(&spans[orig_idx..spans_idx]); + error = error.or_else(|| { + Some(ParseError::TypeMismatch( + positional.shape.to_type(), + arg.ty, + arg.span, + )) + }); + Expression::garbage(span) + } else { + arg + }; + call.positional.push(arg); + positional_idx += 1; + } else { + call.positional.push(Expression::garbage(arg_span)); + error = error.or_else(|| { + Some(ParseError::ExtraPositional( + signature.call_signature(), + arg_span, + )) + }) + } + + error = error.or(err); + spans_idx += 1; + } + + let err = check_call(command_span, &signature, &call); + error = error.or(err); + + if signature.creates_scope { + working_set.exit_scope(); + } + + // FIXME: output type unknown + (Box::new(call), error) +} + +pub fn parse_call( + working_set: &mut StateWorkingSet, + spans: &[Span], + expand_aliases: bool, + head: Span, +) -> (Expression, Option) { + trace!("parsing: call"); + + if spans.is_empty() { + return ( + garbage(head), + Some(ParseError::UnknownState( + "Encountered command with zero spans".into(), + span(spans), + )), + ); + } + + let mut pos = 0; + let cmd_start = pos; + let mut name_spans = vec![]; + let mut name = vec![]; + + for word_span in spans[cmd_start..].iter() { + // Find the longest group of words that could form a command + let bytes = working_set.get_span_contents(*word_span); + + if is_math_expression_byte(bytes[0]) { + break; + } + + name_spans.push(*word_span); + + let name_part = working_set.get_span_contents(*word_span); + if name.is_empty() { + name.extend(name_part); + } else { + name.push(b' '); + name.extend(name_part); + } + + if expand_aliases { + // If the word is an alias, expand it and re-parse the expression + if let Some(expansion) = working_set.find_alias(&name) { + trace!("expanding alias"); + + let orig_span = spans[pos]; + let mut new_spans: Vec = vec![]; + new_spans.extend(&spans[0..pos]); + new_spans.extend(expansion); + if spans.len() > pos { + new_spans.extend(&spans[(pos + 1)..]); + } + + let (result, err) = parse_expression(working_set, &new_spans, false); + + let expression = match result { + Expression { + expr: Expr::Call(mut call), + span, + ty, + custom_completion, + } => { + call.head = orig_span; + Expression { + expr: Expr::Call(call), + span, + ty, + custom_completion, + } + } + x => x, + }; + + return (expression, err); + } + } + + pos += 1; + } + + let mut maybe_decl_id = working_set.find_decl(&name); + + while maybe_decl_id.is_none() { + // Find the longest command match + if name_spans.len() <= 1 { + // Keep the first word even if it does not match -- could be external command + break; + } + + name_spans.pop(); + pos -= 1; + + let mut name = vec![]; + for name_span in &name_spans { + let name_part = working_set.get_span_contents(*name_span); + if name.is_empty() { + name.extend(name_part); + } else { + name.push(b' '); + name.extend(name_part); + } + } + maybe_decl_id = working_set.find_decl(&name); + } + + if let Some(decl_id) = maybe_decl_id { + // Before the internal parsing we check if there is no let or alias declarations + // that are missing their name, e.g.: let = 1 or alias = 2 + if spans.len() > 1 { + let test_equal = working_set.get_span_contents(spans[1]); + + if test_equal == [b'='] { + trace!("incomplete statement"); + + return ( + garbage(span(spans)), + Some(ParseError::UnknownState( + "Incomplete statement".into(), + span(spans), + )), + ); + } + } + + trace!("parsing: internal call"); + + // parse internal command + let (call, err) = parse_internal_call( + working_set, + span(&spans[cmd_start..pos]), + &spans[pos..], + decl_id, + ); + ( + Expression { + expr: Expr::Call(call), + span: span(spans), + ty: Type::Unknown, // FIXME: calls should have known output types + custom_completion: None, + }, + err, + ) + } else { + // We might be parsing left-unbounded range ("..10") + let bytes = working_set.get_span_contents(spans[0]); + trace!("parsing: range {:?} ", bytes); + if let (Some(b'.'), Some(b'.')) = (bytes.get(0), bytes.get(1)) { + trace!("-- found leading range indicator"); + let (range_expr, range_err) = parse_range(working_set, spans[0]); + if range_err.is_none() { + trace!("-- successfully parsed range"); + return (range_expr, range_err); + } + } + trace!("parsing: external call"); + + // Otherwise, try external command + parse_external_call(working_set, spans) + } +} + +pub fn parse_int(token: &[u8], span: Span) -> (Expression, Option) { + if let Some(token) = token.strip_prefix(b"0x") { + if let Ok(v) = i64::from_str_radix(&String::from_utf8_lossy(token), 16) { + ( + Expression { + expr: Expr::Int(v), + span, + ty: Type::Int, + custom_completion: None, + }, + None, + ) + } else { + ( + garbage(span), + Some(ParseError::Mismatch( + "int".into(), + "incompatible int".into(), + span, + )), + ) + } + } else if let Some(token) = token.strip_prefix(b"0b") { + if let Ok(v) = i64::from_str_radix(&String::from_utf8_lossy(token), 2) { + ( + Expression { + expr: Expr::Int(v), + span, + ty: Type::Int, + custom_completion: None, + }, + None, + ) + } else { + ( + garbage(span), + Some(ParseError::Mismatch( + "int".into(), + "incompatible int".into(), + span, + )), + ) + } + } else if let Some(token) = token.strip_prefix(b"0o") { + if let Ok(v) = i64::from_str_radix(&String::from_utf8_lossy(token), 8) { + ( + Expression { + expr: Expr::Int(v), + span, + ty: Type::Int, + custom_completion: None, + }, + None, + ) + } else { + ( + garbage(span), + Some(ParseError::Mismatch( + "int".into(), + "incompatible int".into(), + span, + )), + ) + } + } else if let Ok(x) = String::from_utf8_lossy(token).parse::() { + ( + Expression { + expr: Expr::Int(x), + span, + ty: Type::Int, + custom_completion: None, + }, + None, + ) + } else { + ( + garbage(span), + Some(ParseError::Expected("int".into(), span)), + ) + } +} + +pub fn parse_float(token: &[u8], span: Span) -> (Expression, Option) { + if let Ok(x) = String::from_utf8_lossy(token).parse::() { + ( + Expression { + expr: Expr::Float(x), + span, + ty: Type::Float, + custom_completion: None, + }, + None, + ) + } else { + ( + garbage(span), + Some(ParseError::Expected("float".into(), span)), + ) + } +} + +pub fn parse_number(token: &[u8], span: Span) -> (Expression, Option) { + if let (x, None) = parse_int(token, span) { + (x, None) + } else if let (x, None) = parse_float(token, span) { + (x, None) + } else { + ( + garbage(span), + Some(ParseError::Expected("number".into(), span)), + ) + } +} + +pub fn parse_range( + working_set: &mut StateWorkingSet, + span: Span, +) -> (Expression, Option) { + trace!("parsing: range"); + + // Range follows the following syntax: [][][] + // where is ".." + // and is ".." or "..<" + // and one of the or bounds must be present (just '..' is not allowed since it + // looks like parent directory) + + let contents = working_set.get_span_contents(span); + let token = if let Ok(s) = String::from_utf8(contents.into()) { + s + } else { + return (garbage(span), Some(ParseError::NonUtf8(span))); + }; + + // First, figure out what exact operators are used and determine their positions + let dotdot_pos: Vec<_> = token.match_indices("..").map(|(pos, _)| pos).collect(); + + let (next_op_pos, range_op_pos) = + match dotdot_pos.len() { + 1 => (None, dotdot_pos[0]), + 2 => (Some(dotdot_pos[0]), dotdot_pos[1]), + _ => return ( + garbage(span), + Some(ParseError::Expected( + "one range operator ('..' or '..<') and optionally one next operator ('..')" + .into(), + span, + )), + ), + }; + + let (inclusion, range_op_str, range_op_span) = if let Some(pos) = token.find("..<") { + if pos == range_op_pos { + let op_str = "..<"; + let op_span = Span::new( + span.start + range_op_pos, + span.start + range_op_pos + op_str.len(), + ); + (RangeInclusion::RightExclusive, "..<", op_span) + } else { + return ( + garbage(span), + Some(ParseError::Expected( + "inclusive operator preceding second range bound".into(), + span, + )), + ); + } + } else { + let op_str = ".."; + let op_span = Span::new( + span.start + range_op_pos, + span.start + range_op_pos + op_str.len(), + ); + (RangeInclusion::Inclusive, "..", op_span) + }; + + // Now, based on the operator positions, figure out where the bounds & next are located and + // parse them + // TODO: Actually parse the next number in the range + let from = if token.starts_with("..") { + // token starts with either next operator, or range operator -- we don't care which one + None + } else { + let from_span = Span::new(span.start, span.start + dotdot_pos[0]); + match parse_value(working_set, from_span, &SyntaxShape::Number) { + (expression, None) => Some(Box::new(expression)), + _ => { + return ( + garbage(span), + Some(ParseError::Expected("number".into(), span)), + ) + } + } + }; + + let to = if token.ends_with(range_op_str) { + None + } else { + let to_span = Span::new(range_op_span.end, span.end); + match parse_value(working_set, to_span, &SyntaxShape::Number) { + (expression, None) => Some(Box::new(expression)), + _ => { + return ( + garbage(span), + Some(ParseError::Expected("number".into(), span)), + ) + } + } + }; + + trace!("-- from: {:?} to: {:?}", from, to); + + if let (None, None) = (&from, &to) { + return ( + garbage(span), + Some(ParseError::Expected( + "at least one range bound set".into(), + span, + )), + ); + } + + let (next, next_op_span) = if let Some(pos) = next_op_pos { + let next_op_span = Span::new(span.start + pos, span.start + pos + "..".len()); + let next_span = Span::new(next_op_span.end, range_op_span.start); + + match parse_value(working_set, next_span, &SyntaxShape::Number) { + (expression, None) => (Some(Box::new(expression)), next_op_span), + _ => { + return ( + garbage(span), + Some(ParseError::Expected("number".into(), span)), + ) + } + } + } else { + (None, span) + }; + + let range_op = RangeOperator { + inclusion, + span: range_op_span, + next_op_span, + }; + + ( + Expression { + expr: Expr::Range(from, next, to, range_op), + span, + ty: Type::Range, + custom_completion: None, + }, + None, + ) +} + +pub(crate) fn parse_dollar_expr( + working_set: &mut StateWorkingSet, + span: Span, +) -> (Expression, Option) { + let contents = working_set.get_span_contents(span); + + if contents.starts_with(b"$\"") || contents.starts_with(b"$'") { + parse_string_interpolation(working_set, span) + } else if let (expr, None) = parse_range(working_set, span) { + (expr, None) + } else { + parse_full_cell_path(working_set, None, span) + } +} + +pub fn parse_string_interpolation( + working_set: &mut StateWorkingSet, + span: Span, +) -> (Expression, Option) { + #[derive(PartialEq, Eq, Debug)] + enum InterpolationMode { + String, + Expression, + } + let mut error = None; + + let contents = working_set.get_span_contents(span); + + let (start, end) = if contents.starts_with(b"$\"") { + let end = if contents.ends_with(b"\"") && contents.len() > 2 { + span.end - 1 + } else { + span.end + }; + (span.start + 2, end) + } else if contents.starts_with(b"$'") { + let end = if contents.ends_with(b"'") && contents.len() > 2 { + span.end - 1 + } else { + span.end + }; + (span.start + 2, end) + } else { + (span.start, span.end) + }; + + let inner_span = Span { start, end }; + let contents = working_set.get_span_contents(inner_span).to_vec(); + + let mut output = vec![]; + let mut mode = InterpolationMode::String; + let mut token_start = start; + let mut depth = 0; + + let mut b = start; + + #[allow(clippy::needless_range_loop)] + while b != end { + if contents[b - start] == b'(' && mode == InterpolationMode::String { + depth = 1; + mode = InterpolationMode::Expression; + if token_start < b { + let span = Span { + start: token_start, + end: b, + }; + let str_contents = working_set.get_span_contents(span); + output.push(Expression { + expr: Expr::String(String::from_utf8_lossy(str_contents).to_string()), + span, + ty: Type::String, + custom_completion: None, + }); + } + token_start = b; + } else if contents[b - start] == b'(' && mode == InterpolationMode::Expression { + depth += 1; + } else if contents[b - start] == b')' && mode == InterpolationMode::Expression { + match depth { + 0 => {} + 1 => { + mode = InterpolationMode::String; + + if token_start < b { + let span = Span { + start: token_start, + end: b + 1, + }; + + let (expr, err) = parse_full_cell_path(working_set, None, span); + error = error.or(err); + output.push(expr); + } + + token_start = b + 1; + } + _ => depth -= 1, + } + } + b += 1; + } + + match mode { + InterpolationMode::String => { + if token_start < end { + let span = Span { + start: token_start, + end, + }; + let str_contents = working_set.get_span_contents(span); + output.push(Expression { + expr: Expr::String(String::from_utf8_lossy(str_contents).to_string()), + span, + ty: Type::String, + custom_completion: None, + }); + } + } + InterpolationMode::Expression => { + if token_start < end { + let span = Span { + start: token_start, + end, + }; + + let (expr, err) = parse_full_cell_path(working_set, None, span); + error = error.or(err); + output.push(expr); + } + } + } + + ( + Expression { + expr: Expr::StringInterpolation(output), + span, + ty: Type::String, + custom_completion: None, + }, + error, + ) +} + +pub fn parse_variable_expr( + working_set: &mut StateWorkingSet, + span: Span, +) -> (Expression, Option) { + let contents = working_set.get_span_contents(span); + + if contents == b"$true" { + return ( + Expression { + expr: Expr::Bool(true), + span, + ty: Type::Bool, + custom_completion: None, + }, + None, + ); + } else if contents == b"$false" { + return ( + Expression { + expr: Expr::Bool(false), + span, + ty: Type::Bool, + custom_completion: None, + }, + None, + ); + } else if contents == b"$nothing" { + return ( + Expression { + expr: Expr::Nothing, + span, + ty: Type::Nothing, + custom_completion: None, + }, + None, + ); + } else if contents == b"$nu" { + return ( + Expression { + expr: Expr::Var(nu_protocol::NU_VARIABLE_ID), + span, + ty: Type::Unknown, + custom_completion: None, + }, + None, + ); + } else if contents == b"$scope" { + return ( + Expression { + expr: Expr::Var(nu_protocol::SCOPE_VARIABLE_ID), + span, + ty: Type::Unknown, + custom_completion: None, + }, + None, + ); + } else if contents == b"$in" { + return ( + Expression { + expr: Expr::Var(nu_protocol::IN_VARIABLE_ID), + span, + ty: Type::Unknown, + custom_completion: None, + }, + None, + ); + } else if contents == b"$config" { + return ( + Expression { + expr: Expr::Var(nu_protocol::CONFIG_VARIABLE_ID), + span, + ty: Type::Unknown, + custom_completion: None, + }, + None, + ); + } else if contents == b"$env" { + return ( + Expression { + expr: Expr::Var(nu_protocol::ENV_VARIABLE_ID), + span, + ty: Type::Unknown, + custom_completion: None, + }, + None, + ); + } + + let (id, err) = parse_variable(working_set, span); + + if err.is_none() { + if let Some(id) = id { + ( + Expression { + expr: Expr::Var(id), + span, + ty: working_set.get_variable(id).clone(), + custom_completion: None, + }, + None, + ) + } else { + (garbage(span), Some(ParseError::VariableNotFound(span))) + } + } else { + (garbage(span), err) + } +} + +pub fn parse_cell_path( + working_set: &mut StateWorkingSet, + tokens: impl Iterator, + mut expect_dot: bool, + span: Span, +) -> (Vec, Option) { + let mut error = None; + let mut tail = vec![]; + + for path_element in tokens { + let bytes = working_set.get_span_contents(path_element.span); + + if expect_dot { + expect_dot = false; + if bytes.len() != 1 || bytes[0] != b'.' { + error = error.or_else(|| Some(ParseError::Expected('.'.into(), path_element.span))); + } + } else { + expect_dot = true; + + match parse_int(bytes, path_element.span) { + ( + Expression { + expr: Expr::Int(val), + span, + .. + }, + None, + ) => tail.push(PathMember::Int { + val: val as usize, + span, + }), + _ => { + let (result, err) = parse_string(working_set, path_element.span); + error = error.or(err); + match result { + Expression { + expr: Expr::String(string), + span, + .. + } => { + tail.push(PathMember::String { val: string, span }); + } + _ => { + error = + error.or_else(|| Some(ParseError::Expected("string".into(), span))); + } + } + } + } + } + } + + (tail, error) +} + +pub fn parse_full_cell_path( + working_set: &mut StateWorkingSet, + implicit_head: Option, + span: Span, +) -> (Expression, Option) { + let full_cell_span = span; + let source = working_set.get_span_contents(span); + let mut error = None; + + let (tokens, err) = lex(source, span.start, &[b'\n', b'\r'], &[b'.'], true); + error = error.or(err); + + let mut tokens = tokens.into_iter().peekable(); + if let Some(head) = tokens.peek() { + let bytes = working_set.get_span_contents(head.span); + let (head, expect_dot) = if bytes.starts_with(b"(") { + trace!("parsing: paren-head of full cell path"); + + let head_span = head.span; + let mut start = head.span.start; + let mut end = head.span.end; + + if bytes.starts_with(b"(") { + start += 1; + } + if bytes.ends_with(b")") { + end -= 1; + } else { + error = error + .or_else(|| Some(ParseError::Unclosed(")".into(), Span { start: end, end }))); + } + + let span = Span { start, end }; + + let source = working_set.get_span_contents(span); + + let (output, err) = lex(source, span.start, &[b'\n', b'\r'], &[], true); + error = error.or(err); + + let (output, err) = lite_parse(&output); + error = error.or(err); + + let (output, err) = parse_block(working_set, &output, true); + error = error.or(err); + + let block_id = working_set.add_block(output); + tokens.next(); + + ( + Expression { + expr: Expr::Subexpression(block_id), + span: head_span, + ty: Type::Unknown, // FIXME + custom_completion: None, + }, + true, + ) + } else if bytes.starts_with(b"[") { + trace!("parsing: table head of full cell path"); + + let (output, err) = parse_table_expression(working_set, head.span); + error = error.or(err); + + tokens.next(); + + (output, true) + } else if bytes.starts_with(b"{") { + trace!("parsing: record head of full cell path"); + let (output, err) = parse_record(working_set, head.span); + error = error.or(err); + + tokens.next(); + + (output, true) + } else if bytes.starts_with(b"$") { + trace!("parsing: $variable head of full cell path"); + + let (out, err) = parse_variable_expr(working_set, head.span); + error = error.or(err); + + tokens.next(); + + (out, true) + } else if let Some(var_id) = implicit_head { + ( + Expression { + expr: Expr::Var(var_id), + span, + ty: Type::Unknown, + custom_completion: None, + }, + false, + ) + } else { + return ( + garbage(span), + Some(ParseError::Mismatch( + "variable or subexpression".into(), + String::from_utf8_lossy(bytes).to_string(), + span, + )), + ); + }; + + let (tail, err) = parse_cell_path(working_set, tokens, expect_dot, span); + error = error.or(err); + + if !tail.is_empty() { + ( + Expression { + expr: Expr::FullCellPath(Box::new(FullCellPath { head, tail })), + ty: Type::Unknown, + span: full_cell_span, + custom_completion: None, + }, + error, + ) + } else { + let ty = head.ty.clone(); + ( + Expression { + expr: Expr::FullCellPath(Box::new(FullCellPath { head, tail })), + ty, + span: full_cell_span, + custom_completion: None, + }, + error, + ) + } + } else { + (garbage(span), error) + } +} + +pub fn parse_filepath( + working_set: &mut StateWorkingSet, + span: Span, +) -> (Expression, Option) { + let bytes = working_set.get_span_contents(span); + let bytes = trim_quotes(bytes); + trace!("parsing: filepath"); + + if let Ok(token) = String::from_utf8(bytes.into()) { + trace!("-- found {}", token); + ( + Expression { + expr: Expr::Filepath(token), + span, + ty: Type::String, + custom_completion: None, + }, + None, + ) + } else { + ( + garbage(span), + Some(ParseError::Expected("filepath".into(), span)), + ) + } +} + +/// Parse a duration type, eg '10day' +pub fn parse_duration( + working_set: &mut StateWorkingSet, + span: Span, +) -> (Expression, Option) { + trace!("parsing: duration"); + + fn parse_decimal_str_to_number(decimal: &str) -> Option { + let string_to_parse = format!("0.{}", decimal); + if let Ok(x) = string_to_parse.parse::() { + return Some((1_f64 / x) as i64); + } + None + } + + let bytes = working_set.get_span_contents(span); + let token = String::from_utf8_lossy(bytes).to_string(); + + let unit_groups = [ + (Unit::Nanosecond, "NS", None), + (Unit::Microsecond, "US", Some((Unit::Nanosecond, 1000))), + (Unit::Millisecond, "MS", Some((Unit::Microsecond, 1000))), + (Unit::Second, "SEC", Some((Unit::Millisecond, 1000))), + (Unit::Minute, "MIN", Some((Unit::Second, 60))), + (Unit::Hour, "HR", Some((Unit::Minute, 60))), + (Unit::Day, "DAY", Some((Unit::Minute, 1440))), + (Unit::Week, "WK", Some((Unit::Day, 7))), + ]; + if let Some(unit) = unit_groups + .iter() + .find(|&x| token.to_uppercase().ends_with(x.1)) + { + let mut lhs = token.clone(); + for _ in 0..unit.1.len() { + lhs.pop(); + } + + let input: Vec<&str> = lhs.split('.').collect(); + let (value, unit_to_use) = match &input[..] { + [number_str] => (number_str.parse::().ok(), unit.0), + [number_str, decimal_part_str] => match unit.2 { + Some(unit_to_convert_to) => match ( + number_str.parse::(), + parse_decimal_str_to_number(decimal_part_str), + ) { + (Ok(number), Some(decimal_part)) => ( + Some( + (number * unit_to_convert_to.1) + (unit_to_convert_to.1 / decimal_part), + ), + unit_to_convert_to.0, + ), + _ => (None, unit.0), + }, + None => (None, unit.0), + }, + _ => (None, unit.0), + }; + + if let Some(x) = value { + trace!("-- found {} {:?}", x, unit_to_use); + + let lhs_span = Span::new(span.start, span.start + lhs.len()); + let unit_span = Span::new(span.start + lhs.len(), span.end); + return ( + Expression { + expr: Expr::ValueWithUnit( + Box::new(Expression { + expr: Expr::Int(x), + span: lhs_span, + ty: Type::Number, + custom_completion: None, + }), + Spanned { + item: unit_to_use, + span: unit_span, + }, + ), + span, + ty: Type::Duration, + custom_completion: None, + }, + None, + ); + } + } + + ( + garbage(span), + Some(ParseError::Mismatch( + "duration".into(), + "non-duration unit".into(), + span, + )), + ) +} + +/// Parse a unit type, eg '10kb' +pub fn parse_filesize( + working_set: &mut StateWorkingSet, + span: Span, +) -> (Expression, Option) { + trace!("parsing: duration"); + + fn parse_decimal_str_to_number(decimal: &str) -> Option { + let string_to_parse = format!("0.{}", decimal); + if let Ok(x) = string_to_parse.parse::() { + return Some((1_f64 / x) as i64); + } + None + } + + let bytes = working_set.get_span_contents(span); + let token = String::from_utf8_lossy(bytes).to_string(); + + let unit_groups = [ + (Unit::Kilobyte, "KB", Some((Unit::Byte, 1000))), + (Unit::Megabyte, "MB", Some((Unit::Kilobyte, 1000))), + (Unit::Gigabyte, "GB", Some((Unit::Megabyte, 1000))), + (Unit::Terabyte, "TB", Some((Unit::Gigabyte, 1000))), + (Unit::Petabyte, "PB", Some((Unit::Terabyte, 1000))), + (Unit::Kibibyte, "KIB", Some((Unit::Byte, 1024))), + (Unit::Mebibyte, "MIB", Some((Unit::Kibibyte, 1024))), + (Unit::Gibibyte, "GIB", Some((Unit::Mebibyte, 1024))), + (Unit::Tebibyte, "TIB", Some((Unit::Gibibyte, 1024))), + (Unit::Pebibyte, "PIB", Some((Unit::Tebibyte, 1024))), + (Unit::Byte, "B", None), + ]; + if let Some(unit) = unit_groups + .iter() + .find(|&x| token.to_uppercase().ends_with(x.1)) + { + let mut lhs = token.clone(); + for _ in 0..unit.1.len() { + lhs.pop(); + } + + let input: Vec<&str> = lhs.split('.').collect(); + let (value, unit_to_use) = match &input[..] { + [number_str] => (number_str.parse::().ok(), unit.0), + [number_str, decimal_part_str] => match unit.2 { + Some(unit_to_convert_to) => match ( + number_str.parse::(), + parse_decimal_str_to_number(decimal_part_str), + ) { + (Ok(number), Some(decimal_part)) => ( + Some( + (number * unit_to_convert_to.1) + (unit_to_convert_to.1 / decimal_part), + ), + unit_to_convert_to.0, + ), + _ => (None, unit.0), + }, + None => (None, unit.0), + }, + _ => (None, unit.0), + }; + + if let Some(x) = value { + trace!("-- found {} {:?}", x, unit_to_use); + + let lhs_span = Span::new(span.start, span.start + lhs.len()); + let unit_span = Span::new(span.start + lhs.len(), span.end); + return ( + Expression { + expr: Expr::ValueWithUnit( + Box::new(Expression { + expr: Expr::Int(x), + span: lhs_span, + ty: Type::Number, + custom_completion: None, + }), + Spanned { + item: unit_to_use, + span: unit_span, + }, + ), + span, + ty: Type::Filesize, + custom_completion: None, + }, + None, + ); + } + } + + ( + garbage(span), + Some(ParseError::Mismatch( + "filesize".into(), + "non-filesize unit".into(), + span, + )), + ) +} + +pub fn parse_glob_pattern( + working_set: &mut StateWorkingSet, + span: Span, +) -> (Expression, Option) { + trace!("parsing: glob pattern"); + + let bytes = working_set.get_span_contents(span); + let bytes = trim_quotes(bytes); + + if let Ok(token) = String::from_utf8(bytes.into()) { + trace!("-- found {}", token); + ( + Expression { + expr: Expr::GlobPattern(token), + span, + ty: Type::String, + custom_completion: None, + }, + None, + ) + } else { + ( + garbage(span), + Some(ParseError::Expected("string".into(), span)), + ) + } +} + +pub fn parse_string( + working_set: &mut StateWorkingSet, + span: Span, +) -> (Expression, Option) { + trace!("parsing: string"); + + let bytes = working_set.get_span_contents(span); + + let bytes = trim_quotes(bytes); + + if let Ok(token) = String::from_utf8(bytes.into()) { + trace!("-- found {}", token); + + ( + Expression { + expr: Expr::String(token), + span, + ty: Type::String, + custom_completion: None, + }, + None, + ) + } else { + ( + garbage(span), + Some(ParseError::Expected("string".into(), span)), + ) + } +} + +pub fn parse_string_strict( + working_set: &mut StateWorkingSet, + span: Span, +) -> (Expression, Option) { + trace!("parsing: string, with required delimiters"); + + let bytes = working_set.get_span_contents(span); + + // Check for unbalanced quotes: + if (bytes.starts_with(b"\"") || (bytes.starts_with(b"$\""))) && !bytes.ends_with(b"\"") { + return (garbage(span), Some(ParseError::Unclosed("\"".into(), span))); + } + if (bytes.starts_with(b"\'") || (bytes.starts_with(b"$\""))) && !bytes.ends_with(b"\'") { + return (garbage(span), Some(ParseError::Unclosed("\'".into(), span))); + } + + let (bytes, quoted) = if (bytes.starts_with(b"\"") && bytes.ends_with(b"\"") && bytes.len() > 1) + || (bytes.starts_with(b"\'") && bytes.ends_with(b"\'") && bytes.len() > 1) + { + (&bytes[1..(bytes.len() - 1)], true) + } else { + (bytes, false) + }; + + if let Ok(token) = String::from_utf8(bytes.into()) { + trace!("-- found {}", token); + + if quoted { + ( + Expression { + expr: Expr::String(token), + span, + ty: Type::String, + custom_completion: None, + }, + None, + ) + } else if token.contains(' ') { + ( + garbage(span), + Some(ParseError::Expected("string".into(), span)), + ) + } else { + ( + Expression { + expr: Expr::String(token), + span, + ty: Type::String, + custom_completion: None, + }, + None, + ) + } + } else { + ( + garbage(span), + Some(ParseError::Expected("string".into(), span)), + ) + } +} + +//TODO: Handle error case for unknown shapes +pub fn parse_shape_name( + _working_set: &StateWorkingSet, + bytes: &[u8], + span: Span, +) -> (SyntaxShape, Option) { + let result = match bytes { + b"any" => SyntaxShape::Any, + b"block" => SyntaxShape::Block(None), //FIXME: Blocks should have known output types + b"cell-path" => SyntaxShape::CellPath, + b"duration" => SyntaxShape::Duration, + b"path" => SyntaxShape::Filepath, + b"expr" => SyntaxShape::Expression, + b"filesize" => SyntaxShape::Filesize, + b"glob" => SyntaxShape::GlobPattern, + b"int" => SyntaxShape::Int, + b"math" => SyntaxShape::MathExpression, + b"number" => SyntaxShape::Number, + b"operator" => SyntaxShape::Operator, + b"range" => SyntaxShape::Range, + b"cond" => SyntaxShape::RowCondition, + b"bool" => SyntaxShape::Boolean, + b"signature" => SyntaxShape::Signature, + b"string" => SyntaxShape::String, + b"variable" => SyntaxShape::Variable, + _ => return (SyntaxShape::Any, Some(ParseError::UnknownType(span))), + }; + + (result, None) +} + +pub fn parse_type(_working_set: &StateWorkingSet, bytes: &[u8]) -> Type { + match bytes { + b"int" => Type::Int, + b"float" => Type::Float, + b"range" => Type::Range, + b"bool" => Type::Bool, + b"string" => Type::String, + b"block" => Type::Block, + b"duration" => Type::Duration, + b"date" => Type::Date, + b"filesize" => Type::Filesize, + b"number" => Type::Number, + b"table" => Type::Table, + b"error" => Type::Error, + b"binary" => Type::Binary, + + _ => Type::Unknown, + } +} + +pub fn parse_import_pattern( + working_set: &mut StateWorkingSet, + spans: &[Span], +) -> (Expression, Option) { + let mut error = None; + + let (head, head_span) = if let Some(head_span) = spans.get(0) { + ( + working_set.get_span_contents(*head_span).to_vec(), + head_span, + ) + } else { + return ( + garbage(span(spans)), + Some(ParseError::WrongImportPattern(span(spans))), + ); + }; + + let (import_pattern, err) = if let Some(tail_span) = spans.get(1) { + // FIXME: expand this to handle deeper imports once we support module imports + let tail = working_set.get_span_contents(*tail_span); + if tail == b"*" { + ( + ImportPattern { + head: ImportPatternHead { + name: head, + span: *head_span, + }, + members: vec![ImportPatternMember::Glob { span: *tail_span }], + hidden: HashSet::new(), + }, + None, + ) + } else if tail.starts_with(b"[") { + let (result, err) = + parse_list_expression(working_set, *tail_span, &SyntaxShape::String); + error = error.or(err); + + let mut output = vec![]; + + match result { + Expression { + expr: Expr::List(list), + .. + } => { + for l in list { + let contents = working_set.get_span_contents(l.span); + output.push((contents.to_vec(), l.span)); + } + + ( + ImportPattern { + head: ImportPatternHead { + name: head, + span: *head_span, + }, + members: vec![ImportPatternMember::List { names: output }], + hidden: HashSet::new(), + }, + None, + ) + } + _ => ( + ImportPattern { + head: ImportPatternHead { + name: head, + span: *head_span, + }, + members: vec![], + hidden: HashSet::new(), + }, + Some(ParseError::ExportNotFound(result.span)), + ), + } + } else { + let tail = trim_quotes(tail); + ( + ImportPattern { + head: ImportPatternHead { + name: head, + span: *head_span, + }, + members: vec![ImportPatternMember::Name { + name: tail.to_vec(), + span: *tail_span, + }], + hidden: HashSet::new(), + }, + None, + ) + } + } else { + ( + ImportPattern { + head: ImportPatternHead { + name: head, + span: *head_span, + }, + members: vec![], + hidden: HashSet::new(), + }, + None, + ) + }; + + ( + Expression { + expr: Expr::ImportPattern(import_pattern), + span: span(&spans[1..]), + ty: Type::List(Box::new(Type::String)), + custom_completion: None, + }, + error.or(err), + ) +} + +pub fn parse_var_with_opt_type( + working_set: &mut StateWorkingSet, + spans: &[Span], + spans_idx: &mut usize, +) -> (Expression, Option) { + let bytes = working_set.get_span_contents(spans[*spans_idx]).to_vec(); + + if bytes.contains(&b' ') || bytes.contains(&b'"') || bytes.contains(&b'\'') { + return ( + garbage(spans[*spans_idx]), + Some(ParseError::VariableNotValid(spans[*spans_idx])), + ); + } + + if bytes.ends_with(b":") { + // We end with colon, so the next span should be the type + if *spans_idx + 1 < spans.len() { + *spans_idx += 1; + let type_bytes = working_set.get_span_contents(spans[*spans_idx]); + + let ty = parse_type(working_set, type_bytes); + + let id = working_set.add_variable(bytes[0..(bytes.len() - 1)].to_vec(), ty.clone()); + + ( + Expression { + expr: Expr::VarDecl(id), + span: span(&spans[*spans_idx - 1..*spans_idx + 1]), + ty, + custom_completion: None, + }, + None, + ) + } else { + let id = working_set.add_variable(bytes[0..(bytes.len() - 1)].to_vec(), Type::Unknown); + ( + Expression { + expr: Expr::VarDecl(id), + span: spans[*spans_idx], + ty: Type::Unknown, + custom_completion: None, + }, + Some(ParseError::MissingType(spans[*spans_idx])), + ) + } + } else if bytes == b"$config" || bytes == b"config" { + ( + Expression { + expr: Expr::Var(CONFIG_VARIABLE_ID), + span: spans[*spans_idx], + ty: Type::Unknown, + custom_completion: None, + }, + None, + ) + } else { + let id = working_set.add_variable(bytes, Type::Unknown); + + ( + Expression { + expr: Expr::VarDecl(id), + span: span(&spans[*spans_idx..*spans_idx + 1]), + ty: Type::Unknown, + custom_completion: None, + }, + None, + ) + } +} + +pub fn expand_to_cell_path( + working_set: &mut StateWorkingSet, + expression: &mut Expression, + var_id: VarId, +) { + if let Expression { + expr: Expr::String(_), + span, + .. + } = expression + { + // Re-parse the string as if it were a cell-path + let (new_expression, _err) = parse_full_cell_path(working_set, Some(var_id), *span); + + *expression = new_expression; + } +} + +pub fn parse_row_condition( + working_set: &mut StateWorkingSet, + spans: &[Span], +) -> (Expression, Option) { + let var_id = working_set.add_variable(b"$it".to_vec(), Type::Unknown); + let (expression, err) = parse_math_expression(working_set, spans, Some(var_id)); + let span = span(spans); + + let block_id = match expression.expr { + Expr::Block(block_id) => block_id, + _ => { + // We have an expression, so let's convert this into a block. + let mut block = Block::new(); + let mut pipeline = Pipeline::new(); + pipeline.expressions.push(expression); + + block.stmts.push(Statement::Pipeline(pipeline)); + + block.signature.required_positional.push(PositionalArg { + name: "$it".into(), + desc: "row condition".into(), + shape: SyntaxShape::Any, + var_id: Some(var_id), + }); + + let mut seen = vec![]; + let mut seen_decls = vec![]; + let captures = find_captures_in_block(working_set, &block, &mut seen, &mut seen_decls); + + block.captures = captures; + + working_set.add_block(block) + } + }; + + ( + Expression { + ty: Type::Bool, + span, + expr: Expr::RowCondition(block_id), + custom_completion: None, + }, + err, + ) +} + +pub fn parse_signature( + working_set: &mut StateWorkingSet, + span: Span, +) -> (Expression, Option) { + let bytes = working_set.get_span_contents(span); + + let mut error = None; + let mut start = span.start; + let mut end = span.end; + + if bytes.starts_with(b"[") { + start += 1; + } else { + error = error.or_else(|| { + Some(ParseError::Expected( + "[".into(), + Span { + start, + end: start + 1, + }, + )) + }); + } + + if bytes.ends_with(b"]") { + end -= 1; + } else { + error = error.or_else(|| Some(ParseError::Unclosed("]".into(), Span { start: end, end }))); + } + + let (sig, err) = parse_signature_helper(working_set, Span { start, end }); + error = error.or(err); + + ( + Expression { + expr: Expr::Signature(sig), + span, + ty: Type::Signature, + custom_completion: None, + }, + error, + ) +} + +pub fn parse_signature_helper( + working_set: &mut StateWorkingSet, + span: Span, +) -> (Box, Option) { + enum ParseMode { + ArgMode, + TypeMode, + } + + enum Arg { + Positional(PositionalArg, bool), // bool - required + Flag(Flag), + } + + let mut error = None; + let source = working_set.get_span_contents(span); + + let (output, err) = lex(source, span.start, &[b'\n', b'\r', b','], &[b':'], false); + error = error.or(err); + + let mut args: Vec = vec![]; + let mut rest_arg = None; + let mut parse_mode = ParseMode::ArgMode; + + for token in &output { + match token { + Token { + contents: crate::TokenContents::Item, + span, + } => { + let span = *span; + let contents = working_set.get_span_contents(span); + + if contents == b":" { + match parse_mode { + ParseMode::ArgMode => { + parse_mode = ParseMode::TypeMode; + } + ParseMode::TypeMode => { + // We're seeing two types for the same thing for some reason, error + error = + error.or_else(|| Some(ParseError::Expected("type".into(), span))); + } + } + } else { + match parse_mode { + ParseMode::ArgMode => { + if contents.starts_with(b"--") && contents.len() > 2 { + // Long flag + let flags: Vec<_> = + contents.split(|x| x == &b'(').map(|x| x.to_vec()).collect(); + + let long = String::from_utf8_lossy(&flags[0][2..]).to_string(); + let variable_name = flags[0][2..].to_vec(); + let var_id = working_set.add_variable(variable_name, Type::Unknown); + + if flags.len() == 1 { + args.push(Arg::Flag(Flag { + arg: None, + desc: String::new(), + long, + short: None, + required: false, + var_id: Some(var_id), + })); + } else { + let short_flag = &flags[1]; + let short_flag = if !short_flag.starts_with(b"-") + || !short_flag.ends_with(b")") + { + error = error.or_else(|| { + Some(ParseError::Expected("short flag".into(), span)) + }); + short_flag + } else { + &short_flag[1..(short_flag.len() - 1)] + }; + + let short_flag = + String::from_utf8_lossy(short_flag).to_string(); + let chars: Vec = short_flag.chars().collect(); + let long = String::from_utf8_lossy(&flags[0][2..]).to_string(); + let variable_name = flags[0][2..].to_vec(); + let var_id = + working_set.add_variable(variable_name, Type::Unknown); + + if chars.len() == 1 { + args.push(Arg::Flag(Flag { + arg: None, + desc: String::new(), + long, + short: Some(chars[0]), + required: false, + var_id: Some(var_id), + })); + } else { + error = error.or_else(|| { + Some(ParseError::Expected("short flag".into(), span)) + }); + } + } + } else if contents.starts_with(b"-") && contents.len() > 1 { + // Short flag + + let short_flag = &contents[1..]; + let short_flag = String::from_utf8_lossy(short_flag).to_string(); + let chars: Vec = short_flag.chars().collect(); + + if chars.len() > 1 { + error = error.or_else(|| { + Some(ParseError::Expected("short flag".into(), span)) + }); + } + + let mut encoded_var_name = vec![0u8; 4]; + let len = chars[0].encode_utf8(&mut encoded_var_name).len(); + let variable_name = encoded_var_name[0..len].to_vec(); + let var_id = working_set.add_variable(variable_name, Type::Unknown); + + args.push(Arg::Flag(Flag { + arg: None, + desc: String::new(), + long: String::new(), + short: Some(chars[0]), + required: false, + var_id: Some(var_id), + })); + } else if contents.starts_with(b"(-") { + let short_flag = &contents[2..]; + + let short_flag = if !short_flag.ends_with(b")") { + error = error.or_else(|| { + Some(ParseError::Expected("short flag".into(), span)) + }); + short_flag + } else { + &short_flag[..(short_flag.len() - 1)] + }; + + let short_flag = String::from_utf8_lossy(short_flag).to_string(); + let chars: Vec = short_flag.chars().collect(); + + if chars.len() == 1 { + match args.last_mut() { + Some(Arg::Flag(flag)) => { + if flag.short.is_some() { + error = error.or_else(|| { + Some(ParseError::Expected( + "one short flag".into(), + span, + )) + }); + } else { + flag.short = Some(chars[0]); + } + } + _ => { + error = error.or_else(|| { + Some(ParseError::Expected( + "unknown flag".into(), + span, + )) + }); + } + } + } else { + error = error.or_else(|| { + Some(ParseError::Expected("short flag".into(), span)) + }); + } + } else if contents.ends_with(b"?") { + let contents: Vec<_> = contents[..(contents.len() - 1)].into(); + let name = String::from_utf8_lossy(&contents).to_string(); + + let var_id = working_set.add_variable(contents, Type::Unknown); + + // Positional arg, optional + args.push(Arg::Positional( + PositionalArg { + desc: String::new(), + name, + shape: SyntaxShape::Any, + var_id: Some(var_id), + }, + false, + )) + } else if let Some(contents) = contents.strip_prefix(b"...") { + let name = String::from_utf8_lossy(contents).to_string(); + let contents_vec: Vec = contents.to_vec(); + + let var_id = working_set.add_variable(contents_vec, Type::Unknown); + + if rest_arg.is_none() { + rest_arg = Some(Arg::Positional( + PositionalArg { + desc: String::new(), + name, + shape: SyntaxShape::Any, + var_id: Some(var_id), + }, + false, + )); + } else { + error = error.or(Some(ParseError::MultipleRestParams(span))) + } + } else { + let name = String::from_utf8_lossy(contents).to_string(); + let contents_vec = contents.to_vec(); + + let var_id = working_set.add_variable(contents_vec, Type::Unknown); + + // Positional arg, required + args.push(Arg::Positional( + PositionalArg { + desc: String::new(), + name, + shape: SyntaxShape::Any, + var_id: Some(var_id), + }, + true, + )) + } + } + ParseMode::TypeMode => { + if let Some(last) = args.last_mut() { + let (syntax_shape, err) = + parse_shape_name(working_set, contents, span); + error = error.or(err); + //TODO check if we're replacing a custom parameter already + match last { + Arg::Positional(PositionalArg { shape, var_id, .. }, ..) => { + working_set.set_variable_type(var_id.expect("internal error: all custom parameters must have var_ids"), syntax_shape.to_type()); + *shape = syntax_shape; + } + Arg::Flag(Flag { arg, var_id, .. }) => { + // Flags with a boolean type are just present/not-present switches + if syntax_shape != SyntaxShape::Boolean { + working_set.set_variable_type(var_id.expect("internal error: all custom parameters must have var_ids"), syntax_shape.to_type()); + *arg = Some(syntax_shape) + } + } + } + } + parse_mode = ParseMode::ArgMode; + } + } + } + } + Token { + contents: crate::TokenContents::Comment, + span, + } => { + let contents = working_set.get_span_contents(Span { + start: span.start + 1, + end: span.end, + }); + + let mut contents = String::from_utf8_lossy(contents).to_string(); + contents = contents.trim().into(); + + if let Some(last) = args.last_mut() { + match last { + Arg::Flag(flag) => { + if !flag.desc.is_empty() { + flag.desc.push('\n'); + } + flag.desc.push_str(&contents); + } + Arg::Positional(positional, ..) => { + if !positional.desc.is_empty() { + positional.desc.push('\n'); + } + positional.desc.push_str(&contents); + } + } + } + } + _ => {} + } + } + + let mut sig = Signature::new(String::new()); + + if let Some(Arg::Positional(positional, ..)) = rest_arg { + if positional.name.is_empty() { + error = error.or(Some(ParseError::RestNeedsName(span))) + } else if sig.rest_positional.is_none() { + sig.rest_positional = Some(PositionalArg { + name: positional.name, + ..positional + }) + } else { + // Too many rest params + error = error.or(Some(ParseError::MultipleRestParams(span))) + } + } + for arg in args { + match arg { + Arg::Positional(positional, required) => { + if required { + sig.required_positional.push(positional) + } else { + sig.optional_positional.push(positional) + } + } + Arg::Flag(flag) => sig.named.push(flag), + } + } + + (Box::new(sig), error) +} + +pub fn parse_list_expression( + working_set: &mut StateWorkingSet, + span: Span, + element_shape: &SyntaxShape, +) -> (Expression, Option) { + let bytes = working_set.get_span_contents(span); + + let mut error = None; + + let mut start = span.start; + let mut end = span.end; + + if bytes.starts_with(b"[") { + start += 1; + } + if bytes.ends_with(b"]") { + end -= 1; + } else { + error = error.or_else(|| Some(ParseError::Unclosed("]".into(), Span { start: end, end }))); + } + + let inner_span = Span { start, end }; + let source = working_set.get_span_contents(inner_span); + + let (output, err) = lex(source, inner_span.start, &[b'\n', b'\r', b','], &[], true); + error = error.or(err); + + let (output, err) = lite_parse(&output); + error = error.or(err); + + let mut args = vec![]; + + let mut contained_type: Option = None; + + if !output.block.is_empty() { + for arg in &output.block[0].commands { + let mut spans_idx = 0; + + while spans_idx < arg.parts.len() { + let (arg, err) = + parse_multispan_value(working_set, &arg.parts, &mut spans_idx, element_shape); + error = error.or(err); + + if let Some(ref ctype) = contained_type { + if *ctype != arg.ty { + contained_type = Some(Type::Unknown); + } + } else { + contained_type = Some(arg.ty.clone()); + } + + args.push(arg); + + spans_idx += 1; + } + } + } + + ( + Expression { + expr: Expr::List(args), + span, + ty: Type::List(Box::new(if let Some(ty) = contained_type { + ty + } else { + Type::Unknown + })), + custom_completion: None, + }, + error, + ) +} + +pub fn parse_table_expression( + working_set: &mut StateWorkingSet, + original_span: Span, +) -> (Expression, Option) { + let bytes = working_set.get_span_contents(original_span); + let mut error = None; + + let mut start = original_span.start; + let mut end = original_span.end; + + if bytes.starts_with(b"[") { + start += 1; + } + if bytes.ends_with(b"]") { + end -= 1; + } else { + error = error.or_else(|| Some(ParseError::Unclosed("]".into(), Span { start: end, end }))); + } + + let inner_span = Span { start, end }; + + let source = working_set.get_span_contents(inner_span); + + let (output, err) = lex(source, start, &[b'\n', b'\r', b','], &[], true); + error = error.or(err); + + let (output, err) = lite_parse(&output); + error = error.or(err); + + match output.block.len() { + 0 => ( + Expression { + expr: Expr::List(vec![]), + span: original_span, + ty: Type::List(Box::new(Type::Unknown)), + custom_completion: None, + }, + None, + ), + 1 => { + // List + parse_list_expression(working_set, original_span, &SyntaxShape::Any) + } + _ => { + let mut table_headers = vec![]; + + let (headers, err) = parse_value( + working_set, + output.block[0].commands[0].parts[0], + &SyntaxShape::List(Box::new(SyntaxShape::Any)), + ); + error = error.or(err); + + if let Expression { + expr: Expr::List(headers), + .. + } = headers + { + table_headers = headers; + } + + let mut rows = vec![]; + for part in &output.block[1].commands[0].parts { + let (values, err) = parse_value( + working_set, + *part, + &SyntaxShape::List(Box::new(SyntaxShape::Any)), + ); + error = error.or(err); + if let Expression { + expr: Expr::List(values), + span, + .. + } = values + { + match values.len().cmp(&table_headers.len()) { + std::cmp::Ordering::Less => { + error = error.or_else(|| { + Some(ParseError::MissingColumns(table_headers.len(), span)) + }) + } + std::cmp::Ordering::Equal => {} + std::cmp::Ordering::Greater => { + error = error.or_else(|| { + Some(ParseError::ExtraColumns( + table_headers.len(), + values[table_headers.len()].span, + )) + }) + } + } + + rows.push(values); + } + } + + ( + Expression { + expr: Expr::Table(table_headers, rows), + span: original_span, + ty: Type::Table, + custom_completion: None, + }, + error, + ) + } + } +} + +pub fn parse_block_expression( + working_set: &mut StateWorkingSet, + shape: &SyntaxShape, + span: Span, +) -> (Expression, Option) { + trace!("parsing: block expression"); + + let bytes = working_set.get_span_contents(span); + let mut error = None; + + let mut start = span.start; + let mut end = span.end; + + if bytes.starts_with(b"{") { + start += 1; + } else { + return ( + garbage(span), + Some(ParseError::Expected("block".into(), span)), + ); + } + if bytes.ends_with(b"}") { + end -= 1; + } else { + error = error.or_else(|| Some(ParseError::Unclosed("}".into(), Span { start: end, end }))); + } + + let inner_span = Span { start, end }; + + let source = working_set.get_span_contents(inner_span); + + let (output, err) = lex(source, start, &[], &[], false); + error = error.or(err); + + working_set.enter_scope(); + + // Check to see if we have parameters + let (mut signature, amt_to_skip): (Option>, usize) = match output.first() { + Some(Token { + contents: TokenContents::Pipe, + span, + }) => { + // We've found a parameter list + let start_point = span.start; + let mut token_iter = output.iter().enumerate().skip(1); + let mut end_span = None; + let mut amt_to_skip = 1; + + for token in &mut token_iter { + if let Token { + contents: TokenContents::Pipe, + span, + } = token.1 + { + end_span = Some(span); + amt_to_skip = token.0; + break; + } + } + + let end_point = if let Some(span) = end_span { + span.end + } else { + end + }; + + let (signature, err) = parse_signature_helper( + working_set, + Span { + start: start_point, + end: end_point, + }, + ); + error = error.or(err); + + (Some(signature), amt_to_skip) + } + _ => (None, 0), + }; + + let (output, err) = lite_parse(&output[amt_to_skip..]); + error = error.or(err); + + // TODO: Finish this + if let SyntaxShape::Block(Some(v)) = shape { + if signature.is_none() && v.len() == 1 { + // We'll assume there's an `$it` present + let var_id = working_set.add_variable(b"$it".to_vec(), Type::Unknown); + + let mut new_sigature = Signature::new(""); + new_sigature.required_positional.push(PositionalArg { + var_id: Some(var_id), + name: "$it".into(), + desc: String::new(), + shape: SyntaxShape::Any, + }); + + signature = Some(Box::new(new_sigature)); + } + } + + let (mut output, err) = parse_block(working_set, &output, false); + error = error.or(err); + + if let Some(signature) = signature { + output.signature = signature; + } else if let Some(last) = working_set.delta.scope.last() { + // FIXME: this only supports the top $it. Is this sufficient? + + if let Some(var_id) = last.get_var(b"$it") { + let mut signature = Signature::new(""); + signature.required_positional.push(PositionalArg { + var_id: Some(*var_id), + name: "$it".into(), + desc: String::new(), + shape: SyntaxShape::Any, + }); + output.signature = Box::new(signature); + } + } + + let mut seen = vec![]; + let mut seen_decls = vec![]; + let captures = find_captures_in_block(working_set, &output, &mut seen, &mut seen_decls); + + output.captures = captures; + output.span = Some(span); + + working_set.exit_scope(); + + let block_id = working_set.add_block(output); + + ( + Expression { + expr: Expr::Block(block_id), + span, + ty: Type::Block, + custom_completion: None, + }, + error, + ) +} + +pub fn parse_value( + working_set: &mut StateWorkingSet, + span: Span, + shape: &SyntaxShape, +) -> (Expression, Option) { + let bytes = working_set.get_span_contents(span); + + // First, check the special-cases. These will likely represent specific values as expressions + // and may fit a variety of shapes. + // + // We check variable first because immediately following we check for variables with cell paths + // which might result in a value that fits other shapes (and require the variable to already be + // declared) + if shape == &SyntaxShape::Variable { + trace!("parsing: variable"); + + return parse_variable_expr(working_set, span); + } else if bytes.starts_with(b"$") { + trace!("parsing: dollar expression"); + + return parse_dollar_expr(working_set, span); + } else if bytes.starts_with(b"(") { + trace!("parsing: range or full path"); + + if let (expr, None) = parse_range(working_set, span) { + return (expr, None); + } else { + return parse_full_cell_path(working_set, None, span); + } + } else if bytes.starts_with(b"{") { + trace!("parsing: block or full path"); + if !matches!(shape, SyntaxShape::Block(..)) { + if let (expr, None) = parse_full_cell_path(working_set, None, span) { + return (expr, None); + } + } + if matches!(shape, SyntaxShape::Block(_)) || matches!(shape, SyntaxShape::Any) { + return parse_block_expression(working_set, shape, span); + } else if matches!(shape, SyntaxShape::Record) { + return parse_record(working_set, span); + } else { + return ( + Expression::garbage(span), + Some(ParseError::Expected("non-block value".into(), span)), + ); + } + } else if bytes.starts_with(b"[") { + match shape { + SyntaxShape::Any + | SyntaxShape::List(_) + | SyntaxShape::Table + | SyntaxShape::Signature => {} + _ => { + return ( + Expression::garbage(span), + Some(ParseError::Expected("non-[] value".into(), span)), + ); + } + } + } + + match shape { + SyntaxShape::Custom(shape, custom_completion) => { + let (mut expression, err) = parse_value(working_set, span, shape); + expression.custom_completion = Some(custom_completion.clone()); + (expression, err) + } + SyntaxShape::Number => parse_number(bytes, span), + SyntaxShape::Int => parse_int(bytes, span), + SyntaxShape::Duration => parse_duration(working_set, span), + SyntaxShape::Filesize => parse_filesize(working_set, span), + SyntaxShape::Range => parse_range(working_set, span), + SyntaxShape::Filepath => parse_filepath(working_set, span), + SyntaxShape::GlobPattern => parse_glob_pattern(working_set, span), + SyntaxShape::String => parse_string(working_set, span), + SyntaxShape::Block(_) => { + if bytes.starts_with(b"{") { + trace!("parsing value as a block expression"); + + parse_block_expression(working_set, shape, span) + } else { + ( + Expression::garbage(span), + Some(ParseError::Expected("block".into(), span)), + ) + } + } + SyntaxShape::Signature => { + if bytes.starts_with(b"[") { + parse_signature(working_set, span) + } else { + ( + Expression::garbage(span), + Some(ParseError::Expected("signature".into(), span)), + ) + } + } + SyntaxShape::List(elem) => { + if bytes.starts_with(b"[") { + parse_list_expression(working_set, span, elem) + } else { + ( + Expression::garbage(span), + Some(ParseError::Expected("list".into(), span)), + ) + } + } + SyntaxShape::Table => { + if bytes.starts_with(b"[") { + parse_table_expression(working_set, span) + } else { + ( + Expression::garbage(span), + Some(ParseError::Expected("table".into(), span)), + ) + } + } + SyntaxShape::CellPath => { + let source = working_set.get_span_contents(span); + let mut error = None; + + let (tokens, err) = lex(source, span.start, &[b'\n', b'\r'], &[b'.'], true); + error = error.or(err); + + let tokens = tokens.into_iter().peekable(); + + let (cell_path, err) = parse_cell_path(working_set, tokens, false, span); + error = error.or(err); + + ( + Expression { + expr: Expr::CellPath(CellPath { members: cell_path }), + span, + ty: Type::CellPath, + custom_completion: None, + }, + error, + ) + } + SyntaxShape::Boolean => { + // Redundant, though we catch bad boolean parses here + if bytes == b"$true" || bytes == b"$false" { + ( + Expression { + expr: Expr::Bool(true), + span, + ty: Type::Bool, + custom_completion: None, + }, + None, + ) + } else { + ( + garbage(span), + Some(ParseError::Expected("bool".into(), span)), + ) + } + } + SyntaxShape::Any => { + if bytes.starts_with(b"[") { + //parse_value(working_set, span, &SyntaxShape::Table) + parse_full_cell_path(working_set, None, span) + } else { + let shapes = [ + SyntaxShape::Int, + SyntaxShape::Number, + SyntaxShape::Range, + SyntaxShape::Filesize, + SyntaxShape::Duration, + SyntaxShape::Block(None), + SyntaxShape::String, + ]; + for shape in shapes.iter() { + if let (s, None) = parse_value(working_set, span, shape) { + return (s, None); + } + } + ( + garbage(span), + Some(ParseError::Expected("any shape".into(), span)), + ) + } + } + _ => (garbage(span), Some(ParseError::IncompleteParser(span))), + } +} + +pub fn parse_operator( + working_set: &mut StateWorkingSet, + span: Span, +) -> (Expression, Option) { + let contents = working_set.get_span_contents(span); + + let operator = match contents { + b"==" => Operator::Equal, + b"!=" => Operator::NotEqual, + b"<" => Operator::LessThan, + b"<=" => Operator::LessThanOrEqual, + b">" => Operator::GreaterThan, + b">=" => Operator::GreaterThanOrEqual, + b"=~" => Operator::Contains, + b"!~" => Operator::NotContains, + b"+" => Operator::Plus, + b"-" => Operator::Minus, + b"*" => Operator::Multiply, + b"/" => Operator::Divide, + b"in" => Operator::In, + b"not-in" => Operator::NotIn, + b"mod" => Operator::Modulo, + b"&&" => Operator::And, + b"||" => Operator::Or, + b"**" => Operator::Pow, + _ => { + return ( + garbage(span), + Some(ParseError::Expected("operator".into(), span)), + ); + } + }; + + ( + Expression { + expr: Expr::Operator(operator), + span, + ty: Type::Unknown, + custom_completion: None, + }, + None, + ) +} + +pub fn parse_math_expression( + working_set: &mut StateWorkingSet, + spans: &[Span], + lhs_row_var_id: Option, +) -> (Expression, Option) { + // As the expr_stack grows, we increase the required precedence to grow larger + // If, at any time, the operator we're looking at is the same or lower precedence + // of what is in the expression stack, we collapse the expression stack. + // + // This leads to an expression stack that grows under increasing precedence and collapses + // under decreasing/sustained precedence + // + // The end result is a stack that we can fold into binary operations as right associations + // safely. + + let mut expr_stack: Vec = vec![]; + + let mut idx = 0; + let mut last_prec = 1000000; + + let mut error = None; + let (lhs, err) = parse_value(working_set, spans[0], &SyntaxShape::Any); + error = error.or(err); + idx += 1; + + expr_stack.push(lhs); + + while idx < spans.len() { + let (op, err) = parse_operator(working_set, spans[idx]); + error = error.or(err); + + let op_prec = op.precedence(); + + idx += 1; + + if idx == spans.len() { + // Handle broken math expr `1 +` etc + error = error.or(Some(ParseError::IncompleteMathExpression(spans[idx - 1]))); + + expr_stack.push(Expression::garbage(spans[idx - 1])); + expr_stack.push(Expression::garbage(spans[idx - 1])); + + break; + } + + let (rhs, err) = parse_value(working_set, spans[idx], &SyntaxShape::Any); + error = error.or(err); + + if op_prec <= last_prec && expr_stack.len() > 1 { + // Collapse the right associated operations first + // so that we can get back to a stack with a lower precedence + let mut rhs = expr_stack + .pop() + .expect("internal error: expression stack empty"); + let mut op = expr_stack + .pop() + .expect("internal error: expression stack empty"); + + let mut lhs = expr_stack + .pop() + .expect("internal error: expression stack empty"); + + if let Some(row_var_id) = lhs_row_var_id { + expand_to_cell_path(working_set, &mut lhs, row_var_id); + } + + let (result_ty, err) = math_result_type(working_set, &mut lhs, &mut op, &mut rhs); + error = error.or(err); + + let op_span = span(&[lhs.span, rhs.span]); + expr_stack.push(Expression { + expr: Expr::BinaryOp(Box::new(lhs), Box::new(op), Box::new(rhs)), + span: op_span, + ty: result_ty, + custom_completion: None, + }); + } + expr_stack.push(op); + expr_stack.push(rhs); + + last_prec = op_prec; + + idx += 1; + } + + while expr_stack.len() != 1 { + let mut rhs = expr_stack + .pop() + .expect("internal error: expression stack empty"); + let mut op = expr_stack + .pop() + .expect("internal error: expression stack empty"); + let mut lhs = expr_stack + .pop() + .expect("internal error: expression stack empty"); + + if let Some(row_var_id) = lhs_row_var_id { + expand_to_cell_path(working_set, &mut lhs, row_var_id); + } + + let (result_ty, err) = math_result_type(working_set, &mut lhs, &mut op, &mut rhs); + error = error.or(err); + + let binary_op_span = span(&[lhs.span, rhs.span]); + expr_stack.push(Expression { + expr: Expr::BinaryOp(Box::new(lhs), Box::new(op), Box::new(rhs)), + span: binary_op_span, + ty: result_ty, + custom_completion: None, + }); + } + + let output = expr_stack + .pop() + .expect("internal error: expression stack empty"); + + (output, error) +} + +pub fn parse_expression( + working_set: &mut StateWorkingSet, + spans: &[Span], + expand_aliases: bool, +) -> (Expression, Option) { + let mut pos = 0; + let mut shorthand = vec![]; + + while pos < spans.len() { + // Check if there is any environment shorthand + let name = working_set.get_span_contents(spans[pos]); + let split = name.split(|x| *x == b'='); + let split: Vec<_> = split.collect(); + if split.len() == 2 && !split[0].is_empty() { + let point = split[0].len() + 1; + + let lhs = parse_string_strict( + working_set, + Span { + start: spans[pos].start, + end: spans[pos].start + point - 1, + }, + ); + let rhs = if spans[pos].start + point < spans[pos].end { + parse_string_strict( + working_set, + Span { + start: spans[pos].start + point, + end: spans[pos].end, + }, + ) + } else { + ( + Expression { + expr: Expr::String(String::new()), + span: spans[pos], + ty: Type::Nothing, + custom_completion: None, + }, + None, + ) + }; + + if lhs.1.is_none() && rhs.1.is_none() { + shorthand.push((lhs.0, rhs.0)); + pos += 1; + } else { + break; + } + } else { + break; + } + } + + if pos == spans.len() { + return ( + garbage(span(spans)), + Some(ParseError::UnknownCommand(spans[0])), + ); + } + + let bytes = working_set.get_span_contents(spans[pos]); + + let (output, err) = if is_math_expression_byte(bytes[0]) { + parse_math_expression(working_set, &spans[pos..], None) + } else { + // For now, check for special parses of certain keywords + match bytes { + b"def" => ( + parse_call(working_set, &spans[pos..], expand_aliases, spans[0]).0, + Some(ParseError::StatementInPipeline("def".into(), spans[0])), + ), + b"let" => ( + parse_call(working_set, &spans[pos..], expand_aliases, spans[0]).0, + Some(ParseError::StatementInPipeline("let".into(), spans[0])), + ), + b"alias" => ( + parse_call(working_set, &spans[pos..], expand_aliases, spans[0]).0, + Some(ParseError::StatementInPipeline("alias".into(), spans[0])), + ), + b"module" => ( + parse_call(working_set, &spans[pos..], expand_aliases, spans[0]).0, + Some(ParseError::StatementInPipeline("module".into(), spans[0])), + ), + b"use" => ( + parse_call(working_set, &spans[pos..], expand_aliases, spans[0]).0, + Some(ParseError::StatementInPipeline("use".into(), spans[0])), + ), + b"source" => ( + parse_call(working_set, &spans[pos..], expand_aliases, spans[0]).0, + Some(ParseError::StatementInPipeline("source".into(), spans[0])), + ), + b"export" => ( + parse_call(working_set, &spans[pos..], expand_aliases, spans[0]).0, + Some(ParseError::UnexpectedKeyword("export".into(), spans[0])), + ), + b"hide" => ( + parse_call(working_set, &spans[pos..], expand_aliases, spans[0]).0, + Some(ParseError::StatementInPipeline("hide".into(), spans[0])), + ), + #[cfg(feature = "plugin")] + b"register" => ( + parse_call(working_set, &spans[pos..], expand_aliases, spans[0]).0, + Some(ParseError::StatementInPipeline("plugin".into(), spans[0])), + ), + + b"for" => parse_for(working_set, spans), + _ => parse_call(working_set, &spans[pos..], expand_aliases, spans[0]), + } + }; + + let with_env = working_set.find_decl(b"with-env"); + + if !shorthand.is_empty() { + if let Some(decl_id) = with_env { + let mut block = Block::default(); + let ty = output.ty.clone(); + block.stmts = vec![Statement::Pipeline(Pipeline { + expressions: vec![output], + })]; + + let mut seen = vec![]; + let mut seen_decls = vec![]; + let captures = find_captures_in_block(working_set, &block, &mut seen, &mut seen_decls); + block.captures = captures; + + let block_id = working_set.add_block(block); + + let mut env_vars = vec![]; + for sh in shorthand { + env_vars.push(sh.0); + env_vars.push(sh.1); + } + + let positional = vec![ + Expression { + expr: Expr::List(env_vars), + span: span(&spans[..pos]), + ty: Type::Unknown, + custom_completion: None, + }, + Expression { + expr: Expr::Block(block_id), + span: span(&spans[pos..]), + ty, + custom_completion: None, + }, + ]; + + ( + Expression { + expr: Expr::Call(Box::new(Call { + head: span(spans), + decl_id, + named: vec![], + positional, + })), + custom_completion: None, + span: span(spans), + ty: Type::Unknown, + }, + err, + ) + } else { + (output, err) + } + } else { + (output, err) + } +} + +pub fn parse_variable( + working_set: &mut StateWorkingSet, + span: Span, +) -> (Option, Option) { + let bytes = working_set.get_span_contents(span); + + if is_variable(bytes) { + if let Some(var_id) = working_set.find_variable(bytes) { + (Some(var_id), None) + } else { + (None, None) + } + } else { + (None, Some(ParseError::Expected("variable".into(), span))) + } +} + +pub fn parse_statement( + working_set: &mut StateWorkingSet, + lite_command: &LiteCommand, +) -> (Statement, Option) { + let name = working_set.get_span_contents(lite_command.parts[0]); + + match name { + b"def" | b"def-env" => parse_def(working_set, lite_command), + b"let" => parse_let(working_set, &lite_command.parts), + b"for" => { + let (expr, err) = parse_for(working_set, &lite_command.parts); + (Statement::Pipeline(Pipeline::from_vec(vec![expr])), err) + } + b"alias" => parse_alias(working_set, &lite_command.parts), + b"module" => parse_module(working_set, &lite_command.parts), + b"use" => parse_use(working_set, &lite_command.parts), + b"source" => parse_source(working_set, &lite_command.parts), + b"export" => ( + garbage_statement(&lite_command.parts), + Some(ParseError::UnexpectedKeyword( + "export".into(), + lite_command.parts[0], + )), + ), + b"hide" => parse_hide(working_set, &lite_command.parts), + #[cfg(feature = "plugin")] + b"register" => parse_register(working_set, &lite_command.parts), + _ => { + let (expr, err) = parse_expression(working_set, &lite_command.parts, true); + (Statement::Pipeline(Pipeline::from_vec(vec![expr])), err) + } + } +} + +pub fn parse_record( + working_set: &mut StateWorkingSet, + span: Span, +) -> (Expression, Option) { + let bytes = working_set.get_span_contents(span); + + let mut error = None; + let mut start = span.start; + let mut end = span.end; + + if bytes.starts_with(b"{") { + start += 1; + } else { + error = error.or_else(|| { + Some(ParseError::Expected( + "{".into(), + Span { + start, + end: start + 1, + }, + )) + }); + } + + if bytes.ends_with(b"}") { + end -= 1; + } else { + error = error.or_else(|| Some(ParseError::Unclosed("}".into(), Span { start: end, end }))); + } + + let inner_span = Span { start, end }; + let source = working_set.get_span_contents(inner_span); + + let (tokens, err) = lex(source, start, &[b'\n', b'\r', b','], &[b':'], true); + error = error.or(err); + + let mut output = vec![]; + let mut idx = 0; + + while idx < tokens.len() { + let (field, err) = parse_value(working_set, tokens[idx].span, &SyntaxShape::Any); + error = error.or(err); + + idx += 1; + if idx == tokens.len() { + return ( + garbage(span), + Some(ParseError::Expected("record".into(), span)), + ); + } + let colon = working_set.get_span_contents(tokens[idx].span); + idx += 1; + if idx == tokens.len() || colon != b":" { + //FIXME: need better error + return ( + garbage(span), + Some(ParseError::Expected("record".into(), span)), + ); + } + let (value, err) = parse_value(working_set, tokens[idx].span, &SyntaxShape::Any); + error = error.or(err); + idx += 1; + + output.push((field, value)); + } + + ( + Expression { + expr: Expr::Record(output), + span, + ty: Type::Unknown, //FIXME: but we don't know the contents of the fields, do we? + custom_completion: None, + }, + error, + ) +} + +pub fn parse_block( + working_set: &mut StateWorkingSet, + lite_block: &LiteBlock, + scoped: bool, +) -> (Block, Option) { + trace!("parsing block: {:?}", lite_block); + + if scoped { + working_set.enter_scope(); + } + + let mut error = None; + + // Pre-declare any definition so that definitions + // that share the same block can see each other + for pipeline in &lite_block.block { + if pipeline.commands.len() == 1 { + if let Some(err) = parse_def_predecl(working_set, &pipeline.commands[0].parts) { + error = error.or(Some(err)); + } + } + } + + let block: Block = lite_block + .block + .iter() + .enumerate() + .map(|(idx, pipeline)| { + if pipeline.commands.len() > 1 { + let mut output = pipeline + .commands + .iter() + .map(|command| { + let (expr, err) = parse_expression(working_set, &command.parts, true); + + if error.is_none() { + error = err; + } + + expr + }) + .collect::>(); + + for expr in output.iter_mut().skip(1) { + if expr.has_in_variable(working_set) { + *expr = wrap_expr_with_collect(working_set, expr); + } + } + + Statement::Pipeline(Pipeline { + expressions: output, + }) + } else { + let (mut stmt, err) = parse_statement(working_set, &pipeline.commands[0]); + + if idx == 0 { + if let Some(let_decl_id) = working_set.find_decl(b"let") { + if let Some(let_env_decl_id) = working_set.find_decl(b"let-env") { + if let Statement::Pipeline(pipeline) = &mut stmt { + for expr in pipeline.expressions.iter_mut() { + if let Expression { + expr: Expr::Call(call), + .. + } = expr + { + if call.decl_id == let_decl_id + || call.decl_id == let_env_decl_id + { + // Do an expansion + if let Some(Expression { + expr: Expr::Keyword(_, _, expr), + .. + }) = call.positional.get_mut(1) + { + if expr.has_in_variable(working_set) { + *expr = Box::new(wrap_expr_with_collect( + working_set, + expr, + )); + } + } + continue; + } + } + } + } + } + } + } + + if error.is_none() { + error = err; + } + + stmt + } + }) + .into(); + + if scoped { + working_set.exit_scope(); + } + + (block, error) +} + +pub fn find_captures_in_block( + working_set: &StateWorkingSet, + block: &Block, + seen: &mut Vec, + seen_decls: &mut Vec, +) -> Vec { + let mut output = vec![]; + + for flag in &block.signature.named { + if let Some(var_id) = flag.var_id { + seen.push(var_id); + } + } + + for positional in &block.signature.required_positional { + if let Some(var_id) = positional.var_id { + seen.push(var_id); + } + } + for positional in &block.signature.optional_positional { + if let Some(var_id) = positional.var_id { + seen.push(var_id); + } + } + for positional in &block.signature.rest_positional { + if let Some(var_id) = positional.var_id { + seen.push(var_id); + } + } + + for stmt in &block.stmts { + match stmt { + Statement::Pipeline(pipeline) => { + let result = find_captures_in_pipeline(working_set, pipeline, seen, seen_decls); + output.extend(&result); + } + Statement::Declaration(_) => {} + } + } + + output +} + +fn find_captures_in_pipeline( + working_set: &StateWorkingSet, + pipeline: &Pipeline, + seen: &mut Vec, + seen_decls: &mut Vec, +) -> Vec { + let mut output = vec![]; + for expr in &pipeline.expressions { + let result = find_captures_in_expr(working_set, expr, seen, seen_decls); + output.extend(&result); + } + + output +} + +pub fn find_captures_in_expr( + working_set: &StateWorkingSet, + expr: &Expression, + seen: &mut Vec, + seen_decls: &mut Vec, +) -> Vec { + let mut output = vec![]; + match &expr.expr { + Expr::BinaryOp(lhs, _, rhs) => { + let lhs_result = find_captures_in_expr(working_set, lhs, seen, seen_decls); + let rhs_result = find_captures_in_expr(working_set, rhs, seen, seen_decls); + + output.extend(&lhs_result); + output.extend(&rhs_result); + } + Expr::Block(block_id) => { + let block = working_set.get_block(*block_id); + let result = find_captures_in_block(working_set, block, seen, seen_decls); + output.extend(&result); + } + Expr::Bool(_) => {} + Expr::Call(call) => { + let decl = working_set.get_decl(call.decl_id); + if !seen_decls.contains(&call.decl_id) { + if let Some(block_id) = decl.get_block_id() { + let block = working_set.get_block(block_id); + if !block.captures.is_empty() { + output.extend(&block.captures) + } else { + seen_decls.push(call.decl_id); + let result = find_captures_in_block(working_set, block, seen, seen_decls); + output.extend(&result); + } + } + } + + for named in &call.named { + if let Some(arg) = &named.1 { + let result = find_captures_in_expr(working_set, arg, seen, seen_decls); + output.extend(&result); + } + } + + for positional in &call.positional { + let result = find_captures_in_expr(working_set, positional, seen, seen_decls); + output.extend(&result); + } + } + Expr::CellPath(_) => {} + Expr::ExternalCall(head, exprs) => { + let result = find_captures_in_expr(working_set, head, seen, seen_decls); + output.extend(&result); + + for expr in exprs { + let result = find_captures_in_expr(working_set, expr, seen, seen_decls); + output.extend(&result); + } + } + Expr::Filepath(_) => {} + Expr::Float(_) => {} + Expr::FullCellPath(cell_path) => { + let result = find_captures_in_expr(working_set, &cell_path.head, seen, seen_decls); + output.extend(&result); + } + Expr::ImportPattern(_) => {} + Expr::Garbage => {} + Expr::Nothing => {} + Expr::GlobPattern(_) => {} + Expr::Int(_) => {} + Expr::Keyword(_, _, expr) => { + let result = find_captures_in_expr(working_set, expr, seen, seen_decls); + output.extend(&result); + } + Expr::List(exprs) => { + for expr in exprs { + let result = find_captures_in_expr(working_set, expr, seen, seen_decls); + output.extend(&result); + } + } + Expr::Operator(_) => {} + Expr::Range(expr1, expr2, expr3, _) => { + if let Some(expr) = expr1 { + let result = find_captures_in_expr(working_set, expr, seen, seen_decls); + output.extend(&result); + } + if let Some(expr) = expr2 { + let result = find_captures_in_expr(working_set, expr, seen, seen_decls); + output.extend(&result); + } + if let Some(expr) = expr3 { + let result = find_captures_in_expr(working_set, expr, seen, seen_decls); + output.extend(&result); + } + } + Expr::Record(fields) => { + for (field_name, field_value) in fields { + output.extend(&find_captures_in_expr( + working_set, + field_name, + seen, + seen_decls, + )); + output.extend(&find_captures_in_expr( + working_set, + field_value, + seen, + seen_decls, + )); + } + } + Expr::Signature(sig) => { + // println!("Signature found! Adding var_ids"); + // Something with a declaration, similar to a var decl, will introduce more VarIds into the stack at eval + for pos in &sig.required_positional { + if let Some(var_id) = pos.var_id { + seen.push(var_id); + } + } + for pos in &sig.optional_positional { + if let Some(var_id) = pos.var_id { + seen.push(var_id); + } + } + if let Some(rest) = &sig.rest_positional { + if let Some(var_id) = rest.var_id { + seen.push(var_id); + } + } + for named in &sig.named { + if let Some(var_id) = named.var_id { + seen.push(var_id); + } + } + } + Expr::String(_) => {} + Expr::StringInterpolation(exprs) => { + for expr in exprs { + let result = find_captures_in_expr(working_set, expr, seen, seen_decls); + output.extend(&result); + } + } + Expr::RowCondition(block_id) | Expr::Subexpression(block_id) => { + let block = working_set.get_block(*block_id); + let result = find_captures_in_block(working_set, block, seen, seen_decls); + output.extend(&result); + } + Expr::Table(headers, values) => { + for header in headers { + let result = find_captures_in_expr(working_set, header, seen, seen_decls); + output.extend(&result); + } + for row in values { + for cell in row { + let result = find_captures_in_expr(working_set, cell, seen, seen_decls); + output.extend(&result); + } + } + } + Expr::ValueWithUnit(expr, _) => { + let result = find_captures_in_expr(working_set, expr, seen, seen_decls); + output.extend(&result); + } + Expr::Var(var_id) => { + if (*var_id > ENV_VARIABLE_ID || *var_id == IN_VARIABLE_ID) && !seen.contains(var_id) { + output.push(*var_id); + } + } + Expr::VarDecl(var_id) => { + seen.push(*var_id); + } + } + output +} + +fn wrap_expr_with_collect(working_set: &mut StateWorkingSet, expr: &Expression) -> Expression { + let span = expr.span; + + if let Some(decl_id) = working_set.find_decl(b"collect") { + let mut output = vec![]; + + let var_id = working_set.next_var_id(); + let mut signature = Signature::new(""); + signature.required_positional.push(PositionalArg { + var_id: Some(var_id), + name: "$in".into(), + desc: String::new(), + shape: SyntaxShape::Any, + }); + + let mut expr = expr.clone(); + expr.replace_in_variable(working_set, var_id); + + let mut block = Block { + stmts: vec![Statement::Pipeline(Pipeline { + expressions: vec![expr], + })], + signature: Box::new(signature), + ..Default::default() + }; + + let mut seen = vec![]; + let mut seen_decls = vec![]; + let captures = find_captures_in_block(working_set, &block, &mut seen, &mut seen_decls); + + block.captures = captures; + + let block_id = working_set.add_block(block); + + output.push(Expression { + expr: Expr::Block(block_id), + span, + ty: Type::Unknown, + custom_completion: None, + }); + + // The containing, synthetic call to `collect`. + // We don't want to have a real span as it will confuse flattening + // The args are where we'll get the real info + Expression { + expr: Expr::Call(Box::new(Call { + head: Span::new(0, 0), + named: vec![], + positional: output, + decl_id, + })), + span, + ty: Type::String, + custom_completion: None, + } + } else { + Expression::garbage(span) + } +} + +// Parses a vector of u8 to create an AST Block. If a file name is given, then +// the name is stored in the working set. When parsing a source without a file +// name, the source of bytes is stored as "source" +pub fn parse( + working_set: &mut StateWorkingSet, + fname: Option<&str>, + contents: &[u8], + scoped: bool, +) -> (Block, Option) { + trace!("starting top-level parse"); + + let mut error = None; + + let span_offset = working_set.next_span_start(); + + let name = match fname { + Some(fname) => fname.to_string(), + None => "source".to_string(), + }; + + working_set.add_file(name, contents); + + let (output, err) = lex(contents, span_offset, &[], &[], false); + error = error.or(err); + + let (output, err) = lite_parse(&output); + error = error.or(err); + + let (output, err) = parse_block(working_set, &output, scoped); + error = error.or(err); + + (output, error) +} diff --git a/crates/nu-parser/src/type_check.rs b/crates/nu-parser/src/type_check.rs new file mode 100644 index 0000000000..217b2643ec --- /dev/null +++ b/crates/nu-parser/src/type_check.rs @@ -0,0 +1,374 @@ +use crate::ParseError; +use nu_protocol::{ + ast::{Expr, Expression, Operator}, + engine::StateWorkingSet, + Type, +}; + +pub fn type_compatible(lhs: &Type, rhs: &Type) -> bool { + match (lhs, rhs) { + (Type::List(c), Type::List(d)) => type_compatible(c, d), + (Type::Number, Type::Int) => true, + (Type::Number, Type::Float) => true, + (Type::Unknown, _) => true, + (_, Type::Unknown) => true, + (lhs, rhs) => lhs == rhs, + } +} + +pub fn math_result_type( + _working_set: &StateWorkingSet, + lhs: &mut Expression, + op: &mut Expression, + rhs: &mut Expression, +) -> (Type, Option) { + //println!("checking: {:?} {:?} {:?}", lhs, op, rhs); + match &op.expr { + Expr::Operator(operator) => match operator { + Operator::Plus => match (&lhs.ty, &rhs.ty) { + (Type::Int, Type::Int) => (Type::Int, None), + (Type::Float, Type::Int) => (Type::Float, None), + (Type::Int, Type::Float) => (Type::Float, None), + (Type::Float, Type::Float) => (Type::Float, None), + (Type::String, Type::String) => (Type::String, None), + (Type::Duration, Type::Duration) => (Type::Duration, None), + (Type::Filesize, Type::Filesize) => (Type::Filesize, None), + + (Type::Unknown, _) => (Type::Unknown, None), + (_, Type::Unknown) => (Type::Unknown, None), + (Type::Int, _) => { + let ty = rhs.ty.clone(); + *rhs = Expression::garbage(rhs.span); + ( + Type::Unknown, + Some(ParseError::UnsupportedOperation( + op.span, + lhs.span, + lhs.ty.clone(), + rhs.span, + ty, + )), + ) + } + _ => { + *op = Expression::garbage(op.span); + ( + Type::Unknown, + Some(ParseError::UnsupportedOperation( + op.span, + lhs.span, + lhs.ty.clone(), + rhs.span, + rhs.ty.clone(), + )), + ) + } + }, + Operator::Minus => match (&lhs.ty, &rhs.ty) { + (Type::Int, Type::Int) => (Type::Int, None), + (Type::Float, Type::Int) => (Type::Float, None), + (Type::Int, Type::Float) => (Type::Float, None), + (Type::Float, Type::Float) => (Type::Float, None), + (Type::Duration, Type::Duration) => (Type::Duration, None), + (Type::Filesize, Type::Filesize) => (Type::Filesize, None), + + (Type::Unknown, _) => (Type::Unknown, None), + (_, Type::Unknown) => (Type::Unknown, None), + _ => { + *op = Expression::garbage(op.span); + ( + Type::Unknown, + Some(ParseError::UnsupportedOperation( + op.span, + lhs.span, + lhs.ty.clone(), + rhs.span, + rhs.ty.clone(), + )), + ) + } + }, + Operator::Multiply => match (&lhs.ty, &rhs.ty) { + (Type::Int, Type::Int) => (Type::Int, None), + (Type::Float, Type::Int) => (Type::Float, None), + (Type::Int, Type::Float) => (Type::Float, None), + (Type::Float, Type::Float) => (Type::Float, None), + + (Type::Filesize, Type::Int) => (Type::Filesize, None), + (Type::Int, Type::Filesize) => (Type::Filesize, None), + (Type::Duration, Type::Int) => (Type::Filesize, None), + (Type::Int, Type::Duration) => (Type::Filesize, None), + + (Type::Unknown, _) => (Type::Unknown, None), + (_, Type::Unknown) => (Type::Unknown, None), + _ => { + *op = Expression::garbage(op.span); + ( + Type::Unknown, + Some(ParseError::UnsupportedOperation( + op.span, + lhs.span, + lhs.ty.clone(), + rhs.span, + rhs.ty.clone(), + )), + ) + } + }, + Operator::Pow => match (&lhs.ty, &rhs.ty) { + (Type::Int, Type::Int) => (Type::Int, None), + (Type::Float, Type::Int) => (Type::Float, None), + (Type::Int, Type::Float) => (Type::Float, None), + (Type::Float, Type::Float) => (Type::Float, None), + + (Type::Unknown, _) => (Type::Unknown, None), + (_, Type::Unknown) => (Type::Unknown, None), + _ => { + *op = Expression::garbage(op.span); + ( + Type::Unknown, + Some(ParseError::UnsupportedOperation( + op.span, + lhs.span, + lhs.ty.clone(), + rhs.span, + rhs.ty.clone(), + )), + ) + } + }, + Operator::Divide | Operator::Modulo => match (&lhs.ty, &rhs.ty) { + (Type::Int, Type::Int) => (Type::Int, None), + (Type::Float, Type::Int) => (Type::Float, None), + (Type::Int, Type::Float) => (Type::Float, None), + (Type::Float, Type::Float) => (Type::Float, None), + (Type::Filesize, Type::Filesize) => (Type::Float, None), + (Type::Duration, Type::Duration) => (Type::Float, None), + + (Type::Filesize, Type::Int) => (Type::Filesize, None), + (Type::Duration, Type::Int) => (Type::Duration, None), + + (Type::Unknown, _) => (Type::Unknown, None), + (_, Type::Unknown) => (Type::Unknown, None), + _ => { + *op = Expression::garbage(op.span); + ( + Type::Unknown, + Some(ParseError::UnsupportedOperation( + op.span, + lhs.span, + lhs.ty.clone(), + rhs.span, + rhs.ty.clone(), + )), + ) + } + }, + Operator::And | Operator::Or => match (&lhs.ty, &rhs.ty) { + (Type::Bool, Type::Bool) => (Type::Bool, None), + + (Type::Unknown, _) => (Type::Unknown, None), + (_, Type::Unknown) => (Type::Unknown, None), + _ => { + *op = Expression::garbage(op.span); + ( + Type::Unknown, + Some(ParseError::UnsupportedOperation( + op.span, + lhs.span, + lhs.ty.clone(), + rhs.span, + rhs.ty.clone(), + )), + ) + } + }, + Operator::LessThan => match (&lhs.ty, &rhs.ty) { + (Type::Int, Type::Int) => (Type::Bool, None), + (Type::Float, Type::Int) => (Type::Bool, None), + (Type::Int, Type::Float) => (Type::Bool, None), + (Type::Float, Type::Float) => (Type::Bool, None), + (Type::Duration, Type::Duration) => (Type::Bool, None), + (Type::Filesize, Type::Filesize) => (Type::Bool, None), + + (Type::Unknown, _) => (Type::Bool, None), + (_, Type::Unknown) => (Type::Bool, None), + _ => { + *op = Expression::garbage(op.span); + ( + Type::Unknown, + Some(ParseError::UnsupportedOperation( + op.span, + lhs.span, + lhs.ty.clone(), + rhs.span, + rhs.ty.clone(), + )), + ) + } + }, + Operator::LessThanOrEqual => match (&lhs.ty, &rhs.ty) { + (Type::Int, Type::Int) => (Type::Bool, None), + (Type::Float, Type::Int) => (Type::Bool, None), + (Type::Int, Type::Float) => (Type::Bool, None), + (Type::Float, Type::Float) => (Type::Bool, None), + (Type::Duration, Type::Duration) => (Type::Bool, None), + (Type::Filesize, Type::Filesize) => (Type::Bool, None), + + (Type::Unknown, _) => (Type::Bool, None), + (_, Type::Unknown) => (Type::Bool, None), + _ => { + *op = Expression::garbage(op.span); + ( + Type::Unknown, + Some(ParseError::UnsupportedOperation( + op.span, + lhs.span, + lhs.ty.clone(), + rhs.span, + rhs.ty.clone(), + )), + ) + } + }, + Operator::GreaterThan => match (&lhs.ty, &rhs.ty) { + (Type::Int, Type::Int) => (Type::Bool, None), + (Type::Float, Type::Int) => (Type::Bool, None), + (Type::Int, Type::Float) => (Type::Bool, None), + (Type::Float, Type::Float) => (Type::Bool, None), + (Type::Duration, Type::Duration) => (Type::Bool, None), + (Type::Filesize, Type::Filesize) => (Type::Bool, None), + + (Type::Unknown, _) => (Type::Bool, None), + (_, Type::Unknown) => (Type::Bool, None), + _ => { + *op = Expression::garbage(op.span); + ( + Type::Unknown, + Some(ParseError::UnsupportedOperation( + op.span, + lhs.span, + lhs.ty.clone(), + rhs.span, + rhs.ty.clone(), + )), + ) + } + }, + Operator::GreaterThanOrEqual => match (&lhs.ty, &rhs.ty) { + (Type::Int, Type::Int) => (Type::Bool, None), + (Type::Float, Type::Int) => (Type::Bool, None), + (Type::Int, Type::Float) => (Type::Bool, None), + (Type::Float, Type::Float) => (Type::Bool, None), + (Type::Duration, Type::Duration) => (Type::Bool, None), + (Type::Filesize, Type::Filesize) => (Type::Bool, None), + + (Type::Unknown, _) => (Type::Bool, None), + (_, Type::Unknown) => (Type::Bool, None), + _ => { + *op = Expression::garbage(op.span); + ( + Type::Unknown, + Some(ParseError::UnsupportedOperation( + op.span, + lhs.span, + lhs.ty.clone(), + rhs.span, + rhs.ty.clone(), + )), + ) + } + }, + Operator::Equal => (Type::Bool, None), + Operator::NotEqual => (Type::Bool, None), + Operator::Contains => match (&lhs.ty, &rhs.ty) { + (Type::String, Type::String) => (Type::Bool, None), + (Type::Unknown, _) => (Type::Bool, None), + (_, Type::Unknown) => (Type::Bool, None), + _ => { + *op = Expression::garbage(op.span); + ( + Type::Unknown, + Some(ParseError::UnsupportedOperation( + op.span, + lhs.span, + lhs.ty.clone(), + rhs.span, + rhs.ty.clone(), + )), + ) + } + }, + Operator::NotContains => match (&lhs.ty, &rhs.ty) { + (Type::String, Type::String) => (Type::Bool, None), + (Type::Unknown, _) => (Type::Bool, None), + (_, Type::Unknown) => (Type::Bool, None), + _ => { + *op = Expression::garbage(op.span); + ( + Type::Unknown, + Some(ParseError::UnsupportedOperation( + op.span, + lhs.span, + lhs.ty.clone(), + rhs.span, + rhs.ty.clone(), + )), + ) + } + }, + Operator::In => match (&lhs.ty, &rhs.ty) { + (t, Type::List(u)) if type_compatible(t, u) => (Type::Bool, None), + (Type::Int | Type::Float, Type::Range) => (Type::Bool, None), + (Type::String, Type::String) => (Type::Bool, None), + (Type::String, Type::Record(_)) => (Type::Bool, None), + + (Type::Unknown, _) => (Type::Bool, None), + (_, Type::Unknown) => (Type::Bool, None), + _ => { + *op = Expression::garbage(op.span); + ( + Type::Unknown, + Some(ParseError::UnsupportedOperation( + op.span, + lhs.span, + lhs.ty.clone(), + rhs.span, + rhs.ty.clone(), + )), + ) + } + }, + Operator::NotIn => match (&lhs.ty, &rhs.ty) { + (t, Type::List(u)) if type_compatible(t, u) => (Type::Bool, None), + (Type::Int | Type::Float, Type::Range) => (Type::Bool, None), + (Type::String, Type::String) => (Type::Bool, None), + (Type::String, Type::Record(_)) => (Type::Bool, None), + + (Type::Unknown, _) => (Type::Bool, None), + (_, Type::Unknown) => (Type::Bool, None), + _ => { + *op = Expression::garbage(op.span); + ( + Type::Unknown, + Some(ParseError::UnsupportedOperation( + op.span, + lhs.span, + lhs.ty.clone(), + rhs.span, + rhs.ty.clone(), + )), + ) + } + }, + }, + _ => { + *op = Expression::garbage(op.span); + + ( + Type::Unknown, + Some(ParseError::IncompleteMathExpression(op.span)), + ) + } + } +} diff --git a/crates/nu-parser/tests/test_lex.rs b/crates/nu-parser/tests/test_lex.rs new file mode 100644 index 0000000000..b96f63cbfe --- /dev/null +++ b/crates/nu-parser/tests/test_lex.rs @@ -0,0 +1,138 @@ +use nu_parser::{lex, ParseError, Token, TokenContents}; +use nu_protocol::Span; + +#[test] +fn lex_basic() { + let file = b"let x = 4"; + + let output = lex(file, 0, &[], &[], true); + + assert!(output.1.is_none()); +} + +#[test] +fn lex_newline() { + let file = b"let x = 300\nlet y = 500;"; + + let output = lex(file, 0, &[], &[], true); + + assert!(output.0.contains(&Token { + contents: TokenContents::Eol, + span: Span { start: 11, end: 12 } + })); +} + +#[test] +fn lex_empty() { + let file = b""; + + let output = lex(file, 0, &[], &[], true); + + assert!(output.0.is_empty()); + assert!(output.1.is_none()); +} + +#[test] +fn lex_parenthesis() { + // The whole parenthesis is an item for the lexer + let file = b"let x = (300 + (322 * 444));"; + + let output = lex(file, 0, &[], &[], true); + + assert_eq!( + output.0.get(3).unwrap(), + &Token { + contents: TokenContents::Item, + span: Span { start: 8, end: 27 } + } + ); +} + +#[test] +fn lex_comment() { + let file = b"let x = 300 # a comment \n $x + 444"; + + let output = lex(file, 0, &[], &[], false); + + assert_eq!( + output.0.get(4).unwrap(), + &Token { + contents: TokenContents::Comment, + span: Span { start: 12, end: 24 } + } + ); +} + +#[test] +fn lex_is_incomplete() { + let file = b"let x = 300 | ;"; + + let output = lex(file, 0, &[], &[], true); + + let err = output.1.unwrap(); + assert!(matches!(err, ParseError::ExtraTokens(_))); +} + +#[test] +fn lex_incomplete_paren() { + let file = b"let x = (300 + ( 4 + 1)"; + + let output = lex(file, 0, &[], &[], true); + + let err = output.1.unwrap(); + assert!(matches!(err, ParseError::UnexpectedEof(v, _) if v == ")")); +} + +#[test] +fn lex_incomplete_quote() { + let file = b"let x = '300 + 4 + 1"; + + let output = lex(file, 0, &[], &[], true); + + let err = output.1.unwrap(); + assert!(matches!(err, ParseError::UnexpectedEof(v, _) if v == "'")); +} + +#[test] +fn lex_comments() { + // Comments should keep the end of line token + // Code: + // let z = 4 + // let x = 4 #comment + // let y = 1 # comment + let file = b"let z = 4 #comment \n let x = 4 # comment\n let y = 1 # comment"; + + let output = lex(file, 0, &[], &[], false); + + assert_eq!( + output.0.get(4).unwrap(), + &Token { + contents: TokenContents::Comment, + span: Span { start: 10, end: 19 } + } + ); + assert_eq!( + output.0.get(5).unwrap(), + &Token { + contents: TokenContents::Eol, + span: Span { start: 19, end: 20 } + } + ); + + // When there is no space between the comment and the new line the span + // for the command and the EOL overlaps + assert_eq!( + output.0.get(10).unwrap(), + &Token { + contents: TokenContents::Comment, + span: Span { start: 31, end: 40 } + } + ); + assert_eq!( + output.0.get(11).unwrap(), + &Token { + contents: TokenContents::Eol, + span: Span { start: 40, end: 41 } + } + ); +} diff --git a/crates/nu-parser/tests/test_lite_parser.rs b/crates/nu-parser/tests/test_lite_parser.rs new file mode 100644 index 0000000000..f9c638e892 --- /dev/null +++ b/crates/nu-parser/tests/test_lite_parser.rs @@ -0,0 +1,242 @@ +use nu_parser::{lex, lite_parse, LiteBlock, ParseError}; +use nu_protocol::Span; + +fn lite_parse_helper(input: &[u8]) -> Result { + let (output, err) = lex(input, 0, &[], &[], false); + if let Some(err) = err { + return Err(err); + } + + let (output, err) = lite_parse(&output); + if let Some(err) = err { + return Err(err); + } + + Ok(output) +} + +#[test] +fn comment_before() -> Result<(), ParseError> { + // Code: + // # this is a comment + // def foo bar + let input = b"# this is a comment\ndef foo bar"; + + let lite_block = lite_parse_helper(input)?; + + assert_eq!(lite_block.block.len(), 1); + assert_eq!(lite_block.block[0].commands.len(), 1); + assert_eq!(lite_block.block[0].commands[0].comments.len(), 1); + assert_eq!(lite_block.block[0].commands[0].parts.len(), 3); + + assert_eq!( + lite_block.block[0].commands[0].comments[0], + Span { start: 0, end: 19 } + ); + + Ok(()) +} + +#[test] +fn comment_beside() -> Result<(), ParseError> { + // Code: + // def foo bar # this is a comment + let input = b"def foo bar # this is a comment"; + + let lite_block = lite_parse_helper(input)?; + + assert_eq!(lite_block.block.len(), 1); + assert_eq!(lite_block.block[0].commands.len(), 1); + assert_eq!(lite_block.block[0].commands[0].comments.len(), 1); + assert_eq!(lite_block.block[0].commands[0].parts.len(), 3); + + assert_eq!( + lite_block.block[0].commands[0].comments[0], + Span { start: 12, end: 31 } + ); + + Ok(()) +} + +#[test] +fn comments_stack() -> Result<(), ParseError> { + // Code: + // # this is a comment + // # another comment + // # def foo bar + let input = b"# this is a comment\n# another comment\ndef foo bar "; + + let lite_block = lite_parse_helper(input)?; + + assert_eq!(lite_block.block.len(), 1); + assert_eq!(lite_block.block[0].commands[0].comments.len(), 2); + assert_eq!(lite_block.block[0].commands[0].parts.len(), 3); + + assert_eq!( + lite_block.block[0].commands[0].comments[0], + Span { start: 0, end: 19 } + ); + + assert_eq!( + lite_block.block[0].commands[0].comments[1], + Span { start: 20, end: 37 } + ); + + Ok(()) +} + +#[test] +fn separated_comments_dont_stack() -> Result<(), ParseError> { + // Code: + // # this is a comment + // + // # another comment + // # def foo bar + let input = b"# this is a comment\n\n# another comment\ndef foo bar "; + + let lite_block = lite_parse_helper(input)?; + + assert_eq!(lite_block.block.len(), 1); + assert_eq!(lite_block.block[0].commands[0].comments.len(), 1); + assert_eq!(lite_block.block[0].commands[0].parts.len(), 3); + + assert_eq!( + lite_block.block[0].commands[0].comments[0], + Span { start: 21, end: 38 } + ); + + Ok(()) +} + +#[test] +fn multiple_statements() -> Result<(), ParseError> { + // Code: + // # A comment + // let a = ( 3 + ( + // 4 + + // 5 )) + // let b = 1 # comment + let input = b"# comment \n let a = ( 3 + (\n 4 + \n 5 )) \n let b = 1 # comment"; + + let lite_block = lite_parse_helper(input)?; + + assert_eq!(lite_block.block.len(), 2); + assert_eq!(lite_block.block[0].commands[0].comments.len(), 1); + assert_eq!(lite_block.block[0].commands[0].parts.len(), 4); + assert_eq!( + lite_block.block[0].commands[0].comments[0], + Span { start: 0, end: 10 } + ); + + assert_eq!(lite_block.block[1].commands[0].comments.len(), 1); + assert_eq!(lite_block.block[1].commands[0].parts.len(), 4); + assert_eq!( + lite_block.block[1].commands[0].comments[0], + Span { start: 52, end: 61 } + ); + + Ok(()) +} + +#[test] +fn multiple_commands() -> Result<(), ParseError> { + // Pipes add commands to the lite parser + // Code: + // let a = ls | where name == 1 + // let b = 1 # comment + let input = b"let a = ls | where name == 1 \n let b = 1 # comment"; + + let lite_block = lite_parse_helper(input)?; + + assert_eq!(lite_block.block.len(), 2); + assert_eq!(lite_block.block[0].commands.len(), 2); + assert_eq!(lite_block.block[1].commands.len(), 1); + + assert_eq!( + lite_block.block[1].commands[0].comments[0], + Span { start: 41, end: 50 } + ); + + Ok(()) +} + +#[test] +fn multiple_commands_with_comment() -> Result<(), ParseError> { + // Pipes add commands to the lite parser + // The comments are attached to the commands next to them + // Code: + // let a = ls | where name == 1 # comment + // let b = 1 # comment + //let a = ls | where name == 1 # comment \n let b = 1 # comment + let input = b"let a = ls | where name == 1 # comment\n let b = 1 # comment"; + + let lite_block = lite_parse_helper(input)?; + + assert_eq!(lite_block.block.len(), 2); + assert_eq!(lite_block.block[0].commands.len(), 2); + assert_eq!(lite_block.block[1].commands.len(), 1); + + assert_eq!( + lite_block.block[0].commands[1].comments[0], + Span { start: 29, end: 38 } + ); + + Ok(()) +} + +#[test] +fn multiple_commands_with_pipes() -> Result<(), ParseError> { + // The comments inside () get encapsulated in the whole item + // Code: + // # comment 1 + // # comment 2 + // let a = ( ls + // | where name =~ some # another comment + // | each { |file| rm file.name } # final comment + // ) + // # comment A + // let b = 0; + let input = b"# comment 1 +# comment 2 +let a = ( ls +| where name =~ some # another comment +| each { |file| rm file.name }) # final comment +# comment A +let b = 0 +"; + + let lite_block = lite_parse_helper(input)?; + + assert_eq!(lite_block.block.len(), 2); + assert_eq!(lite_block.block[0].commands[0].comments.len(), 3); + assert_eq!(lite_block.block[0].commands[0].parts.len(), 4); + + assert_eq!( + lite_block.block[0].commands[0].parts[3], + Span { + start: 32, + end: 107 + } + ); + + assert_eq!( + lite_block.block[0].commands[0].comments[2], + Span { + start: 108, + end: 123 + } + ); + + assert_eq!(lite_block.block[1].commands[0].comments.len(), 1); + assert_eq!(lite_block.block[1].commands[0].parts.len(), 4); + + assert_eq!( + lite_block.block[1].commands[0].comments[0], + Span { + start: 124, + end: 135 + } + ); + + Ok(()) +} diff --git a/crates/nu-parser/tests/test_parser.rs b/crates/nu-parser/tests/test_parser.rs new file mode 100644 index 0000000000..6c70c474b7 --- /dev/null +++ b/crates/nu-parser/tests/test_parser.rs @@ -0,0 +1,564 @@ +use nu_parser::ParseError; +use nu_parser::*; +use nu_protocol::{ + ast::{Expr, Expression, Pipeline, Statement}, + engine::{Command, EngineState, Stack, StateWorkingSet}, + Signature, SyntaxShape, +}; + +#[cfg(test)] +#[derive(Clone)] +pub struct Let; + +#[cfg(test)] +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", + ) + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &nu_protocol::ast::Call, + _input: nu_protocol::PipelineData, + ) -> Result { + todo!() + } +} + +#[test] +pub fn parse_int() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + let (block, err) = parse(&mut working_set, None, b"3", true); + + assert!(err.is_none()); + assert!(block.len() == 1); + match &block[0] { + Statement::Pipeline(Pipeline { expressions }) => { + assert!(expressions.len() == 1); + assert!(matches!( + expressions[0], + Expression { + expr: Expr::Int(3), + .. + } + )) + } + _ => panic!("No match"), + } +} + +#[test] +pub fn parse_call() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + let sig = Signature::build("foo").named("--jazz", SyntaxShape::Int, "jazz!!", Some('j')); + working_set.add_decl(sig.predeclare()); + + let (block, err) = parse(&mut working_set, None, b"foo", true); + + assert!(err.is_none()); + assert!(block.len() == 1); + + match &block[0] { + Statement::Pipeline(Pipeline { expressions }) => { + assert_eq!(expressions.len(), 1); + + if let Expression { + expr: Expr::Call(call), + .. + } = &expressions[0] + { + assert_eq!(call.decl_id, 0); + } + } + _ => panic!("not a call"), + } +} + +#[test] +pub fn parse_call_missing_flag_arg() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + let sig = Signature::build("foo").named("jazz", SyntaxShape::Int, "jazz!!", Some('j')); + working_set.add_decl(sig.predeclare()); + + let (_, err) = parse(&mut working_set, None, b"foo --jazz", true); + assert!(matches!(err, Some(ParseError::MissingFlagParam(..)))); +} + +#[test] +pub fn parse_call_missing_short_flag_arg() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + let sig = Signature::build("foo").named("--jazz", SyntaxShape::Int, "jazz!!", Some('j')); + working_set.add_decl(sig.predeclare()); + + let (_, err) = parse(&mut working_set, None, b"foo -j", true); + assert!(matches!(err, Some(ParseError::MissingFlagParam(..)))); +} + +#[test] +pub fn parse_call_too_many_shortflag_args() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + let sig = Signature::build("foo") + .named("--jazz", SyntaxShape::Int, "jazz!!", Some('j')) + .named("--math", SyntaxShape::Int, "math!!", Some('m')); + working_set.add_decl(sig.predeclare()); + let (_, err) = parse(&mut working_set, None, b"foo -mj", true); + assert!(matches!( + err, + Some(ParseError::ShortFlagBatchCantTakeArg(..)) + )); +} + +#[test] +pub fn parse_call_unknown_shorthand() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + let sig = Signature::build("foo").switch("--jazz", "jazz!!", Some('j')); + working_set.add_decl(sig.predeclare()); + let (_, err) = parse(&mut working_set, None, b"foo -mj", true); + assert!(matches!(err, Some(ParseError::UnknownFlag(..)))); +} + +#[test] +pub fn parse_call_extra_positional() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + let sig = Signature::build("foo").switch("--jazz", "jazz!!", Some('j')); + working_set.add_decl(sig.predeclare()); + let (_, err) = parse(&mut working_set, None, b"foo -j 100", true); + assert!(matches!(err, Some(ParseError::ExtraPositional(..)))); +} + +#[test] +pub fn parse_call_missing_req_positional() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + let sig = Signature::build("foo").required("jazz", SyntaxShape::Int, "jazz!!"); + working_set.add_decl(sig.predeclare()); + let (_, err) = parse(&mut working_set, None, b"foo", true); + assert!(matches!(err, Some(ParseError::MissingPositional(..)))); +} + +#[test] +pub fn parse_call_missing_req_flag() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + let sig = Signature::build("foo").required_named("--jazz", SyntaxShape::Int, "jazz!!", None); + working_set.add_decl(sig.predeclare()); + let (_, err) = parse(&mut working_set, None, b"foo", true); + assert!(matches!(err, Some(ParseError::MissingRequiredFlag(..)))); +} + +#[test] +fn test_nothing_comparisson_eq() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + let (block, err) = parse(&mut working_set, None, b"2 == $nothing", true); + + assert!(err.is_none()); + assert!(block.len() == 1); + match &block[0] { + Statement::Pipeline(Pipeline { expressions }) => { + assert!(expressions.len() == 1); + assert!(matches!( + &expressions[0], + Expression { + expr: Expr::BinaryOp(..), + .. + } + )) + } + _ => panic!("No match"), + } +} + +#[test] +fn test_nothing_comparisson_neq() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + let (block, err) = parse(&mut working_set, None, b"2 != $nothing", true); + + assert!(err.is_none()); + assert!(block.len() == 1); + match &block[0] { + Statement::Pipeline(Pipeline { expressions }) => { + assert!(expressions.len() == 1); + assert!(matches!( + &expressions[0], + Expression { + expr: Expr::BinaryOp(..), + .. + } + )) + } + _ => panic!("No match"), + } +} + +mod range { + use super::*; + use nu_protocol::ast::{RangeInclusion, RangeOperator}; + + #[test] + fn parse_inclusive_range() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + let (block, err) = parse(&mut working_set, None, b"0..10", true); + + assert!(err.is_none()); + assert!(block.len() == 1); + match &block[0] { + Statement::Pipeline(Pipeline { expressions }) => { + assert!(expressions.len() == 1); + assert!(matches!( + expressions[0], + Expression { + expr: Expr::Range( + Some(_), + None, + Some(_), + RangeOperator { + inclusion: RangeInclusion::Inclusive, + .. + } + ), + .. + } + )) + } + _ => panic!("No match"), + } + } + + #[test] + fn parse_exclusive_range() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + let (block, err) = parse(&mut working_set, None, b"0..<10", true); + + assert!(err.is_none()); + assert!(block.len() == 1); + match &block[0] { + Statement::Pipeline(Pipeline { expressions }) => { + assert!(expressions.len() == 1); + assert!(matches!( + expressions[0], + Expression { + expr: Expr::Range( + Some(_), + None, + Some(_), + RangeOperator { + inclusion: RangeInclusion::RightExclusive, + .. + } + ), + .. + } + )) + } + _ => panic!("No match"), + } + } + + #[test] + fn parse_reverse_range() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + let (block, err) = parse(&mut working_set, None, b"10..0", true); + + assert!(err.is_none()); + assert!(block.len() == 1); + match &block[0] { + Statement::Pipeline(Pipeline { expressions }) => { + assert!(expressions.len() == 1); + assert!(matches!( + expressions[0], + Expression { + expr: Expr::Range( + Some(_), + None, + Some(_), + RangeOperator { + inclusion: RangeInclusion::Inclusive, + .. + } + ), + .. + } + )) + } + _ => panic!("No match"), + } + } + + #[test] + fn parse_subexpression_range() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + let (block, err) = parse(&mut working_set, None, b"(3 - 3)..<(8 + 2)", true); + + assert!(err.is_none()); + assert!(block.len() == 1); + match &block[0] { + Statement::Pipeline(Pipeline { expressions }) => { + assert!(expressions.len() == 1); + assert!(matches!( + expressions[0], + Expression { + expr: Expr::Range( + Some(_), + None, + Some(_), + RangeOperator { + inclusion: RangeInclusion::RightExclusive, + .. + } + ), + .. + } + )) + } + _ => panic!("No match"), + } + } + + #[test] + fn parse_variable_range() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + working_set.add_decl(Box::new(Let)); + + let (block, err) = parse(&mut working_set, None, b"let a = 2; $a..10", true); + + assert!(err.is_none()); + assert!(block.len() == 2); + match &block[1] { + Statement::Pipeline(Pipeline { expressions }) => { + assert!(expressions.len() == 1); + assert!(matches!( + expressions[0], + Expression { + expr: Expr::Range( + Some(_), + None, + Some(_), + RangeOperator { + inclusion: RangeInclusion::Inclusive, + .. + } + ), + .. + } + )) + } + _ => panic!("No match"), + } + } + + #[test] + fn parse_subexpression_variable_range() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + working_set.add_decl(Box::new(Let)); + + let (block, err) = parse(&mut working_set, None, b"let a = 2; $a..<($a + 10)", true); + + assert!(err.is_none()); + assert!(block.len() == 2); + match &block[1] { + Statement::Pipeline(Pipeline { expressions }) => { + assert!(expressions.len() == 1); + assert!(matches!( + expressions[0], + Expression { + expr: Expr::Range( + Some(_), + None, + Some(_), + RangeOperator { + inclusion: RangeInclusion::RightExclusive, + .. + } + ), + .. + } + )) + } + _ => panic!("No match"), + } + } + + #[test] + fn parse_right_unbounded_range() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + let (block, err) = parse(&mut working_set, None, b"0..", true); + + assert!(err.is_none()); + assert!(block.len() == 1); + match &block[0] { + Statement::Pipeline(Pipeline { expressions }) => { + assert!(expressions.len() == 1); + assert!(matches!( + expressions[0], + Expression { + expr: Expr::Range( + Some(_), + None, + None, + RangeOperator { + inclusion: RangeInclusion::Inclusive, + .. + } + ), + .. + } + )) + } + _ => panic!("No match"), + } + } + + #[test] + fn parse_left_unbounded_range() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + let (block, err) = parse(&mut working_set, None, b"..10", true); + + assert!(err.is_none()); + assert!(block.len() == 1); + match &block[0] { + Statement::Pipeline(Pipeline { expressions }) => { + assert!(expressions.len() == 1); + assert!(matches!( + expressions[0], + Expression { + expr: Expr::Range( + None, + None, + Some(_), + RangeOperator { + inclusion: RangeInclusion::Inclusive, + .. + } + ), + .. + } + )) + } + _ => panic!("No match"), + } + } + + #[test] + fn parse_negative_range() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + let (block, err) = parse(&mut working_set, None, b"-10..-3", true); + + assert!(err.is_none()); + assert!(block.len() == 1); + match &block[0] { + Statement::Pipeline(Pipeline { expressions }) => { + assert!(expressions.len() == 1); + assert!(matches!( + expressions[0], + Expression { + expr: Expr::Range( + Some(_), + None, + Some(_), + RangeOperator { + inclusion: RangeInclusion::Inclusive, + .. + } + ), + .. + } + )) + } + _ => panic!("No match"), + } + } + + #[test] + fn parse_float_range() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + let (block, err) = parse(&mut working_set, None, b"2.0..4.0..10.0", true); + + assert!(err.is_none()); + assert!(block.len() == 1); + match &block[0] { + Statement::Pipeline(Pipeline { expressions }) => { + assert!(expressions.len() == 1); + assert!(matches!( + expressions[0], + Expression { + expr: Expr::Range( + Some(_), + Some(_), + Some(_), + RangeOperator { + inclusion: RangeInclusion::Inclusive, + .. + } + ), + .. + } + )) + } + _ => panic!("No match"), + } + } + + #[test] + fn bad_parse_does_crash() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + let (_, err) = parse(&mut working_set, None, b"(0)..\"a\"", true); + + assert!(err.is_some()); + } +} diff --git a/crates/nu-path/Cargo.toml b/crates/nu-path/Cargo.toml index df7b77163b..51099a624d 100644 --- a/crates/nu-path/Cargo.toml +++ b/crates/nu-path/Cargo.toml @@ -1,12 +1,22 @@ [package] authors = ["The Nu Project Contributors"] description = "Path handling library for Nushell" +<<<<<<< HEAD edition = "2018" license = "MIT" name = "nu-path" version = "0.43.0" +======= +edition = "2021" +license = "MIT" +name = "nu-path" +version = "0.37.1" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce [dependencies] dirs-next = "2.0.0" dunce = "1.0.1" +<<<<<<< HEAD +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce diff --git a/crates/nu-path/src/dots.rs b/crates/nu-path/src/dots.rs index 0439b36d95..169fdc2352 100644 --- a/crates/nu-path/src/dots.rs +++ b/crates/nu-path/src/dots.rs @@ -19,7 +19,11 @@ fn handle_dots_push(string: &mut String, count: u8) { string.pop(); // remove last '/' } +<<<<<<< HEAD /// Expands any occurrence of more than two dots into a sequence of ../ (or ..\ on windows), e.g., +======= +/// Expands any occurence of more than two dots into a sequence of ../ (or ..\ on windows), e.g., +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce /// "..." into "../..", "...." into "../../../", etc. pub fn expand_ndots(path: impl AsRef) -> PathBuf { // Check if path is valid UTF-8 and if not, return it as it is to avoid breaking it via string diff --git a/crates/nu-path/src/expansions.rs b/crates/nu-path/src/expansions.rs index 3393a5793f..0d70d644b8 100644 --- a/crates/nu-path/src/expansions.rs +++ b/crates/nu-path/src/expansions.rs @@ -26,19 +26,32 @@ where } } +<<<<<<< HEAD /// Resolve all symbolic links and all components (tilde, ., .., ...+) and return the path in its /// absolute form. /// /// Fails under the same conditions as /// [std::fs::canonicalize](https://doc.rust-lang.org/std/fs/fn.canonicalize.html). pub fn canonicalize(path: impl AsRef) -> io::Result { +======= +fn canonicalize(path: impl AsRef) -> io::Result { +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce let path = expand_tilde(path); let path = expand_ndots(path); dunce::canonicalize(path) } +<<<<<<< HEAD /// Same as canonicalize() but the input path is specified relative to another path +======= +/// Resolve all symbolic links and all components (tilde, ., .., ...+) and return the path in its +/// absolute form. +/// +/// Fails under the same conditions as +/// [std::fs::canonicalize](https://doc.rust-lang.org/std/fs/fn.canonicalize.html). +/// The input path is specified relative to another path +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce pub fn canonicalize_with(path: P, relative_to: Q) -> io::Result where P: AsRef, @@ -49,6 +62,15 @@ where canonicalize(path) } +<<<<<<< HEAD +======= +fn expand_path(path: impl AsRef) -> PathBuf { + let path = expand_tilde(path); + let path = expand_ndots(path); + expand_dots(path) +} + +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce /// Resolve only path components (tilde, ., .., ...+), if possible. /// /// The function works in a "best effort" mode: It does not fail but rather returns the unexpanded @@ -57,6 +79,7 @@ where /// Furthermore, unlike canonicalize(), it does not use sys calls (such as readlink). /// /// Does not convert to absolute form nor does it resolve symlinks. +<<<<<<< HEAD pub fn expand_path(path: impl AsRef) -> PathBuf { let path = expand_tilde(path); let path = expand_ndots(path); @@ -64,6 +87,9 @@ pub fn expand_path(path: impl AsRef) -> PathBuf { } /// Same as expand_path() but the input path is specified relative to another path +======= +/// The input path is specified relative to another path +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce pub fn expand_path_with(path: P, relative_to: Q) -> PathBuf where P: AsRef, diff --git a/crates/nu-path/src/helpers.rs b/crates/nu-path/src/helpers.rs new file mode 100644 index 0000000000..eaab1958c7 --- /dev/null +++ b/crates/nu-path/src/helpers.rs @@ -0,0 +1,9 @@ +use std::path::PathBuf; + +pub fn home_dir() -> Option { + dirs_next::home_dir() +} + +pub fn config_dir() -> Option { + dirs_next::config_dir() +} diff --git a/crates/nu-path/src/lib.rs b/crates/nu-path/src/lib.rs index 9606bc15cf..a5809d6fc7 100644 --- a/crates/nu-path/src/lib.rs +++ b/crates/nu-path/src/lib.rs @@ -1,8 +1,17 @@ mod dots; mod expansions; +<<<<<<< HEAD mod tilde; mod util; pub use expansions::{canonicalize, canonicalize_with, expand_path, expand_path_with}; +======= +mod helpers; +mod tilde; +mod util; + +pub use expansions::{canonicalize_with, expand_path_with}; +pub use helpers::{config_dir, home_dir}; +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce pub use tilde::expand_tilde; pub use util::trim_trailing_slash; diff --git a/crates/nu-path/src/tilde.rs b/crates/nu-path/src/tilde.rs index e1c7ec56a3..6268ef2fb9 100644 --- a/crates/nu-path/src/tilde.rs +++ b/crates/nu-path/src/tilde.rs @@ -1,6 +1,10 @@ use std::path::{Path, PathBuf}; +<<<<<<< HEAD fn expand_tilde_with(path: impl AsRef, home: Option) -> PathBuf { +======= +fn expand_tilde_with_home(path: impl AsRef, home: Option) -> PathBuf { +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce let path = path.as_ref(); if !path.starts_with("~") { @@ -27,7 +31,11 @@ fn expand_tilde_with(path: impl AsRef, home: Option) -> PathBuf { /// Expand tilde ("~") into a home directory if it is the first path component pub fn expand_tilde(path: impl AsRef) -> PathBuf { // TODO: Extend this to work with "~user" style of home paths +<<<<<<< HEAD expand_tilde_with(path, dirs_next::home_dir()) +======= + expand_tilde_with_home(path, dirs_next::home_dir()) +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } #[cfg(test)] @@ -37,17 +45,29 @@ mod tests { fn check_expanded(s: &str) { let home = Path::new("/home"); let buf = Some(PathBuf::from(home)); +<<<<<<< HEAD assert!(expand_tilde_with(Path::new(s), buf).starts_with(&home)); +======= + assert!(expand_tilde_with_home(Path::new(s), buf).starts_with(&home)); +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce // Tests the special case in expand_tilde for "/" as home let home = Path::new("/"); let buf = Some(PathBuf::from(home)); +<<<<<<< HEAD assert!(!expand_tilde_with(Path::new(s), buf).starts_with("//")); +======= + assert!(!expand_tilde_with_home(Path::new(s), buf).starts_with("//")); +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } fn check_not_expanded(s: &str) { let home = PathBuf::from("/home"); +<<<<<<< HEAD let expanded = expand_tilde_with(Path::new(s), Some(home)); +======= + let expanded = expand_tilde_with_home(Path::new(s), Some(home)); +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce assert!(expanded == Path::new(s)); } diff --git a/crates/nu-plugin/Cargo.toml b/crates/nu-plugin/Cargo.toml index 87c640d5bd..d78ca6785d 100644 --- a/crates/nu-plugin/Cargo.toml +++ b/crates/nu-plugin/Cargo.toml @@ -1,4 +1,5 @@ [package] +<<<<<<< HEAD authors = ["The Nu Project Contributors"] description = "Nushell Plugin" edition = "2018" @@ -20,3 +21,15 @@ serde = { version="1.0", features=["derive"] } serde_json = "1.0" [build-dependencies] +======= +name = "nu-plugin" +version = "0.1.0" +edition = "2021" + +[dependencies] +capnp = "0.14.3" +nu-protocol = { path = "../nu-protocol" } +nu-engine = { path = "../nu-engine" } +serde = {version = "1.0.130", features = ["derive"]} +serde_json = { version = "1.0"} +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce diff --git a/crates/nu-plugin/src/lib.rs b/crates/nu-plugin/src/lib.rs index 8b6d5122a7..5959de8638 100644 --- a/crates/nu-plugin/src/lib.rs +++ b/crates/nu-plugin/src/lib.rs @@ -1,6 +1,19 @@ +<<<<<<< HEAD pub mod jsonrpc; mod plugin; pub mod test_helpers; pub use crate::plugin::{serve_plugin, Plugin}; +======= +mod plugin; +mod protocol; +mod serializers; + +#[allow(dead_code)] +mod plugin_capnp; + +pub use plugin::{get_signature, serve_plugin, Plugin, PluginDeclaration}; +pub use protocol::{EvaluatedCall, LabeledError}; +pub use serializers::{capnp::CapnpSerializer, json::JsonSerializer, EncodingType}; +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce diff --git a/crates/nu-plugin/src/plugin/declaration.rs b/crates/nu-plugin/src/plugin/declaration.rs new file mode 100644 index 0000000000..e6f3a6f457 --- /dev/null +++ b/crates/nu-plugin/src/plugin/declaration.rs @@ -0,0 +1,133 @@ +use crate::{EncodingType, EvaluatedCall}; + +use super::{create_command, OUTPUT_BUFFER_SIZE}; +use crate::protocol::{CallInfo, PluginCall, PluginResponse}; +use std::io::BufReader; +use std::path::{Path, PathBuf}; + +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ast::Call, Signature}; +use nu_protocol::{PipelineData, ShellError}; + +#[derive(Clone)] +pub struct PluginDeclaration { + name: String, + signature: Signature, + filename: PathBuf, + shell: Option, + encoding: EncodingType, +} + +impl PluginDeclaration { + pub fn new( + filename: PathBuf, + signature: Signature, + encoding: EncodingType, + shell: Option, + ) -> Self { + Self { + name: signature.name.clone(), + signature, + filename, + encoding, + shell, + } + } +} + +impl Command for PluginDeclaration { + fn name(&self) -> &str { + &self.name + } + + fn signature(&self) -> Signature { + self.signature.clone() + } + + fn usage(&self) -> &str { + self.signature.usage.as_str() + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + // Call the command with self path + // Decode information from plugin + // Create PipelineData + let source_file = Path::new(&self.filename); + let mut plugin_cmd = create_command(source_file, &self.shell); + + let mut child = plugin_cmd.spawn().map_err(|err| { + let decl = engine_state.get_decl(call.decl_id); + ShellError::SpannedLabeledError( + format!("Unable to spawn plugin for {}", decl.name()), + format!("{}", err), + call.head, + ) + })?; + + let input = input.into_value(call.head); + + // Create message to plugin to indicate that signature is required and + // send call to plugin asking for signature + if let Some(mut stdin_writer) = child.stdin.take() { + let encoding_clone = self.encoding.clone(); + let plugin_call = PluginCall::CallInfo(Box::new(CallInfo { + name: self.name.clone(), + call: EvaluatedCall::try_from_call(call, engine_state, stack)?, + input, + })); + std::thread::spawn(move || { + // PluginCall information + encoding_clone.encode_call(&plugin_call, &mut stdin_writer) + }); + } + + // Deserialize response from plugin to extract the resulting value + let pipeline_data = if let Some(stdout_reader) = &mut child.stdout { + let reader = stdout_reader; + let mut buf_read = BufReader::with_capacity(OUTPUT_BUFFER_SIZE, reader); + + let response = self.encoding.decode_response(&mut buf_read).map_err(|err| { + let decl = engine_state.get_decl(call.decl_id); + ShellError::SpannedLabeledError( + format!("Unable to decode call for {}", decl.name()), + err.to_string(), + call.head, + ) + }); + + match response { + Ok(PluginResponse::Value(value)) => { + Ok(PipelineData::Value(value.as_ref().clone(), None)) + } + Ok(PluginResponse::Error(err)) => Err(err.into()), + Ok(PluginResponse::Signature(..)) => Err(ShellError::SpannedLabeledError( + "Plugin missing value".into(), + "Received a signature from plugin instead of value".into(), + call.head, + )), + Err(err) => Err(err), + } + } else { + Err(ShellError::SpannedLabeledError( + "Error with stdout reader".into(), + "no stdout reader".into(), + call.head, + )) + }; + + // We need to call .wait() on the child, or we'll risk summoning the zombie horde + let _ = child.wait(); + + pipeline_data + } + + fn is_plugin(&self) -> Option<(&PathBuf, &str, &Option)> { + Some((&self.filename, self.encoding.to_str(), &self.shell)) + } +} diff --git a/samples/wasm/.cargo-ok b/crates/nu-plugin/src/plugin/is_plugin similarity index 100% rename from samples/wasm/.cargo-ok rename to crates/nu-plugin/src/plugin/is_plugin diff --git a/crates/nu-plugin/src/plugin/mod.rs b/crates/nu-plugin/src/plugin/mod.rs new file mode 100644 index 0000000000..91e1e1657a --- /dev/null +++ b/crates/nu-plugin/src/plugin/mod.rs @@ -0,0 +1,188 @@ +mod declaration; +pub use declaration::PluginDeclaration; + +use crate::protocol::{LabeledError, PluginCall, PluginResponse}; +use crate::EncodingType; +use std::io::BufReader; +use std::path::{Path, PathBuf}; +use std::process::{Command as CommandSys, Stdio}; + +use nu_protocol::ShellError; +use nu_protocol::{Signature, Value}; + +use super::EvaluatedCall; + +const OUTPUT_BUFFER_SIZE: usize = 8192; + +pub trait PluginEncoder: Clone { + fn encode_call( + &self, + plugin_call: &PluginCall, + writer: &mut impl std::io::Write, + ) -> Result<(), ShellError>; + + fn decode_call(&self, reader: &mut impl std::io::BufRead) -> Result; + + fn encode_response( + &self, + plugin_response: &PluginResponse, + writer: &mut impl std::io::Write, + ) -> Result<(), ShellError>; + + fn decode_response( + &self, + reader: &mut impl std::io::BufRead, + ) -> Result; +} + +fn create_command(path: &Path, shell: &Option) -> CommandSys { + let mut process = match (path.extension(), shell) { + (_, Some(shell)) => { + let mut process = std::process::Command::new(shell); + process.arg(path); + + process + } + (Some(extension), None) => { + let (shell, separator) = match extension.to_str() { + Some("cmd") | Some("bat") => (Some("cmd"), Some("/c")), + Some("sh") => (Some("sh"), Some("-c")), + Some("py") => (Some("python"), None), + _ => (None, None), + }; + + match (shell, separator) { + (Some(shell), Some(separator)) => { + let mut process = std::process::Command::new(shell); + process.arg(separator); + process.arg(path); + + process + } + (Some(shell), None) => { + let mut process = std::process::Command::new(shell); + process.arg(path); + + process + } + _ => std::process::Command::new(path), + } + } + (None, None) => std::process::Command::new(path), + }; + + // Both stdout and stdin are piped so we can receive information from the plugin + process.stdout(Stdio::piped()).stdin(Stdio::piped()); + + process +} + +pub fn get_signature( + path: &Path, + encoding: &EncodingType, + shell: &Option, +) -> Result, ShellError> { + let mut plugin_cmd = create_command(path, shell); + + let mut child = plugin_cmd.spawn().map_err(|err| { + ShellError::PluginFailedToLoad(format!("Error spawning child process: {}", err)) + })?; + + // Create message to plugin to indicate that signature is required and + // send call to plugin asking for signature + if let Some(mut stdin_writer) = child.stdin.take() { + let encoding_clone = encoding.clone(); + std::thread::spawn(move || { + encoding_clone.encode_call(&PluginCall::Signature, &mut stdin_writer) + }); + } + + // deserialize response from plugin to extract the signature + let signatures = if let Some(stdout_reader) = &mut child.stdout { + let reader = stdout_reader; + let mut buf_read = BufReader::with_capacity(OUTPUT_BUFFER_SIZE, reader); + let response = encoding.decode_response(&mut buf_read)?; + + match response { + PluginResponse::Signature(sign) => Ok(sign), + PluginResponse::Error(err) => Err(err.into()), + _ => Err(ShellError::PluginFailedToLoad( + "Plugin missing signature".into(), + )), + } + } else { + Err(ShellError::PluginFailedToLoad( + "Plugin missing stdout reader".into(), + )) + }?; + + match child.wait() { + Ok(_) => Ok(signatures), + Err(err) => Err(ShellError::PluginFailedToLoad(format!("{}", err))), + } +} + +// The next trait and functions are part of the plugin that is being created +// The `Plugin` trait defines the API which plugins use to "hook" into nushell. +pub trait Plugin { + fn signature(&self) -> Vec; + fn run( + &mut self, + name: &str, + call: &EvaluatedCall, + input: &Value, + ) -> Result; +} + +// Function used in the plugin definition for the communication protocol between +// nushell and the external plugin. +// When creating a new plugin you have to use this function as the main +// entry point for the plugin, e.g. +// +// fn main() { +// serve_plugin(plugin) +// } +// +// where plugin is your struct that implements the Plugin trait +// +// Note. When defining a plugin in other language but Rust, you will have to compile +// the plugin.capnp schema to create the object definitions that will be returned from +// the plugin. +// The object that is expected to be received by nushell is the PluginResponse struct. +// That should be encoded correctly and sent to StdOut for nushell to decode and +// and present its result +pub fn serve_plugin(plugin: &mut impl Plugin, encoder: impl PluginEncoder) { + let mut stdin_buf = BufReader::with_capacity(OUTPUT_BUFFER_SIZE, std::io::stdin()); + let plugin_call = encoder.decode_call(&mut stdin_buf); + + match plugin_call { + Err(err) => { + let response = PluginResponse::Error(err.into()); + encoder + .encode_response(&response, &mut std::io::stdout()) + .expect("Error encoding response"); + } + Ok(plugin_call) => { + match plugin_call { + // Sending the signature back to nushell to create the declaration definition + PluginCall::Signature => { + let response = PluginResponse::Signature(plugin.signature()); + encoder + .encode_response(&response, &mut std::io::stdout()) + .expect("Error encoding response"); + } + PluginCall::CallInfo(call_info) => { + let value = plugin.run(&call_info.name, &call_info.call, &call_info.input); + + let response = match value { + Ok(value) => PluginResponse::Value(Box::new(value)), + Err(err) => PluginResponse::Error(err), + }; + encoder + .encode_response(&response, &mut std::io::stdout()) + .expect("Error encoding response"); + } + } + } + } +} diff --git a/crates/nu-plugin/src/plugin_capnp.rs b/crates/nu-plugin/src/plugin_capnp.rs new file mode 100644 index 0000000000..15f4b51cdf --- /dev/null +++ b/crates/nu-plugin/src/plugin_capnp.rs @@ -0,0 +1,4214 @@ +// @generated by the capnpc-rust plugin to the Cap'n Proto schema compiler. +// DO NOT EDIT. +// source: crates/nu-plugin/src/serializers/capnp/schema/plugin.capnp + +pub mod err { + /* T */ + pub use self::Which::{Err, Ok}; + + #[derive(Copy, Clone)] + pub struct Owned { + _phantom: ::core::marker::PhantomData, + } + impl<'a, T> ::capnp::traits::Owned<'a> for Owned + where + T: for<'c> ::capnp::traits::Owned<'c>, + { + type Reader = Reader<'a, T>; + type Builder = Builder<'a, T>; + } + impl<'a, T> ::capnp::traits::OwnedStruct<'a> for Owned + where + T: for<'c> ::capnp::traits::Owned<'c>, + { + type Reader = Reader<'a, T>; + type Builder = Builder<'a, T>; + } + impl ::capnp::traits::Pipelined for Owned + where + T: for<'c> ::capnp::traits::Owned<'c>, + { + type Pipeline = Pipeline; + } + + #[derive(Clone, Copy)] + pub struct Reader<'a, T> + where + T: for<'c> ::capnp::traits::Owned<'c>, + { + reader: ::capnp::private::layout::StructReader<'a>, + _phantom: ::core::marker::PhantomData, + } + + impl<'a, T> ::capnp::traits::HasTypeId for Reader<'a, T> + where + T: for<'c> ::capnp::traits::Owned<'c>, + { + #[inline] + fn type_id() -> u64 { + _private::TYPE_ID + } + } + impl<'a, T> ::capnp::traits::FromStructReader<'a> for Reader<'a, T> + where + T: for<'c> ::capnp::traits::Owned<'c>, + { + fn new(reader: ::capnp::private::layout::StructReader<'a>) -> Reader<'a, T> { + Reader { + reader, + _phantom: ::core::marker::PhantomData, + } + } + } + + impl<'a, T> ::capnp::traits::FromPointerReader<'a> for Reader<'a, T> + where + T: for<'c> ::capnp::traits::Owned<'c>, + { + fn get_from_pointer( + reader: &::capnp::private::layout::PointerReader<'a>, + default: ::core::option::Option<&'a [capnp::Word]>, + ) -> ::capnp::Result> { + ::core::result::Result::Ok(::capnp::traits::FromStructReader::new( + reader.get_struct(default)?, + )) + } + } + + impl<'a, T> ::capnp::traits::IntoInternalStructReader<'a> for Reader<'a, T> + where + T: for<'c> ::capnp::traits::Owned<'c>, + { + fn into_internal_struct_reader(self) -> ::capnp::private::layout::StructReader<'a> { + self.reader + } + } + + impl<'a, T> ::capnp::traits::Imbue<'a> for Reader<'a, T> + where + T: for<'c> ::capnp::traits::Owned<'c>, + { + fn imbue(&mut self, cap_table: &'a ::capnp::private::layout::CapTable) { + self.reader + .imbue(::capnp::private::layout::CapTableReader::Plain(cap_table)) + } + } + + impl<'a, T> Reader<'a, T> + where + T: for<'c> ::capnp::traits::Owned<'c>, + { + pub fn reborrow(&self) -> Reader<'_, T> { + Reader { ..*self } + } + + pub fn total_size(&self) -> ::capnp::Result<::capnp::MessageSize> { + self.reader.total_size() + } + pub fn has_err(&self) -> bool { + if self.reader.get_data_field::(0) != 0 { + return false; + } + !self.reader.get_pointer_field(0).is_null() + } + pub fn has_ok(&self) -> bool { + if self.reader.get_data_field::(0) != 1 { + return false; + } + !self.reader.get_pointer_field(0).is_null() + } + #[inline] + pub fn which(self) -> ::core::result::Result, ::capnp::NotInSchema> { + match self.reader.get_data_field::(0) { + 0 => ::core::result::Result::Ok(Err( + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(0), + ::core::option::Option::None, + ), + )), + 1 => ::core::result::Result::Ok(Ok( + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(0), + ::core::option::Option::None, + ), + )), + x => ::core::result::Result::Err(::capnp::NotInSchema(x)), + } + } + } + + pub struct Builder<'a, T> + where + T: for<'c> ::capnp::traits::Owned<'c>, + { + builder: ::capnp::private::layout::StructBuilder<'a>, + _phantom: ::core::marker::PhantomData, + } + impl<'a, T> ::capnp::traits::HasStructSize for Builder<'a, T> + where + T: for<'c> ::capnp::traits::Owned<'c>, + { + #[inline] + fn struct_size() -> ::capnp::private::layout::StructSize { + _private::STRUCT_SIZE + } + } + impl<'a, T> ::capnp::traits::HasTypeId for Builder<'a, T> + where + T: for<'c> ::capnp::traits::Owned<'c>, + { + #[inline] + fn type_id() -> u64 { + _private::TYPE_ID + } + } + impl<'a, T> ::capnp::traits::FromStructBuilder<'a> for Builder<'a, T> + where + T: for<'c> ::capnp::traits::Owned<'c>, + { + fn new(builder: ::capnp::private::layout::StructBuilder<'a>) -> Builder<'a, T> { + Builder { + builder, + _phantom: ::core::marker::PhantomData, + } + } + } + + impl<'a, T> ::capnp::traits::ImbueMut<'a> for Builder<'a, T> + where + T: for<'c> ::capnp::traits::Owned<'c>, + { + fn imbue_mut(&mut self, cap_table: &'a mut ::capnp::private::layout::CapTable) { + self.builder + .imbue(::capnp::private::layout::CapTableBuilder::Plain(cap_table)) + } + } + + impl<'a, T> ::capnp::traits::FromPointerBuilder<'a> for Builder<'a, T> + where + T: for<'c> ::capnp::traits::Owned<'c>, + { + fn init_pointer( + builder: ::capnp::private::layout::PointerBuilder<'a>, + _size: u32, + ) -> Builder<'a, T> { + ::capnp::traits::FromStructBuilder::new(builder.init_struct(_private::STRUCT_SIZE)) + } + fn get_from_pointer( + builder: ::capnp::private::layout::PointerBuilder<'a>, + default: ::core::option::Option<&'a [capnp::Word]>, + ) -> ::capnp::Result> { + ::core::result::Result::Ok(::capnp::traits::FromStructBuilder::new( + builder.get_struct(_private::STRUCT_SIZE, default)?, + )) + } + } + + impl<'a, T> ::capnp::traits::SetPointerBuilder for Reader<'a, T> + where + T: for<'c> ::capnp::traits::Owned<'c>, + { + fn set_pointer_builder<'b>( + pointer: ::capnp::private::layout::PointerBuilder<'b>, + value: Reader<'a, T>, + canonicalize: bool, + ) -> ::capnp::Result<()> { + pointer.set_struct(&value.reader, canonicalize) + } + } + + impl<'a, T> Builder<'a, T> + where + T: for<'c> ::capnp::traits::Owned<'c>, + { + pub fn into_reader(self) -> Reader<'a, T> { + ::capnp::traits::FromStructReader::new(self.builder.into_reader()) + } + pub fn reborrow(&mut self) -> Builder<'_, T> { + Builder { ..*self } + } + pub fn reborrow_as_reader(&self) -> Reader<'_, T> { + ::capnp::traits::FromStructReader::new(self.builder.into_reader()) + } + + pub fn total_size(&self) -> ::capnp::Result<::capnp::MessageSize> { + self.builder.into_reader().total_size() + } + #[inline] + pub fn set_err(&mut self, value: ::capnp::text::Reader<'_>) { + self.builder.set_data_field::(0, 0); + self.builder.get_pointer_field(0).set_text(value); + } + #[inline] + pub fn init_err(self, size: u32) -> ::capnp::text::Builder<'a> { + self.builder.set_data_field::(0, 0); + self.builder.get_pointer_field(0).init_text(size) + } + pub fn has_err(&self) -> bool { + if self.builder.get_data_field::(0) != 0 { + return false; + } + !self.builder.get_pointer_field(0).is_null() + } + #[inline] + pub fn initn_ok(self, length: u32) -> >::Builder { + self.builder.set_data_field::(0, 1); + ::capnp::any_pointer::Builder::new(self.builder.get_pointer_field(0)).initn_as(length) + } + #[inline] + pub fn set_ok( + &mut self, + value: >::Reader, + ) -> ::capnp::Result<()> { + self.builder.set_data_field::(0, 1); + ::capnp::traits::SetPointerBuilder::set_pointer_builder( + self.builder.get_pointer_field(0), + value, + false, + ) + } + #[inline] + pub fn init_ok(self) -> >::Builder { + self.builder.set_data_field::(0, 1); + ::capnp::any_pointer::Builder::new(self.builder.get_pointer_field(0)).init_as() + } + pub fn has_ok(&self) -> bool { + if self.builder.get_data_field::(0) != 1 { + return false; + } + !self.builder.get_pointer_field(0).is_null() + } + #[inline] + pub fn which(self) -> ::core::result::Result, ::capnp::NotInSchema> { + match self.builder.get_data_field::(0) { + 0 => ::core::result::Result::Ok(Err( + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(0), + ::core::option::Option::None, + ), + )), + 1 => ::core::result::Result::Ok(Ok( + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(0), + ::core::option::Option::None, + ), + )), + x => ::core::result::Result::Err(::capnp::NotInSchema(x)), + } + } + } + + pub struct Pipeline { + _typeless: ::capnp::any_pointer::Pipeline, + _phantom: ::core::marker::PhantomData, + } + impl ::capnp::capability::FromTypelessPipeline for Pipeline { + fn new(typeless: ::capnp::any_pointer::Pipeline) -> Pipeline { + Pipeline { + _typeless: typeless, + _phantom: ::core::marker::PhantomData, + } + } + } + impl Pipeline + where + T: ::capnp::traits::Pipelined, + ::Pipeline: ::capnp::capability::FromTypelessPipeline, + { + } + mod _private { + use capnp::private::layout; + pub const STRUCT_SIZE: layout::StructSize = layout::StructSize { + data: 1, + pointers: 1, + }; + pub const TYPE_ID: u64 = 0xaed6_5bd3_8214_33f8; + } + pub enum Which { + Err(A0), + Ok(A1), + } + pub type WhichReader<'a, T> = Which< + ::capnp::Result<::capnp::text::Reader<'a>>, + ::capnp::Result<>::Reader>, + >; + pub type WhichBuilder<'a, T> = Which< + ::capnp::Result<::capnp::text::Builder<'a>>, + ::capnp::Result<>::Builder>, + >; +} + +pub mod map { + /* Key,Value */ + #[derive(Copy, Clone)] + pub struct Owned { + _phantom: ::core::marker::PhantomData<(Key, Value)>, + } + impl<'a, Key, Value> ::capnp::traits::Owned<'a> for Owned + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + type Reader = Reader<'a, Key, Value>; + type Builder = Builder<'a, Key, Value>; + } + impl<'a, Key, Value> ::capnp::traits::OwnedStruct<'a> for Owned + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + type Reader = Reader<'a, Key, Value>; + type Builder = Builder<'a, Key, Value>; + } + impl ::capnp::traits::Pipelined for Owned + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + type Pipeline = Pipeline; + } + + #[derive(Clone, Copy)] + pub struct Reader<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + reader: ::capnp::private::layout::StructReader<'a>, + _phantom: ::core::marker::PhantomData<(Key, Value)>, + } + + impl<'a, Key, Value> ::capnp::traits::HasTypeId for Reader<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + #[inline] + fn type_id() -> u64 { + _private::TYPE_ID + } + } + impl<'a, Key, Value> ::capnp::traits::FromStructReader<'a> for Reader<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + fn new(reader: ::capnp::private::layout::StructReader<'a>) -> Reader<'a, Key, Value> { + Reader { + reader, + _phantom: ::core::marker::PhantomData, + } + } + } + + impl<'a, Key, Value> ::capnp::traits::FromPointerReader<'a> for Reader<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + fn get_from_pointer( + reader: &::capnp::private::layout::PointerReader<'a>, + default: ::core::option::Option<&'a [capnp::Word]>, + ) -> ::capnp::Result> { + ::core::result::Result::Ok(::capnp::traits::FromStructReader::new( + reader.get_struct(default)?, + )) + } + } + + impl<'a, Key, Value> ::capnp::traits::IntoInternalStructReader<'a> for Reader<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + fn into_internal_struct_reader(self) -> ::capnp::private::layout::StructReader<'a> { + self.reader + } + } + + impl<'a, Key, Value> ::capnp::traits::Imbue<'a> for Reader<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + fn imbue(&mut self, cap_table: &'a ::capnp::private::layout::CapTable) { + self.reader + .imbue(::capnp::private::layout::CapTableReader::Plain(cap_table)) + } + } + + impl<'a, Key, Value> Reader<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + pub fn reborrow(&self) -> Reader<'_, Key, Value> { + Reader { ..*self } + } + + pub fn total_size(&self) -> ::capnp::Result<::capnp::MessageSize> { + self.reader.total_size() + } + #[inline] + pub fn get_entries( + self, + ) -> ::capnp::Result< + ::capnp::struct_list::Reader<'a, crate::plugin_capnp::map::entry::Owned>, + > { + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(0), + ::core::option::Option::None, + ) + } + pub fn has_entries(&self) -> bool { + !self.reader.get_pointer_field(0).is_null() + } + } + + pub struct Builder<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + builder: ::capnp::private::layout::StructBuilder<'a>, + _phantom: ::core::marker::PhantomData<(Key, Value)>, + } + impl<'a, Key, Value> ::capnp::traits::HasStructSize for Builder<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + #[inline] + fn struct_size() -> ::capnp::private::layout::StructSize { + _private::STRUCT_SIZE + } + } + impl<'a, Key, Value> ::capnp::traits::HasTypeId for Builder<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + #[inline] + fn type_id() -> u64 { + _private::TYPE_ID + } + } + impl<'a, Key, Value> ::capnp::traits::FromStructBuilder<'a> for Builder<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + fn new(builder: ::capnp::private::layout::StructBuilder<'a>) -> Builder<'a, Key, Value> { + Builder { + builder, + _phantom: ::core::marker::PhantomData, + } + } + } + + impl<'a, Key, Value> ::capnp::traits::ImbueMut<'a> for Builder<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + fn imbue_mut(&mut self, cap_table: &'a mut ::capnp::private::layout::CapTable) { + self.builder + .imbue(::capnp::private::layout::CapTableBuilder::Plain(cap_table)) + } + } + + impl<'a, Key, Value> ::capnp::traits::FromPointerBuilder<'a> for Builder<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + fn init_pointer( + builder: ::capnp::private::layout::PointerBuilder<'a>, + _size: u32, + ) -> Builder<'a, Key, Value> { + ::capnp::traits::FromStructBuilder::new(builder.init_struct(_private::STRUCT_SIZE)) + } + fn get_from_pointer( + builder: ::capnp::private::layout::PointerBuilder<'a>, + default: ::core::option::Option<&'a [capnp::Word]>, + ) -> ::capnp::Result> { + ::core::result::Result::Ok(::capnp::traits::FromStructBuilder::new( + builder.get_struct(_private::STRUCT_SIZE, default)?, + )) + } + } + + impl<'a, Key, Value> ::capnp::traits::SetPointerBuilder for Reader<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + fn set_pointer_builder<'b>( + pointer: ::capnp::private::layout::PointerBuilder<'b>, + value: Reader<'a, Key, Value>, + canonicalize: bool, + ) -> ::capnp::Result<()> { + pointer.set_struct(&value.reader, canonicalize) + } + } + + impl<'a, Key, Value> Builder<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + pub fn into_reader(self) -> Reader<'a, Key, Value> { + ::capnp::traits::FromStructReader::new(self.builder.into_reader()) + } + pub fn reborrow(&mut self) -> Builder<'_, Key, Value> { + Builder { ..*self } + } + pub fn reborrow_as_reader(&self) -> Reader<'_, Key, Value> { + ::capnp::traits::FromStructReader::new(self.builder.into_reader()) + } + + pub fn total_size(&self) -> ::capnp::Result<::capnp::MessageSize> { + self.builder.into_reader().total_size() + } + #[inline] + pub fn get_entries( + self, + ) -> ::capnp::Result< + ::capnp::struct_list::Builder<'a, crate::plugin_capnp::map::entry::Owned>, + > { + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(0), + ::core::option::Option::None, + ) + } + #[inline] + pub fn set_entries( + &mut self, + value: ::capnp::struct_list::Reader< + 'a, + crate::plugin_capnp::map::entry::Owned, + >, + ) -> ::capnp::Result<()> { + ::capnp::traits::SetPointerBuilder::set_pointer_builder( + self.builder.get_pointer_field(0), + value, + false, + ) + } + #[inline] + pub fn init_entries( + self, + size: u32, + ) -> ::capnp::struct_list::Builder<'a, crate::plugin_capnp::map::entry::Owned> + { + ::capnp::traits::FromPointerBuilder::init_pointer( + self.builder.get_pointer_field(0), + size, + ) + } + pub fn has_entries(&self) -> bool { + !self.builder.get_pointer_field(0).is_null() + } + } + + pub struct Pipeline { + _typeless: ::capnp::any_pointer::Pipeline, + _phantom: ::core::marker::PhantomData<(Key, Value)>, + } + impl ::capnp::capability::FromTypelessPipeline for Pipeline { + fn new(typeless: ::capnp::any_pointer::Pipeline) -> Pipeline { + Pipeline { + _typeless: typeless, + _phantom: ::core::marker::PhantomData, + } + } + } + impl Pipeline + where + Key: ::capnp::traits::Pipelined, + ::Pipeline: ::capnp::capability::FromTypelessPipeline, + Value: ::capnp::traits::Pipelined, + ::Pipeline: ::capnp::capability::FromTypelessPipeline, + { + } + mod _private { + use capnp::private::layout; + pub const STRUCT_SIZE: layout::StructSize = layout::StructSize { + data: 0, + pointers: 1, + }; + pub const TYPE_ID: u64 = 0x9783_acc1_0be0_6fce; + } + + pub mod entry { + /* Key,Value */ + #[derive(Copy, Clone)] + pub struct Owned { + _phantom: ::core::marker::PhantomData<(Key, Value)>, + } + impl<'a, Key, Value> ::capnp::traits::Owned<'a> for Owned + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + type Reader = Reader<'a, Key, Value>; + type Builder = Builder<'a, Key, Value>; + } + impl<'a, Key, Value> ::capnp::traits::OwnedStruct<'a> for Owned + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + type Reader = Reader<'a, Key, Value>; + type Builder = Builder<'a, Key, Value>; + } + impl ::capnp::traits::Pipelined for Owned + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + type Pipeline = Pipeline; + } + + #[derive(Clone, Copy)] + pub struct Reader<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + reader: ::capnp::private::layout::StructReader<'a>, + _phantom: ::core::marker::PhantomData<(Key, Value)>, + } + + impl<'a, Key, Value> ::capnp::traits::HasTypeId for Reader<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + #[inline] + fn type_id() -> u64 { + _private::TYPE_ID + } + } + impl<'a, Key, Value> ::capnp::traits::FromStructReader<'a> for Reader<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + fn new(reader: ::capnp::private::layout::StructReader<'a>) -> Reader<'a, Key, Value> { + Reader { + reader, + _phantom: ::core::marker::PhantomData, + } + } + } + + impl<'a, Key, Value> ::capnp::traits::FromPointerReader<'a> for Reader<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + fn get_from_pointer( + reader: &::capnp::private::layout::PointerReader<'a>, + default: ::core::option::Option<&'a [capnp::Word]>, + ) -> ::capnp::Result> { + ::core::result::Result::Ok(::capnp::traits::FromStructReader::new( + reader.get_struct(default)?, + )) + } + } + + impl<'a, Key, Value> ::capnp::traits::IntoInternalStructReader<'a> for Reader<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + fn into_internal_struct_reader(self) -> ::capnp::private::layout::StructReader<'a> { + self.reader + } + } + + impl<'a, Key, Value> ::capnp::traits::Imbue<'a> for Reader<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + fn imbue(&mut self, cap_table: &'a ::capnp::private::layout::CapTable) { + self.reader + .imbue(::capnp::private::layout::CapTableReader::Plain(cap_table)) + } + } + + impl<'a, Key, Value> Reader<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + pub fn reborrow(&self) -> Reader<'_, Key, Value> { + Reader { ..*self } + } + + pub fn total_size(&self) -> ::capnp::Result<::capnp::MessageSize> { + self.reader.total_size() + } + #[inline] + pub fn get_key(self) -> ::capnp::Result<>::Reader> { + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(0), + ::core::option::Option::None, + ) + } + pub fn has_key(&self) -> bool { + !self.reader.get_pointer_field(0).is_null() + } + #[inline] + pub fn get_value( + self, + ) -> ::capnp::Result<>::Reader> { + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(1), + ::core::option::Option::None, + ) + } + pub fn has_value(&self) -> bool { + !self.reader.get_pointer_field(1).is_null() + } + } + + pub struct Builder<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + builder: ::capnp::private::layout::StructBuilder<'a>, + _phantom: ::core::marker::PhantomData<(Key, Value)>, + } + impl<'a, Key, Value> ::capnp::traits::HasStructSize for Builder<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + #[inline] + fn struct_size() -> ::capnp::private::layout::StructSize { + _private::STRUCT_SIZE + } + } + impl<'a, Key, Value> ::capnp::traits::HasTypeId for Builder<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + #[inline] + fn type_id() -> u64 { + _private::TYPE_ID + } + } + impl<'a, Key, Value> ::capnp::traits::FromStructBuilder<'a> for Builder<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + fn new( + builder: ::capnp::private::layout::StructBuilder<'a>, + ) -> Builder<'a, Key, Value> { + Builder { + builder, + _phantom: ::core::marker::PhantomData, + } + } + } + + impl<'a, Key, Value> ::capnp::traits::ImbueMut<'a> for Builder<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + fn imbue_mut(&mut self, cap_table: &'a mut ::capnp::private::layout::CapTable) { + self.builder + .imbue(::capnp::private::layout::CapTableBuilder::Plain(cap_table)) + } + } + + impl<'a, Key, Value> ::capnp::traits::FromPointerBuilder<'a> for Builder<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + fn init_pointer( + builder: ::capnp::private::layout::PointerBuilder<'a>, + _size: u32, + ) -> Builder<'a, Key, Value> { + ::capnp::traits::FromStructBuilder::new(builder.init_struct(_private::STRUCT_SIZE)) + } + fn get_from_pointer( + builder: ::capnp::private::layout::PointerBuilder<'a>, + default: ::core::option::Option<&'a [capnp::Word]>, + ) -> ::capnp::Result> { + ::core::result::Result::Ok(::capnp::traits::FromStructBuilder::new( + builder.get_struct(_private::STRUCT_SIZE, default)?, + )) + } + } + + impl<'a, Key, Value> ::capnp::traits::SetPointerBuilder for Reader<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + fn set_pointer_builder<'b>( + pointer: ::capnp::private::layout::PointerBuilder<'b>, + value: Reader<'a, Key, Value>, + canonicalize: bool, + ) -> ::capnp::Result<()> { + pointer.set_struct(&value.reader, canonicalize) + } + } + + impl<'a, Key, Value> Builder<'a, Key, Value> + where + Key: for<'c> ::capnp::traits::Owned<'c>, + Value: for<'c> ::capnp::traits::Owned<'c>, + { + pub fn into_reader(self) -> Reader<'a, Key, Value> { + ::capnp::traits::FromStructReader::new(self.builder.into_reader()) + } + pub fn reborrow(&mut self) -> Builder<'_, Key, Value> { + Builder { ..*self } + } + pub fn reborrow_as_reader(&self) -> Reader<'_, Key, Value> { + ::capnp::traits::FromStructReader::new(self.builder.into_reader()) + } + + pub fn total_size(&self) -> ::capnp::Result<::capnp::MessageSize> { + self.builder.into_reader().total_size() + } + #[inline] + pub fn get_key(self) -> ::capnp::Result<>::Builder> { + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(0), + ::core::option::Option::None, + ) + } + #[inline] + pub fn initn_key(self, length: u32) -> >::Builder { + ::capnp::any_pointer::Builder::new(self.builder.get_pointer_field(0)) + .initn_as(length) + } + #[inline] + pub fn set_key( + &mut self, + value: >::Reader, + ) -> ::capnp::Result<()> { + ::capnp::traits::SetPointerBuilder::set_pointer_builder( + self.builder.get_pointer_field(0), + value, + false, + ) + } + #[inline] + pub fn init_key(self) -> >::Builder { + ::capnp::any_pointer::Builder::new(self.builder.get_pointer_field(0)).init_as() + } + pub fn has_key(&self) -> bool { + !self.builder.get_pointer_field(0).is_null() + } + #[inline] + pub fn get_value( + self, + ) -> ::capnp::Result<>::Builder> { + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(1), + ::core::option::Option::None, + ) + } + #[inline] + pub fn initn_value( + self, + length: u32, + ) -> >::Builder { + ::capnp::any_pointer::Builder::new(self.builder.get_pointer_field(1)) + .initn_as(length) + } + #[inline] + pub fn set_value( + &mut self, + value: >::Reader, + ) -> ::capnp::Result<()> { + ::capnp::traits::SetPointerBuilder::set_pointer_builder( + self.builder.get_pointer_field(1), + value, + false, + ) + } + #[inline] + pub fn init_value(self) -> >::Builder { + ::capnp::any_pointer::Builder::new(self.builder.get_pointer_field(1)).init_as() + } + pub fn has_value(&self) -> bool { + !self.builder.get_pointer_field(1).is_null() + } + } + + pub struct Pipeline { + _typeless: ::capnp::any_pointer::Pipeline, + _phantom: ::core::marker::PhantomData<(Key, Value)>, + } + impl ::capnp::capability::FromTypelessPipeline for Pipeline { + fn new(typeless: ::capnp::any_pointer::Pipeline) -> Pipeline { + Pipeline { + _typeless: typeless, + _phantom: ::core::marker::PhantomData, + } + } + } + impl Pipeline + where + Key: ::capnp::traits::Pipelined, + ::Pipeline: + ::capnp::capability::FromTypelessPipeline, + Value: ::capnp::traits::Pipelined, + ::Pipeline: + ::capnp::capability::FromTypelessPipeline, + { + pub fn get_key(&self) -> ::Pipeline { + ::capnp::capability::FromTypelessPipeline::new(self._typeless.get_pointer_field(0)) + } + pub fn get_value(&self) -> ::Pipeline { + ::capnp::capability::FromTypelessPipeline::new(self._typeless.get_pointer_field(1)) + } + } + mod _private { + use capnp::private::layout; + pub const STRUCT_SIZE: layout::StructSize = layout::StructSize { + data: 0, + pointers: 2, + }; + pub const TYPE_ID: u64 = 0xde4c_62ad_0c9f_67c3; + } + } +} + +pub mod span { + #[derive(Copy, Clone)] + pub struct Owned(()); + impl<'a> ::capnp::traits::Owned<'a> for Owned { + type Reader = Reader<'a>; + type Builder = Builder<'a>; + } + impl<'a> ::capnp::traits::OwnedStruct<'a> for Owned { + type Reader = Reader<'a>; + type Builder = Builder<'a>; + } + impl ::capnp::traits::Pipelined for Owned { + type Pipeline = Pipeline; + } + + #[derive(Clone, Copy)] + pub struct Reader<'a> { + reader: ::capnp::private::layout::StructReader<'a>, + } + + impl<'a> ::capnp::traits::HasTypeId for Reader<'a> { + #[inline] + fn type_id() -> u64 { + _private::TYPE_ID + } + } + impl<'a> ::capnp::traits::FromStructReader<'a> for Reader<'a> { + fn new(reader: ::capnp::private::layout::StructReader<'a>) -> Reader<'a> { + Reader { reader } + } + } + + impl<'a> ::capnp::traits::FromPointerReader<'a> for Reader<'a> { + fn get_from_pointer( + reader: &::capnp::private::layout::PointerReader<'a>, + default: ::core::option::Option<&'a [capnp::Word]>, + ) -> ::capnp::Result> { + ::core::result::Result::Ok(::capnp::traits::FromStructReader::new( + reader.get_struct(default)?, + )) + } + } + + impl<'a> ::capnp::traits::IntoInternalStructReader<'a> for Reader<'a> { + fn into_internal_struct_reader(self) -> ::capnp::private::layout::StructReader<'a> { + self.reader + } + } + + impl<'a> ::capnp::traits::Imbue<'a> for Reader<'a> { + fn imbue(&mut self, cap_table: &'a ::capnp::private::layout::CapTable) { + self.reader + .imbue(::capnp::private::layout::CapTableReader::Plain(cap_table)) + } + } + + impl<'a> Reader<'a> { + pub fn reborrow(&self) -> Reader<'_> { + Reader { ..*self } + } + + pub fn total_size(&self) -> ::capnp::Result<::capnp::MessageSize> { + self.reader.total_size() + } + #[inline] + pub fn get_start(self) -> u64 { + self.reader.get_data_field::(0) + } + #[inline] + pub fn get_end(self) -> u64 { + self.reader.get_data_field::(1) + } + } + + pub struct Builder<'a> { + builder: ::capnp::private::layout::StructBuilder<'a>, + } + impl<'a> ::capnp::traits::HasStructSize for Builder<'a> { + #[inline] + fn struct_size() -> ::capnp::private::layout::StructSize { + _private::STRUCT_SIZE + } + } + impl<'a> ::capnp::traits::HasTypeId for Builder<'a> { + #[inline] + fn type_id() -> u64 { + _private::TYPE_ID + } + } + impl<'a> ::capnp::traits::FromStructBuilder<'a> for Builder<'a> { + fn new(builder: ::capnp::private::layout::StructBuilder<'a>) -> Builder<'a> { + Builder { builder } + } + } + + impl<'a> ::capnp::traits::ImbueMut<'a> for Builder<'a> { + fn imbue_mut(&mut self, cap_table: &'a mut ::capnp::private::layout::CapTable) { + self.builder + .imbue(::capnp::private::layout::CapTableBuilder::Plain(cap_table)) + } + } + + impl<'a> ::capnp::traits::FromPointerBuilder<'a> for Builder<'a> { + fn init_pointer( + builder: ::capnp::private::layout::PointerBuilder<'a>, + _size: u32, + ) -> Builder<'a> { + ::capnp::traits::FromStructBuilder::new(builder.init_struct(_private::STRUCT_SIZE)) + } + fn get_from_pointer( + builder: ::capnp::private::layout::PointerBuilder<'a>, + default: ::core::option::Option<&'a [capnp::Word]>, + ) -> ::capnp::Result> { + ::core::result::Result::Ok(::capnp::traits::FromStructBuilder::new( + builder.get_struct(_private::STRUCT_SIZE, default)?, + )) + } + } + + impl<'a> ::capnp::traits::SetPointerBuilder for Reader<'a> { + fn set_pointer_builder<'b>( + pointer: ::capnp::private::layout::PointerBuilder<'b>, + value: Reader<'a>, + canonicalize: bool, + ) -> ::capnp::Result<()> { + pointer.set_struct(&value.reader, canonicalize) + } + } + + impl<'a> Builder<'a> { + pub fn into_reader(self) -> Reader<'a> { + ::capnp::traits::FromStructReader::new(self.builder.into_reader()) + } + pub fn reborrow(&mut self) -> Builder<'_> { + Builder { ..*self } + } + pub fn reborrow_as_reader(&self) -> Reader<'_> { + ::capnp::traits::FromStructReader::new(self.builder.into_reader()) + } + + pub fn total_size(&self) -> ::capnp::Result<::capnp::MessageSize> { + self.builder.into_reader().total_size() + } + #[inline] + pub fn get_start(self) -> u64 { + self.builder.get_data_field::(0) + } + #[inline] + pub fn set_start(&mut self, value: u64) { + self.builder.set_data_field::(0, value); + } + #[inline] + pub fn get_end(self) -> u64 { + self.builder.get_data_field::(1) + } + #[inline] + pub fn set_end(&mut self, value: u64) { + self.builder.set_data_field::(1, value); + } + } + + pub struct Pipeline { + _typeless: ::capnp::any_pointer::Pipeline, + } + impl ::capnp::capability::FromTypelessPipeline for Pipeline { + fn new(typeless: ::capnp::any_pointer::Pipeline) -> Pipeline { + Pipeline { + _typeless: typeless, + } + } + } + impl Pipeline {} + mod _private { + use capnp::private::layout; + pub const STRUCT_SIZE: layout::StructSize = layout::StructSize { + data: 2, + pointers: 0, + }; + pub const TYPE_ID: u64 = 0xe8b6_78b5_d953_4593; + } +} + +pub mod value { + pub use self::Which::{Bool, Float, Int, List, Record, String, Void}; + + #[derive(Copy, Clone)] + pub struct Owned(()); + impl<'a> ::capnp::traits::Owned<'a> for Owned { + type Reader = Reader<'a>; + type Builder = Builder<'a>; + } + impl<'a> ::capnp::traits::OwnedStruct<'a> for Owned { + type Reader = Reader<'a>; + type Builder = Builder<'a>; + } + impl ::capnp::traits::Pipelined for Owned { + type Pipeline = Pipeline; + } + + #[derive(Clone, Copy)] + pub struct Reader<'a> { + reader: ::capnp::private::layout::StructReader<'a>, + } + + impl<'a> ::capnp::traits::HasTypeId for Reader<'a> { + #[inline] + fn type_id() -> u64 { + _private::TYPE_ID + } + } + impl<'a> ::capnp::traits::FromStructReader<'a> for Reader<'a> { + fn new(reader: ::capnp::private::layout::StructReader<'a>) -> Reader<'a> { + Reader { reader } + } + } + + impl<'a> ::capnp::traits::FromPointerReader<'a> for Reader<'a> { + fn get_from_pointer( + reader: &::capnp::private::layout::PointerReader<'a>, + default: ::core::option::Option<&'a [capnp::Word]>, + ) -> ::capnp::Result> { + ::core::result::Result::Ok(::capnp::traits::FromStructReader::new( + reader.get_struct(default)?, + )) + } + } + + impl<'a> ::capnp::traits::IntoInternalStructReader<'a> for Reader<'a> { + fn into_internal_struct_reader(self) -> ::capnp::private::layout::StructReader<'a> { + self.reader + } + } + + impl<'a> ::capnp::traits::Imbue<'a> for Reader<'a> { + fn imbue(&mut self, cap_table: &'a ::capnp::private::layout::CapTable) { + self.reader + .imbue(::capnp::private::layout::CapTableReader::Plain(cap_table)) + } + } + + impl<'a> Reader<'a> { + pub fn reborrow(&self) -> Reader<'_> { + Reader { ..*self } + } + + pub fn total_size(&self) -> ::capnp::Result<::capnp::MessageSize> { + self.reader.total_size() + } + #[inline] + pub fn get_span(self) -> ::capnp::Result> { + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(0), + ::core::option::Option::None, + ) + } + pub fn has_span(&self) -> bool { + !self.reader.get_pointer_field(0).is_null() + } + pub fn has_string(&self) -> bool { + if self.reader.get_data_field::(0) != 4 { + return false; + } + !self.reader.get_pointer_field(1).is_null() + } + pub fn has_list(&self) -> bool { + if self.reader.get_data_field::(0) != 5 { + return false; + } + !self.reader.get_pointer_field(1).is_null() + } + pub fn has_record(&self) -> bool { + if self.reader.get_data_field::(0) != 6 { + return false; + } + !self.reader.get_pointer_field(1).is_null() + } + #[inline] + pub fn which(self) -> ::core::result::Result, ::capnp::NotInSchema> { + match self.reader.get_data_field::(0) { + 0 => ::core::result::Result::Ok(Void(())), + 1 => ::core::result::Result::Ok(Bool(self.reader.get_bool_field(16))), + 2 => ::core::result::Result::Ok(Int(self.reader.get_data_field::(1))), + 3 => ::core::result::Result::Ok(Float(self.reader.get_data_field::(1))), + 4 => ::core::result::Result::Ok(String( + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(1), + ::core::option::Option::None, + ), + )), + 5 => ::core::result::Result::Ok(List( + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(1), + ::core::option::Option::None, + ), + )), + 6 => ::core::result::Result::Ok(Record( + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(1), + ::core::option::Option::None, + ), + )), + x => ::core::result::Result::Err(::capnp::NotInSchema(x)), + } + } + } + + pub struct Builder<'a> { + builder: ::capnp::private::layout::StructBuilder<'a>, + } + impl<'a> ::capnp::traits::HasStructSize for Builder<'a> { + #[inline] + fn struct_size() -> ::capnp::private::layout::StructSize { + _private::STRUCT_SIZE + } + } + impl<'a> ::capnp::traits::HasTypeId for Builder<'a> { + #[inline] + fn type_id() -> u64 { + _private::TYPE_ID + } + } + impl<'a> ::capnp::traits::FromStructBuilder<'a> for Builder<'a> { + fn new(builder: ::capnp::private::layout::StructBuilder<'a>) -> Builder<'a> { + Builder { builder } + } + } + + impl<'a> ::capnp::traits::ImbueMut<'a> for Builder<'a> { + fn imbue_mut(&mut self, cap_table: &'a mut ::capnp::private::layout::CapTable) { + self.builder + .imbue(::capnp::private::layout::CapTableBuilder::Plain(cap_table)) + } + } + + impl<'a> ::capnp::traits::FromPointerBuilder<'a> for Builder<'a> { + fn init_pointer( + builder: ::capnp::private::layout::PointerBuilder<'a>, + _size: u32, + ) -> Builder<'a> { + ::capnp::traits::FromStructBuilder::new(builder.init_struct(_private::STRUCT_SIZE)) + } + fn get_from_pointer( + builder: ::capnp::private::layout::PointerBuilder<'a>, + default: ::core::option::Option<&'a [capnp::Word]>, + ) -> ::capnp::Result> { + ::core::result::Result::Ok(::capnp::traits::FromStructBuilder::new( + builder.get_struct(_private::STRUCT_SIZE, default)?, + )) + } + } + + impl<'a> ::capnp::traits::SetPointerBuilder for Reader<'a> { + fn set_pointer_builder<'b>( + pointer: ::capnp::private::layout::PointerBuilder<'b>, + value: Reader<'a>, + canonicalize: bool, + ) -> ::capnp::Result<()> { + pointer.set_struct(&value.reader, canonicalize) + } + } + + impl<'a> Builder<'a> { + pub fn into_reader(self) -> Reader<'a> { + ::capnp::traits::FromStructReader::new(self.builder.into_reader()) + } + pub fn reborrow(&mut self) -> Builder<'_> { + Builder { ..*self } + } + pub fn reborrow_as_reader(&self) -> Reader<'_> { + ::capnp::traits::FromStructReader::new(self.builder.into_reader()) + } + + pub fn total_size(&self) -> ::capnp::Result<::capnp::MessageSize> { + self.builder.into_reader().total_size() + } + #[inline] + pub fn get_span(self) -> ::capnp::Result> { + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(0), + ::core::option::Option::None, + ) + } + #[inline] + pub fn set_span( + &mut self, + value: crate::plugin_capnp::span::Reader<'_>, + ) -> ::capnp::Result<()> { + ::capnp::traits::SetPointerBuilder::set_pointer_builder( + self.builder.get_pointer_field(0), + value, + false, + ) + } + #[inline] + pub fn init_span(self) -> crate::plugin_capnp::span::Builder<'a> { + ::capnp::traits::FromPointerBuilder::init_pointer(self.builder.get_pointer_field(0), 0) + } + pub fn has_span(&self) -> bool { + !self.builder.get_pointer_field(0).is_null() + } + #[inline] + pub fn set_void(&mut self, _value: ()) { + self.builder.set_data_field::(0, 0); + } + #[inline] + pub fn set_bool(&mut self, value: bool) { + self.builder.set_data_field::(0, 1); + self.builder.set_bool_field(16, value); + } + #[inline] + pub fn set_int(&mut self, value: i64) { + self.builder.set_data_field::(0, 2); + self.builder.set_data_field::(1, value); + } + #[inline] + pub fn set_float(&mut self, value: f64) { + self.builder.set_data_field::(0, 3); + self.builder.set_data_field::(1, value); + } + #[inline] + pub fn set_string(&mut self, value: ::capnp::text::Reader<'_>) { + self.builder.set_data_field::(0, 4); + self.builder.get_pointer_field(1).set_text(value); + } + #[inline] + pub fn init_string(self, size: u32) -> ::capnp::text::Builder<'a> { + self.builder.set_data_field::(0, 4); + self.builder.get_pointer_field(1).init_text(size) + } + pub fn has_string(&self) -> bool { + if self.builder.get_data_field::(0) != 4 { + return false; + } + !self.builder.get_pointer_field(1).is_null() + } + #[inline] + pub fn set_list( + &mut self, + value: ::capnp::struct_list::Reader<'a, crate::plugin_capnp::value::Owned>, + ) -> ::capnp::Result<()> { + self.builder.set_data_field::(0, 5); + ::capnp::traits::SetPointerBuilder::set_pointer_builder( + self.builder.get_pointer_field(1), + value, + false, + ) + } + #[inline] + pub fn init_list( + self, + size: u32, + ) -> ::capnp::struct_list::Builder<'a, crate::plugin_capnp::value::Owned> { + self.builder.set_data_field::(0, 5); + ::capnp::traits::FromPointerBuilder::init_pointer( + self.builder.get_pointer_field(1), + size, + ) + } + pub fn has_list(&self) -> bool { + if self.builder.get_data_field::(0) != 5 { + return false; + } + !self.builder.get_pointer_field(1).is_null() + } + #[inline] + pub fn set_record( + &mut self, + value: crate::plugin_capnp::record::Reader<'_>, + ) -> ::capnp::Result<()> { + self.builder.set_data_field::(0, 6); + ::capnp::traits::SetPointerBuilder::set_pointer_builder( + self.builder.get_pointer_field(1), + value, + false, + ) + } + #[inline] + pub fn init_record(self) -> crate::plugin_capnp::record::Builder<'a> { + self.builder.set_data_field::(0, 6); + ::capnp::traits::FromPointerBuilder::init_pointer(self.builder.get_pointer_field(1), 0) + } + pub fn has_record(&self) -> bool { + if self.builder.get_data_field::(0) != 6 { + return false; + } + !self.builder.get_pointer_field(1).is_null() + } + #[inline] + pub fn which(self) -> ::core::result::Result, ::capnp::NotInSchema> { + match self.builder.get_data_field::(0) { + 0 => ::core::result::Result::Ok(Void(())), + 1 => ::core::result::Result::Ok(Bool(self.builder.get_bool_field(16))), + 2 => ::core::result::Result::Ok(Int(self.builder.get_data_field::(1))), + 3 => ::core::result::Result::Ok(Float(self.builder.get_data_field::(1))), + 4 => ::core::result::Result::Ok(String( + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(1), + ::core::option::Option::None, + ), + )), + 5 => ::core::result::Result::Ok(List( + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(1), + ::core::option::Option::None, + ), + )), + 6 => ::core::result::Result::Ok(Record( + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(1), + ::core::option::Option::None, + ), + )), + x => ::core::result::Result::Err(::capnp::NotInSchema(x)), + } + } + } + + pub struct Pipeline { + _typeless: ::capnp::any_pointer::Pipeline, + } + impl ::capnp::capability::FromTypelessPipeline for Pipeline { + fn new(typeless: ::capnp::any_pointer::Pipeline) -> Pipeline { + Pipeline { + _typeless: typeless, + } + } + } + impl Pipeline { + pub fn get_span(&self) -> crate::plugin_capnp::span::Pipeline { + ::capnp::capability::FromTypelessPipeline::new(self._typeless.get_pointer_field(0)) + } + } + mod _private { + use capnp::private::layout; + pub const STRUCT_SIZE: layout::StructSize = layout::StructSize { + data: 2, + pointers: 2, + }; + pub const TYPE_ID: u64 = 0x92a0_59fb_5627_86a8; + } + pub enum Which { + Void(()), + Bool(bool), + Int(i64), + Float(f64), + String(A0), + List(A1), + Record(A2), + } + pub type WhichReader<'a> = Which< + ::capnp::Result<::capnp::text::Reader<'a>>, + ::capnp::Result<::capnp::struct_list::Reader<'a, crate::plugin_capnp::value::Owned>>, + ::capnp::Result>, + >; + pub type WhichBuilder<'a> = Which< + ::capnp::Result<::capnp::text::Builder<'a>>, + ::capnp::Result<::capnp::struct_list::Builder<'a, crate::plugin_capnp::value::Owned>>, + ::capnp::Result>, + >; +} + +pub mod record { + #[derive(Copy, Clone)] + pub struct Owned(()); + impl<'a> ::capnp::traits::Owned<'a> for Owned { + type Reader = Reader<'a>; + type Builder = Builder<'a>; + } + impl<'a> ::capnp::traits::OwnedStruct<'a> for Owned { + type Reader = Reader<'a>; + type Builder = Builder<'a>; + } + impl ::capnp::traits::Pipelined for Owned { + type Pipeline = Pipeline; + } + + #[derive(Clone, Copy)] + pub struct Reader<'a> { + reader: ::capnp::private::layout::StructReader<'a>, + } + + impl<'a> ::capnp::traits::HasTypeId for Reader<'a> { + #[inline] + fn type_id() -> u64 { + _private::TYPE_ID + } + } + impl<'a> ::capnp::traits::FromStructReader<'a> for Reader<'a> { + fn new(reader: ::capnp::private::layout::StructReader<'a>) -> Reader<'a> { + Reader { reader } + } + } + + impl<'a> ::capnp::traits::FromPointerReader<'a> for Reader<'a> { + fn get_from_pointer( + reader: &::capnp::private::layout::PointerReader<'a>, + default: ::core::option::Option<&'a [capnp::Word]>, + ) -> ::capnp::Result> { + ::core::result::Result::Ok(::capnp::traits::FromStructReader::new( + reader.get_struct(default)?, + )) + } + } + + impl<'a> ::capnp::traits::IntoInternalStructReader<'a> for Reader<'a> { + fn into_internal_struct_reader(self) -> ::capnp::private::layout::StructReader<'a> { + self.reader + } + } + + impl<'a> ::capnp::traits::Imbue<'a> for Reader<'a> { + fn imbue(&mut self, cap_table: &'a ::capnp::private::layout::CapTable) { + self.reader + .imbue(::capnp::private::layout::CapTableReader::Plain(cap_table)) + } + } + + impl<'a> Reader<'a> { + pub fn reborrow(&self) -> Reader<'_> { + Reader { ..*self } + } + + pub fn total_size(&self) -> ::capnp::Result<::capnp::MessageSize> { + self.reader.total_size() + } + #[inline] + pub fn get_cols(self) -> ::capnp::Result<::capnp::text_list::Reader<'a>> { + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(0), + ::core::option::Option::None, + ) + } + pub fn has_cols(&self) -> bool { + !self.reader.get_pointer_field(0).is_null() + } + #[inline] + pub fn get_vals( + self, + ) -> ::capnp::Result<::capnp::struct_list::Reader<'a, crate::plugin_capnp::value::Owned>> + { + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(1), + ::core::option::Option::None, + ) + } + pub fn has_vals(&self) -> bool { + !self.reader.get_pointer_field(1).is_null() + } + } + + pub struct Builder<'a> { + builder: ::capnp::private::layout::StructBuilder<'a>, + } + impl<'a> ::capnp::traits::HasStructSize for Builder<'a> { + #[inline] + fn struct_size() -> ::capnp::private::layout::StructSize { + _private::STRUCT_SIZE + } + } + impl<'a> ::capnp::traits::HasTypeId for Builder<'a> { + #[inline] + fn type_id() -> u64 { + _private::TYPE_ID + } + } + impl<'a> ::capnp::traits::FromStructBuilder<'a> for Builder<'a> { + fn new(builder: ::capnp::private::layout::StructBuilder<'a>) -> Builder<'a> { + Builder { builder } + } + } + + impl<'a> ::capnp::traits::ImbueMut<'a> for Builder<'a> { + fn imbue_mut(&mut self, cap_table: &'a mut ::capnp::private::layout::CapTable) { + self.builder + .imbue(::capnp::private::layout::CapTableBuilder::Plain(cap_table)) + } + } + + impl<'a> ::capnp::traits::FromPointerBuilder<'a> for Builder<'a> { + fn init_pointer( + builder: ::capnp::private::layout::PointerBuilder<'a>, + _size: u32, + ) -> Builder<'a> { + ::capnp::traits::FromStructBuilder::new(builder.init_struct(_private::STRUCT_SIZE)) + } + fn get_from_pointer( + builder: ::capnp::private::layout::PointerBuilder<'a>, + default: ::core::option::Option<&'a [capnp::Word]>, + ) -> ::capnp::Result> { + ::core::result::Result::Ok(::capnp::traits::FromStructBuilder::new( + builder.get_struct(_private::STRUCT_SIZE, default)?, + )) + } + } + + impl<'a> ::capnp::traits::SetPointerBuilder for Reader<'a> { + fn set_pointer_builder<'b>( + pointer: ::capnp::private::layout::PointerBuilder<'b>, + value: Reader<'a>, + canonicalize: bool, + ) -> ::capnp::Result<()> { + pointer.set_struct(&value.reader, canonicalize) + } + } + + impl<'a> Builder<'a> { + pub fn into_reader(self) -> Reader<'a> { + ::capnp::traits::FromStructReader::new(self.builder.into_reader()) + } + pub fn reborrow(&mut self) -> Builder<'_> { + Builder { ..*self } + } + pub fn reborrow_as_reader(&self) -> Reader<'_> { + ::capnp::traits::FromStructReader::new(self.builder.into_reader()) + } + + pub fn total_size(&self) -> ::capnp::Result<::capnp::MessageSize> { + self.builder.into_reader().total_size() + } + #[inline] + pub fn get_cols(self) -> ::capnp::Result<::capnp::text_list::Builder<'a>> { + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(0), + ::core::option::Option::None, + ) + } + #[inline] + pub fn set_cols(&mut self, value: ::capnp::text_list::Reader<'a>) -> ::capnp::Result<()> { + ::capnp::traits::SetPointerBuilder::set_pointer_builder( + self.builder.get_pointer_field(0), + value, + false, + ) + } + #[inline] + pub fn init_cols(self, size: u32) -> ::capnp::text_list::Builder<'a> { + ::capnp::traits::FromPointerBuilder::init_pointer( + self.builder.get_pointer_field(0), + size, + ) + } + pub fn has_cols(&self) -> bool { + !self.builder.get_pointer_field(0).is_null() + } + #[inline] + pub fn get_vals( + self, + ) -> ::capnp::Result<::capnp::struct_list::Builder<'a, crate::plugin_capnp::value::Owned>> + { + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(1), + ::core::option::Option::None, + ) + } + #[inline] + pub fn set_vals( + &mut self, + value: ::capnp::struct_list::Reader<'a, crate::plugin_capnp::value::Owned>, + ) -> ::capnp::Result<()> { + ::capnp::traits::SetPointerBuilder::set_pointer_builder( + self.builder.get_pointer_field(1), + value, + false, + ) + } + #[inline] + pub fn init_vals( + self, + size: u32, + ) -> ::capnp::struct_list::Builder<'a, crate::plugin_capnp::value::Owned> { + ::capnp::traits::FromPointerBuilder::init_pointer( + self.builder.get_pointer_field(1), + size, + ) + } + pub fn has_vals(&self) -> bool { + !self.builder.get_pointer_field(1).is_null() + } + } + + pub struct Pipeline { + _typeless: ::capnp::any_pointer::Pipeline, + } + impl ::capnp::capability::FromTypelessPipeline for Pipeline { + fn new(typeless: ::capnp::any_pointer::Pipeline) -> Pipeline { + Pipeline { + _typeless: typeless, + } + } + } + impl Pipeline {} + mod _private { + use capnp::private::layout; + pub const STRUCT_SIZE: layout::StructSize = layout::StructSize { + data: 0, + pointers: 2, + }; + pub const TYPE_ID: u64 = 0xd435_7cbb_f79b_12fb; + } +} + +pub mod signature { + #[derive(Copy, Clone)] + pub struct Owned(()); + impl<'a> ::capnp::traits::Owned<'a> for Owned { + type Reader = Reader<'a>; + type Builder = Builder<'a>; + } + impl<'a> ::capnp::traits::OwnedStruct<'a> for Owned { + type Reader = Reader<'a>; + type Builder = Builder<'a>; + } + impl ::capnp::traits::Pipelined for Owned { + type Pipeline = Pipeline; + } + + #[derive(Clone, Copy)] + pub struct Reader<'a> { + reader: ::capnp::private::layout::StructReader<'a>, + } + + impl<'a> ::capnp::traits::HasTypeId for Reader<'a> { + #[inline] + fn type_id() -> u64 { + _private::TYPE_ID + } + } + impl<'a> ::capnp::traits::FromStructReader<'a> for Reader<'a> { + fn new(reader: ::capnp::private::layout::StructReader<'a>) -> Reader<'a> { + Reader { reader } + } + } + + impl<'a> ::capnp::traits::FromPointerReader<'a> for Reader<'a> { + fn get_from_pointer( + reader: &::capnp::private::layout::PointerReader<'a>, + default: ::core::option::Option<&'a [capnp::Word]>, + ) -> ::capnp::Result> { + ::core::result::Result::Ok(::capnp::traits::FromStructReader::new( + reader.get_struct(default)?, + )) + } + } + + impl<'a> ::capnp::traits::IntoInternalStructReader<'a> for Reader<'a> { + fn into_internal_struct_reader(self) -> ::capnp::private::layout::StructReader<'a> { + self.reader + } + } + + impl<'a> ::capnp::traits::Imbue<'a> for Reader<'a> { + fn imbue(&mut self, cap_table: &'a ::capnp::private::layout::CapTable) { + self.reader + .imbue(::capnp::private::layout::CapTableReader::Plain(cap_table)) + } + } + + impl<'a> Reader<'a> { + pub fn reborrow(&self) -> Reader<'_> { + Reader { ..*self } + } + + pub fn total_size(&self) -> ::capnp::Result<::capnp::MessageSize> { + self.reader.total_size() + } + #[inline] + pub fn get_name(self) -> ::capnp::Result<::capnp::text::Reader<'a>> { + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(0), + ::core::option::Option::None, + ) + } + pub fn has_name(&self) -> bool { + !self.reader.get_pointer_field(0).is_null() + } + #[inline] + pub fn get_usage(self) -> ::capnp::Result<::capnp::text::Reader<'a>> { + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(1), + ::core::option::Option::None, + ) + } + pub fn has_usage(&self) -> bool { + !self.reader.get_pointer_field(1).is_null() + } + #[inline] + pub fn get_extra_usage(self) -> ::capnp::Result<::capnp::text::Reader<'a>> { + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(2), + ::core::option::Option::None, + ) + } + pub fn has_extra_usage(&self) -> bool { + !self.reader.get_pointer_field(2).is_null() + } + #[inline] + pub fn get_required_positional( + self, + ) -> ::capnp::Result<::capnp::struct_list::Reader<'a, crate::plugin_capnp::argument::Owned>> + { + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(3), + ::core::option::Option::None, + ) + } + pub fn has_required_positional(&self) -> bool { + !self.reader.get_pointer_field(3).is_null() + } + #[inline] + pub fn get_optional_positional( + self, + ) -> ::capnp::Result<::capnp::struct_list::Reader<'a, crate::plugin_capnp::argument::Owned>> + { + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(4), + ::core::option::Option::None, + ) + } + pub fn has_optional_positional(&self) -> bool { + !self.reader.get_pointer_field(4).is_null() + } + #[inline] + pub fn get_rest(self) -> ::capnp::Result> { + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(5), + ::core::option::Option::None, + ) + } + pub fn has_rest(&self) -> bool { + !self.reader.get_pointer_field(5).is_null() + } + #[inline] + pub fn get_named( + self, + ) -> ::capnp::Result<::capnp::struct_list::Reader<'a, crate::plugin_capnp::flag::Owned>> + { + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(6), + ::core::option::Option::None, + ) + } + pub fn has_named(&self) -> bool { + !self.reader.get_pointer_field(6).is_null() + } + #[inline] + pub fn get_is_filter(self) -> bool { + self.reader.get_bool_field(0) + } + #[inline] + pub fn get_category( + self, + ) -> ::core::result::Result { + ::capnp::traits::FromU16::from_u16(self.reader.get_data_field::(1)) + } + } + + pub struct Builder<'a> { + builder: ::capnp::private::layout::StructBuilder<'a>, + } + impl<'a> ::capnp::traits::HasStructSize for Builder<'a> { + #[inline] + fn struct_size() -> ::capnp::private::layout::StructSize { + _private::STRUCT_SIZE + } + } + impl<'a> ::capnp::traits::HasTypeId for Builder<'a> { + #[inline] + fn type_id() -> u64 { + _private::TYPE_ID + } + } + impl<'a> ::capnp::traits::FromStructBuilder<'a> for Builder<'a> { + fn new(builder: ::capnp::private::layout::StructBuilder<'a>) -> Builder<'a> { + Builder { builder } + } + } + + impl<'a> ::capnp::traits::ImbueMut<'a> for Builder<'a> { + fn imbue_mut(&mut self, cap_table: &'a mut ::capnp::private::layout::CapTable) { + self.builder + .imbue(::capnp::private::layout::CapTableBuilder::Plain(cap_table)) + } + } + + impl<'a> ::capnp::traits::FromPointerBuilder<'a> for Builder<'a> { + fn init_pointer( + builder: ::capnp::private::layout::PointerBuilder<'a>, + _size: u32, + ) -> Builder<'a> { + ::capnp::traits::FromStructBuilder::new(builder.init_struct(_private::STRUCT_SIZE)) + } + fn get_from_pointer( + builder: ::capnp::private::layout::PointerBuilder<'a>, + default: ::core::option::Option<&'a [capnp::Word]>, + ) -> ::capnp::Result> { + ::core::result::Result::Ok(::capnp::traits::FromStructBuilder::new( + builder.get_struct(_private::STRUCT_SIZE, default)?, + )) + } + } + + impl<'a> ::capnp::traits::SetPointerBuilder for Reader<'a> { + fn set_pointer_builder<'b>( + pointer: ::capnp::private::layout::PointerBuilder<'b>, + value: Reader<'a>, + canonicalize: bool, + ) -> ::capnp::Result<()> { + pointer.set_struct(&value.reader, canonicalize) + } + } + + impl<'a> Builder<'a> { + pub fn into_reader(self) -> Reader<'a> { + ::capnp::traits::FromStructReader::new(self.builder.into_reader()) + } + pub fn reborrow(&mut self) -> Builder<'_> { + Builder { ..*self } + } + pub fn reborrow_as_reader(&self) -> Reader<'_> { + ::capnp::traits::FromStructReader::new(self.builder.into_reader()) + } + + pub fn total_size(&self) -> ::capnp::Result<::capnp::MessageSize> { + self.builder.into_reader().total_size() + } + #[inline] + pub fn get_name(self) -> ::capnp::Result<::capnp::text::Builder<'a>> { + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(0), + ::core::option::Option::None, + ) + } + #[inline] + pub fn set_name(&mut self, value: ::capnp::text::Reader<'_>) { + self.builder.get_pointer_field(0).set_text(value); + } + #[inline] + pub fn init_name(self, size: u32) -> ::capnp::text::Builder<'a> { + self.builder.get_pointer_field(0).init_text(size) + } + pub fn has_name(&self) -> bool { + !self.builder.get_pointer_field(0).is_null() + } + #[inline] + pub fn get_usage(self) -> ::capnp::Result<::capnp::text::Builder<'a>> { + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(1), + ::core::option::Option::None, + ) + } + #[inline] + pub fn set_usage(&mut self, value: ::capnp::text::Reader<'_>) { + self.builder.get_pointer_field(1).set_text(value); + } + #[inline] + pub fn init_usage(self, size: u32) -> ::capnp::text::Builder<'a> { + self.builder.get_pointer_field(1).init_text(size) + } + pub fn has_usage(&self) -> bool { + !self.builder.get_pointer_field(1).is_null() + } + #[inline] + pub fn get_extra_usage(self) -> ::capnp::Result<::capnp::text::Builder<'a>> { + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(2), + ::core::option::Option::None, + ) + } + #[inline] + pub fn set_extra_usage(&mut self, value: ::capnp::text::Reader<'_>) { + self.builder.get_pointer_field(2).set_text(value); + } + #[inline] + pub fn init_extra_usage(self, size: u32) -> ::capnp::text::Builder<'a> { + self.builder.get_pointer_field(2).init_text(size) + } + pub fn has_extra_usage(&self) -> bool { + !self.builder.get_pointer_field(2).is_null() + } + #[inline] + pub fn get_required_positional( + self, + ) -> ::capnp::Result<::capnp::struct_list::Builder<'a, crate::plugin_capnp::argument::Owned>> + { + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(3), + ::core::option::Option::None, + ) + } + #[inline] + pub fn set_required_positional( + &mut self, + value: ::capnp::struct_list::Reader<'a, crate::plugin_capnp::argument::Owned>, + ) -> ::capnp::Result<()> { + ::capnp::traits::SetPointerBuilder::set_pointer_builder( + self.builder.get_pointer_field(3), + value, + false, + ) + } + #[inline] + pub fn init_required_positional( + self, + size: u32, + ) -> ::capnp::struct_list::Builder<'a, crate::plugin_capnp::argument::Owned> { + ::capnp::traits::FromPointerBuilder::init_pointer( + self.builder.get_pointer_field(3), + size, + ) + } + pub fn has_required_positional(&self) -> bool { + !self.builder.get_pointer_field(3).is_null() + } + #[inline] + pub fn get_optional_positional( + self, + ) -> ::capnp::Result<::capnp::struct_list::Builder<'a, crate::plugin_capnp::argument::Owned>> + { + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(4), + ::core::option::Option::None, + ) + } + #[inline] + pub fn set_optional_positional( + &mut self, + value: ::capnp::struct_list::Reader<'a, crate::plugin_capnp::argument::Owned>, + ) -> ::capnp::Result<()> { + ::capnp::traits::SetPointerBuilder::set_pointer_builder( + self.builder.get_pointer_field(4), + value, + false, + ) + } + #[inline] + pub fn init_optional_positional( + self, + size: u32, + ) -> ::capnp::struct_list::Builder<'a, crate::plugin_capnp::argument::Owned> { + ::capnp::traits::FromPointerBuilder::init_pointer( + self.builder.get_pointer_field(4), + size, + ) + } + pub fn has_optional_positional(&self) -> bool { + !self.builder.get_pointer_field(4).is_null() + } + #[inline] + pub fn get_rest(self) -> ::capnp::Result> { + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(5), + ::core::option::Option::None, + ) + } + #[inline] + pub fn set_rest( + &mut self, + value: crate::plugin_capnp::argument::Reader<'_>, + ) -> ::capnp::Result<()> { + ::capnp::traits::SetPointerBuilder::set_pointer_builder( + self.builder.get_pointer_field(5), + value, + false, + ) + } + #[inline] + pub fn init_rest(self) -> crate::plugin_capnp::argument::Builder<'a> { + ::capnp::traits::FromPointerBuilder::init_pointer(self.builder.get_pointer_field(5), 0) + } + pub fn has_rest(&self) -> bool { + !self.builder.get_pointer_field(5).is_null() + } + #[inline] + pub fn get_named( + self, + ) -> ::capnp::Result<::capnp::struct_list::Builder<'a, crate::plugin_capnp::flag::Owned>> + { + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(6), + ::core::option::Option::None, + ) + } + #[inline] + pub fn set_named( + &mut self, + value: ::capnp::struct_list::Reader<'a, crate::plugin_capnp::flag::Owned>, + ) -> ::capnp::Result<()> { + ::capnp::traits::SetPointerBuilder::set_pointer_builder( + self.builder.get_pointer_field(6), + value, + false, + ) + } + #[inline] + pub fn init_named( + self, + size: u32, + ) -> ::capnp::struct_list::Builder<'a, crate::plugin_capnp::flag::Owned> { + ::capnp::traits::FromPointerBuilder::init_pointer( + self.builder.get_pointer_field(6), + size, + ) + } + pub fn has_named(&self) -> bool { + !self.builder.get_pointer_field(6).is_null() + } + #[inline] + pub fn get_is_filter(self) -> bool { + self.builder.get_bool_field(0) + } + #[inline] + pub fn set_is_filter(&mut self, value: bool) { + self.builder.set_bool_field(0, value); + } + #[inline] + pub fn get_category( + self, + ) -> ::core::result::Result { + ::capnp::traits::FromU16::from_u16(self.builder.get_data_field::(1)) + } + #[inline] + pub fn set_category(&mut self, value: crate::plugin_capnp::Category) { + self.builder.set_data_field::(1, value as u16) + } + } + + pub struct Pipeline { + _typeless: ::capnp::any_pointer::Pipeline, + } + impl ::capnp::capability::FromTypelessPipeline for Pipeline { + fn new(typeless: ::capnp::any_pointer::Pipeline) -> Pipeline { + Pipeline { + _typeless: typeless, + } + } + } + impl Pipeline { + pub fn get_rest(&self) -> crate::plugin_capnp::argument::Pipeline { + ::capnp::capability::FromTypelessPipeline::new(self._typeless.get_pointer_field(5)) + } + } + mod _private { + use capnp::private::layout; + pub const STRUCT_SIZE: layout::StructSize = layout::StructSize { + data: 1, + pointers: 7, + }; + pub const TYPE_ID: u64 = 0xec96_eeb4_8cb7_90fa; + } +} + +#[repr(u16)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Category { + Default = 0, + Conversions = 1, + Core = 2, + Date = 3, + Env = 4, + Experimental = 5, + Filesystem = 6, + Filters = 7, + Formats = 8, + Math = 9, + Network = 10, + Random = 11, + Platform = 12, + Shells = 13, + Strings = 14, + System = 15, + Viewers = 16, + Hash = 17, + Generators = 18, +} +impl ::capnp::traits::FromU16 for Category { + #[inline] + fn from_u16(value: u16) -> ::core::result::Result { + match value { + 0 => ::core::result::Result::Ok(Category::Default), + 1 => ::core::result::Result::Ok(Category::Conversions), + 2 => ::core::result::Result::Ok(Category::Core), + 3 => ::core::result::Result::Ok(Category::Date), + 4 => ::core::result::Result::Ok(Category::Env), + 5 => ::core::result::Result::Ok(Category::Experimental), + 6 => ::core::result::Result::Ok(Category::Filesystem), + 7 => ::core::result::Result::Ok(Category::Filters), + 8 => ::core::result::Result::Ok(Category::Formats), + 9 => ::core::result::Result::Ok(Category::Math), + 10 => ::core::result::Result::Ok(Category::Network), + 11 => ::core::result::Result::Ok(Category::Random), + 12 => ::core::result::Result::Ok(Category::Platform), + 13 => ::core::result::Result::Ok(Category::Shells), + 14 => ::core::result::Result::Ok(Category::Strings), + 15 => ::core::result::Result::Ok(Category::System), + 16 => ::core::result::Result::Ok(Category::Viewers), + 17 => ::core::result::Result::Ok(Category::Hash), + 18 => ::core::result::Result::Ok(Category::Generators), + n => ::core::result::Result::Err(::capnp::NotInSchema(n)), + } + } +} +impl ::capnp::traits::ToU16 for Category { + #[inline] + fn to_u16(self) -> u16 { + self as u16 + } +} +impl ::capnp::traits::HasTypeId for Category { + #[inline] + fn type_id() -> u64 { + 0x8920_14c1_76ba_5343u64 + } +} + +pub mod flag { + #[derive(Copy, Clone)] + pub struct Owned(()); + impl<'a> ::capnp::traits::Owned<'a> for Owned { + type Reader = Reader<'a>; + type Builder = Builder<'a>; + } + impl<'a> ::capnp::traits::OwnedStruct<'a> for Owned { + type Reader = Reader<'a>; + type Builder = Builder<'a>; + } + impl ::capnp::traits::Pipelined for Owned { + type Pipeline = Pipeline; + } + + #[derive(Clone, Copy)] + pub struct Reader<'a> { + reader: ::capnp::private::layout::StructReader<'a>, + } + + impl<'a> ::capnp::traits::HasTypeId for Reader<'a> { + #[inline] + fn type_id() -> u64 { + _private::TYPE_ID + } + } + impl<'a> ::capnp::traits::FromStructReader<'a> for Reader<'a> { + fn new(reader: ::capnp::private::layout::StructReader<'a>) -> Reader<'a> { + Reader { reader } + } + } + + impl<'a> ::capnp::traits::FromPointerReader<'a> for Reader<'a> { + fn get_from_pointer( + reader: &::capnp::private::layout::PointerReader<'a>, + default: ::core::option::Option<&'a [capnp::Word]>, + ) -> ::capnp::Result> { + ::core::result::Result::Ok(::capnp::traits::FromStructReader::new( + reader.get_struct(default)?, + )) + } + } + + impl<'a> ::capnp::traits::IntoInternalStructReader<'a> for Reader<'a> { + fn into_internal_struct_reader(self) -> ::capnp::private::layout::StructReader<'a> { + self.reader + } + } + + impl<'a> ::capnp::traits::Imbue<'a> for Reader<'a> { + fn imbue(&mut self, cap_table: &'a ::capnp::private::layout::CapTable) { + self.reader + .imbue(::capnp::private::layout::CapTableReader::Plain(cap_table)) + } + } + + impl<'a> Reader<'a> { + pub fn reborrow(&self) -> Reader<'_> { + Reader { ..*self } + } + + pub fn total_size(&self) -> ::capnp::Result<::capnp::MessageSize> { + self.reader.total_size() + } + #[inline] + pub fn get_long(self) -> ::capnp::Result<::capnp::text::Reader<'a>> { + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(0), + ::core::option::Option::None, + ) + } + pub fn has_long(&self) -> bool { + !self.reader.get_pointer_field(0).is_null() + } + #[inline] + pub fn get_short(self) -> ::capnp::Result<::capnp::text::Reader<'a>> { + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(1), + ::core::option::Option::None, + ) + } + pub fn has_short(&self) -> bool { + !self.reader.get_pointer_field(1).is_null() + } + #[inline] + pub fn get_arg( + self, + ) -> ::core::result::Result { + ::capnp::traits::FromU16::from_u16(self.reader.get_data_field::(0)) + } + #[inline] + pub fn get_required(self) -> bool { + self.reader.get_bool_field(16) + } + #[inline] + pub fn get_desc(self) -> ::capnp::Result<::capnp::text::Reader<'a>> { + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(2), + ::core::option::Option::None, + ) + } + pub fn has_desc(&self) -> bool { + !self.reader.get_pointer_field(2).is_null() + } + } + + pub struct Builder<'a> { + builder: ::capnp::private::layout::StructBuilder<'a>, + } + impl<'a> ::capnp::traits::HasStructSize for Builder<'a> { + #[inline] + fn struct_size() -> ::capnp::private::layout::StructSize { + _private::STRUCT_SIZE + } + } + impl<'a> ::capnp::traits::HasTypeId for Builder<'a> { + #[inline] + fn type_id() -> u64 { + _private::TYPE_ID + } + } + impl<'a> ::capnp::traits::FromStructBuilder<'a> for Builder<'a> { + fn new(builder: ::capnp::private::layout::StructBuilder<'a>) -> Builder<'a> { + Builder { builder } + } + } + + impl<'a> ::capnp::traits::ImbueMut<'a> for Builder<'a> { + fn imbue_mut(&mut self, cap_table: &'a mut ::capnp::private::layout::CapTable) { + self.builder + .imbue(::capnp::private::layout::CapTableBuilder::Plain(cap_table)) + } + } + + impl<'a> ::capnp::traits::FromPointerBuilder<'a> for Builder<'a> { + fn init_pointer( + builder: ::capnp::private::layout::PointerBuilder<'a>, + _size: u32, + ) -> Builder<'a> { + ::capnp::traits::FromStructBuilder::new(builder.init_struct(_private::STRUCT_SIZE)) + } + fn get_from_pointer( + builder: ::capnp::private::layout::PointerBuilder<'a>, + default: ::core::option::Option<&'a [capnp::Word]>, + ) -> ::capnp::Result> { + ::core::result::Result::Ok(::capnp::traits::FromStructBuilder::new( + builder.get_struct(_private::STRUCT_SIZE, default)?, + )) + } + } + + impl<'a> ::capnp::traits::SetPointerBuilder for Reader<'a> { + fn set_pointer_builder<'b>( + pointer: ::capnp::private::layout::PointerBuilder<'b>, + value: Reader<'a>, + canonicalize: bool, + ) -> ::capnp::Result<()> { + pointer.set_struct(&value.reader, canonicalize) + } + } + + impl<'a> Builder<'a> { + pub fn into_reader(self) -> Reader<'a> { + ::capnp::traits::FromStructReader::new(self.builder.into_reader()) + } + pub fn reborrow(&mut self) -> Builder<'_> { + Builder { ..*self } + } + pub fn reborrow_as_reader(&self) -> Reader<'_> { + ::capnp::traits::FromStructReader::new(self.builder.into_reader()) + } + + pub fn total_size(&self) -> ::capnp::Result<::capnp::MessageSize> { + self.builder.into_reader().total_size() + } + #[inline] + pub fn get_long(self) -> ::capnp::Result<::capnp::text::Builder<'a>> { + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(0), + ::core::option::Option::None, + ) + } + #[inline] + pub fn set_long(&mut self, value: ::capnp::text::Reader<'_>) { + self.builder.get_pointer_field(0).set_text(value); + } + #[inline] + pub fn init_long(self, size: u32) -> ::capnp::text::Builder<'a> { + self.builder.get_pointer_field(0).init_text(size) + } + pub fn has_long(&self) -> bool { + !self.builder.get_pointer_field(0).is_null() + } + #[inline] + pub fn get_short(self) -> ::capnp::Result<::capnp::text::Builder<'a>> { + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(1), + ::core::option::Option::None, + ) + } + #[inline] + pub fn set_short(&mut self, value: ::capnp::text::Reader<'_>) { + self.builder.get_pointer_field(1).set_text(value); + } + #[inline] + pub fn init_short(self, size: u32) -> ::capnp::text::Builder<'a> { + self.builder.get_pointer_field(1).init_text(size) + } + pub fn has_short(&self) -> bool { + !self.builder.get_pointer_field(1).is_null() + } + #[inline] + pub fn get_arg( + self, + ) -> ::core::result::Result { + ::capnp::traits::FromU16::from_u16(self.builder.get_data_field::(0)) + } + #[inline] + pub fn set_arg(&mut self, value: crate::plugin_capnp::Shape) { + self.builder.set_data_field::(0, value as u16) + } + #[inline] + pub fn get_required(self) -> bool { + self.builder.get_bool_field(16) + } + #[inline] + pub fn set_required(&mut self, value: bool) { + self.builder.set_bool_field(16, value); + } + #[inline] + pub fn get_desc(self) -> ::capnp::Result<::capnp::text::Builder<'a>> { + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(2), + ::core::option::Option::None, + ) + } + #[inline] + pub fn set_desc(&mut self, value: ::capnp::text::Reader<'_>) { + self.builder.get_pointer_field(2).set_text(value); + } + #[inline] + pub fn init_desc(self, size: u32) -> ::capnp::text::Builder<'a> { + self.builder.get_pointer_field(2).init_text(size) + } + pub fn has_desc(&self) -> bool { + !self.builder.get_pointer_field(2).is_null() + } + } + + pub struct Pipeline { + _typeless: ::capnp::any_pointer::Pipeline, + } + impl ::capnp::capability::FromTypelessPipeline for Pipeline { + fn new(typeless: ::capnp::any_pointer::Pipeline) -> Pipeline { + Pipeline { + _typeless: typeless, + } + } + } + impl Pipeline {} + mod _private { + use capnp::private::layout; + pub const STRUCT_SIZE: layout::StructSize = layout::StructSize { + data: 1, + pointers: 3, + }; + pub const TYPE_ID: u64 = 0xc3e5_b612_6800_e050; + } +} + +pub mod argument { + #[derive(Copy, Clone)] + pub struct Owned(()); + impl<'a> ::capnp::traits::Owned<'a> for Owned { + type Reader = Reader<'a>; + type Builder = Builder<'a>; + } + impl<'a> ::capnp::traits::OwnedStruct<'a> for Owned { + type Reader = Reader<'a>; + type Builder = Builder<'a>; + } + impl ::capnp::traits::Pipelined for Owned { + type Pipeline = Pipeline; + } + + #[derive(Clone, Copy)] + pub struct Reader<'a> { + reader: ::capnp::private::layout::StructReader<'a>, + } + + impl<'a> ::capnp::traits::HasTypeId for Reader<'a> { + #[inline] + fn type_id() -> u64 { + _private::TYPE_ID + } + } + impl<'a> ::capnp::traits::FromStructReader<'a> for Reader<'a> { + fn new(reader: ::capnp::private::layout::StructReader<'a>) -> Reader<'a> { + Reader { reader } + } + } + + impl<'a> ::capnp::traits::FromPointerReader<'a> for Reader<'a> { + fn get_from_pointer( + reader: &::capnp::private::layout::PointerReader<'a>, + default: ::core::option::Option<&'a [capnp::Word]>, + ) -> ::capnp::Result> { + ::core::result::Result::Ok(::capnp::traits::FromStructReader::new( + reader.get_struct(default)?, + )) + } + } + + impl<'a> ::capnp::traits::IntoInternalStructReader<'a> for Reader<'a> { + fn into_internal_struct_reader(self) -> ::capnp::private::layout::StructReader<'a> { + self.reader + } + } + + impl<'a> ::capnp::traits::Imbue<'a> for Reader<'a> { + fn imbue(&mut self, cap_table: &'a ::capnp::private::layout::CapTable) { + self.reader + .imbue(::capnp::private::layout::CapTableReader::Plain(cap_table)) + } + } + + impl<'a> Reader<'a> { + pub fn reborrow(&self) -> Reader<'_> { + Reader { ..*self } + } + + pub fn total_size(&self) -> ::capnp::Result<::capnp::MessageSize> { + self.reader.total_size() + } + #[inline] + pub fn get_name(self) -> ::capnp::Result<::capnp::text::Reader<'a>> { + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(0), + ::core::option::Option::None, + ) + } + pub fn has_name(&self) -> bool { + !self.reader.get_pointer_field(0).is_null() + } + #[inline] + pub fn get_desc(self) -> ::capnp::Result<::capnp::text::Reader<'a>> { + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(1), + ::core::option::Option::None, + ) + } + pub fn has_desc(&self) -> bool { + !self.reader.get_pointer_field(1).is_null() + } + #[inline] + pub fn get_shape( + self, + ) -> ::core::result::Result { + ::capnp::traits::FromU16::from_u16(self.reader.get_data_field::(0)) + } + } + + pub struct Builder<'a> { + builder: ::capnp::private::layout::StructBuilder<'a>, + } + impl<'a> ::capnp::traits::HasStructSize for Builder<'a> { + #[inline] + fn struct_size() -> ::capnp::private::layout::StructSize { + _private::STRUCT_SIZE + } + } + impl<'a> ::capnp::traits::HasTypeId for Builder<'a> { + #[inline] + fn type_id() -> u64 { + _private::TYPE_ID + } + } + impl<'a> ::capnp::traits::FromStructBuilder<'a> for Builder<'a> { + fn new(builder: ::capnp::private::layout::StructBuilder<'a>) -> Builder<'a> { + Builder { builder } + } + } + + impl<'a> ::capnp::traits::ImbueMut<'a> for Builder<'a> { + fn imbue_mut(&mut self, cap_table: &'a mut ::capnp::private::layout::CapTable) { + self.builder + .imbue(::capnp::private::layout::CapTableBuilder::Plain(cap_table)) + } + } + + impl<'a> ::capnp::traits::FromPointerBuilder<'a> for Builder<'a> { + fn init_pointer( + builder: ::capnp::private::layout::PointerBuilder<'a>, + _size: u32, + ) -> Builder<'a> { + ::capnp::traits::FromStructBuilder::new(builder.init_struct(_private::STRUCT_SIZE)) + } + fn get_from_pointer( + builder: ::capnp::private::layout::PointerBuilder<'a>, + default: ::core::option::Option<&'a [capnp::Word]>, + ) -> ::capnp::Result> { + ::core::result::Result::Ok(::capnp::traits::FromStructBuilder::new( + builder.get_struct(_private::STRUCT_SIZE, default)?, + )) + } + } + + impl<'a> ::capnp::traits::SetPointerBuilder for Reader<'a> { + fn set_pointer_builder<'b>( + pointer: ::capnp::private::layout::PointerBuilder<'b>, + value: Reader<'a>, + canonicalize: bool, + ) -> ::capnp::Result<()> { + pointer.set_struct(&value.reader, canonicalize) + } + } + + impl<'a> Builder<'a> { + pub fn into_reader(self) -> Reader<'a> { + ::capnp::traits::FromStructReader::new(self.builder.into_reader()) + } + pub fn reborrow(&mut self) -> Builder<'_> { + Builder { ..*self } + } + pub fn reborrow_as_reader(&self) -> Reader<'_> { + ::capnp::traits::FromStructReader::new(self.builder.into_reader()) + } + + pub fn total_size(&self) -> ::capnp::Result<::capnp::MessageSize> { + self.builder.into_reader().total_size() + } + #[inline] + pub fn get_name(self) -> ::capnp::Result<::capnp::text::Builder<'a>> { + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(0), + ::core::option::Option::None, + ) + } + #[inline] + pub fn set_name(&mut self, value: ::capnp::text::Reader<'_>) { + self.builder.get_pointer_field(0).set_text(value); + } + #[inline] + pub fn init_name(self, size: u32) -> ::capnp::text::Builder<'a> { + self.builder.get_pointer_field(0).init_text(size) + } + pub fn has_name(&self) -> bool { + !self.builder.get_pointer_field(0).is_null() + } + #[inline] + pub fn get_desc(self) -> ::capnp::Result<::capnp::text::Builder<'a>> { + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(1), + ::core::option::Option::None, + ) + } + #[inline] + pub fn set_desc(&mut self, value: ::capnp::text::Reader<'_>) { + self.builder.get_pointer_field(1).set_text(value); + } + #[inline] + pub fn init_desc(self, size: u32) -> ::capnp::text::Builder<'a> { + self.builder.get_pointer_field(1).init_text(size) + } + pub fn has_desc(&self) -> bool { + !self.builder.get_pointer_field(1).is_null() + } + #[inline] + pub fn get_shape( + self, + ) -> ::core::result::Result { + ::capnp::traits::FromU16::from_u16(self.builder.get_data_field::(0)) + } + #[inline] + pub fn set_shape(&mut self, value: crate::plugin_capnp::Shape) { + self.builder.set_data_field::(0, value as u16) + } + } + + pub struct Pipeline { + _typeless: ::capnp::any_pointer::Pipeline, + } + impl ::capnp::capability::FromTypelessPipeline for Pipeline { + fn new(typeless: ::capnp::any_pointer::Pipeline) -> Pipeline { + Pipeline { + _typeless: typeless, + } + } + } + impl Pipeline {} + mod _private { + use capnp::private::layout; + pub const STRUCT_SIZE: layout::StructSize = layout::StructSize { + data: 1, + pointers: 2, + }; + pub const TYPE_ID: u64 = 0xe49c_3d71_859d_47e9; + } +} + +#[repr(u16)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Shape { + None = 0, + Any = 1, + String = 2, + Number = 3, + Int = 4, + Boolean = 5, +} +impl ::capnp::traits::FromU16 for Shape { + #[inline] + fn from_u16(value: u16) -> ::core::result::Result { + match value { + 0 => ::core::result::Result::Ok(Shape::None), + 1 => ::core::result::Result::Ok(Shape::Any), + 2 => ::core::result::Result::Ok(Shape::String), + 3 => ::core::result::Result::Ok(Shape::Number), + 4 => ::core::result::Result::Ok(Shape::Int), + 5 => ::core::result::Result::Ok(Shape::Boolean), + n => ::core::result::Result::Err(::capnp::NotInSchema(n)), + } + } +} +impl ::capnp::traits::ToU16 for Shape { + #[inline] + fn to_u16(self) -> u16 { + self as u16 + } +} +impl ::capnp::traits::HasTypeId for Shape { + #[inline] + fn type_id() -> u64 { + 0xaa46_1154_9e24_a910u64 + } +} + +pub mod evaluated_call { + #[derive(Copy, Clone)] + pub struct Owned(()); + impl<'a> ::capnp::traits::Owned<'a> for Owned { + type Reader = Reader<'a>; + type Builder = Builder<'a>; + } + impl<'a> ::capnp::traits::OwnedStruct<'a> for Owned { + type Reader = Reader<'a>; + type Builder = Builder<'a>; + } + impl ::capnp::traits::Pipelined for Owned { + type Pipeline = Pipeline; + } + + #[derive(Clone, Copy)] + pub struct Reader<'a> { + reader: ::capnp::private::layout::StructReader<'a>, + } + + impl<'a> ::capnp::traits::HasTypeId for Reader<'a> { + #[inline] + fn type_id() -> u64 { + _private::TYPE_ID + } + } + impl<'a> ::capnp::traits::FromStructReader<'a> for Reader<'a> { + fn new(reader: ::capnp::private::layout::StructReader<'a>) -> Reader<'a> { + Reader { reader } + } + } + + impl<'a> ::capnp::traits::FromPointerReader<'a> for Reader<'a> { + fn get_from_pointer( + reader: &::capnp::private::layout::PointerReader<'a>, + default: ::core::option::Option<&'a [capnp::Word]>, + ) -> ::capnp::Result> { + ::core::result::Result::Ok(::capnp::traits::FromStructReader::new( + reader.get_struct(default)?, + )) + } + } + + impl<'a> ::capnp::traits::IntoInternalStructReader<'a> for Reader<'a> { + fn into_internal_struct_reader(self) -> ::capnp::private::layout::StructReader<'a> { + self.reader + } + } + + impl<'a> ::capnp::traits::Imbue<'a> for Reader<'a> { + fn imbue(&mut self, cap_table: &'a ::capnp::private::layout::CapTable) { + self.reader + .imbue(::capnp::private::layout::CapTableReader::Plain(cap_table)) + } + } + + impl<'a> Reader<'a> { + pub fn reborrow(&self) -> Reader<'_> { + Reader { ..*self } + } + + pub fn total_size(&self) -> ::capnp::Result<::capnp::MessageSize> { + self.reader.total_size() + } + #[inline] + pub fn get_head(self) -> ::capnp::Result> { + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(0), + ::core::option::Option::None, + ) + } + pub fn has_head(&self) -> bool { + !self.reader.get_pointer_field(0).is_null() + } + #[inline] + pub fn get_positional( + self, + ) -> ::capnp::Result<::capnp::struct_list::Reader<'a, crate::plugin_capnp::value::Owned>> + { + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(1), + ::core::option::Option::None, + ) + } + pub fn has_positional(&self) -> bool { + !self.reader.get_pointer_field(1).is_null() + } + #[inline] + pub fn get_named( + self, + ) -> ::capnp::Result< + crate::plugin_capnp::map::Reader< + 'a, + ::capnp::text::Owned, + crate::plugin_capnp::value::Owned, + >, + > { + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(2), + ::core::option::Option::None, + ) + } + pub fn has_named(&self) -> bool { + !self.reader.get_pointer_field(2).is_null() + } + } + + pub struct Builder<'a> { + builder: ::capnp::private::layout::StructBuilder<'a>, + } + impl<'a> ::capnp::traits::HasStructSize for Builder<'a> { + #[inline] + fn struct_size() -> ::capnp::private::layout::StructSize { + _private::STRUCT_SIZE + } + } + impl<'a> ::capnp::traits::HasTypeId for Builder<'a> { + #[inline] + fn type_id() -> u64 { + _private::TYPE_ID + } + } + impl<'a> ::capnp::traits::FromStructBuilder<'a> for Builder<'a> { + fn new(builder: ::capnp::private::layout::StructBuilder<'a>) -> Builder<'a> { + Builder { builder } + } + } + + impl<'a> ::capnp::traits::ImbueMut<'a> for Builder<'a> { + fn imbue_mut(&mut self, cap_table: &'a mut ::capnp::private::layout::CapTable) { + self.builder + .imbue(::capnp::private::layout::CapTableBuilder::Plain(cap_table)) + } + } + + impl<'a> ::capnp::traits::FromPointerBuilder<'a> for Builder<'a> { + fn init_pointer( + builder: ::capnp::private::layout::PointerBuilder<'a>, + _size: u32, + ) -> Builder<'a> { + ::capnp::traits::FromStructBuilder::new(builder.init_struct(_private::STRUCT_SIZE)) + } + fn get_from_pointer( + builder: ::capnp::private::layout::PointerBuilder<'a>, + default: ::core::option::Option<&'a [capnp::Word]>, + ) -> ::capnp::Result> { + ::core::result::Result::Ok(::capnp::traits::FromStructBuilder::new( + builder.get_struct(_private::STRUCT_SIZE, default)?, + )) + } + } + + impl<'a> ::capnp::traits::SetPointerBuilder for Reader<'a> { + fn set_pointer_builder<'b>( + pointer: ::capnp::private::layout::PointerBuilder<'b>, + value: Reader<'a>, + canonicalize: bool, + ) -> ::capnp::Result<()> { + pointer.set_struct(&value.reader, canonicalize) + } + } + + impl<'a> Builder<'a> { + pub fn into_reader(self) -> Reader<'a> { + ::capnp::traits::FromStructReader::new(self.builder.into_reader()) + } + pub fn reborrow(&mut self) -> Builder<'_> { + Builder { ..*self } + } + pub fn reborrow_as_reader(&self) -> Reader<'_> { + ::capnp::traits::FromStructReader::new(self.builder.into_reader()) + } + + pub fn total_size(&self) -> ::capnp::Result<::capnp::MessageSize> { + self.builder.into_reader().total_size() + } + #[inline] + pub fn get_head(self) -> ::capnp::Result> { + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(0), + ::core::option::Option::None, + ) + } + #[inline] + pub fn set_head( + &mut self, + value: crate::plugin_capnp::span::Reader<'_>, + ) -> ::capnp::Result<()> { + ::capnp::traits::SetPointerBuilder::set_pointer_builder( + self.builder.get_pointer_field(0), + value, + false, + ) + } + #[inline] + pub fn init_head(self) -> crate::plugin_capnp::span::Builder<'a> { + ::capnp::traits::FromPointerBuilder::init_pointer(self.builder.get_pointer_field(0), 0) + } + pub fn has_head(&self) -> bool { + !self.builder.get_pointer_field(0).is_null() + } + #[inline] + pub fn get_positional( + self, + ) -> ::capnp::Result<::capnp::struct_list::Builder<'a, crate::plugin_capnp::value::Owned>> + { + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(1), + ::core::option::Option::None, + ) + } + #[inline] + pub fn set_positional( + &mut self, + value: ::capnp::struct_list::Reader<'a, crate::plugin_capnp::value::Owned>, + ) -> ::capnp::Result<()> { + ::capnp::traits::SetPointerBuilder::set_pointer_builder( + self.builder.get_pointer_field(1), + value, + false, + ) + } + #[inline] + pub fn init_positional( + self, + size: u32, + ) -> ::capnp::struct_list::Builder<'a, crate::plugin_capnp::value::Owned> { + ::capnp::traits::FromPointerBuilder::init_pointer( + self.builder.get_pointer_field(1), + size, + ) + } + pub fn has_positional(&self) -> bool { + !self.builder.get_pointer_field(1).is_null() + } + #[inline] + pub fn get_named( + self, + ) -> ::capnp::Result< + crate::plugin_capnp::map::Builder< + 'a, + ::capnp::text::Owned, + crate::plugin_capnp::value::Owned, + >, + > { + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(2), + ::core::option::Option::None, + ) + } + #[inline] + pub fn set_named( + &mut self, + value: crate::plugin_capnp::map::Reader< + '_, + ::capnp::text::Owned, + crate::plugin_capnp::value::Owned, + >, + ) -> ::capnp::Result<()> { + as ::capnp::traits::SetPointerBuilder>::set_pointer_builder( + self.builder.get_pointer_field(2), + value, + false, + ) + } + #[inline] + pub fn init_named( + self, + ) -> crate::plugin_capnp::map::Builder< + 'a, + ::capnp::text::Owned, + crate::plugin_capnp::value::Owned, + > { + ::capnp::traits::FromPointerBuilder::init_pointer(self.builder.get_pointer_field(2), 0) + } + pub fn has_named(&self) -> bool { + !self.builder.get_pointer_field(2).is_null() + } + } + + pub struct Pipeline { + _typeless: ::capnp::any_pointer::Pipeline, + } + impl ::capnp::capability::FromTypelessPipeline for Pipeline { + fn new(typeless: ::capnp::any_pointer::Pipeline) -> Pipeline { + Pipeline { + _typeless: typeless, + } + } + } + impl Pipeline { + pub fn get_head(&self) -> crate::plugin_capnp::span::Pipeline { + ::capnp::capability::FromTypelessPipeline::new(self._typeless.get_pointer_field(0)) + } + pub fn get_named( + &self, + ) -> crate::plugin_capnp::map::Pipeline< + ::capnp::text::Owned, + crate::plugin_capnp::value::Owned, + > { + ::capnp::capability::FromTypelessPipeline::new(self._typeless.get_pointer_field(2)) + } + } + mod _private { + use capnp::private::layout; + pub const STRUCT_SIZE: layout::StructSize = layout::StructSize { + data: 0, + pointers: 3, + }; + pub const TYPE_ID: u64 = 0x84fb_ac77_3ee4_48a4; + } +} + +pub mod call_info { + #[derive(Copy, Clone)] + pub struct Owned(()); + impl<'a> ::capnp::traits::Owned<'a> for Owned { + type Reader = Reader<'a>; + type Builder = Builder<'a>; + } + impl<'a> ::capnp::traits::OwnedStruct<'a> for Owned { + type Reader = Reader<'a>; + type Builder = Builder<'a>; + } + impl ::capnp::traits::Pipelined for Owned { + type Pipeline = Pipeline; + } + + #[derive(Clone, Copy)] + pub struct Reader<'a> { + reader: ::capnp::private::layout::StructReader<'a>, + } + + impl<'a> ::capnp::traits::HasTypeId for Reader<'a> { + #[inline] + fn type_id() -> u64 { + _private::TYPE_ID + } + } + impl<'a> ::capnp::traits::FromStructReader<'a> for Reader<'a> { + fn new(reader: ::capnp::private::layout::StructReader<'a>) -> Reader<'a> { + Reader { reader } + } + } + + impl<'a> ::capnp::traits::FromPointerReader<'a> for Reader<'a> { + fn get_from_pointer( + reader: &::capnp::private::layout::PointerReader<'a>, + default: ::core::option::Option<&'a [capnp::Word]>, + ) -> ::capnp::Result> { + ::core::result::Result::Ok(::capnp::traits::FromStructReader::new( + reader.get_struct(default)?, + )) + } + } + + impl<'a> ::capnp::traits::IntoInternalStructReader<'a> for Reader<'a> { + fn into_internal_struct_reader(self) -> ::capnp::private::layout::StructReader<'a> { + self.reader + } + } + + impl<'a> ::capnp::traits::Imbue<'a> for Reader<'a> { + fn imbue(&mut self, cap_table: &'a ::capnp::private::layout::CapTable) { + self.reader + .imbue(::capnp::private::layout::CapTableReader::Plain(cap_table)) + } + } + + impl<'a> Reader<'a> { + pub fn reborrow(&self) -> Reader<'_> { + Reader { ..*self } + } + + pub fn total_size(&self) -> ::capnp::Result<::capnp::MessageSize> { + self.reader.total_size() + } + #[inline] + pub fn get_name(self) -> ::capnp::Result<::capnp::text::Reader<'a>> { + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(0), + ::core::option::Option::None, + ) + } + pub fn has_name(&self) -> bool { + !self.reader.get_pointer_field(0).is_null() + } + #[inline] + pub fn get_call(self) -> ::capnp::Result> { + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(1), + ::core::option::Option::None, + ) + } + pub fn has_call(&self) -> bool { + !self.reader.get_pointer_field(1).is_null() + } + #[inline] + pub fn get_input(self) -> ::capnp::Result> { + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(2), + ::core::option::Option::None, + ) + } + pub fn has_input(&self) -> bool { + !self.reader.get_pointer_field(2).is_null() + } + } + + pub struct Builder<'a> { + builder: ::capnp::private::layout::StructBuilder<'a>, + } + impl<'a> ::capnp::traits::HasStructSize for Builder<'a> { + #[inline] + fn struct_size() -> ::capnp::private::layout::StructSize { + _private::STRUCT_SIZE + } + } + impl<'a> ::capnp::traits::HasTypeId for Builder<'a> { + #[inline] + fn type_id() -> u64 { + _private::TYPE_ID + } + } + impl<'a> ::capnp::traits::FromStructBuilder<'a> for Builder<'a> { + fn new(builder: ::capnp::private::layout::StructBuilder<'a>) -> Builder<'a> { + Builder { builder } + } + } + + impl<'a> ::capnp::traits::ImbueMut<'a> for Builder<'a> { + fn imbue_mut(&mut self, cap_table: &'a mut ::capnp::private::layout::CapTable) { + self.builder + .imbue(::capnp::private::layout::CapTableBuilder::Plain(cap_table)) + } + } + + impl<'a> ::capnp::traits::FromPointerBuilder<'a> for Builder<'a> { + fn init_pointer( + builder: ::capnp::private::layout::PointerBuilder<'a>, + _size: u32, + ) -> Builder<'a> { + ::capnp::traits::FromStructBuilder::new(builder.init_struct(_private::STRUCT_SIZE)) + } + fn get_from_pointer( + builder: ::capnp::private::layout::PointerBuilder<'a>, + default: ::core::option::Option<&'a [capnp::Word]>, + ) -> ::capnp::Result> { + ::core::result::Result::Ok(::capnp::traits::FromStructBuilder::new( + builder.get_struct(_private::STRUCT_SIZE, default)?, + )) + } + } + + impl<'a> ::capnp::traits::SetPointerBuilder for Reader<'a> { + fn set_pointer_builder<'b>( + pointer: ::capnp::private::layout::PointerBuilder<'b>, + value: Reader<'a>, + canonicalize: bool, + ) -> ::capnp::Result<()> { + pointer.set_struct(&value.reader, canonicalize) + } + } + + impl<'a> Builder<'a> { + pub fn into_reader(self) -> Reader<'a> { + ::capnp::traits::FromStructReader::new(self.builder.into_reader()) + } + pub fn reborrow(&mut self) -> Builder<'_> { + Builder { ..*self } + } + pub fn reborrow_as_reader(&self) -> Reader<'_> { + ::capnp::traits::FromStructReader::new(self.builder.into_reader()) + } + + pub fn total_size(&self) -> ::capnp::Result<::capnp::MessageSize> { + self.builder.into_reader().total_size() + } + #[inline] + pub fn get_name(self) -> ::capnp::Result<::capnp::text::Builder<'a>> { + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(0), + ::core::option::Option::None, + ) + } + #[inline] + pub fn set_name(&mut self, value: ::capnp::text::Reader<'_>) { + self.builder.get_pointer_field(0).set_text(value); + } + #[inline] + pub fn init_name(self, size: u32) -> ::capnp::text::Builder<'a> { + self.builder.get_pointer_field(0).init_text(size) + } + pub fn has_name(&self) -> bool { + !self.builder.get_pointer_field(0).is_null() + } + #[inline] + pub fn get_call(self) -> ::capnp::Result> { + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(1), + ::core::option::Option::None, + ) + } + #[inline] + pub fn set_call( + &mut self, + value: crate::plugin_capnp::evaluated_call::Reader<'_>, + ) -> ::capnp::Result<()> { + ::capnp::traits::SetPointerBuilder::set_pointer_builder( + self.builder.get_pointer_field(1), + value, + false, + ) + } + #[inline] + pub fn init_call(self) -> crate::plugin_capnp::evaluated_call::Builder<'a> { + ::capnp::traits::FromPointerBuilder::init_pointer(self.builder.get_pointer_field(1), 0) + } + pub fn has_call(&self) -> bool { + !self.builder.get_pointer_field(1).is_null() + } + #[inline] + pub fn get_input(self) -> ::capnp::Result> { + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(2), + ::core::option::Option::None, + ) + } + #[inline] + pub fn set_input( + &mut self, + value: crate::plugin_capnp::value::Reader<'_>, + ) -> ::capnp::Result<()> { + ::capnp::traits::SetPointerBuilder::set_pointer_builder( + self.builder.get_pointer_field(2), + value, + false, + ) + } + #[inline] + pub fn init_input(self) -> crate::plugin_capnp::value::Builder<'a> { + ::capnp::traits::FromPointerBuilder::init_pointer(self.builder.get_pointer_field(2), 0) + } + pub fn has_input(&self) -> bool { + !self.builder.get_pointer_field(2).is_null() + } + } + + pub struct Pipeline { + _typeless: ::capnp::any_pointer::Pipeline, + } + impl ::capnp::capability::FromTypelessPipeline for Pipeline { + fn new(typeless: ::capnp::any_pointer::Pipeline) -> Pipeline { + Pipeline { + _typeless: typeless, + } + } + } + impl Pipeline { + pub fn get_call(&self) -> crate::plugin_capnp::evaluated_call::Pipeline { + ::capnp::capability::FromTypelessPipeline::new(self._typeless.get_pointer_field(1)) + } + pub fn get_input(&self) -> crate::plugin_capnp::value::Pipeline { + ::capnp::capability::FromTypelessPipeline::new(self._typeless.get_pointer_field(2)) + } + } + mod _private { + use capnp::private::layout; + pub const STRUCT_SIZE: layout::StructSize = layout::StructSize { + data: 0, + pointers: 3, + }; + pub const TYPE_ID: u64 = 0x8e03_127e_9170_7d6a; + } +} + +pub mod plugin_call { + pub use self::Which::{CallInfo, Signature}; + + #[derive(Copy, Clone)] + pub struct Owned(()); + impl<'a> ::capnp::traits::Owned<'a> for Owned { + type Reader = Reader<'a>; + type Builder = Builder<'a>; + } + impl<'a> ::capnp::traits::OwnedStruct<'a> for Owned { + type Reader = Reader<'a>; + type Builder = Builder<'a>; + } + impl ::capnp::traits::Pipelined for Owned { + type Pipeline = Pipeline; + } + + #[derive(Clone, Copy)] + pub struct Reader<'a> { + reader: ::capnp::private::layout::StructReader<'a>, + } + + impl<'a> ::capnp::traits::HasTypeId for Reader<'a> { + #[inline] + fn type_id() -> u64 { + _private::TYPE_ID + } + } + impl<'a> ::capnp::traits::FromStructReader<'a> for Reader<'a> { + fn new(reader: ::capnp::private::layout::StructReader<'a>) -> Reader<'a> { + Reader { reader } + } + } + + impl<'a> ::capnp::traits::FromPointerReader<'a> for Reader<'a> { + fn get_from_pointer( + reader: &::capnp::private::layout::PointerReader<'a>, + default: ::core::option::Option<&'a [capnp::Word]>, + ) -> ::capnp::Result> { + ::core::result::Result::Ok(::capnp::traits::FromStructReader::new( + reader.get_struct(default)?, + )) + } + } + + impl<'a> ::capnp::traits::IntoInternalStructReader<'a> for Reader<'a> { + fn into_internal_struct_reader(self) -> ::capnp::private::layout::StructReader<'a> { + self.reader + } + } + + impl<'a> ::capnp::traits::Imbue<'a> for Reader<'a> { + fn imbue(&mut self, cap_table: &'a ::capnp::private::layout::CapTable) { + self.reader + .imbue(::capnp::private::layout::CapTableReader::Plain(cap_table)) + } + } + + impl<'a> Reader<'a> { + pub fn reborrow(&self) -> Reader<'_> { + Reader { ..*self } + } + + pub fn total_size(&self) -> ::capnp::Result<::capnp::MessageSize> { + self.reader.total_size() + } + pub fn has_call_info(&self) -> bool { + if self.reader.get_data_field::(0) != 1 { + return false; + } + !self.reader.get_pointer_field(0).is_null() + } + #[inline] + pub fn which(self) -> ::core::result::Result, ::capnp::NotInSchema> { + match self.reader.get_data_field::(0) { + 0 => ::core::result::Result::Ok(Signature(())), + 1 => ::core::result::Result::Ok(CallInfo( + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(0), + ::core::option::Option::None, + ), + )), + x => ::core::result::Result::Err(::capnp::NotInSchema(x)), + } + } + } + + pub struct Builder<'a> { + builder: ::capnp::private::layout::StructBuilder<'a>, + } + impl<'a> ::capnp::traits::HasStructSize for Builder<'a> { + #[inline] + fn struct_size() -> ::capnp::private::layout::StructSize { + _private::STRUCT_SIZE + } + } + impl<'a> ::capnp::traits::HasTypeId for Builder<'a> { + #[inline] + fn type_id() -> u64 { + _private::TYPE_ID + } + } + impl<'a> ::capnp::traits::FromStructBuilder<'a> for Builder<'a> { + fn new(builder: ::capnp::private::layout::StructBuilder<'a>) -> Builder<'a> { + Builder { builder } + } + } + + impl<'a> ::capnp::traits::ImbueMut<'a> for Builder<'a> { + fn imbue_mut(&mut self, cap_table: &'a mut ::capnp::private::layout::CapTable) { + self.builder + .imbue(::capnp::private::layout::CapTableBuilder::Plain(cap_table)) + } + } + + impl<'a> ::capnp::traits::FromPointerBuilder<'a> for Builder<'a> { + fn init_pointer( + builder: ::capnp::private::layout::PointerBuilder<'a>, + _size: u32, + ) -> Builder<'a> { + ::capnp::traits::FromStructBuilder::new(builder.init_struct(_private::STRUCT_SIZE)) + } + fn get_from_pointer( + builder: ::capnp::private::layout::PointerBuilder<'a>, + default: ::core::option::Option<&'a [capnp::Word]>, + ) -> ::capnp::Result> { + ::core::result::Result::Ok(::capnp::traits::FromStructBuilder::new( + builder.get_struct(_private::STRUCT_SIZE, default)?, + )) + } + } + + impl<'a> ::capnp::traits::SetPointerBuilder for Reader<'a> { + fn set_pointer_builder<'b>( + pointer: ::capnp::private::layout::PointerBuilder<'b>, + value: Reader<'a>, + canonicalize: bool, + ) -> ::capnp::Result<()> { + pointer.set_struct(&value.reader, canonicalize) + } + } + + impl<'a> Builder<'a> { + pub fn into_reader(self) -> Reader<'a> { + ::capnp::traits::FromStructReader::new(self.builder.into_reader()) + } + pub fn reborrow(&mut self) -> Builder<'_> { + Builder { ..*self } + } + pub fn reborrow_as_reader(&self) -> Reader<'_> { + ::capnp::traits::FromStructReader::new(self.builder.into_reader()) + } + + pub fn total_size(&self) -> ::capnp::Result<::capnp::MessageSize> { + self.builder.into_reader().total_size() + } + #[inline] + pub fn set_signature(&mut self, _value: ()) { + self.builder.set_data_field::(0, 0); + } + #[inline] + pub fn set_call_info( + &mut self, + value: crate::plugin_capnp::call_info::Reader<'_>, + ) -> ::capnp::Result<()> { + self.builder.set_data_field::(0, 1); + ::capnp::traits::SetPointerBuilder::set_pointer_builder( + self.builder.get_pointer_field(0), + value, + false, + ) + } + #[inline] + pub fn init_call_info(self) -> crate::plugin_capnp::call_info::Builder<'a> { + self.builder.set_data_field::(0, 1); + ::capnp::traits::FromPointerBuilder::init_pointer(self.builder.get_pointer_field(0), 0) + } + pub fn has_call_info(&self) -> bool { + if self.builder.get_data_field::(0) != 1 { + return false; + } + !self.builder.get_pointer_field(0).is_null() + } + #[inline] + pub fn which(self) -> ::core::result::Result, ::capnp::NotInSchema> { + match self.builder.get_data_field::(0) { + 0 => ::core::result::Result::Ok(Signature(())), + 1 => ::core::result::Result::Ok(CallInfo( + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(0), + ::core::option::Option::None, + ), + )), + x => ::core::result::Result::Err(::capnp::NotInSchema(x)), + } + } + } + + pub struct Pipeline { + _typeless: ::capnp::any_pointer::Pipeline, + } + impl ::capnp::capability::FromTypelessPipeline for Pipeline { + fn new(typeless: ::capnp::any_pointer::Pipeline) -> Pipeline { + Pipeline { + _typeless: typeless, + } + } + } + impl Pipeline {} + mod _private { + use capnp::private::layout; + pub const STRUCT_SIZE: layout::StructSize = layout::StructSize { + data: 1, + pointers: 1, + }; + pub const TYPE_ID: u64 = 0xde86_64b2_7f80_4db1; + } + pub enum Which { + Signature(()), + CallInfo(A0), + } + pub type WhichReader<'a> = Which<::capnp::Result>>; + pub type WhichBuilder<'a> = Which<::capnp::Result>>; +} + +pub mod plugin_response { + pub use self::Which::{Error, Signature, Value}; + + #[derive(Copy, Clone)] + pub struct Owned(()); + impl<'a> ::capnp::traits::Owned<'a> for Owned { + type Reader = Reader<'a>; + type Builder = Builder<'a>; + } + impl<'a> ::capnp::traits::OwnedStruct<'a> for Owned { + type Reader = Reader<'a>; + type Builder = Builder<'a>; + } + impl ::capnp::traits::Pipelined for Owned { + type Pipeline = Pipeline; + } + + #[derive(Clone, Copy)] + pub struct Reader<'a> { + reader: ::capnp::private::layout::StructReader<'a>, + } + + impl<'a> ::capnp::traits::HasTypeId for Reader<'a> { + #[inline] + fn type_id() -> u64 { + _private::TYPE_ID + } + } + impl<'a> ::capnp::traits::FromStructReader<'a> for Reader<'a> { + fn new(reader: ::capnp::private::layout::StructReader<'a>) -> Reader<'a> { + Reader { reader } + } + } + + impl<'a> ::capnp::traits::FromPointerReader<'a> for Reader<'a> { + fn get_from_pointer( + reader: &::capnp::private::layout::PointerReader<'a>, + default: ::core::option::Option<&'a [capnp::Word]>, + ) -> ::capnp::Result> { + ::core::result::Result::Ok(::capnp::traits::FromStructReader::new( + reader.get_struct(default)?, + )) + } + } + + impl<'a> ::capnp::traits::IntoInternalStructReader<'a> for Reader<'a> { + fn into_internal_struct_reader(self) -> ::capnp::private::layout::StructReader<'a> { + self.reader + } + } + + impl<'a> ::capnp::traits::Imbue<'a> for Reader<'a> { + fn imbue(&mut self, cap_table: &'a ::capnp::private::layout::CapTable) { + self.reader + .imbue(::capnp::private::layout::CapTableReader::Plain(cap_table)) + } + } + + impl<'a> Reader<'a> { + pub fn reborrow(&self) -> Reader<'_> { + Reader { ..*self } + } + + pub fn total_size(&self) -> ::capnp::Result<::capnp::MessageSize> { + self.reader.total_size() + } + pub fn has_error(&self) -> bool { + if self.reader.get_data_field::(0) != 0 { + return false; + } + !self.reader.get_pointer_field(0).is_null() + } + pub fn has_signature(&self) -> bool { + if self.reader.get_data_field::(0) != 1 { + return false; + } + !self.reader.get_pointer_field(0).is_null() + } + pub fn has_value(&self) -> bool { + if self.reader.get_data_field::(0) != 2 { + return false; + } + !self.reader.get_pointer_field(0).is_null() + } + #[inline] + pub fn which(self) -> ::core::result::Result, ::capnp::NotInSchema> { + match self.reader.get_data_field::(0) { + 0 => ::core::result::Result::Ok(Error( + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(0), + ::core::option::Option::None, + ), + )), + 1 => ::core::result::Result::Ok(Signature( + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(0), + ::core::option::Option::None, + ), + )), + 2 => ::core::result::Result::Ok(Value( + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(0), + ::core::option::Option::None, + ), + )), + x => ::core::result::Result::Err(::capnp::NotInSchema(x)), + } + } + } + + pub struct Builder<'a> { + builder: ::capnp::private::layout::StructBuilder<'a>, + } + impl<'a> ::capnp::traits::HasStructSize for Builder<'a> { + #[inline] + fn struct_size() -> ::capnp::private::layout::StructSize { + _private::STRUCT_SIZE + } + } + impl<'a> ::capnp::traits::HasTypeId for Builder<'a> { + #[inline] + fn type_id() -> u64 { + _private::TYPE_ID + } + } + impl<'a> ::capnp::traits::FromStructBuilder<'a> for Builder<'a> { + fn new(builder: ::capnp::private::layout::StructBuilder<'a>) -> Builder<'a> { + Builder { builder } + } + } + + impl<'a> ::capnp::traits::ImbueMut<'a> for Builder<'a> { + fn imbue_mut(&mut self, cap_table: &'a mut ::capnp::private::layout::CapTable) { + self.builder + .imbue(::capnp::private::layout::CapTableBuilder::Plain(cap_table)) + } + } + + impl<'a> ::capnp::traits::FromPointerBuilder<'a> for Builder<'a> { + fn init_pointer( + builder: ::capnp::private::layout::PointerBuilder<'a>, + _size: u32, + ) -> Builder<'a> { + ::capnp::traits::FromStructBuilder::new(builder.init_struct(_private::STRUCT_SIZE)) + } + fn get_from_pointer( + builder: ::capnp::private::layout::PointerBuilder<'a>, + default: ::core::option::Option<&'a [capnp::Word]>, + ) -> ::capnp::Result> { + ::core::result::Result::Ok(::capnp::traits::FromStructBuilder::new( + builder.get_struct(_private::STRUCT_SIZE, default)?, + )) + } + } + + impl<'a> ::capnp::traits::SetPointerBuilder for Reader<'a> { + fn set_pointer_builder<'b>( + pointer: ::capnp::private::layout::PointerBuilder<'b>, + value: Reader<'a>, + canonicalize: bool, + ) -> ::capnp::Result<()> { + pointer.set_struct(&value.reader, canonicalize) + } + } + + impl<'a> Builder<'a> { + pub fn into_reader(self) -> Reader<'a> { + ::capnp::traits::FromStructReader::new(self.builder.into_reader()) + } + pub fn reborrow(&mut self) -> Builder<'_> { + Builder { ..*self } + } + pub fn reborrow_as_reader(&self) -> Reader<'_> { + ::capnp::traits::FromStructReader::new(self.builder.into_reader()) + } + + pub fn total_size(&self) -> ::capnp::Result<::capnp::MessageSize> { + self.builder.into_reader().total_size() + } + #[inline] + pub fn set_error( + &mut self, + value: crate::plugin_capnp::labeled_error::Reader<'_>, + ) -> ::capnp::Result<()> { + self.builder.set_data_field::(0, 0); + ::capnp::traits::SetPointerBuilder::set_pointer_builder( + self.builder.get_pointer_field(0), + value, + false, + ) + } + #[inline] + pub fn init_error(self) -> crate::plugin_capnp::labeled_error::Builder<'a> { + self.builder.set_data_field::(0, 0); + ::capnp::traits::FromPointerBuilder::init_pointer(self.builder.get_pointer_field(0), 0) + } + pub fn has_error(&self) -> bool { + if self.builder.get_data_field::(0) != 0 { + return false; + } + !self.builder.get_pointer_field(0).is_null() + } + #[inline] + pub fn set_signature( + &mut self, + value: ::capnp::struct_list::Reader<'a, crate::plugin_capnp::signature::Owned>, + ) -> ::capnp::Result<()> { + self.builder.set_data_field::(0, 1); + ::capnp::traits::SetPointerBuilder::set_pointer_builder( + self.builder.get_pointer_field(0), + value, + false, + ) + } + #[inline] + pub fn init_signature( + self, + size: u32, + ) -> ::capnp::struct_list::Builder<'a, crate::plugin_capnp::signature::Owned> { + self.builder.set_data_field::(0, 1); + ::capnp::traits::FromPointerBuilder::init_pointer( + self.builder.get_pointer_field(0), + size, + ) + } + pub fn has_signature(&self) -> bool { + if self.builder.get_data_field::(0) != 1 { + return false; + } + !self.builder.get_pointer_field(0).is_null() + } + #[inline] + pub fn set_value( + &mut self, + value: crate::plugin_capnp::value::Reader<'_>, + ) -> ::capnp::Result<()> { + self.builder.set_data_field::(0, 2); + ::capnp::traits::SetPointerBuilder::set_pointer_builder( + self.builder.get_pointer_field(0), + value, + false, + ) + } + #[inline] + pub fn init_value(self) -> crate::plugin_capnp::value::Builder<'a> { + self.builder.set_data_field::(0, 2); + ::capnp::traits::FromPointerBuilder::init_pointer(self.builder.get_pointer_field(0), 0) + } + pub fn has_value(&self) -> bool { + if self.builder.get_data_field::(0) != 2 { + return false; + } + !self.builder.get_pointer_field(0).is_null() + } + #[inline] + pub fn which(self) -> ::core::result::Result, ::capnp::NotInSchema> { + match self.builder.get_data_field::(0) { + 0 => ::core::result::Result::Ok(Error( + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(0), + ::core::option::Option::None, + ), + )), + 1 => ::core::result::Result::Ok(Signature( + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(0), + ::core::option::Option::None, + ), + )), + 2 => ::core::result::Result::Ok(Value( + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(0), + ::core::option::Option::None, + ), + )), + x => ::core::result::Result::Err(::capnp::NotInSchema(x)), + } + } + } + + pub struct Pipeline { + _typeless: ::capnp::any_pointer::Pipeline, + } + impl ::capnp::capability::FromTypelessPipeline for Pipeline { + fn new(typeless: ::capnp::any_pointer::Pipeline) -> Pipeline { + Pipeline { + _typeless: typeless, + } + } + } + impl Pipeline {} + mod _private { + use capnp::private::layout; + pub const STRUCT_SIZE: layout::StructSize = layout::StructSize { + data: 1, + pointers: 1, + }; + pub const TYPE_ID: u64 = 0xb9ba_b3c7_9388_b7db; + } + pub enum Which { + Error(A0), + Signature(A1), + Value(A2), + } + pub type WhichReader<'a> = Which< + ::capnp::Result>, + ::capnp::Result<::capnp::struct_list::Reader<'a, crate::plugin_capnp::signature::Owned>>, + ::capnp::Result>, + >; + pub type WhichBuilder<'a> = Which< + ::capnp::Result>, + ::capnp::Result<::capnp::struct_list::Builder<'a, crate::plugin_capnp::signature::Owned>>, + ::capnp::Result>, + >; +} + +pub mod labeled_error { + #[derive(Copy, Clone)] + pub struct Owned(()); + impl<'a> ::capnp::traits::Owned<'a> for Owned { + type Reader = Reader<'a>; + type Builder = Builder<'a>; + } + impl<'a> ::capnp::traits::OwnedStruct<'a> for Owned { + type Reader = Reader<'a>; + type Builder = Builder<'a>; + } + impl ::capnp::traits::Pipelined for Owned { + type Pipeline = Pipeline; + } + + #[derive(Clone, Copy)] + pub struct Reader<'a> { + reader: ::capnp::private::layout::StructReader<'a>, + } + + impl<'a> ::capnp::traits::HasTypeId for Reader<'a> { + #[inline] + fn type_id() -> u64 { + _private::TYPE_ID + } + } + impl<'a> ::capnp::traits::FromStructReader<'a> for Reader<'a> { + fn new(reader: ::capnp::private::layout::StructReader<'a>) -> Reader<'a> { + Reader { reader } + } + } + + impl<'a> ::capnp::traits::FromPointerReader<'a> for Reader<'a> { + fn get_from_pointer( + reader: &::capnp::private::layout::PointerReader<'a>, + default: ::core::option::Option<&'a [capnp::Word]>, + ) -> ::capnp::Result> { + ::core::result::Result::Ok(::capnp::traits::FromStructReader::new( + reader.get_struct(default)?, + )) + } + } + + impl<'a> ::capnp::traits::IntoInternalStructReader<'a> for Reader<'a> { + fn into_internal_struct_reader(self) -> ::capnp::private::layout::StructReader<'a> { + self.reader + } + } + + impl<'a> ::capnp::traits::Imbue<'a> for Reader<'a> { + fn imbue(&mut self, cap_table: &'a ::capnp::private::layout::CapTable) { + self.reader + .imbue(::capnp::private::layout::CapTableReader::Plain(cap_table)) + } + } + + impl<'a> Reader<'a> { + pub fn reborrow(&self) -> Reader<'_> { + Reader { ..*self } + } + + pub fn total_size(&self) -> ::capnp::Result<::capnp::MessageSize> { + self.reader.total_size() + } + #[inline] + pub fn get_label(self) -> ::capnp::Result<::capnp::text::Reader<'a>> { + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(0), + ::core::option::Option::None, + ) + } + pub fn has_label(&self) -> bool { + !self.reader.get_pointer_field(0).is_null() + } + #[inline] + pub fn get_msg(self) -> ::capnp::Result<::capnp::text::Reader<'a>> { + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(1), + ::core::option::Option::None, + ) + } + pub fn has_msg(&self) -> bool { + !self.reader.get_pointer_field(1).is_null() + } + #[inline] + pub fn get_span(self) -> ::capnp::Result> { + ::capnp::traits::FromPointerReader::get_from_pointer( + &self.reader.get_pointer_field(2), + ::core::option::Option::None, + ) + } + pub fn has_span(&self) -> bool { + !self.reader.get_pointer_field(2).is_null() + } + } + + pub struct Builder<'a> { + builder: ::capnp::private::layout::StructBuilder<'a>, + } + impl<'a> ::capnp::traits::HasStructSize for Builder<'a> { + #[inline] + fn struct_size() -> ::capnp::private::layout::StructSize { + _private::STRUCT_SIZE + } + } + impl<'a> ::capnp::traits::HasTypeId for Builder<'a> { + #[inline] + fn type_id() -> u64 { + _private::TYPE_ID + } + } + impl<'a> ::capnp::traits::FromStructBuilder<'a> for Builder<'a> { + fn new(builder: ::capnp::private::layout::StructBuilder<'a>) -> Builder<'a> { + Builder { builder } + } + } + + impl<'a> ::capnp::traits::ImbueMut<'a> for Builder<'a> { + fn imbue_mut(&mut self, cap_table: &'a mut ::capnp::private::layout::CapTable) { + self.builder + .imbue(::capnp::private::layout::CapTableBuilder::Plain(cap_table)) + } + } + + impl<'a> ::capnp::traits::FromPointerBuilder<'a> for Builder<'a> { + fn init_pointer( + builder: ::capnp::private::layout::PointerBuilder<'a>, + _size: u32, + ) -> Builder<'a> { + ::capnp::traits::FromStructBuilder::new(builder.init_struct(_private::STRUCT_SIZE)) + } + fn get_from_pointer( + builder: ::capnp::private::layout::PointerBuilder<'a>, + default: ::core::option::Option<&'a [capnp::Word]>, + ) -> ::capnp::Result> { + ::core::result::Result::Ok(::capnp::traits::FromStructBuilder::new( + builder.get_struct(_private::STRUCT_SIZE, default)?, + )) + } + } + + impl<'a> ::capnp::traits::SetPointerBuilder for Reader<'a> { + fn set_pointer_builder<'b>( + pointer: ::capnp::private::layout::PointerBuilder<'b>, + value: Reader<'a>, + canonicalize: bool, + ) -> ::capnp::Result<()> { + pointer.set_struct(&value.reader, canonicalize) + } + } + + impl<'a> Builder<'a> { + pub fn into_reader(self) -> Reader<'a> { + ::capnp::traits::FromStructReader::new(self.builder.into_reader()) + } + pub fn reborrow(&mut self) -> Builder<'_> { + Builder { ..*self } + } + pub fn reborrow_as_reader(&self) -> Reader<'_> { + ::capnp::traits::FromStructReader::new(self.builder.into_reader()) + } + + pub fn total_size(&self) -> ::capnp::Result<::capnp::MessageSize> { + self.builder.into_reader().total_size() + } + #[inline] + pub fn get_label(self) -> ::capnp::Result<::capnp::text::Builder<'a>> { + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(0), + ::core::option::Option::None, + ) + } + #[inline] + pub fn set_label(&mut self, value: ::capnp::text::Reader<'_>) { + self.builder.get_pointer_field(0).set_text(value); + } + #[inline] + pub fn init_label(self, size: u32) -> ::capnp::text::Builder<'a> { + self.builder.get_pointer_field(0).init_text(size) + } + pub fn has_label(&self) -> bool { + !self.builder.get_pointer_field(0).is_null() + } + #[inline] + pub fn get_msg(self) -> ::capnp::Result<::capnp::text::Builder<'a>> { + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(1), + ::core::option::Option::None, + ) + } + #[inline] + pub fn set_msg(&mut self, value: ::capnp::text::Reader<'_>) { + self.builder.get_pointer_field(1).set_text(value); + } + #[inline] + pub fn init_msg(self, size: u32) -> ::capnp::text::Builder<'a> { + self.builder.get_pointer_field(1).init_text(size) + } + pub fn has_msg(&self) -> bool { + !self.builder.get_pointer_field(1).is_null() + } + #[inline] + pub fn get_span(self) -> ::capnp::Result> { + ::capnp::traits::FromPointerBuilder::get_from_pointer( + self.builder.get_pointer_field(2), + ::core::option::Option::None, + ) + } + #[inline] + pub fn set_span( + &mut self, + value: crate::plugin_capnp::span::Reader<'_>, + ) -> ::capnp::Result<()> { + ::capnp::traits::SetPointerBuilder::set_pointer_builder( + self.builder.get_pointer_field(2), + value, + false, + ) + } + #[inline] + pub fn init_span(self) -> crate::plugin_capnp::span::Builder<'a> { + ::capnp::traits::FromPointerBuilder::init_pointer(self.builder.get_pointer_field(2), 0) + } + pub fn has_span(&self) -> bool { + !self.builder.get_pointer_field(2).is_null() + } + } + + pub struct Pipeline { + _typeless: ::capnp::any_pointer::Pipeline, + } + impl ::capnp::capability::FromTypelessPipeline for Pipeline { + fn new(typeless: ::capnp::any_pointer::Pipeline) -> Pipeline { + Pipeline { + _typeless: typeless, + } + } + } + impl Pipeline { + pub fn get_span(&self) -> crate::plugin_capnp::span::Pipeline { + ::capnp::capability::FromTypelessPipeline::new(self._typeless.get_pointer_field(2)) + } + } + mod _private { + use capnp::private::layout; + pub const STRUCT_SIZE: layout::StructSize = layout::StructSize { + data: 0, + pointers: 3, + }; + pub const TYPE_ID: u64 = 0x94d1_6904_99e7_04fe; + } +} diff --git a/crates/nu-plugin/src/protocol/evaluated_call.rs b/crates/nu-plugin/src/protocol/evaluated_call.rs new file mode 100644 index 0000000000..6f70cef641 --- /dev/null +++ b/crates/nu-plugin/src/protocol/evaluated_call.rs @@ -0,0 +1,163 @@ +use nu_engine::eval_expression; +use nu_protocol::{ + ast::Call, + engine::{EngineState, Stack}, + FromValue, ShellError, Span, Spanned, Value, +}; +use serde::{Deserialize, Serialize}; + +// The evaluated call is used with the Plugins because the plugin doesn't have +// access to the Stack and the EngineState. For that reason, before encoding the +// message to the plugin all the arguments to the original call (which are expressions) +// are evaluated and passed to Values +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EvaluatedCall { + pub head: Span, + pub positional: Vec, + pub named: Vec<(Spanned, Option)>, +} + +impl EvaluatedCall { + pub fn try_from_call( + call: &Call, + engine_state: &EngineState, + stack: &mut Stack, + ) -> Result { + let positional = call + .positional + .iter() + .map(|expr| eval_expression(engine_state, stack, expr)) + .collect::, ShellError>>()?; + + let mut named = Vec::with_capacity(call.named.len()); + for (string, expr) in call.named.iter() { + let value = match expr { + None => None, + Some(expr) => Some(eval_expression(engine_state, stack, expr)?), + }; + + named.push((string.clone(), value)) + } + + Ok(Self { + head: call.head, + positional, + named, + }) + } + + pub fn has_flag(&self, flag_name: &str) -> bool { + for name in &self.named { + if flag_name == name.0.item { + return true; + } + } + + false + } + + pub fn get_flag_value(&self, flag_name: &str) -> Option { + for name in &self.named { + if flag_name == name.0.item { + return name.1.clone(); + } + } + + None + } + + pub fn nth(&self, pos: usize) -> Option { + self.positional.get(pos).cloned() + } + + pub fn get_flag(&self, name: &str) -> Result, ShellError> { + if let Some(value) = self.get_flag_value(name) { + FromValue::from_value(&value).map(Some) + } else { + Ok(None) + } + } + + pub fn rest(&self, starting_pos: usize) -> Result, ShellError> { + self.positional + .iter() + .skip(starting_pos) + .map(|value| FromValue::from_value(value)) + .collect() + } + + pub fn opt(&self, pos: usize) -> Result, ShellError> { + if let Some(value) = self.nth(pos) { + FromValue::from_value(&value).map(Some) + } else { + Ok(None) + } + } + + pub fn req(&self, pos: usize) -> Result { + if let Some(value) = self.nth(pos) { + FromValue::from_value(&value) + } else { + Err(ShellError::AccessBeyondEnd( + self.positional.len(), + self.head, + )) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use nu_protocol::{Span, Spanned, Value}; + + #[test] + fn call_to_value() { + let call = EvaluatedCall { + head: Span { start: 0, end: 10 }, + positional: vec![ + Value::Float { + val: 1.0, + span: Span { start: 0, end: 10 }, + }, + Value::String { + val: "something".into(), + span: Span { start: 0, end: 10 }, + }, + ], + named: vec![ + ( + Spanned { + item: "name".to_string(), + span: Span { start: 0, end: 10 }, + }, + Some(Value::Float { + val: 1.0, + span: Span { start: 0, end: 10 }, + }), + ), + ( + Spanned { + item: "flag".to_string(), + span: Span { start: 0, end: 10 }, + }, + None, + ), + ], + }; + + let name: Option = call.get_flag("name").unwrap(); + assert_eq!(name, Some(1.0)); + + assert!(call.has_flag("flag")); + + let required: f64 = call.req(0).unwrap(); + assert!((required - 1.0).abs() < f64::EPSILON); + + let optional: Option = call.opt(1).unwrap(); + assert_eq!(optional, Some("something".to_string())); + + let rest: Vec = call.rest(1).unwrap(); + assert_eq!(rest, vec!["something".to_string()]); + } +} diff --git a/crates/nu-plugin/src/protocol/mod.rs b/crates/nu-plugin/src/protocol/mod.rs new file mode 100644 index 0000000000..ace953a99b --- /dev/null +++ b/crates/nu-plugin/src/protocol/mod.rs @@ -0,0 +1,90 @@ +mod evaluated_call; + +pub use evaluated_call::EvaluatedCall; +use nu_protocol::{ShellError, Signature, Span, Value}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct CallInfo { + pub name: String, + pub call: EvaluatedCall, + pub input: Value, +} + +// Information sent to the plugin +#[derive(Serialize, Deserialize, Debug)] +pub enum PluginCall { + Signature, + CallInfo(Box), +} + +#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)] +pub struct LabeledError { + pub label: String, + pub msg: String, + pub span: Option, +} + +impl From for ShellError { + fn from(error: LabeledError) -> Self { + match error.span { + Some(span) => ShellError::SpannedLabeledError(error.label, error.msg, span), + None => ShellError::LabeledError(error.label, error.msg), + } + } +} + +impl From for LabeledError { + fn from(error: ShellError) -> Self { + match error { + ShellError::SpannedLabeledError(label, msg, span) => LabeledError { + label, + msg, + span: Some(span), + }, + ShellError::LabeledError(label, msg) => LabeledError { + label, + msg, + span: None, + }, + ShellError::CantConvert(expected, input, span) => LabeledError { + label: format!("Can't convert to {}", expected), + msg: format!("can't convert {} to {}", expected, input), + span: Some(span), + }, + ShellError::DidYouMean(suggestion, span) => LabeledError { + label: "Name not found".into(), + msg: format!("did you mean '{}'", suggestion), + span: Some(span), + }, + ShellError::PluginFailedToLoad(msg) => LabeledError { + label: "Plugin failed to load".into(), + msg, + span: None, + }, + ShellError::PluginFailedToEncode(msg) => LabeledError { + label: "Plugin failed to encode".into(), + msg, + span: None, + }, + ShellError::PluginFailedToDecode(msg) => LabeledError { + label: "Plugin failed to decode".into(), + msg, + span: None, + }, + err => LabeledError { + label: "Error - Add to LabeledError From".into(), + msg: err.to_string(), + span: None, + }, + } + } +} + +// Information received from the plugin +#[derive(Serialize, Deserialize)] +pub enum PluginResponse { + Error(LabeledError), + Signature(Vec), + Value(Box), +} diff --git a/crates/nu-plugin/src/serializers/capnp/call.rs b/crates/nu-plugin/src/serializers/capnp/call.rs new file mode 100644 index 0000000000..264c92c9b2 --- /dev/null +++ b/crates/nu-plugin/src/serializers/capnp/call.rs @@ -0,0 +1,222 @@ +use super::value; +use crate::{plugin_capnp::evaluated_call, EvaluatedCall}; +use nu_protocol::{ShellError, Span, Spanned, Value}; + +pub(crate) fn serialize_call( + call: &EvaluatedCall, + mut builder: evaluated_call::Builder, +) -> Result<(), ShellError> { + let mut head = builder.reborrow().init_head(); + head.set_start(call.head.start as u64); + head.set_end(call.head.end as u64); + + serialize_positional(&call.positional, builder.reborrow()); + serialize_named(&call.named, builder)?; + + Ok(()) +} + +fn serialize_positional(positional: &[Value], mut builder: evaluated_call::Builder) { + let mut positional_builder = builder.reborrow().init_positional(positional.len() as u32); + + for (index, value) in positional.iter().enumerate() { + value::serialize_value(value, positional_builder.reborrow().get(index as u32)) + } +} + +fn serialize_named( + named: &[(Spanned, Option)], + mut builder: evaluated_call::Builder, +) -> Result<(), ShellError> { + let mut named_builder = builder + .reborrow() + .init_named() + .init_entries(named.len() as u32); + + for (index, (key, expression)) in named.iter().enumerate() { + let mut entry_builder = named_builder.reborrow().get(index as u32); + entry_builder + .reborrow() + .set_key(key.item.as_str()) + .map_err(|e| ShellError::PluginFailedToEncode(e.to_string()))?; + + if let Some(value) = expression { + let value_builder = entry_builder.init_value(); + value::serialize_value(value, value_builder); + } + } + + Ok(()) +} + +pub(crate) fn deserialize_call( + reader: evaluated_call::Reader, +) -> Result { + let head_reader = reader + .get_head() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + let head = Span { + start: head_reader.get_start() as usize, + end: head_reader.get_end() as usize, + }; + + let positional = deserialize_positionals(head, reader)?; + let named = deserialize_named(head, reader)?; + + Ok(EvaluatedCall { + head, + positional, + named, + }) +} + +fn deserialize_positionals( + span: Span, + reader: evaluated_call::Reader, +) -> Result, ShellError> { + let positional_reader = reader + .get_positional() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + positional_reader + .iter() + .map(move |x| value::deserialize_value(x, span)) + .collect() +} + +type NamedList = Vec<(Spanned, Option)>; + +fn deserialize_named(span: Span, reader: evaluated_call::Reader) -> Result { + let named_reader = reader + .get_named() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + let entries_list = named_reader + .get_entries() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + let mut entries: Vec<(Spanned, Option)> = + Vec::with_capacity(entries_list.len() as usize); + + for entry_reader in entries_list { + let item = entry_reader + .get_key() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))? + .to_string(); + + let value = if entry_reader.has_value() { + let value_reader = entry_reader + .get_value() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + let value = value::deserialize_value(value_reader, span) + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + Some(value) + } else { + None + }; + + let key = Spanned { item, span }; + + entries.push((key, value)) + } + + Ok(entries) +} + +#[cfg(test)] +mod tests { + use capnp::serialize; + use core::panic; + + use super::*; + use nu_protocol::{Span, Spanned, Value}; + + fn write_buffer( + call: &EvaluatedCall, + writer: &mut impl std::io::Write, + ) -> Result<(), ShellError> { + let mut message = ::capnp::message::Builder::new_default(); + + let builder = message.init_root::(); + serialize_call(call, builder)?; + + serialize::write_message(writer, &message) + .map_err(|e| ShellError::PluginFailedToLoad(e.to_string())) + } + + fn read_buffer(reader: &mut impl std::io::BufRead) -> Result { + let message_reader = + serialize::read_message(reader, ::capnp::message::ReaderOptions::new()).unwrap(); + + let reader = message_reader + .get_root::() + .map_err(|e| ShellError::PluginFailedToLoad(e.to_string()))?; + + deserialize_call(reader) + } + + #[test] + fn call_round_trip() { + let call = EvaluatedCall { + head: Span { start: 0, end: 10 }, + positional: vec![ + Value::Float { + val: 1.0, + span: Span { start: 0, end: 10 }, + }, + Value::String { + val: "something".into(), + span: Span { start: 0, end: 10 }, + }, + ], + named: vec![ + ( + Spanned { + item: "name".to_string(), + span: Span { start: 0, end: 10 }, + }, + Some(Value::Float { + val: 1.0, + span: Span { start: 0, end: 10 }, + }), + ), + ( + Spanned { + item: "flag".to_string(), + span: Span { start: 0, end: 10 }, + }, + None, + ), + ], + }; + + let mut buffer: Vec = Vec::new(); + write_buffer(&call, &mut buffer).expect("unable to serialize message"); + let returned_call = read_buffer(&mut buffer.as_slice()).expect("unable to read buffer"); + + assert_eq!(call.head, returned_call.head); + assert_eq!(call.positional.len(), returned_call.positional.len()); + + call.positional + .iter() + .zip(returned_call.positional.iter()) + .for_each(|(lhs, rhs)| assert_eq!(lhs, rhs)); + + call.named + .iter() + .zip(returned_call.named.iter()) + .for_each(|(lhs, rhs)| { + // Comparing the keys + assert_eq!(lhs.0.item, rhs.0.item); + + match (&lhs.1, &rhs.1) { + (None, None) => {} + (Some(a), Some(b)) => assert_eq!(a, b), + _ => panic!("not matching values"), + } + }); + } +} diff --git a/crates/nu-plugin/src/serializers/capnp/mod.rs b/crates/nu-plugin/src/serializers/capnp/mod.rs new file mode 100644 index 0000000000..74707cd327 --- /dev/null +++ b/crates/nu-plugin/src/serializers/capnp/mod.rs @@ -0,0 +1,43 @@ +mod call; +mod plugin_call; +mod signature; +mod value; + +use nu_protocol::ShellError; + +use crate::{plugin::PluginEncoder, protocol::PluginResponse}; + +#[derive(Clone)] +pub struct CapnpSerializer; + +impl PluginEncoder for CapnpSerializer { + fn encode_call( + &self, + plugin_call: &crate::protocol::PluginCall, + writer: &mut impl std::io::Write, + ) -> Result<(), nu_protocol::ShellError> { + plugin_call::encode_call(plugin_call, writer) + } + + fn decode_call( + &self, + reader: &mut impl std::io::BufRead, + ) -> Result { + plugin_call::decode_call(reader) + } + + fn encode_response( + &self, + plugin_response: &PluginResponse, + writer: &mut impl std::io::Write, + ) -> Result<(), ShellError> { + plugin_call::encode_response(plugin_response, writer) + } + + fn decode_response( + &self, + reader: &mut impl std::io::BufRead, + ) -> Result { + plugin_call::decode_response(reader) + } +} diff --git a/crates/nu-plugin/src/serializers/capnp/plugin_call.rs b/crates/nu-plugin/src/serializers/capnp/plugin_call.rs new file mode 100644 index 0000000000..fcaa27af0d --- /dev/null +++ b/crates/nu-plugin/src/serializers/capnp/plugin_call.rs @@ -0,0 +1,416 @@ +use super::signature::deserialize_signature; +use super::{call, signature, value}; +use crate::plugin_capnp::{plugin_call, plugin_response}; +use crate::protocol::{CallInfo, LabeledError, PluginCall, PluginResponse}; +use capnp::serialize; +use nu_protocol::{ShellError, Signature, Span}; + +pub fn encode_call( + plugin_call: &PluginCall, + writer: &mut impl std::io::Write, +) -> Result<(), ShellError> { + let mut message = ::capnp::message::Builder::new_default(); + + let mut builder = message.init_root::(); + + match &plugin_call { + PluginCall::Signature => builder.set_signature(()), + PluginCall::CallInfo(call_info) => { + let mut call_info_builder = builder.reborrow().init_call_info(); + + // Serializing name from the call + call_info_builder.set_name(call_info.name.as_str()); + + // Serializing argument information from the call + let call_builder = call_info_builder + .reborrow() + .get_call() + .map_err(|e| ShellError::PluginFailedToEncode(e.to_string()))?; + + call::serialize_call(&call_info.call, call_builder) + .map_err(|e| ShellError::PluginFailedToEncode(e.to_string()))?; + + // Serializing the input value from the call info + let value_builder = call_info_builder + .reborrow() + .get_input() + .map_err(|e| ShellError::PluginFailedToEncode(e.to_string()))?; + + value::serialize_value(&call_info.input, value_builder); + } + }; + + serialize::write_message(writer, &message) + .map_err(|e| ShellError::PluginFailedToEncode(e.to_string())) +} + +pub fn decode_call(reader: &mut impl std::io::BufRead) -> Result { + let message_reader = serialize::read_message(reader, ::capnp::message::ReaderOptions::new()) + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + let reader = message_reader + .get_root::() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + match reader.which() { + Err(capnp::NotInSchema(_)) => Err(ShellError::PluginFailedToDecode( + "value not in schema".into(), + )), + Ok(plugin_call::Signature(())) => Ok(PluginCall::Signature), + Ok(plugin_call::CallInfo(reader)) => { + let reader = reader.map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + let name = reader + .get_name() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + let call_reader = reader + .get_call() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + let call = call::deserialize_call(call_reader)?; + + let input_reader = reader + .get_input() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + let input = value::deserialize_value(input_reader, call.head)?; + + Ok(PluginCall::CallInfo(Box::new(CallInfo { + name: name.to_string(), + call, + input, + }))) + } + } +} + +pub fn encode_response( + plugin_response: &PluginResponse, + writer: &mut impl std::io::Write, +) -> Result<(), ShellError> { + let mut message = ::capnp::message::Builder::new_default(); + + let mut builder = message.init_root::(); + + match &plugin_response { + PluginResponse::Error(msg) => { + let mut error_builder = builder.reborrow().init_error(); + error_builder.set_label(&msg.label); + error_builder.set_msg(&msg.msg); + + if let Some(span) = msg.span { + let mut span_builder = error_builder.reborrow().init_span(); + span_builder.set_start(span.start as u64); + span_builder.set_end(span.end as u64); + } + } + PluginResponse::Signature(signatures) => { + let mut signature_list_builder = + builder.reborrow().init_signature(signatures.len() as u32); + + for (index, signature) in signatures.iter().enumerate() { + let signature_builder = signature_list_builder.reborrow().get(index as u32); + signature::serialize_signature(signature, signature_builder) + } + } + PluginResponse::Value(val) => { + let value_builder = builder.reborrow().init_value(); + value::serialize_value(val, value_builder); + } + }; + + serialize::write_message(writer, &message) + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string())) +} + +pub fn decode_response(reader: &mut impl std::io::BufRead) -> Result { + let message_reader = serialize::read_message(reader, ::capnp::message::ReaderOptions::new()) + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + let reader = message_reader + .get_root::() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + match reader.which() { + Err(capnp::NotInSchema(_)) => Err(ShellError::PluginFailedToDecode( + "value not in schema".into(), + )), + Ok(plugin_response::Error(reader)) => { + let reader = reader.map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + let msg = reader + .get_msg() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + let label = reader + .get_label() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + let span = if reader.has_span() { + let span = reader + .get_span() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + Some(Span { + start: span.get_start() as usize, + end: span.get_end() as usize, + }) + } else { + None + }; + + let error = LabeledError { + label: label.into(), + msg: msg.into(), + span, + }; + + Ok(PluginResponse::Error(error)) + } + Ok(plugin_response::Signature(reader)) => { + let reader = reader.map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + let signatures = reader + .iter() + .map(deserialize_signature) + .collect::, ShellError>>()?; + + Ok(PluginResponse::Signature(signatures)) + } + Ok(plugin_response::Value(reader)) => { + let reader = reader.map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + let span = reader + .get_span() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + let val = value::deserialize_value( + reader, + Span { + start: span.get_start() as usize, + end: span.get_end() as usize, + }, + ) + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + Ok(PluginResponse::Value(Box::new(val))) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::protocol::{EvaluatedCall, LabeledError, PluginCall, PluginResponse}; + use nu_protocol::{Signature, Span, Spanned, SyntaxShape, Value}; + + #[test] + fn callinfo_round_trip_signature() { + let plugin_call = PluginCall::Signature; + + let mut buffer: Vec = Vec::new(); + encode_call(&plugin_call, &mut buffer).expect("unable to serialize message"); + let returned = decode_call(&mut buffer.as_slice()).expect("unable to deserialize message"); + + match returned { + PluginCall::Signature => {} + PluginCall::CallInfo(_) => panic!("decoded into wrong value"), + } + } + + #[test] + fn callinfo_round_trip_callinfo() { + let name = "test".to_string(); + + let input = Value::Bool { + val: false, + span: Span { start: 1, end: 20 }, + }; + + let call = EvaluatedCall { + head: Span { start: 0, end: 10 }, + positional: vec![ + Value::Float { + val: 1.0, + span: Span { start: 0, end: 10 }, + }, + Value::String { + val: "something".into(), + span: Span { start: 0, end: 10 }, + }, + ], + named: vec![( + Spanned { + item: "name".to_string(), + span: Span { start: 0, end: 10 }, + }, + Some(Value::Float { + val: 1.0, + span: Span { start: 0, end: 10 }, + }), + )], + }; + + let plugin_call = PluginCall::CallInfo(Box::new(CallInfo { + name: name.clone(), + call: call.clone(), + input: input.clone(), + })); + + let mut buffer: Vec = Vec::new(); + encode_call(&plugin_call, &mut buffer).expect("unable to serialize message"); + let returned = decode_call(&mut buffer.as_slice()).expect("unable to deserialize message"); + + match returned { + PluginCall::Signature => panic!("returned wrong call type"), + PluginCall::CallInfo(call_info) => { + assert_eq!(name, call_info.name); + assert_eq!(input, call_info.input); + assert_eq!(call.head, call_info.call.head); + assert_eq!(call.positional.len(), call_info.call.positional.len()); + + call.positional + .iter() + .zip(call_info.call.positional.iter()) + .for_each(|(lhs, rhs)| assert_eq!(lhs, rhs)); + + call.named + .iter() + .zip(call_info.call.named.iter()) + .for_each(|(lhs, rhs)| { + // Comparing the keys + assert_eq!(lhs.0.item, rhs.0.item); + + match (&lhs.1, &rhs.1) { + (None, None) => {} + (Some(a), Some(b)) => assert_eq!(a, b), + _ => panic!("not matching values"), + } + }); + } + } + } + + #[test] + fn response_round_trip_signature() { + let signature = Signature::build("nu-plugin") + .required("first", SyntaxShape::String, "first required") + .required("second", SyntaxShape::Int, "second required") + .required_named("first_named", SyntaxShape::String, "first named", Some('f')) + .required_named( + "second_named", + SyntaxShape::String, + "second named", + Some('s'), + ) + .rest("remaining", SyntaxShape::Int, "remaining"); + + let response = PluginResponse::Signature(vec![signature.clone()]); + + let mut buffer: Vec = Vec::new(); + encode_response(&response, &mut buffer).expect("unable to serialize message"); + let returned = + decode_response(&mut buffer.as_slice()).expect("unable to deserialize message"); + + match returned { + PluginResponse::Error(_) => panic!("returned wrong call type"), + PluginResponse::Value(_) => panic!("returned wrong call type"), + PluginResponse::Signature(returned_signature) => { + assert!(returned_signature.len() == 1); + assert_eq!(signature.name, returned_signature[0].name); + assert_eq!(signature.usage, returned_signature[0].usage); + assert_eq!(signature.extra_usage, returned_signature[0].extra_usage); + assert_eq!(signature.is_filter, returned_signature[0].is_filter); + + signature + .required_positional + .iter() + .zip(returned_signature[0].required_positional.iter()) + .for_each(|(lhs, rhs)| assert_eq!(lhs, rhs)); + + signature + .optional_positional + .iter() + .zip(returned_signature[0].optional_positional.iter()) + .for_each(|(lhs, rhs)| assert_eq!(lhs, rhs)); + + signature + .named + .iter() + .zip(returned_signature[0].named.iter()) + .for_each(|(lhs, rhs)| assert_eq!(lhs, rhs)); + + assert_eq!( + signature.rest_positional, + returned_signature[0].rest_positional, + ); + } + } + } + + #[test] + fn response_round_trip_value() { + let value = Value::Int { + val: 10, + span: Span { start: 2, end: 30 }, + }; + + let response = PluginResponse::Value(Box::new(value.clone())); + + let mut buffer: Vec = Vec::new(); + encode_response(&response, &mut buffer).expect("unable to serialize message"); + let returned = + decode_response(&mut buffer.as_slice()).expect("unable to deserialize message"); + + match returned { + PluginResponse::Error(_) => panic!("returned wrong call type"), + PluginResponse::Signature(_) => panic!("returned wrong call type"), + PluginResponse::Value(returned_value) => { + assert_eq!(&value, returned_value.as_ref()) + } + } + } + + #[test] + fn response_round_trip_error() { + let error = LabeledError { + label: "label".into(), + msg: "msg".into(), + span: Some(Span { start: 2, end: 30 }), + }; + let response = PluginResponse::Error(error.clone()); + + let mut buffer: Vec = Vec::new(); + encode_response(&response, &mut buffer).expect("unable to serialize message"); + let returned = + decode_response(&mut buffer.as_slice()).expect("unable to deserialize message"); + + match returned { + PluginResponse::Error(msg) => assert_eq!(error, msg), + PluginResponse::Signature(_) => panic!("returned wrong call type"), + PluginResponse::Value(_) => panic!("returned wrong call type"), + } + } + + #[test] + fn response_round_trip_error_none() { + let error = LabeledError { + label: "label".into(), + msg: "msg".into(), + span: None, + }; + let response = PluginResponse::Error(error.clone()); + + let mut buffer: Vec = Vec::new(); + encode_response(&response, &mut buffer).expect("unable to serialize message"); + let returned = + decode_response(&mut buffer.as_slice()).expect("unable to deserialize message"); + + match returned { + PluginResponse::Error(msg) => assert_eq!(error, msg), + PluginResponse::Signature(_) => panic!("returned wrong call type"), + PluginResponse::Value(_) => panic!("returned wrong call type"), + } + } +} diff --git a/crates/nu-plugin/src/serializers/capnp/schema/plugin.capnp b/crates/nu-plugin/src/serializers/capnp/schema/plugin.capnp new file mode 100644 index 0000000000..41533d1903 --- /dev/null +++ b/crates/nu-plugin/src/serializers/capnp/schema/plugin.capnp @@ -0,0 +1,150 @@ +@0xb299d30dc02d72bc; +# Schema representing all the structs that are used to comunicate with +# the plugins. +# This schema, together with the command capnp proto is used to generate +# the rust file that defines the serialization/deserialization objects +# required to comunicate with the plugins created for nushell +# +# If you modify the schema remember to compile it to generate the corresponding +# rust file and place that file into the main nu-plugin folder. +# After compiling, you may need to run cargo fmt on the file so it passes the CI + +struct Err(T) { + union { + err @0 :Text; + ok @1 :T; + } +} + +struct Map(Key, Value) { + struct Entry { + key @0 :Key; + value @1 :Value; + } + entries @0 :List(Entry); +} + +# Main plugin structures +struct Span { + start @0 :UInt64; + end @1 :UInt64; +} + +# Resulting value from plugin +struct Value { + span @0: Span; + + union { + void @1 :Void; + bool @2 :Bool; + int @3 :Int64; + float @4 :Float64; + string @5 :Text; + list @6 :List(Value); + record @7: Record; + } +} + +struct Record { + cols @0 :List(Text); + vals @1 :List(Value); +} + +# Structs required to define the plugin signature +struct Signature { + name @0 :Text; + usage @1 :Text; + extraUsage @2 :Text; + requiredPositional @3 :List(Argument); + optionalPositional @4 :List(Argument); + # Optional value. Check for existence when deserializing + rest @5 :Argument; + named @6 :List(Flag); + isFilter @7 :Bool; + category @8 :Category; +} + +enum Category { + default @0; + conversions @1; + core @2; + date @3; + env @4; + experimental @5; + filesystem @6; + filters @7; + formats @8; + math @9; + network @10; + random @11; + platform @12; + shells @13; + strings @14; + system @15; + viewers @16; + hash @17; + generators @18; +} + +struct Flag { + long @0 :Text; + # Optional value. Check for existence when deserializing (has_short) + short @1 :Text; + arg @2 :Shape; + required @3 :Bool; + desc @4 :Text; +} + +struct Argument { + name @0 :Text; + desc @1 :Text; + shape @2 :Shape; +} + +# If we require more complex signatures for the plugins this could be +# changed to a union +enum Shape { + none @0; + any @1; + string @2; + number @3; + int @4; + boolean @5; +} + +struct EvaluatedCall { + head @0: Span; + positional @1 :List(Value); + # The value in the map can be optional + # Check for existence when deserializing + named @2 :Map(Text, Value); +} + +struct CallInfo { + name @0 :Text; + call @1 :EvaluatedCall; + input @2 :Value; +} + +# Main communication structs with the plugin +struct PluginCall { + union { + signature @0 :Void; + callInfo @1 :CallInfo; + } +} + +struct PluginResponse { + union { + error @0 :LabeledError; + signature @1 :List(Signature); + value @2 :Value; + } +} + +struct LabeledError { + label @0 :Text; + msg @1 :Text; + # Optional Value. When decoding check if it exists (has_span) + span @2 :Span; +} diff --git a/crates/nu-plugin/src/serializers/capnp/signature.rs b/crates/nu-plugin/src/serializers/capnp/signature.rs new file mode 100644 index 0000000000..51f19da743 --- /dev/null +++ b/crates/nu-plugin/src/serializers/capnp/signature.rs @@ -0,0 +1,392 @@ +use crate::plugin_capnp::{argument, flag, signature, Category as PluginCategory, Shape}; +use nu_protocol::{Category, Flag, PositionalArg, ShellError, Signature, SyntaxShape}; + +pub(crate) fn serialize_signature(signature: &Signature, mut builder: signature::Builder) { + builder.set_name(signature.name.as_str()); + builder.set_usage(signature.usage.as_str()); + builder.set_extra_usage(signature.extra_usage.as_str()); + builder.set_is_filter(signature.is_filter); + + match signature.category { + Category::Default => builder.set_category(PluginCategory::Default), + Category::Conversions => builder.set_category(PluginCategory::Conversions), + Category::Core => builder.set_category(PluginCategory::Core), + Category::Date => builder.set_category(PluginCategory::Date), + Category::Env => builder.set_category(PluginCategory::Env), + Category::Experimental => builder.set_category(PluginCategory::Experimental), + Category::FileSystem => builder.set_category(PluginCategory::Filesystem), + Category::Filters => builder.set_category(PluginCategory::Filters), + Category::Formats => builder.set_category(PluginCategory::Formats), + Category::Math => builder.set_category(PluginCategory::Math), + Category::Network => builder.set_category(PluginCategory::Network), + Category::Random => builder.set_category(PluginCategory::Random), + Category::Platform => builder.set_category(PluginCategory::Platform), + Category::Shells => builder.set_category(PluginCategory::Shells), + Category::Strings => builder.set_category(PluginCategory::Strings), + Category::System => builder.set_category(PluginCategory::System), + Category::Viewers => builder.set_category(PluginCategory::Viewers), + Category::Hash => builder.set_category(PluginCategory::Hash), + Category::Generators => builder.set_category(PluginCategory::Generators), + _ => builder.set_category(PluginCategory::Default), + } + + // Serializing list of required arguments + let mut required_list = builder + .reborrow() + .init_required_positional(signature.required_positional.len() as u32); + + for (index, arg) in signature.required_positional.iter().enumerate() { + let inner_builder = required_list.reborrow().get(index as u32); + serialize_argument(arg, inner_builder) + } + + // Serializing list of optional arguments + let mut optional_list = builder + .reborrow() + .init_optional_positional(signature.optional_positional.len() as u32); + + for (index, arg) in signature.optional_positional.iter().enumerate() { + let inner_builder = optional_list.reborrow().get(index as u32); + serialize_argument(arg, inner_builder) + } + + // Serializing rest argument + if let Some(arg) = &signature.rest_positional { + let rest_argument = builder.reborrow().init_rest(); + serialize_argument(arg, rest_argument) + } + + // Serializing the named arguments + let mut named_list = builder.reborrow().init_named(signature.named.len() as u32); + for (index, arg) in signature.named.iter().enumerate() { + let inner_builder = named_list.reborrow().get(index as u32); + serialize_flag(arg, inner_builder) + } +} + +fn serialize_argument(arg: &PositionalArg, mut builder: argument::Builder) { + builder.set_name(arg.name.as_str()); + builder.set_desc(arg.desc.as_str()); + + match arg.shape { + SyntaxShape::Boolean => builder.set_shape(Shape::Boolean), + SyntaxShape::String => builder.set_shape(Shape::String), + SyntaxShape::Int => builder.set_shape(Shape::Int), + SyntaxShape::Number => builder.set_shape(Shape::Number), + _ => builder.set_shape(Shape::Any), + } +} + +fn serialize_flag(arg: &Flag, mut builder: flag::Builder) { + builder.set_long(arg.long.as_str()); + builder.set_required(arg.required); + builder.set_desc(arg.desc.as_str()); + + if let Some(val) = arg.short { + let mut inner_builder = builder.reborrow().init_short(1); + inner_builder.push_str(format!("{}", val).as_str()); + } + + match &arg.arg { + None => builder.set_arg(Shape::None), + Some(shape) => match shape { + SyntaxShape::Boolean => builder.set_arg(Shape::Boolean), + SyntaxShape::String => builder.set_arg(Shape::String), + SyntaxShape::Int => builder.set_arg(Shape::Int), + SyntaxShape::Number => builder.set_arg(Shape::Number), + _ => builder.set_arg(Shape::Any), + }, + } +} + +pub(crate) fn deserialize_signature(reader: signature::Reader) -> Result { + let name = reader + .get_name() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + let usage = reader + .get_usage() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + let extra_usage = reader + .get_extra_usage() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + let is_filter = reader.get_is_filter(); + + let category = match reader + .get_category() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))? + { + PluginCategory::Default => Category::Default, + PluginCategory::Conversions => Category::Conversions, + PluginCategory::Core => Category::Core, + PluginCategory::Date => Category::Date, + PluginCategory::Env => Category::Env, + PluginCategory::Experimental => Category::Experimental, + PluginCategory::Filesystem => Category::FileSystem, + PluginCategory::Filters => Category::Filters, + PluginCategory::Formats => Category::Formats, + PluginCategory::Math => Category::Math, + PluginCategory::Strings => Category::Strings, + PluginCategory::System => Category::System, + PluginCategory::Viewers => Category::Viewers, + PluginCategory::Network => Category::Network, + PluginCategory::Random => Category::Random, + PluginCategory::Platform => Category::Platform, + PluginCategory::Shells => Category::Shells, + PluginCategory::Hash => Category::Hash, + PluginCategory::Generators => Category::Generators, + }; + + // Deserializing required arguments + let required_list = reader + .get_required_positional() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + let required_positional = required_list + .iter() + .map(deserialize_argument) + .collect::, ShellError>>()?; + + // Deserializing optional arguments + let optional_list = reader + .get_optional_positional() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + let optional_positional = optional_list + .iter() + .map(deserialize_argument) + .collect::, ShellError>>()?; + + // Deserializing rest arguments + let rest_positional = if reader.has_rest() { + let argument_reader = reader + .get_rest() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + Some(deserialize_argument(argument_reader)?) + } else { + None + }; + + // Deserializing named arguments + let named_list = reader + .get_named() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + let named = named_list + .iter() + .map(deserialize_flag) + .collect::, ShellError>>()?; + + Ok(Signature { + name: name.to_string(), + usage: usage.to_string(), + extra_usage: extra_usage.to_string(), + required_positional, + optional_positional, + rest_positional, + named, + is_filter, + creates_scope: false, + category, + }) +} + +fn deserialize_argument(reader: argument::Reader) -> Result { + let name = reader + .get_name() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + let desc = reader + .get_desc() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + let shape = reader + .get_shape() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + let shape = match shape { + Shape::String => SyntaxShape::String, + Shape::Int => SyntaxShape::Int, + Shape::Number => SyntaxShape::Number, + Shape::Boolean => SyntaxShape::Boolean, + Shape::Any => SyntaxShape::Any, + Shape::None => SyntaxShape::Any, + }; + + Ok(PositionalArg { + name: name.to_string(), + desc: desc.to_string(), + shape, + var_id: None, + }) +} + +fn deserialize_flag(reader: flag::Reader) -> Result { + let long = reader + .get_long() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + let desc = reader + .get_desc() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + let required = reader.get_required(); + + let short = if reader.has_short() { + let short_reader = reader + .get_short() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + short_reader.chars().next() + } else { + None + }; + + let arg = reader + .get_arg() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + let arg = match arg { + Shape::None => None, + Shape::Any => Some(SyntaxShape::Any), + Shape::String => Some(SyntaxShape::String), + Shape::Int => Some(SyntaxShape::Int), + Shape::Number => Some(SyntaxShape::Number), + Shape::Boolean => Some(SyntaxShape::Boolean), + }; + + Ok(Flag { + long: long.to_string(), + short, + arg, + required, + desc: desc.to_string(), + var_id: None, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use capnp::serialize; + use nu_protocol::{Category, Signature, SyntaxShape}; + + pub fn write_buffer( + signature: &Signature, + writer: &mut impl std::io::Write, + ) -> Result<(), ShellError> { + let mut message = ::capnp::message::Builder::new_default(); + + let builder = message.init_root::(); + + serialize_signature(signature, builder); + + serialize::write_message(writer, &message) + .map_err(|e| ShellError::PluginFailedToEncode(e.to_string())) + } + + pub fn read_buffer(reader: &mut impl std::io::BufRead) -> Result { + let message_reader = + serialize::read_message(reader, ::capnp::message::ReaderOptions::new()).unwrap(); + + let reader = message_reader + .get_root::() + .map_err(|e| ShellError::PluginFailedToEncode(e.to_string()))?; + + deserialize_signature(reader) + } + + #[test] + fn value_round_trip() { + let signature = Signature::build("nu-plugin") + .required("first", SyntaxShape::String, "first required") + .required("second", SyntaxShape::Int, "second required") + .required_named("first_named", SyntaxShape::String, "first named", Some('f')) + .required_named("second_named", SyntaxShape::Int, "first named", Some('s')) + .required_named("name", SyntaxShape::String, "first named", Some('n')) + .required_named("string", SyntaxShape::String, "second named", Some('x')) + .switch("switch", "some switch", None) + .rest("remaining", SyntaxShape::Int, "remaining") + .category(Category::Conversions); + + let mut buffer: Vec = Vec::new(); + write_buffer(&signature, &mut buffer).expect("unable to serialize message"); + let returned_signature = + read_buffer(&mut buffer.as_slice()).expect("unable to deserialize message"); + + assert_eq!(signature.name, returned_signature.name); + assert_eq!(signature.usage, returned_signature.usage); + assert_eq!(signature.extra_usage, returned_signature.extra_usage); + assert_eq!(signature.is_filter, returned_signature.is_filter); + assert_eq!(signature.category, returned_signature.category); + + signature + .required_positional + .iter() + .zip(returned_signature.required_positional.iter()) + .for_each(|(lhs, rhs)| assert_eq!(lhs, rhs)); + + signature + .optional_positional + .iter() + .zip(returned_signature.optional_positional.iter()) + .for_each(|(lhs, rhs)| assert_eq!(lhs, rhs)); + + signature + .named + .iter() + .zip(returned_signature.named.iter()) + .for_each(|(lhs, rhs)| assert_eq!(lhs, rhs)); + + assert_eq!( + signature.rest_positional, + returned_signature.rest_positional, + ); + } + + #[test] + fn value_round_trip_2() { + let signature = Signature::build("test-1") + .desc("Signature test 1 for plugin. Returns Value::Nothing") + .required("a", SyntaxShape::Int, "required integer value") + .required("b", SyntaxShape::String, "required string value") + .optional("opt", SyntaxShape::Boolean, "Optional boolean") + .switch("flag", "a flag for the signature", Some('f')) + .named("named", SyntaxShape::String, "named string", Some('n')) + .category(Category::Experimental); + + let mut buffer: Vec = Vec::new(); + write_buffer(&signature, &mut buffer).expect("unable to serialize message"); + let returned_signature = + read_buffer(&mut buffer.as_slice()).expect("unable to deserialize message"); + + assert_eq!(signature.name, returned_signature.name); + assert_eq!(signature.usage, returned_signature.usage); + assert_eq!(signature.extra_usage, returned_signature.extra_usage); + assert_eq!(signature.is_filter, returned_signature.is_filter); + assert_eq!(signature.category, returned_signature.category); + + signature + .required_positional + .iter() + .zip(returned_signature.required_positional.iter()) + .for_each(|(lhs, rhs)| assert_eq!(lhs, rhs)); + + signature + .optional_positional + .iter() + .zip(returned_signature.optional_positional.iter()) + .for_each(|(lhs, rhs)| assert_eq!(lhs, rhs)); + + signature + .named + .iter() + .zip(returned_signature.named.iter()) + .for_each(|(lhs, rhs)| assert_eq!(lhs, rhs)); + + assert_eq!( + signature.rest_positional, + returned_signature.rest_positional, + ); + } +} diff --git a/crates/nu-plugin/src/serializers/capnp/value.rs b/crates/nu-plugin/src/serializers/capnp/value.rs new file mode 100644 index 0000000000..1cef8a8766 --- /dev/null +++ b/crates/nu-plugin/src/serializers/capnp/value.rs @@ -0,0 +1,375 @@ +use crate::plugin_capnp::value; +use nu_protocol::{ShellError, Span, Value}; + +pub(crate) fn serialize_value(value: &Value, mut builder: value::Builder) { + let value_span = match value { + Value::Nothing { span } => { + builder.set_void(()); + *span + } + Value::Bool { val, span } => { + builder.set_bool(*val); + *span + } + Value::Int { val, span } => { + builder.set_int(*val); + *span + } + Value::Float { val, span } => { + builder.set_float(*val); + *span + } + Value::String { val, span } => { + builder.set_string(val); + *span + } + Value::Record { cols, vals, span } => { + let mut record_builder = builder.reborrow().init_record(); + + let mut cols_builder = record_builder.reborrow().init_cols(cols.len() as u32); + cols.iter() + .enumerate() + .for_each(|(index, col)| cols_builder.set(index as u32, col.as_str())); + + let mut values_builder = record_builder.reborrow().init_vals(vals.len() as u32); + vals.iter().enumerate().for_each(|(index, value)| { + let inner_builder = values_builder.reborrow().get(index as u32); + serialize_value(value, inner_builder); + }); + + *span + } + Value::List { vals, span } => { + let mut list_builder = builder.reborrow().init_list(vals.len() as u32); + for (index, value) in vals.iter().enumerate() { + let inner_builder = list_builder.reborrow().get(index as u32); + serialize_value(value, inner_builder); + } + + *span + } + _ => { + // If there is the need to pass other type of value to the plugin + // we have to define the encoding for that object in this match + + // FIXME: put this in a proper span + Span { start: 0, end: 0 } + } + }; + + let mut span = builder.reborrow().init_span(); + span.set_start(value_span.start as u64); + span.set_end(value_span.end as u64); +} + +pub(crate) fn deserialize_value(reader: value::Reader, head: Span) -> Result { + let span_reader = reader + .get_span() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + let span = Span { + start: span_reader.get_start() as usize, + end: span_reader.get_end() as usize, + }; + + match reader.which() { + Ok(value::Void(())) => Ok(Value::Nothing { span }), + Ok(value::Bool(val)) => Ok(Value::Bool { val, span }), + Ok(value::Int(val)) => Ok(Value::Int { val, span }), + Ok(value::Float(val)) => Ok(Value::Float { val, span }), + Ok(value::String(val)) => { + let string = val + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))? + .to_string(); + Ok(Value::String { val: string, span }) + } + Ok(value::Record(record)) => { + let record = record.map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + let cols = record + .get_cols() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))? + .iter() + .map(|col| { + col.map_err(|e| ShellError::PluginFailedToDecode(e.to_string())) + .map(|col| col.to_string()) + }) + .collect::, ShellError>>()?; + + let vals = record + .get_vals() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))? + .iter() + .map(move |x| deserialize_value(x, span)) + .collect::, ShellError>>()?; + + Ok(Value::Record { cols, vals, span }) + } + Ok(value::List(vals)) => { + let values = vals.map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + let values_list = values + .iter() + .map(move |x| deserialize_value(x, span)) + .collect::, ShellError>>()?; + + Ok(Value::List { + vals: values_list, + span, + }) + } + Err(capnp::NotInSchema(_)) => Ok(Value::Nothing { span: head }), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use capnp::serialize; + use nu_protocol::{Span, Value}; + + pub fn write_buffer(value: &Value, writer: &mut impl std::io::Write) -> Result<(), ShellError> { + let mut message = ::capnp::message::Builder::new_default(); + + let mut builder = message.init_root::(); + + serialize_value(value, builder.reborrow()); + + serialize::write_message(writer, &message) + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string())) + } + + pub fn read_buffer(reader: &mut impl std::io::BufRead) -> Result { + let message_reader = + serialize::read_message(reader, ::capnp::message::ReaderOptions::new()).unwrap(); + + let reader = message_reader + .get_root::() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + let span = reader + .get_span() + .map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?; + + deserialize_value( + reader.reborrow(), + Span { + start: span.get_start() as usize, + end: span.get_end() as usize, + }, + ) + } + + #[test] + fn value_round_trip() { + let values = [ + Value::Bool { + val: false, + span: Span { start: 1, end: 20 }, + }, + Value::Int { + val: 10, + span: Span { start: 2, end: 30 }, + }, + Value::Float { + val: 10.0, + span: Span { start: 3, end: 40 }, + }, + Value::String { + val: "a string".into(), + span: Span { start: 4, end: 50 }, + }, + ]; + + for value in values { + let mut buffer: Vec = Vec::new(); + write_buffer(&value, &mut buffer).expect("unable to serialize message"); + let returned_value = + read_buffer(&mut buffer.as_slice()).expect("unable to deserialize message"); + + assert_eq!(value, returned_value) + } + } + + #[test] + fn value_nothing_round_trip() { + // Since nothing doesn't implement PartialOrd, we only compare that the + // encoded and decoded spans are correct + let value = Value::Nothing { + span: Span { start: 0, end: 10 }, + }; + + let mut buffer: Vec = Vec::new(); + write_buffer(&value, &mut buffer).expect("unable to serialize message"); + let returned_value = + read_buffer(&mut buffer.as_slice()).expect("unable to deserialize message"); + + assert_eq!( + value.span().expect("span"), + returned_value.span().expect("span") + ) + } + + #[test] + fn list_round_trip() { + let values = vec![ + Value::Bool { + val: false, + span: Span { start: 1, end: 20 }, + }, + Value::Int { + val: 10, + span: Span { start: 2, end: 30 }, + }, + Value::Float { + val: 10.0, + span: Span { start: 3, end: 40 }, + }, + Value::String { + val: "a string".into(), + span: Span { start: 4, end: 50 }, + }, + ]; + + let value = Value::List { + vals: values, + span: Span { start: 1, end: 10 }, + }; + + let mut buffer: Vec = Vec::new(); + write_buffer(&value, &mut buffer).expect("unable to serialize message"); + let returned_value = + read_buffer(&mut buffer.as_slice()).expect("unable to deserialize message"); + + assert_eq!( + value.span().expect("span"), + returned_value.span().expect("span") + ) + } + + #[test] + fn nested_list_round_trip() { + let inner_values = vec![ + Value::Bool { + val: false, + span: Span { start: 1, end: 20 }, + }, + Value::Int { + val: 10, + span: Span { start: 2, end: 30 }, + }, + Value::Float { + val: 10.0, + span: Span { start: 3, end: 40 }, + }, + Value::String { + val: "inner string".into(), + span: Span { start: 4, end: 50 }, + }, + ]; + + let values = vec![ + Value::Bool { + val: true, + span: Span { start: 1, end: 20 }, + }, + Value::Int { + val: 66, + span: Span { start: 2, end: 30 }, + }, + Value::Float { + val: 66.6, + span: Span { start: 3, end: 40 }, + }, + Value::String { + val: "a string".into(), + span: Span { start: 4, end: 50 }, + }, + Value::List { + vals: inner_values, + span: Span { start: 5, end: 60 }, + }, + ]; + + let value = Value::List { + vals: values, + span: Span { start: 1, end: 10 }, + }; + + let mut buffer: Vec = Vec::new(); + write_buffer(&value, &mut buffer).expect("unable to serialize message"); + let returned_value = + read_buffer(&mut buffer.as_slice()).expect("unable to deserialize message"); + + assert_eq!( + value.span().expect("span"), + returned_value.span().expect("span") + ) + } + + #[test] + fn record_round_trip() { + let inner_values = vec![ + Value::Bool { + val: false, + span: Span { start: 1, end: 20 }, + }, + Value::Int { + val: 10, + span: Span { start: 2, end: 30 }, + }, + Value::Float { + val: 10.0, + span: Span { start: 3, end: 40 }, + }, + Value::String { + val: "inner string".into(), + span: Span { start: 4, end: 50 }, + }, + ]; + + let vals = vec![ + Value::Bool { + val: true, + span: Span { start: 1, end: 20 }, + }, + Value::Int { + val: 66, + span: Span { start: 2, end: 30 }, + }, + Value::Float { + val: 66.6, + span: Span { start: 3, end: 40 }, + }, + Value::String { + val: "a string".into(), + span: Span { start: 4, end: 50 }, + }, + Value::List { + vals: inner_values, + span: Span { start: 5, end: 60 }, + }, + ]; + + let cols = vec![ + "bool".to_string(), + "int".to_string(), + "float".to_string(), + "string".to_string(), + "list".to_string(), + ]; + + let record = Value::Record { + cols, + vals, + span: Span { start: 1, end: 20 }, + }; + + let mut buffer: Vec = Vec::new(); + write_buffer(&record, &mut buffer).expect("unable to serialize message"); + let returned_record = + read_buffer(&mut buffer.as_slice()).expect("unable to deserialize message"); + + assert_eq!(record, returned_record) + } +} diff --git a/crates/nu-plugin/src/serializers/json.rs b/crates/nu-plugin/src/serializers/json.rs new file mode 100644 index 0000000000..b21ccae1b4 --- /dev/null +++ b/crates/nu-plugin/src/serializers/json.rs @@ -0,0 +1,284 @@ +use nu_protocol::ShellError; + +use crate::{plugin::PluginEncoder, protocol::PluginResponse}; + +#[derive(Clone)] +pub struct JsonSerializer; + +impl PluginEncoder for JsonSerializer { + fn encode_call( + &self, + plugin_call: &crate::protocol::PluginCall, + writer: &mut impl std::io::Write, + ) -> Result<(), nu_protocol::ShellError> { + serde_json::to_writer(writer, plugin_call) + .map_err(|err| ShellError::PluginFailedToEncode(err.to_string())) + } + + fn decode_call( + &self, + reader: &mut impl std::io::BufRead, + ) -> Result { + serde_json::from_reader(reader) + .map_err(|err| ShellError::PluginFailedToEncode(err.to_string())) + } + + fn encode_response( + &self, + plugin_response: &PluginResponse, + writer: &mut impl std::io::Write, + ) -> Result<(), ShellError> { + serde_json::to_writer(writer, plugin_response) + .map_err(|err| ShellError::PluginFailedToEncode(err.to_string())) + } + + fn decode_response( + &self, + reader: &mut impl std::io::BufRead, + ) -> Result { + serde_json::from_reader(reader) + .map_err(|err| ShellError::PluginFailedToEncode(err.to_string())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::protocol::{CallInfo, EvaluatedCall, LabeledError, PluginCall, PluginResponse}; + use nu_protocol::{Signature, Span, Spanned, SyntaxShape, Value}; + + #[test] + fn callinfo_round_trip_signature() { + let plugin_call = PluginCall::Signature; + let encoder = JsonSerializer {}; + + let mut buffer: Vec = Vec::new(); + encoder + .encode_call(&plugin_call, &mut buffer) + .expect("unable to serialize message"); + let returned = encoder + .decode_call(&mut buffer.as_slice()) + .expect("unable to deserialize message"); + + match returned { + PluginCall::Signature => {} + PluginCall::CallInfo(_) => panic!("decoded into wrong value"), + } + } + + #[test] + fn callinfo_round_trip_callinfo() { + let name = "test".to_string(); + + let input = Value::Bool { + val: false, + span: Span { start: 1, end: 20 }, + }; + + let call = EvaluatedCall { + head: Span { start: 0, end: 10 }, + positional: vec![ + Value::Float { + val: 1.0, + span: Span { start: 0, end: 10 }, + }, + Value::String { + val: "something".into(), + span: Span { start: 0, end: 10 }, + }, + ], + named: vec![( + Spanned { + item: "name".to_string(), + span: Span { start: 0, end: 10 }, + }, + Some(Value::Float { + val: 1.0, + span: Span { start: 0, end: 10 }, + }), + )], + }; + + let plugin_call = PluginCall::CallInfo(Box::new(CallInfo { + name: name.clone(), + call: call.clone(), + input: input.clone(), + })); + + let encoder = JsonSerializer {}; + let mut buffer: Vec = Vec::new(); + encoder + .encode_call(&plugin_call, &mut buffer) + .expect("unable to serialize message"); + let returned = encoder + .decode_call(&mut buffer.as_slice()) + .expect("unable to deserialize message"); + + match returned { + PluginCall::Signature => panic!("returned wrong call type"), + PluginCall::CallInfo(call_info) => { + assert_eq!(name, call_info.name); + assert_eq!(input, call_info.input); + assert_eq!(call.head, call_info.call.head); + assert_eq!(call.positional.len(), call_info.call.positional.len()); + + call.positional + .iter() + .zip(call_info.call.positional.iter()) + .for_each(|(lhs, rhs)| assert_eq!(lhs, rhs)); + + call.named + .iter() + .zip(call_info.call.named.iter()) + .for_each(|(lhs, rhs)| { + // Comparing the keys + assert_eq!(lhs.0.item, rhs.0.item); + + match (&lhs.1, &rhs.1) { + (None, None) => {} + (Some(a), Some(b)) => assert_eq!(a, b), + _ => panic!("not matching values"), + } + }); + } + } + } + + #[test] + fn response_round_trip_signature() { + let signature = Signature::build("nu-plugin") + .required("first", SyntaxShape::String, "first required") + .required("second", SyntaxShape::Int, "second required") + .required_named("first_named", SyntaxShape::String, "first named", Some('f')) + .required_named( + "second_named", + SyntaxShape::String, + "second named", + Some('s'), + ) + .rest("remaining", SyntaxShape::Int, "remaining"); + + let response = PluginResponse::Signature(vec![signature.clone()]); + + let encoder = JsonSerializer {}; + let mut buffer: Vec = Vec::new(); + encoder + .encode_response(&response, &mut buffer) + .expect("unable to serialize message"); + let returned = encoder + .decode_response(&mut buffer.as_slice()) + .expect("unable to deserialize message"); + + match returned { + PluginResponse::Error(_) => panic!("returned wrong call type"), + PluginResponse::Value(_) => panic!("returned wrong call type"), + PluginResponse::Signature(returned_signature) => { + assert!(returned_signature.len() == 1); + assert_eq!(signature.name, returned_signature[0].name); + assert_eq!(signature.usage, returned_signature[0].usage); + assert_eq!(signature.extra_usage, returned_signature[0].extra_usage); + assert_eq!(signature.is_filter, returned_signature[0].is_filter); + + signature + .required_positional + .iter() + .zip(returned_signature[0].required_positional.iter()) + .for_each(|(lhs, rhs)| assert_eq!(lhs, rhs)); + + signature + .optional_positional + .iter() + .zip(returned_signature[0].optional_positional.iter()) + .for_each(|(lhs, rhs)| assert_eq!(lhs, rhs)); + + signature + .named + .iter() + .zip(returned_signature[0].named.iter()) + .for_each(|(lhs, rhs)| assert_eq!(lhs, rhs)); + + assert_eq!( + signature.rest_positional, + returned_signature[0].rest_positional, + ); + } + } + } + + #[test] + fn response_round_trip_value() { + let value = Value::Int { + val: 10, + span: Span { start: 2, end: 30 }, + }; + + let response = PluginResponse::Value(Box::new(value.clone())); + + let encoder = JsonSerializer {}; + let mut buffer: Vec = Vec::new(); + encoder + .encode_response(&response, &mut buffer) + .expect("unable to serialize message"); + let returned = encoder + .decode_response(&mut buffer.as_slice()) + .expect("unable to deserialize message"); + + match returned { + PluginResponse::Error(_) => panic!("returned wrong call type"), + PluginResponse::Signature(_) => panic!("returned wrong call type"), + PluginResponse::Value(returned_value) => { + assert_eq!(&value, returned_value.as_ref()) + } + } + } + + #[test] + fn response_round_trip_error() { + let error = LabeledError { + label: "label".into(), + msg: "msg".into(), + span: Some(Span { start: 2, end: 30 }), + }; + let response = PluginResponse::Error(error.clone()); + + let encoder = JsonSerializer {}; + let mut buffer: Vec = Vec::new(); + encoder + .encode_response(&response, &mut buffer) + .expect("unable to serialize message"); + let returned = encoder + .decode_response(&mut buffer.as_slice()) + .expect("unable to deserialize message"); + + match returned { + PluginResponse::Error(msg) => assert_eq!(error, msg), + PluginResponse::Signature(_) => panic!("returned wrong call type"), + PluginResponse::Value(_) => panic!("returned wrong call type"), + } + } + + #[test] + fn response_round_trip_error_none() { + let error = LabeledError { + label: "label".into(), + msg: "msg".into(), + span: None, + }; + let response = PluginResponse::Error(error.clone()); + + let encoder = JsonSerializer {}; + let mut buffer: Vec = Vec::new(); + encoder + .encode_response(&response, &mut buffer) + .expect("unable to serialize message"); + let returned = encoder + .decode_response(&mut buffer.as_slice()) + .expect("unable to deserialize message"); + + match returned { + PluginResponse::Error(msg) => assert_eq!(error, msg), + PluginResponse::Signature(_) => panic!("returned wrong call type"), + PluginResponse::Value(_) => panic!("returned wrong call type"), + } + } +} diff --git a/crates/nu-plugin/src/serializers/mod.rs b/crates/nu-plugin/src/serializers/mod.rs new file mode 100644 index 0000000000..937732b122 --- /dev/null +++ b/crates/nu-plugin/src/serializers/mod.rs @@ -0,0 +1,74 @@ +use nu_protocol::ShellError; + +use crate::{ + plugin::PluginEncoder, + protocol::{PluginCall, PluginResponse}, +}; + +pub mod capnp; +pub mod json; + +#[derive(Clone)] +pub enum EncodingType { + Capnp(capnp::CapnpSerializer), + Json(json::JsonSerializer), +} + +impl EncodingType { + pub fn try_from_bytes(bytes: &[u8]) -> Option { + match bytes { + b"capnp" => Some(Self::Capnp(capnp::CapnpSerializer {})), + b"json" => Some(Self::Json(json::JsonSerializer {})), + _ => None, + } + } + + pub fn encode_call( + &self, + plugin_call: &PluginCall, + writer: &mut impl std::io::Write, + ) -> Result<(), ShellError> { + match self { + EncodingType::Capnp(encoder) => encoder.encode_call(plugin_call, writer), + EncodingType::Json(encoder) => encoder.encode_call(plugin_call, writer), + } + } + + pub fn decode_call( + &self, + reader: &mut impl std::io::BufRead, + ) -> Result { + match self { + EncodingType::Capnp(encoder) => encoder.decode_call(reader), + EncodingType::Json(encoder) => encoder.decode_call(reader), + } + } + + pub fn encode_response( + &self, + plugin_response: &PluginResponse, + writer: &mut impl std::io::Write, + ) -> Result<(), ShellError> { + match self { + EncodingType::Capnp(encoder) => encoder.encode_response(plugin_response, writer), + EncodingType::Json(encoder) => encoder.encode_response(plugin_response, writer), + } + } + + pub fn decode_response( + &self, + reader: &mut impl std::io::BufRead, + ) -> Result { + match self { + EncodingType::Capnp(encoder) => encoder.decode_response(reader), + EncodingType::Json(encoder) => encoder.decode_response(reader), + } + } + + pub fn to_str(&self) -> &'static str { + match self { + Self::Capnp(_) => "capnp", + Self::Json(_) => "json", + } + } +} diff --git a/crates/nu-pretty-hex/Cargo.toml b/crates/nu-pretty-hex/Cargo.toml index cff77a5bc5..fddc2563e3 100644 --- a/crates/nu-pretty-hex/Cargo.toml +++ b/crates/nu-pretty-hex/Cargo.toml @@ -1,10 +1,17 @@ [package] authors = ["Andrei Volnin ", "The Nu Project Contributors"] description = "Pretty hex dump of bytes slice in the common style." +<<<<<<< HEAD edition = "2018" license = "MIT" name = "nu-pretty-hex" version = "0.43.0" +======= +edition = "2021" +license = "MIT" +name = "nu-pretty-hex" +version = "0.41.0" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce [lib] doctest = false @@ -16,7 +23,11 @@ name = "nu_pretty_hex" path = "src/main.rs" [dependencies] +<<<<<<< HEAD nu-ansi-term = { path="../nu-ansi-term", version = "0.43.0" } +======= +nu-ansi-term = "0.42.0" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce rand = "0.8.3" [dev-dependencies] diff --git a/crates/nu-pretty-hex/src/main.rs b/crates/nu-pretty-hex/src/main.rs index 4156cb5c2a..1863770b98 100644 --- a/crates/nu-pretty-hex/src/main.rs +++ b/crates/nu-pretty-hex/src/main.rs @@ -7,6 +7,10 @@ fn main() { width: 16, group: 4, chunk: 1, +<<<<<<< HEAD +======= + address_offset: 0, +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce skip: Some(10), // length: Some(5), // length: None, diff --git a/crates/nu-pretty-hex/src/pretty_hex.rs b/crates/nu-pretty-hex/src/pretty_hex.rs index 99ebaf022c..0cfb2a7b2c 100644 --- a/crates/nu-pretty-hex/src/pretty_hex.rs +++ b/crates/nu-pretty-hex/src/pretty_hex.rs @@ -57,6 +57,11 @@ pub struct HexConfig { pub group: usize, /// Source bytes per chunk (word). 0 for single word. pub chunk: usize, +<<<<<<< HEAD +======= + /// Offset to start counting addresses from + pub address_offset: usize, +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce /// Bytes from 0 to skip pub skip: Option, /// Length to return @@ -73,6 +78,10 @@ impl Default for HexConfig { width: 16, group: 4, chunk: 1, +<<<<<<< HEAD +======= + address_offset: 0, +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce skip: None, length: None, } @@ -164,6 +173,11 @@ where let skip = cfg.skip.unwrap_or(0); +<<<<<<< HEAD +======= + let address_offset = cfg.address_offset; + +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce let source_part_vec: Vec = source .as_ref() .iter() @@ -205,11 +219,19 @@ where writer, "{}{:08x}{}: ", style.prefix(), +<<<<<<< HEAD i * cfg.width + skip, style.suffix() )?; } else { write!(writer, "{:08x}: ", i * cfg.width + skip,)?; +======= + i * cfg.width + skip + address_offset, + style.suffix() + )?; + } else { + write!(writer, "{:08x}: ", i * cfg.width + skip + address_offset,)?; +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } } for (i, x) in row.as_ref().iter().enumerate() { diff --git a/crates/nu-protocol/Cargo.toml b/crates/nu-protocol/Cargo.toml index bb15832600..70d4194fd5 100644 --- a/crates/nu-protocol/Cargo.toml +++ b/crates/nu-protocol/Cargo.toml @@ -1,4 +1,5 @@ [package] +<<<<<<< HEAD authors = ["The Nu Project Contributors"] description = "Core values and protocols for Nushell" edition = "2018" @@ -36,3 +37,31 @@ features = ["docs", "zip_with", "csv-file", "temporal", "performant", "pretty_fm dataframe = ["polars"] [build-dependencies] +======= +name = "nu-protocol" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +thiserror = "1.0.29" +miette = "3.0.0" +serde = {version = "1.0.130", features = ["derive"]} +chrono = { version="0.4.19", features=["serde"] } +indexmap = { version="1.7", features=["serde-1"] } +chrono-humanize = "0.2.1" +byte-unit = "4.0.9" +im = "15.0.0" +serde_json = { version = "1.0", optional = true } +nu-json = { path = "../nu-json" } +typetag = "0.1.8" +num-format = "0.4.0" +sys-locale = "0.1.0" + +[features] +plugin = ["serde_json"] + +[dev-dependencies] +serde_json = "1.0" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce diff --git a/crates/nu-protocol/README.md b/crates/nu-protocol/README.md new file mode 100644 index 0000000000..9443d74fd5 --- /dev/null +++ b/crates/nu-protocol/README.md @@ -0,0 +1,3 @@ +# nu-protocol + +The nu-protocol crate holds the definitions of structs/traits that are used throughout Nushell. This gives us one way to expose them to many other crates, as well as make these definitions available to each other, without causing mutually recursive dependencies. \ No newline at end of file diff --git a/crates/nu-protocol/src/ast/block.rs b/crates/nu-protocol/src/ast/block.rs new file mode 100644 index 0000000000..79a1f150ce --- /dev/null +++ b/crates/nu-protocol/src/ast/block.rs @@ -0,0 +1,71 @@ +use std::ops::{Index, IndexMut}; + +use crate::{Signature, Span, VarId}; + +use super::Statement; + +#[derive(Debug, Clone)] +pub struct Block { + pub signature: Box, + pub stmts: Vec, + pub captures: Vec, + pub redirect_env: bool, + pub span: Option, // None option encodes no span to avoid using test_span() +} + +impl Block { + pub fn len(&self) -> usize { + self.stmts.len() + } + + pub fn is_empty(&self) -> bool { + self.stmts.is_empty() + } +} + +impl Index for Block { + type Output = Statement; + + fn index(&self, index: usize) -> &Self::Output { + &self.stmts[index] + } +} + +impl IndexMut for Block { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.stmts[index] + } +} + +impl Default for Block { + fn default() -> Self { + Self::new() + } +} + +impl Block { + pub fn new() -> Self { + Self { + signature: Box::new(Signature::new("")), + stmts: vec![], + captures: vec![], + redirect_env: false, + span: None, + } + } +} + +impl From for Block +where + T: Iterator, +{ + fn from(stmts: T) -> Self { + Self { + signature: Box::new(Signature::new("")), + stmts: stmts.collect(), + captures: vec![], + redirect_env: false, + span: None, + } + } +} diff --git a/crates/nu-protocol/src/ast/call.rs b/crates/nu-protocol/src/ast/call.rs new file mode 100644 index 0000000000..365648a24e --- /dev/null +++ b/crates/nu-protocol/src/ast/call.rs @@ -0,0 +1,56 @@ +use super::Expression; +use crate::{DeclId, Span, Spanned}; + +#[derive(Debug, Clone)] +pub struct Call { + /// identifier of the declaration to call + pub decl_id: DeclId, + pub head: Span, + pub positional: Vec, + pub named: Vec<(Spanned, Option)>, +} + +impl Call { + pub fn new(head: Span) -> Call { + Self { + decl_id: 0, + head, + positional: vec![], + named: vec![], + } + } + + pub fn has_flag(&self, flag_name: &str) -> bool { + for name in &self.named { + if flag_name == name.0.item { + return true; + } + } + + false + } + + pub fn get_flag_expr(&self, flag_name: &str) -> Option { + for name in &self.named { + if flag_name == name.0.item { + return name.1.clone(); + } + } + + None + } + + pub fn get_named_arg(&self, flag_name: &str) -> Option> { + for name in &self.named { + if flag_name == name.0.item { + return Some(name.0.clone()); + } + } + + None + } + + pub fn nth(&self, pos: usize) -> Option { + self.positional.get(pos).cloned() + } +} diff --git a/crates/nu-protocol/src/ast/cell_path.rs b/crates/nu-protocol/src/ast/cell_path.rs new file mode 100644 index 0000000000..e21c8216bd --- /dev/null +++ b/crates/nu-protocol/src/ast/cell_path.rs @@ -0,0 +1,48 @@ +use super::Expression; +use crate::Span; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum PathMember { + String { val: String, span: Span }, + Int { val: usize, span: Span }, +} + +impl PartialEq for PathMember { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::String { val: l_val, .. }, Self::String { val: r_val, .. }) => l_val == r_val, + (Self::Int { val: l_val, .. }, Self::Int { val: r_val, .. }) => l_val == r_val, + _ => false, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CellPath { + pub members: Vec, +} + +impl CellPath { + pub fn into_string(&self) -> String { + let mut output = String::new(); + + for (idx, elem) in self.members.iter().enumerate() { + if idx > 0 { + output.push('.'); + } + match elem { + PathMember::Int { val, .. } => output.push_str(&format!("{}", val)), + PathMember::String { val, .. } => output.push_str(val), + } + } + + output + } +} + +#[derive(Debug, Clone)] +pub struct FullCellPath { + pub head: Expression, + pub tail: Vec, +} diff --git a/crates/nu-protocol/src/ast/expr.rs b/crates/nu-protocol/src/ast/expr.rs new file mode 100644 index 0000000000..cc6d532251 --- /dev/null +++ b/crates/nu-protocol/src/ast/expr.rs @@ -0,0 +1,39 @@ +use super::{Call, CellPath, Expression, FullCellPath, Operator, RangeOperator}; +use crate::{ast::ImportPattern, BlockId, Signature, Span, Spanned, Unit, VarId}; + +#[derive(Debug, Clone)] +pub enum Expr { + Bool(bool), + Int(i64), + Float(f64), + Range( + Option>, // from + Option>, // next value after "from" + Option>, // to + RangeOperator, + ), + Var(VarId), + VarDecl(VarId), + Call(Box), + ExternalCall(Box, Vec), + Operator(Operator), + RowCondition(BlockId), + BinaryOp(Box, Box, Box), //lhs, op, rhs + Subexpression(BlockId), + Block(BlockId), + List(Vec), + Table(Vec, Vec>), + Record(Vec<(Expression, Expression)>), + Keyword(Vec, Span, Box), + ValueWithUnit(Box, Spanned), + Filepath(String), + GlobPattern(String), + String(String), + CellPath(CellPath), + FullCellPath(Box), + ImportPattern(ImportPattern), + Signature(Box), + StringInterpolation(Vec), + Nothing, + Garbage, +} diff --git a/crates/nu-protocol/src/ast/expression.rs b/crates/nu-protocol/src/ast/expression.rs new file mode 100644 index 0000000000..c05e12d5e7 --- /dev/null +++ b/crates/nu-protocol/src/ast/expression.rs @@ -0,0 +1,405 @@ +use super::{Expr, Operator, Statement}; +use crate::ast::ImportPattern; +use crate::{engine::StateWorkingSet, BlockId, Signature, Span, Type, VarId, IN_VARIABLE_ID}; + +#[derive(Debug, Clone)] +pub struct Expression { + pub expr: Expr, + pub span: Span, + pub ty: Type, + pub custom_completion: Option, +} + +impl Expression { + pub fn garbage(span: Span) -> Expression { + Expression { + expr: Expr::Garbage, + span, + ty: Type::Unknown, + custom_completion: None, + } + } + + pub fn precedence(&self) -> usize { + match &self.expr { + Expr::Operator(operator) => { + // Higher precedence binds tighter + + match operator { + Operator::Pow => 100, + Operator::Multiply | Operator::Divide | Operator::Modulo => 95, + Operator::Plus | Operator::Minus => 90, + Operator::NotContains + | Operator::Contains + | Operator::LessThan + | Operator::LessThanOrEqual + | Operator::GreaterThan + | Operator::GreaterThanOrEqual + | Operator::Equal + | Operator::NotEqual + | Operator::In + | Operator::NotIn => 80, + Operator::And => 50, + Operator::Or => 40, + } + } + _ => 0, + } + } + + pub fn as_block(&self) -> Option { + match self.expr { + Expr::Block(block_id) => Some(block_id), + _ => None, + } + } + + pub fn as_row_condition_block(&self) -> Option { + match self.expr { + Expr::RowCondition(block_id) => Some(block_id), + _ => None, + } + } + + pub fn as_signature(&self) -> Option> { + match &self.expr { + Expr::Signature(sig) => Some(sig.clone()), + _ => None, + } + } + + pub fn as_list(&self) -> Option> { + match &self.expr { + Expr::List(list) => Some(list.clone()), + _ => None, + } + } + + pub fn as_keyword(&self) -> Option<&Expression> { + match &self.expr { + Expr::Keyword(_, _, expr) => Some(expr), + _ => None, + } + } + + pub fn as_var(&self) -> Option { + match self.expr { + Expr::Var(var_id) => Some(var_id), + Expr::VarDecl(var_id) => Some(var_id), + _ => None, + } + } + + pub fn as_string(&self) -> Option { + match &self.expr { + Expr::String(string) => Some(string.clone()), + _ => None, + } + } + + pub fn as_import_pattern(&self) -> Option { + match &self.expr { + Expr::ImportPattern(pattern) => Some(pattern.clone()), + _ => None, + } + } + + pub fn has_in_variable(&self, working_set: &StateWorkingSet) -> bool { + match &self.expr { + Expr::BinaryOp(left, _, right) => { + left.has_in_variable(working_set) || right.has_in_variable(working_set) + } + Expr::Block(block_id) => { + let block = working_set.get_block(*block_id); + + if block.captures.contains(&IN_VARIABLE_ID) { + return true; + } + + if let Some(Statement::Pipeline(pipeline)) = block.stmts.get(0) { + match pipeline.expressions.get(0) { + Some(expr) => expr.has_in_variable(working_set), + None => false, + } + } else { + false + } + } + Expr::Bool(_) => false, + Expr::Call(call) => { + for positional in &call.positional { + if positional.has_in_variable(working_set) { + return true; + } + } + for named in &call.named { + if let Some(expr) = &named.1 { + if expr.has_in_variable(working_set) { + return true; + } + } + } + false + } + Expr::CellPath(_) => false, + Expr::ExternalCall(head, args) => { + if head.has_in_variable(working_set) { + return true; + } + for arg in args { + if arg.has_in_variable(working_set) { + return true; + } + } + false + } + Expr::ImportPattern(_) => false, + Expr::Filepath(_) => false, + Expr::Float(_) => false, + Expr::FullCellPath(full_cell_path) => { + if full_cell_path.head.has_in_variable(working_set) { + return true; + } + false + } + Expr::Garbage => false, + Expr::Nothing => false, + Expr::GlobPattern(_) => false, + Expr::Int(_) => false, + Expr::Keyword(_, _, expr) => expr.has_in_variable(working_set), + Expr::List(list) => { + for l in list { + if l.has_in_variable(working_set) { + return true; + } + } + false + } + Expr::StringInterpolation(items) => { + for i in items { + if i.has_in_variable(working_set) { + return true; + } + } + false + } + Expr::Operator(_) => false, + Expr::Range(left, middle, right, ..) => { + if let Some(left) = &left { + if left.has_in_variable(working_set) { + return true; + } + } + if let Some(middle) = &middle { + if middle.has_in_variable(working_set) { + return true; + } + } + if let Some(right) = &right { + if right.has_in_variable(working_set) { + return true; + } + } + false + } + Expr::Record(fields) => { + for (field_name, field_value) in fields { + if field_name.has_in_variable(working_set) { + return true; + } + if field_value.has_in_variable(working_set) { + return true; + } + } + false + } + Expr::Signature(_) => false, + Expr::String(_) => false, + Expr::RowCondition(block_id) | Expr::Subexpression(block_id) => { + let block = working_set.get_block(*block_id); + + if let Some(Statement::Pipeline(pipeline)) = block.stmts.get(0) { + if let Some(expr) = pipeline.expressions.get(0) { + expr.has_in_variable(working_set) + } else { + false + } + } else { + false + } + } + Expr::Table(headers, cells) => { + for header in headers { + if header.has_in_variable(working_set) { + return true; + } + } + + for row in cells { + for cell in row.iter() { + if cell.has_in_variable(working_set) { + return true; + } + } + } + + false + } + + Expr::ValueWithUnit(expr, _) => expr.has_in_variable(working_set), + Expr::Var(var_id) => *var_id == IN_VARIABLE_ID, + Expr::VarDecl(_) => false, + } + } + + pub fn replace_in_variable(&mut self, working_set: &mut StateWorkingSet, new_var_id: VarId) { + match &mut self.expr { + Expr::BinaryOp(left, _, right) => { + left.replace_in_variable(working_set, new_var_id); + right.replace_in_variable(working_set, new_var_id); + } + Expr::Block(block_id) => { + let block = working_set.get_block(*block_id); + + let new_expr = if let Some(Statement::Pipeline(pipeline)) = block.stmts.get(0) { + if let Some(expr) = pipeline.expressions.get(0) { + let mut new_expr = expr.clone(); + new_expr.replace_in_variable(working_set, new_var_id); + Some(new_expr) + } else { + None + } + } else { + None + }; + + let block = working_set.get_block_mut(*block_id); + + if let Some(new_expr) = new_expr { + if let Some(Statement::Pipeline(pipeline)) = block.stmts.get_mut(0) { + if let Some(expr) = pipeline.expressions.get_mut(0) { + *expr = new_expr + } + } + } + + block.captures = block + .captures + .iter() + .map(|x| if *x != IN_VARIABLE_ID { *x } else { new_var_id }) + .collect(); + } + Expr::Bool(_) => {} + Expr::Call(call) => { + for positional in &mut call.positional { + positional.replace_in_variable(working_set, new_var_id); + } + for named in &mut call.named { + if let Some(expr) = &mut named.1 { + expr.replace_in_variable(working_set, new_var_id) + } + } + } + Expr::CellPath(_) => {} + Expr::ExternalCall(head, args) => { + head.replace_in_variable(working_set, new_var_id); + for arg in args { + arg.replace_in_variable(working_set, new_var_id) + } + } + Expr::Filepath(_) => {} + Expr::Float(_) => {} + Expr::FullCellPath(full_cell_path) => { + full_cell_path + .head + .replace_in_variable(working_set, new_var_id); + } + Expr::ImportPattern(_) => {} + Expr::Garbage => {} + Expr::Nothing => {} + Expr::GlobPattern(_) => {} + Expr::Int(_) => {} + Expr::Keyword(_, _, expr) => expr.replace_in_variable(working_set, new_var_id), + Expr::List(list) => { + for l in list { + l.replace_in_variable(working_set, new_var_id) + } + } + Expr::Operator(_) => {} + Expr::Range(left, middle, right, ..) => { + if let Some(left) = left { + left.replace_in_variable(working_set, new_var_id) + } + if let Some(middle) = middle { + middle.replace_in_variable(working_set, new_var_id) + } + if let Some(right) = right { + right.replace_in_variable(working_set, new_var_id) + } + } + Expr::Record(fields) => { + for (field_name, field_value) in fields { + field_name.replace_in_variable(working_set, new_var_id); + field_value.replace_in_variable(working_set, new_var_id); + } + } + Expr::Signature(_) => {} + Expr::String(_) => {} + Expr::StringInterpolation(items) => { + for i in items { + i.replace_in_variable(working_set, new_var_id) + } + } + Expr::RowCondition(block_id) | Expr::Subexpression(block_id) => { + let block = working_set.get_block(*block_id); + + let new_expr = if let Some(Statement::Pipeline(pipeline)) = block.stmts.get(0) { + if let Some(expr) = pipeline.expressions.get(0) { + let mut new_expr = expr.clone(); + new_expr.replace_in_variable(working_set, new_var_id); + Some(new_expr) + } else { + None + } + } else { + None + }; + + let block = working_set.get_block_mut(*block_id); + + if let Some(new_expr) = new_expr { + if let Some(Statement::Pipeline(pipeline)) = block.stmts.get_mut(0) { + if let Some(expr) = pipeline.expressions.get_mut(0) { + *expr = new_expr + } + } + } + + block.captures = block + .captures + .iter() + .map(|x| if *x != IN_VARIABLE_ID { *x } else { new_var_id }) + .collect(); + } + Expr::Table(headers, cells) => { + for header in headers { + header.replace_in_variable(working_set, new_var_id) + } + + for row in cells { + for cell in row.iter_mut() { + cell.replace_in_variable(working_set, new_var_id) + } + } + } + + Expr::ValueWithUnit(expr, _) => expr.replace_in_variable(working_set, new_var_id), + Expr::Var(x) => { + if *x == IN_VARIABLE_ID { + *x = new_var_id + } + } + Expr::VarDecl(_) => {} + } + } +} diff --git a/crates/nu-protocol/src/ast/import_pattern.rs b/crates/nu-protocol/src/ast/import_pattern.rs new file mode 100644 index 0000000000..a35fb671b9 --- /dev/null +++ b/crates/nu-protocol/src/ast/import_pattern.rs @@ -0,0 +1,69 @@ +use crate::{span, Span}; +use std::collections::HashSet; + +#[derive(Debug, Clone)] +pub enum ImportPatternMember { + Glob { span: Span }, + Name { name: Vec, span: Span }, + List { names: Vec<(Vec, Span)> }, +} + +#[derive(Debug, Clone)] +pub struct ImportPatternHead { + pub name: Vec, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub struct ImportPattern { + pub head: ImportPatternHead, + pub members: Vec, + // communicate to eval which decls/aliases were hidden during `parse_hide()` so it does not + // interpret these as env var names: + pub hidden: HashSet>, +} + +impl ImportPattern { + pub fn new() -> Self { + ImportPattern { + head: ImportPatternHead { + name: vec![], + span: Span { start: 0, end: 0 }, + }, + members: vec![], + hidden: HashSet::new(), + } + } + + pub fn span(&self) -> Span { + let mut spans = vec![self.head.span]; + + for member in &self.members { + match member { + ImportPatternMember::Glob { span } => spans.push(*span), + ImportPatternMember::Name { name: _, span } => spans.push(*span), + ImportPatternMember::List { names } => { + for (_, span) in names { + spans.push(*span); + } + } + } + } + + span(&spans) + } + + pub fn with_hidden(self, hidden: HashSet>) -> Self { + ImportPattern { + head: self.head, + members: self.members, + hidden, + } + } +} + +impl Default for ImportPattern { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/nu-protocol/src/ast/mod.rs b/crates/nu-protocol/src/ast/mod.rs new file mode 100644 index 0000000000..67c9ce76e9 --- /dev/null +++ b/crates/nu-protocol/src/ast/mod.rs @@ -0,0 +1,19 @@ +mod block; +mod call; +mod cell_path; +mod expr; +mod expression; +mod import_pattern; +mod operator; +mod pipeline; +mod statement; + +pub use block::*; +pub use call::*; +pub use cell_path::*; +pub use expr::*; +pub use expression::*; +pub use import_pattern::*; +pub use operator::*; +pub use pipeline::*; +pub use statement::*; diff --git a/crates/nu-protocol/src/ast/operator.rs b/crates/nu-protocol/src/ast/operator.rs new file mode 100644 index 0000000000..690390888f --- /dev/null +++ b/crates/nu-protocol/src/ast/operator.rs @@ -0,0 +1,73 @@ +use crate::Span; + +use serde::{Deserialize, Serialize}; +use std::fmt::Display; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum Operator { + Equal, + NotEqual, + LessThan, + GreaterThan, + LessThanOrEqual, + GreaterThanOrEqual, + Contains, + NotContains, + Plus, + Minus, + Multiply, + Divide, + In, + NotIn, + Modulo, + And, + Or, + Pow, +} + +impl Display for Operator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Operator::Equal => write!(f, "=="), + Operator::NotEqual => write!(f, "!="), + Operator::LessThan => write!(f, "<"), + Operator::GreaterThan => write!(f, ">"), + Operator::Contains => write!(f, "=~"), + Operator::NotContains => write!(f, "!~"), + Operator::Plus => write!(f, "+"), + Operator::Minus => write!(f, "-"), + Operator::Multiply => write!(f, "*"), + Operator::Divide => write!(f, "/"), + Operator::In => write!(f, "in"), + Operator::NotIn => write!(f, "not-in"), + Operator::Modulo => write!(f, "mod"), + Operator::And => write!(f, "&&"), + Operator::Or => write!(f, "||"), + Operator::Pow => write!(f, "**"), + Operator::LessThanOrEqual => write!(f, "<="), + Operator::GreaterThanOrEqual => write!(f, ">="), + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] +pub enum RangeInclusion { + Inclusive, + RightExclusive, +} + +#[derive(Debug, Copy, Clone)] +pub struct RangeOperator { + pub inclusion: RangeInclusion, + pub span: Span, + pub next_op_span: Span, +} + +impl Display for RangeOperator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.inclusion { + RangeInclusion::Inclusive => write!(f, ".."), + RangeInclusion::RightExclusive => write!(f, "..<"), + } + } +} diff --git a/crates/nu-protocol/src/ast/pipeline.rs b/crates/nu-protocol/src/ast/pipeline.rs new file mode 100644 index 0000000000..1f5652fa41 --- /dev/null +++ b/crates/nu-protocol/src/ast/pipeline.rs @@ -0,0 +1,24 @@ +use crate::ast::Expression; + +#[derive(Debug, Clone)] +pub struct Pipeline { + pub expressions: Vec, +} + +impl Default for Pipeline { + fn default() -> Self { + Self::new() + } +} + +impl Pipeline { + pub fn new() -> Self { + Self { + expressions: vec![], + } + } + + pub fn from_vec(expressions: Vec) -> Pipeline { + Self { expressions } + } +} diff --git a/crates/nu-protocol/src/ast/statement.rs b/crates/nu-protocol/src/ast/statement.rs new file mode 100644 index 0000000000..5fa7faef12 --- /dev/null +++ b/crates/nu-protocol/src/ast/statement.rs @@ -0,0 +1,8 @@ +use super::Pipeline; +use crate::DeclId; + +#[derive(Debug, Clone)] +pub enum Statement { + Declaration(DeclId), + Pipeline(Pipeline), +} diff --git a/crates/nu-protocol/src/config.rs b/crates/nu-protocol/src/config.rs new file mode 100644 index 0000000000..471ca0b77c --- /dev/null +++ b/crates/nu-protocol/src/config.rs @@ -0,0 +1,372 @@ +use crate::{BlockId, ShellError, Span, Value}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +const ANIMATE_PROMPT_DEFAULT: bool = true; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct EnvConversion { + pub from_string: Option<(BlockId, Span)>, + pub to_string: Option<(BlockId, Span)>, +} + +impl EnvConversion { + pub fn from_record(value: &Value) -> Result { + let record = value.as_record()?; + + let mut conv_map = HashMap::new(); + + for (k, v) in record.0.iter().zip(record.1) { + if (k == "from_string") || (k == "to_string") { + conv_map.insert(k.as_str(), (v.as_block()?, v.span()?)); + } else { + return Err(ShellError::UnsupportedConfigValue( + "'from_string' and 'to_string' fields".into(), + k.into(), + value.span()?, + )); + } + } + + let from_string = conv_map.get("from_string").cloned(); + let to_string = conv_map.get("to_string").cloned(); + + Ok(EnvConversion { + from_string, + to_string, + }) + } +} + +/// Definition of a parsed keybinding from the config object +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ParsedKeybinding { + pub modifier: Value, + pub keycode: Value, + pub event: Value, + pub mode: Value, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Config { + pub filesize_metric: bool, + pub table_mode: String, + pub use_ls_colors: bool, + pub color_config: HashMap, + pub use_grid_icons: bool, + pub footer_mode: FooterMode, + pub animate_prompt: bool, + pub float_precision: i64, + pub filesize_format: String, + pub use_ansi_coloring: bool, + pub quick_completions: bool, + pub env_conversions: HashMap, + pub edit_mode: String, + pub max_history_size: i64, + pub log_level: String, + pub menu_config: HashMap, + pub keybindings: Vec, + pub history_config: HashMap, +} + +impl Default for Config { + fn default() -> Config { + Config { + filesize_metric: false, + table_mode: "rounded".into(), + use_ls_colors: true, + color_config: HashMap::new(), + use_grid_icons: false, + footer_mode: FooterMode::Never, + animate_prompt: ANIMATE_PROMPT_DEFAULT, + float_precision: 4, + filesize_format: "auto".into(), + use_ansi_coloring: true, + quick_completions: false, + env_conversions: HashMap::new(), // TODO: Add default conversoins + edit_mode: "emacs".into(), + max_history_size: 1000, + log_level: String::new(), + menu_config: HashMap::new(), + keybindings: Vec::new(), + history_config: HashMap::new(), + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum FooterMode { + /// Never show the footer + Never, + /// Always show the footer + Always, + /// Only show the footer if there are more than RowCount rows + RowCount(u64), + /// Calculate the screen height, calculate row count, if display will be bigger than screen, add the footer + Auto, +} + +impl Value { + pub fn into_config(self) -> Result { + let v = self.as_record(); + + let mut config = Config::default(); + + if let Ok(v) = v { + for (key, value) in v.0.iter().zip(v.1) { + match key.as_str() { + "filesize_metric" => { + if let Ok(b) = value.as_bool() { + config.filesize_metric = b; + } else { + eprintln!("$config.filesize_metric is not a bool") + } + } + "table_mode" => { + if let Ok(v) = value.as_string() { + config.table_mode = v; + } else { + eprintln!("$config.table_mode is not a string") + } + } + "use_ls_colors" => { + if let Ok(b) = value.as_bool() { + config.use_ls_colors = b; + } else { + eprintln!("$config.use_ls_colors is not a bool") + } + } + "color_config" => { + if let Ok(map) = create_map(value, &config) { + config.color_config = map; + } else { + eprintln!("$config.color_config is not a record") + } + } + "use_grid_icons" => { + if let Ok(b) = value.as_bool() { + config.use_grid_icons = b; + } else { + eprintln!("$config.use_grid_icons is not a bool") + } + } + "footer_mode" => { + if let Ok(b) = value.as_string() { + let val_str = b.to_lowercase(); + config.footer_mode = match val_str.as_ref() { + "auto" => FooterMode::Auto, + "never" => FooterMode::Never, + "always" => FooterMode::Always, + _ => match &val_str.parse::() { + Ok(number) => FooterMode::RowCount(*number), + _ => FooterMode::Never, + }, + }; + } else { + eprintln!("$config.footer_mode is not a string") + } + } + "animate_prompt" => { + if let Ok(b) = value.as_bool() { + config.animate_prompt = b; + } else { + eprintln!("$config.animate_prompt is not a bool") + } + } + "float_precision" => { + if let Ok(i) = value.as_integer() { + config.float_precision = i; + } else { + eprintln!("$config.float_precision is not an integer") + } + } + "use_ansi_coloring" => { + if let Ok(b) = value.as_bool() { + config.use_ansi_coloring = b; + } else { + eprintln!("$config.use_ansi_coloring is not a bool") + } + } + "quick_completions" => { + if let Ok(b) = value.as_bool() { + config.quick_completions = b; + } else { + eprintln!("$config.quick_completions is not a bool") + } + } + "filesize_format" => { + if let Ok(v) = value.as_string() { + config.filesize_format = v.to_lowercase(); + } else { + eprintln!("$config.filesize_format is not a string") + } + } + "env_conversions" => { + if let Ok((env_vars, conversions)) = value.as_record() { + let mut env_conversions = HashMap::new(); + + for (env_var, record) in env_vars.iter().zip(conversions) { + // println!("{}: {:?}", env_var, record); + if let Ok(conversion) = EnvConversion::from_record(record) { + env_conversions.insert(env_var.into(), conversion); + } else { + eprintln!("$config.env_conversions has incorrect conversion") + } + } + + config.env_conversions = env_conversions; + } else { + eprintln!("$config.env_conversions is not a record") + } + } + "edit_mode" => { + if let Ok(v) = value.as_string() { + config.edit_mode = v.to_lowercase(); + } else { + eprintln!("$config.edit_mode is not a string") + } + } + "max_history_size" => { + if let Ok(i) = value.as_i64() { + config.max_history_size = i; + } else { + eprintln!("$config.max_history_size is not an integer") + } + } + "log_level" => { + if let Ok(v) = value.as_string() { + config.log_level = v.to_lowercase(); + } else { + eprintln!("$config.log_level is not a string") + } + } + "menu_config" => { + if let Ok(map) = create_map(value, &config) { + config.menu_config = map; + } else { + eprintln!("$config.menu_config is not a record") + } + } + "keybindings" => { + if let Ok(keybindings) = create_keybindings(value, &config) { + config.keybindings = keybindings; + } else { + eprintln!("$config.keybindings is not a valid keybindings list") + } + } + "history_config" => { + if let Ok(map) = create_map(value, &config) { + config.history_config = map; + } else { + eprintln!("$config.history_config is not a record") + } + } + x => { + eprintln!("$config.{} is an unknown config setting", x) + } + } + } + } else { + eprintln!("$config is not a record"); + } + + Ok(config) + } +} + +fn create_map(value: &Value, config: &Config) -> Result, ShellError> { + let (cols, inner_vals) = value.as_record()?; + let mut hm: HashMap = HashMap::new(); + + for (k, v) in cols.iter().zip(inner_vals) { + match &v { + Value::Record { + cols: inner_cols, + vals: inner_vals, + span, + } => { + let val = color_value_string(span, inner_cols, inner_vals, config); + hm.insert(k.to_string(), val); + } + _ => { + hm.insert(k.to_string(), v.clone()); + } + } + } + + Ok(hm) +} + +fn color_value_string( + span: &Span, + inner_cols: &[String], + inner_vals: &[Value], + config: &Config, +) -> Value { + // make a string from our config.color_config section that + // looks like this: { fg: "#rrggbb" bg: "#rrggbb" attr: "abc", } + // the real key here was to have quotes around the values but not + // require them around the keys. + + // maybe there's a better way to generate this but i'm not sure + // what it is. + let val: String = inner_cols + .iter() + .zip(inner_vals) + .map(|(x, y)| format!("{}: \"{}\" ", x, y.into_string(", ", config))) + .collect(); + + // now insert the braces at the front and the back to fake the json string + Value::String { + val: format!("{{{}}}", val), + span: *span, + } +} + +// Parses the config object to extract the strings that will compose a keybinding for reedline +fn create_keybindings(value: &Value, config: &Config) -> Result, ShellError> { + match value { + Value::Record { cols, vals, span } => { + // Finding the modifier value in the record + let modifier = extract_value("modifier", cols, vals, span)?; + let keycode = extract_value("keycode", cols, vals, span)?; + let mode = extract_value("mode", cols, vals, span)?; + let event = extract_value("event", cols, vals, span)?; + + let keybinding = ParsedKeybinding { + modifier: modifier.clone(), + keycode: keycode.clone(), + mode: mode.clone(), + event: event.clone(), + }; + + Ok(vec![keybinding]) + } + Value::List { vals, .. } => { + let res = vals + .iter() + .map(|inner_value| create_keybindings(inner_value, config)) + .collect::>, ShellError>>(); + + let res = res? + .into_iter() + .flatten() + .collect::>(); + + Ok(res) + } + _ => Ok(Vec::new()), + } +} + +pub fn extract_value<'record>( + name: &str, + cols: &'record [String], + vals: &'record [Value], + span: &Span, +) -> Result<&'record Value, ShellError> { + cols.iter() + .position(|col| col.as_str() == name) + .and_then(|index| vals.get(index)) + .ok_or_else(|| ShellError::MissingConfigValue(name.to_string(), *span)) +} diff --git a/crates/nu-protocol/src/engine/call_info.rs b/crates/nu-protocol/src/engine/call_info.rs new file mode 100644 index 0000000000..a30dc2d848 --- /dev/null +++ b/crates/nu-protocol/src/engine/call_info.rs @@ -0,0 +1,8 @@ +use crate::ast::Call; +use crate::Span; + +#[derive(Debug, Clone)] +pub struct UnevaluatedCallInfo { + pub args: Call, + pub name_span: Span, +} diff --git a/crates/nu-protocol/src/engine/capture_block.rs b/crates/nu-protocol/src/engine/capture_block.rs new file mode 100644 index 0000000000..447c33e5a3 --- /dev/null +++ b/crates/nu-protocol/src/engine/capture_block.rs @@ -0,0 +1,9 @@ +use std::collections::HashMap; + +use crate::{BlockId, Value, VarId}; + +#[derive(Clone, Debug)] +pub struct CaptureBlock { + pub block_id: BlockId, + pub captures: HashMap, +} diff --git a/crates/nu-protocol/src/engine/command.rs b/crates/nu-protocol/src/engine/command.rs new file mode 100644 index 0000000000..3727723dec --- /dev/null +++ b/crates/nu-protocol/src/engine/command.rs @@ -0,0 +1,78 @@ +use std::path::PathBuf; + +use crate::{ast::Call, BlockId, Example, PipelineData, ShellError, Signature}; + +use super::{EngineState, Stack}; + +pub trait Command: Send + Sync + CommandClone { + fn name(&self) -> &str; + + fn signature(&self) -> Signature; + + fn usage(&self) -> &str; + + fn extra_usage(&self) -> &str { + "" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result; + + fn is_binary(&self) -> bool { + false + } + + // Commands that are not meant to be run by users + fn is_private(&self) -> bool { + false + } + + fn examples(&self) -> Vec { + Vec::new() + } + + // This is a built-in command + fn is_builtin(&self) -> bool { + true + } + + // Is a sub command + fn is_sub(&self) -> bool { + self.name().contains(' ') + } + + // Is a plugin command (returns plugin's path, encoding and type of shell + // if the declaration is a plugin) + fn is_plugin(&self) -> Option<(&PathBuf, &str, &Option)> { + None + } + + // If command is a block i.e. def blah [] { }, get the block id + fn get_block_id(&self) -> Option { + None + } +} + +pub trait CommandClone { + fn clone_box(&self) -> Box; +} + +impl CommandClone for T +where + T: 'static + Command + Clone, +{ + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + +impl Clone for Box { + fn clone(&self) -> Box { + self.clone_box() + } +} diff --git a/crates/nu-protocol/src/engine/engine_state.rs b/crates/nu-protocol/src/engine/engine_state.rs new file mode 100644 index 0000000000..7377feaa41 --- /dev/null +++ b/crates/nu-protocol/src/engine/engine_state.rs @@ -0,0 +1,1292 @@ +use super::{Command, Stack}; +use crate::{ + ast::Block, BlockId, DeclId, Example, Overlay, OverlayId, ShellError, Signature, Span, Type, + VarId, +}; +use core::panic; +use std::{ + collections::HashMap, + sync::{atomic::AtomicBool, Arc}, +}; + +use crate::Value; + +use std::path::Path; + +#[cfg(feature = "plugin")] +use std::path::PathBuf; + +// Tells whether a decl etc. is visible or not +#[derive(Debug, Clone)] +struct Visibility { + decl_ids: HashMap, +} + +impl Visibility { + fn new() -> Self { + Visibility { + decl_ids: HashMap::new(), + } + } + + fn is_decl_id_visible(&self, decl_id: &DeclId) -> bool { + *self.decl_ids.get(decl_id).unwrap_or(&true) // by default it's visible + } + + fn hide_decl_id(&mut self, decl_id: &DeclId) { + self.decl_ids.insert(*decl_id, false); + } + + fn use_decl_id(&mut self, decl_id: &DeclId) { + self.decl_ids.insert(*decl_id, true); + } + + fn merge_with(&mut self, other: Visibility) { + // overwrite own values with the other + self.decl_ids.extend(other.decl_ids); + // self.env_var_ids.extend(other.env_var_ids); + } + + fn append(&mut self, other: &Visibility) { + // take new values from other but keep own values + for (decl_id, visible) in other.decl_ids.iter() { + if !self.decl_ids.contains_key(decl_id) { + self.decl_ids.insert(*decl_id, *visible); + } + } + } +} + +#[derive(Debug, Clone)] +pub struct ScopeFrame { + pub vars: HashMap, VarId>, + predecls: HashMap, DeclId>, // temporary storage for predeclarations + pub decls: HashMap, DeclId>, + pub aliases: HashMap, Vec>, + pub env_vars: HashMap, BlockId>, + pub overlays: HashMap, OverlayId>, + visibility: Visibility, +} + +impl ScopeFrame { + pub fn new() -> Self { + Self { + vars: HashMap::new(), + predecls: HashMap::new(), + decls: HashMap::new(), + aliases: HashMap::new(), + env_vars: HashMap::new(), + overlays: HashMap::new(), + visibility: Visibility::new(), + } + } + + pub fn get_var(&self, var_name: &[u8]) -> Option<&VarId> { + self.vars.get(var_name) + } +} + +impl Default for ScopeFrame { + fn default() -> Self { + Self::new() + } +} + +/// The core global engine state. This includes all global definitions as well as any global state that +/// will persist for the whole session. +/// +/// Declarations, variables, blocks, and other forms of data are held in the global state and referenced +/// elsewhere using their IDs. These IDs are simply their index into the global state. This allows us to +/// more easily handle creating blocks, binding variables and callsites, and more, because each of these +/// will refer to the corresponding IDs rather than their definitions directly. At runtime, this means +/// less copying and smaller structures. +/// +/// Note that the runtime stack is not part of this global state. Runtime stacks are handled differently, +/// but they also rely on using IDs rather than full definitions. +/// +/// A note on implementation: +/// +/// Much of the global definitions are built on the Bodil's 'im' crate. This gives us a way of working with +/// lists of definitions in a way that is very cheap to access, while also allowing us to update them at +/// key points in time (often, the transition between parsing and evaluation). +/// +/// Over the last two years we tried a few different approaches to global state like this. I'll list them +/// here for posterity, so we can more easily know how we got here: +/// +/// * `Rc` - Rc is cheap, but not thread-safe. The moment we wanted to work with external processes, we +/// needed a way send to stdin/stdout. In Rust, the current practice is to spawn a thread to handle both. +/// These threads would need access to the global state, as they'll need to process data as it streams out +/// of the data pipeline. Because Rc isn't thread-safe, this breaks. +/// +/// * `Arc` - Arc is the thread-safe version of the above. Often Arc is used in combination with a Mutex or +/// RwLock, but you can use Arc by itself. We did this a few places in the original Nushell. This *can* work +/// but because of Arc's nature of not allowing mutation if there's a second copy of the Arc around, this +/// ultimately becomes limiting. +/// +/// * `Arc` + `Mutex/RwLock` - the standard practice for thread-safe containers. Unfortunately, this would +/// have meant we would incur a lock penalty every time we needed to access any declaration or block. As we +/// would be reading far more often than writing, it made sense to explore solutions that favor large amounts +/// of reads. +/// +/// * `im` - the `im` crate was ultimately chosen because it has some very nice properties: it gives the +/// ability to cheaply clone these structures, which is nice as EngineState may need to be cloned a fair bit +/// to follow ownership rules for closures and iterators. It also is cheap to access. Favoring reads here fits +/// more closely to what we need with Nushell. And, of course, it's still thread-safe, so we get the same +/// benefits as above. +/// +#[derive(Clone)] +pub struct EngineState { + files: im::Vector<(String, usize, usize)>, + file_contents: im::Vector<(Vec, usize, usize)>, + vars: im::Vector, + decls: im::Vector>, + blocks: im::Vector, + overlays: im::Vector, + pub scope: im::Vector, + pub ctrlc: Option>, + pub env_vars: im::HashMap, + #[cfg(feature = "plugin")] + pub plugin_signatures: Option, +} + +pub const NU_VARIABLE_ID: usize = 0; +pub const SCOPE_VARIABLE_ID: usize = 1; +pub const IN_VARIABLE_ID: usize = 2; +pub const CONFIG_VARIABLE_ID: usize = 3; +pub const ENV_VARIABLE_ID: usize = 4; +// NOTE: If you add more to this list, make sure to update the > checks based on the last in the list + +impl EngineState { + pub fn new() -> Self { + Self { + files: im::vector![], + file_contents: im::vector![], + vars: im::vector![ + Type::Unknown, + Type::Unknown, + Type::Unknown, + Type::Unknown, + Type::Unknown + ], + decls: im::vector![], + blocks: im::vector![], + overlays: im::vector![], + scope: im::vector![ScopeFrame::new()], + ctrlc: None, + env_vars: im::HashMap::new(), + #[cfg(feature = "plugin")] + plugin_signatures: None, + } + } + + /// Merges a `StateDelta` onto the current state. These deltas come from a system, like the parser, that + /// creates a new set of definitions and visible symbols in the current scope. We make this transactional + /// as there are times when we want to run the parser and immediately throw away the results (namely: + /// syntax highlighting and completions). + /// + /// When we want to preserve what the parser has created, we can take its output (the `StateDelta`) and + /// use this function to merge it into the global state. + pub fn merge_delta( + &mut self, + mut delta: StateDelta, + stack: Option<&mut Stack>, + cwd: impl AsRef, + ) -> Result<(), ShellError> { + // Take the mutable reference and extend the permanent state from the working set + self.files.extend(delta.files); + self.file_contents.extend(delta.file_contents); + self.decls.extend(delta.decls); + self.vars.extend(delta.vars); + self.blocks.extend(delta.blocks); + self.overlays.extend(delta.overlays); + + if let Some(last) = self.scope.back_mut() { + let first = delta.scope.remove(0); + for item in first.decls.into_iter() { + last.decls.insert(item.0, item.1); + } + for item in first.vars.into_iter() { + last.vars.insert(item.0, item.1); + } + for item in first.aliases.into_iter() { + last.aliases.insert(item.0, item.1); + } + for item in first.overlays.into_iter() { + last.overlays.insert(item.0, item.1); + } + last.visibility.merge_with(first.visibility); + + #[cfg(feature = "plugin")] + if delta.plugins_changed { + let result = self.update_plugin_file(); + + if result.is_ok() { + delta.plugins_changed = false; + } + + return result; + } + } + + if let Some(stack) = stack { + for mut env_scope in stack.env_vars.drain(..) { + for (k, v) in env_scope.drain() { + self.env_vars.insert(k, v); + } + } + } + + // FIXME: permanent state changes like this hopefully in time can be removed + // and be replaced by just passing the cwd in where needed + std::env::set_current_dir(cwd)?; + + Ok(()) + } + + #[cfg(feature = "plugin")] + pub fn update_plugin_file(&self) -> Result<(), ShellError> { + use std::io::Write; + + // Updating the signatures plugin file with the added signatures + self.plugin_signatures + .as_ref() + .ok_or_else(|| ShellError::PluginFailedToLoad("Plugin file not found".into())) + .and_then(|plugin_path| { + // Always create the file, which will erase previous signatures + std::fs::File::create(plugin_path.as_path()) + .map_err(|err| ShellError::PluginFailedToLoad(err.to_string())) + }) + .and_then(|mut plugin_file| { + // Plugin definitions with parsed signature + self.plugin_decls().try_for_each(|decl| { + // A successful plugin registration already includes the plugin filename + // No need to check the None option + let (path, encoding, shell) = + decl.is_plugin().expect("plugin should have file name"); + let file_name = path + .to_str() + .expect("path was checked during registration as a str"); + + serde_json::to_string_pretty(&decl.signature()) + .map(|signature| { + // Extracting the possible path to the shell used to load the plugin + let shell_str = match shell { + Some(path) => format!( + "-s {}", + path.to_str().expect( + "shell path was checked during registration as a str" + ) + ), + None => "".into(), + }; + + // Each signature is stored in the plugin file with the required + // encoding, shell and signature + // This information will be used when loading the plugin + // information when nushell starts + format!( + "register {} -e {} {} {}\n\n", + file_name, encoding, shell_str, signature + ) + }) + .map_err(|err| ShellError::PluginFailedToLoad(err.to_string())) + .and_then(|line| { + plugin_file + .write_all(line.as_bytes()) + .map_err(|err| ShellError::PluginFailedToLoad(err.to_string())) + }) + }) + }) + } + + pub fn num_files(&self) -> usize { + self.files.len() + } + + pub fn num_vars(&self) -> usize { + self.vars.len() + } + + pub fn num_decls(&self) -> usize { + self.decls.len() + } + + pub fn num_blocks(&self) -> usize { + self.blocks.len() + } + + pub fn num_overlays(&self) -> usize { + self.overlays.len() + } + + pub fn print_vars(&self) { + for var in self.vars.iter().enumerate() { + println!("var{}: {:?}", var.0, var.1); + } + } + + pub fn print_decls(&self) { + for decl in self.decls.iter().enumerate() { + println!("decl{}: {:?}", decl.0, decl.1.signature()); + } + } + + pub fn print_blocks(&self) { + for block in self.blocks.iter().enumerate() { + println!("block{}: {:?}", block.0, block.1); + } + } + + pub fn print_contents(&self) { + for (contents, _, _) in self.file_contents.iter() { + let string = String::from_utf8_lossy(contents); + println!("{}", string); + } + } + + pub fn find_aliases(&self, name: &str) -> Vec> { + let mut output = vec![]; + + for frame in &self.scope { + if let Some(alias) = frame.aliases.get(name.as_bytes()) { + output.push(alias.clone()); + } + } + + output + } + + pub fn find_custom_commands(&self, name: &str) -> Vec { + let mut output = vec![]; + + for frame in &self.scope { + if let Some(decl_id) = frame.decls.get(name.as_bytes()) { + let decl = self.get_decl(*decl_id); + + if let Some(block_id) = decl.get_block_id() { + output.push(self.get_block(block_id).clone()); + } + } + } + + output + } + + pub fn find_decl(&self, name: &[u8]) -> Option { + let mut visibility: Visibility = Visibility::new(); + + for scope in self.scope.iter().rev() { + visibility.append(&scope.visibility); + + if let Some(decl_id) = scope.decls.get(name) { + if visibility.is_decl_id_visible(decl_id) { + return Some(*decl_id); + } + } + } + + None + } + + #[cfg(feature = "plugin")] + pub fn plugin_decls(&self) -> impl Iterator> { + let mut unique_plugin_decls = HashMap::new(); + + // Make sure there are no duplicate decls: Newer one overwrites the older one + for decl in self.decls.iter().filter(|d| d.is_plugin().is_some()) { + unique_plugin_decls.insert(decl.name(), decl); + } + + let mut plugin_decls: Vec<(&str, &Box)> = + unique_plugin_decls.into_iter().collect(); + + // Sort the plugins by name so we don't end up with a random plugin file each time + plugin_decls.sort_by(|a, b| a.0.cmp(b.0)); + plugin_decls.into_iter().map(|(_, decl)| decl) + } + + pub fn find_overlay(&self, name: &[u8]) -> Option { + for scope in self.scope.iter().rev() { + if let Some(overlay_id) = scope.overlays.get(name) { + return Some(*overlay_id); + } + } + + None + } + + pub fn find_commands_by_prefix(&self, name: &[u8]) -> Vec> { + let mut output = vec![]; + + for scope in self.scope.iter().rev() { + for decl in &scope.decls { + if decl.0.starts_with(name) { + output.push(decl.0.clone()); + } + } + } + + output + } + + pub fn get_span_contents(&self, span: &Span) -> &[u8] { + for (contents, start, finish) in &self.file_contents { + if span.start >= *start && span.end <= *finish { + return &contents[(span.start - start)..(span.end - start)]; + } + } + + panic!("internal error: span missing in file contents cache") + } + + pub fn get_var(&self, var_id: VarId) -> &Type { + self.vars + .get(var_id) + .expect("internal error: missing variable") + } + + #[allow(clippy::borrowed_box)] + pub fn get_decl(&self, decl_id: DeclId) -> &Box { + self.decls + .get(decl_id) + .expect("internal error: missing declaration") + } + + /// Get all IDs of all commands within scope, sorted by the commads' names + pub fn get_decl_ids_sorted(&self, include_hidden: bool) -> impl Iterator { + let mut decls_map = HashMap::new(); + + for frame in &self.scope { + let frame_decls = if include_hidden { + frame.decls.clone() + } else { + frame + .decls + .clone() + .into_iter() + .filter(|(_, id)| frame.visibility.is_decl_id_visible(id)) + .collect() + }; + + decls_map.extend(frame_decls); + } + + let mut decls: Vec<(Vec, DeclId)> = decls_map.into_iter().collect(); + + decls.sort_by(|a, b| a.0.cmp(&b.0)); + decls.into_iter().map(|(_, id)| id) + } + + /// Get signatures of all commands within scope. + pub fn get_signatures(&self, include_hidden: bool) -> Vec { + self.get_decl_ids_sorted(include_hidden) + .map(|id| { + let decl = self.get_decl(id); + + let mut signature = (*decl).signature(); + signature.usage = decl.usage().to_string(); + signature.extra_usage = decl.extra_usage().to_string(); + + signature + }) + .collect() + } + + /// Get signatures of all commands within scope. + /// + /// In addition to signatures, it returns whether each command is: + /// a) a plugin + /// b) custom + pub fn get_signatures_with_examples( + &self, + include_hidden: bool, + ) -> Vec<(Signature, Vec, bool, bool)> { + self.get_decl_ids_sorted(include_hidden) + .map(|id| { + let decl = self.get_decl(id); + + let mut signature = (*decl).signature(); + signature.usage = decl.usage().to_string(); + signature.extra_usage = decl.extra_usage().to_string(); + + ( + signature, + decl.examples(), + decl.is_plugin().is_some(), + decl.get_block_id().is_some(), + ) + }) + .collect() + } + + pub fn get_block(&self, block_id: BlockId) -> &Block { + self.blocks + .get(block_id) + .expect("internal error: missing block") + } + + pub fn get_overlay(&self, overlay_id: OverlayId) -> &Overlay { + self.overlays + .get(overlay_id) + .expect("internal error: missing overlay") + } + + pub fn next_span_start(&self) -> usize { + if let Some((_, _, last)) = self.file_contents.last() { + *last + } else { + 0 + } + } + + pub fn files(&self) -> impl Iterator { + self.files.iter() + } + + pub fn get_filename(&self, file_id: usize) -> String { + for file in self.files.iter().enumerate() { + if file.0 == file_id { + return file.1 .0.clone(); + } + } + + "".into() + } + + pub fn get_file_source(&self, file_id: usize) -> String { + for file in self.files.iter().enumerate() { + if file.0 == file_id { + let contents = self.get_span_contents(&Span { + start: file.1 .1, + end: file.1 .2, + }); + let output = String::from_utf8_lossy(contents).to_string(); + + return output; + } + } + + "".into() + } + + pub fn add_file(&mut self, filename: String, contents: Vec) -> usize { + let next_span_start = self.next_span_start(); + let next_span_end = next_span_start + contents.len(); + + self.file_contents + .push_back((contents, next_span_start, next_span_end)); + + self.files + .push_back((filename, next_span_start, next_span_end)); + + self.num_files() - 1 + } +} + +impl Default for EngineState { + fn default() -> Self { + Self::new() + } +} + +/// A temporary extension to the global state. This handles bridging between the global state and the +/// additional declarations and scope changes that are not yet part of the global scope. +/// +/// This working set is created by the parser as a way of handling declarations and scope changes that +/// may later be merged or dropped (and not merged) depending on the needs of the code calling the parser. +pub struct StateWorkingSet<'a> { + pub permanent_state: &'a EngineState, + pub delta: StateDelta, +} + +/// A delta (or change set) between the current global state and a possible future global state. Deltas +/// can be applied to the global state to update it to contain both previous state and the state held +/// within the delta. +pub struct StateDelta { + files: Vec<(String, usize, usize)>, + pub(crate) file_contents: Vec<(Vec, usize, usize)>, + vars: Vec, // indexed by VarId + decls: Vec>, // indexed by DeclId + blocks: Vec, // indexed by BlockId + overlays: Vec, // indexed by OverlayId + pub scope: Vec, + #[cfg(feature = "plugin")] + plugins_changed: bool, // marks whether plugin file should be updated +} + +impl Default for StateDelta { + fn default() -> Self { + Self::new() + } +} + +impl StateDelta { + pub fn new() -> Self { + StateDelta { + files: vec![], + file_contents: vec![], + vars: vec![], + decls: vec![], + blocks: vec![], + overlays: vec![], + scope: vec![ScopeFrame::new()], + #[cfg(feature = "plugin")] + plugins_changed: false, + } + } + + pub fn num_files(&self) -> usize { + self.files.len() + } + + pub fn num_decls(&self) -> usize { + self.decls.len() + } + + pub fn num_blocks(&self) -> usize { + self.blocks.len() + } + + pub fn num_overlays(&self) -> usize { + self.overlays.len() + } + + pub fn enter_scope(&mut self) { + self.scope.push(ScopeFrame::new()); + } + + pub fn exit_scope(&mut self) { + self.scope.pop(); + } +} + +impl<'a> StateWorkingSet<'a> { + pub fn new(permanent_state: &'a EngineState) -> Self { + Self { + delta: StateDelta::new(), + permanent_state, + } + } + + pub fn num_files(&self) -> usize { + self.delta.num_files() + self.permanent_state.num_files() + } + + pub fn num_decls(&self) -> usize { + self.delta.num_decls() + self.permanent_state.num_decls() + } + + pub fn num_blocks(&self) -> usize { + self.delta.num_blocks() + self.permanent_state.num_blocks() + } + + pub fn num_overlays(&self) -> usize { + self.delta.num_overlays() + self.permanent_state.num_overlays() + } + + pub fn add_decl(&mut self, decl: Box) -> DeclId { + let name = decl.name().as_bytes().to_vec(); + + self.delta.decls.push(decl); + let decl_id = self.num_decls() - 1; + + let scope_frame = self + .delta + .scope + .last_mut() + .expect("internal error: missing required scope frame"); + + scope_frame.decls.insert(name, decl_id); + scope_frame.visibility.use_decl_id(&decl_id); + + decl_id + } + + pub fn use_decls(&mut self, decls: Vec<(Vec, DeclId)>) { + let scope_frame = self + .delta + .scope + .last_mut() + .expect("internal error: missing required scope frame"); + + for (name, decl_id) in decls { + scope_frame.decls.insert(name, decl_id); + scope_frame.visibility.use_decl_id(&decl_id); + } + } + + pub fn add_predecl(&mut self, decl: Box) -> Option { + let name = decl.name().as_bytes().to_vec(); + + self.delta.decls.push(decl); + let decl_id = self.num_decls() - 1; + + let scope_frame = self + .delta + .scope + .last_mut() + .expect("internal error: missing required scope frame"); + + scope_frame.predecls.insert(name, decl_id) + } + + #[cfg(feature = "plugin")] + pub fn mark_plugins_file_dirty(&mut self) { + self.delta.plugins_changed = true; + } + + pub fn merge_predecl(&mut self, name: &[u8]) -> Option { + let scope_frame = self + .delta + .scope + .last_mut() + .expect("internal error: missing required scope frame"); + + if let Some(decl_id) = scope_frame.predecls.remove(name) { + scope_frame.decls.insert(name.into(), decl_id); + scope_frame.visibility.use_decl_id(&decl_id); + + return Some(decl_id); + } + + None + } + + pub fn hide_decl(&mut self, name: &[u8]) -> Option { + let mut visibility: Visibility = Visibility::new(); + + // Since we can mutate scope frames in delta, remove the id directly + for scope in self.delta.scope.iter_mut().rev() { + visibility.append(&scope.visibility); + + if let Some(decl_id) = scope.decls.remove(name) { + return Some(decl_id); + } + } + + // We cannot mutate the permanent state => store the information in the current scope frame + let last_scope_frame = self + .delta + .scope + .last_mut() + .expect("internal error: missing required scope frame"); + + for scope in self.permanent_state.scope.iter().rev() { + visibility.append(&scope.visibility); + + if let Some(decl_id) = scope.decls.get(name) { + if visibility.is_decl_id_visible(decl_id) { + // Hide decl only if it's not already hidden + last_scope_frame.visibility.hide_decl_id(decl_id); + return Some(*decl_id); + } + } + } + + None + } + + pub fn hide_decls(&mut self, decls: &[(Vec, DeclId)]) { + for decl in decls.iter() { + self.hide_decl(&decl.0); // let's assume no errors + } + } + + pub fn add_block(&mut self, block: Block) -> BlockId { + self.delta.blocks.push(block); + + self.num_blocks() - 1 + } + + pub fn add_env_var(&mut self, name_span: Span, block: Block) -> BlockId { + self.delta.blocks.push(block); + let block_id = self.num_blocks() - 1; + let name = self.get_span_contents(name_span).to_vec(); + + let scope_frame = self + .delta + .scope + .last_mut() + .expect("internal error: missing required scope frame"); + + scope_frame.env_vars.insert(name, block_id); + + block_id + } + + pub fn add_overlay(&mut self, name: &str, overlay: Overlay) -> OverlayId { + let name = name.as_bytes().to_vec(); + + self.delta.overlays.push(overlay); + let overlay_id = self.num_overlays() - 1; + + let scope_frame = self + .delta + .scope + .last_mut() + .expect("internal error: missing required scope frame"); + + scope_frame.overlays.insert(name, overlay_id); + + overlay_id + } + + pub fn next_span_start(&self) -> usize { + let permanent_span_start = self.permanent_state.next_span_start(); + + if let Some((_, _, last)) = self.delta.file_contents.last() { + *last + } else { + permanent_span_start + } + } + + pub fn global_span_offset(&self) -> usize { + self.permanent_state.next_span_start() + } + + pub fn files(&'a self) -> impl Iterator { + self.permanent_state.files().chain(self.delta.files.iter()) + } + + pub fn get_filename(&self, file_id: usize) -> String { + for file in self.files().enumerate() { + if file.0 == file_id { + return file.1 .0.clone(); + } + } + + "".into() + } + + pub fn get_file_source(&self, file_id: usize) -> String { + for file in self.files().enumerate() { + if file.0 == file_id { + let output = String::from_utf8_lossy(self.get_span_contents(Span { + start: file.1 .1, + end: file.1 .2, + })) + .to_string(); + + return output; + } + } + + "".into() + } + + pub fn add_file(&mut self, filename: String, contents: &[u8]) -> usize { + let next_span_start = self.next_span_start(); + let next_span_end = next_span_start + contents.len(); + + self.delta + .file_contents + .push((contents.to_vec(), next_span_start, next_span_end)); + + self.delta + .files + .push((filename, next_span_start, next_span_end)); + + self.num_files() - 1 + } + + pub fn get_span_contents(&self, span: Span) -> &[u8] { + let permanent_end = self.permanent_state.next_span_start(); + if permanent_end <= span.start { + for (contents, start, finish) in &self.delta.file_contents { + if (span.start >= *start) && (span.end <= *finish) { + return &contents[(span.start - start)..(span.end - start)]; + } + } + } else { + return self.permanent_state.get_span_contents(&span); + } + + panic!("internal error: missing span contents in file cache") + } + + pub fn enter_scope(&mut self) { + self.delta.enter_scope(); + } + + pub fn exit_scope(&mut self) { + self.delta.exit_scope(); + } + + pub fn find_decl(&self, name: &[u8]) -> Option { + let mut visibility: Visibility = Visibility::new(); + + for scope in self.delta.scope.iter().rev() { + visibility.append(&scope.visibility); + + if let Some(decl_id) = scope.predecls.get(name) { + return Some(*decl_id); + } + + if let Some(decl_id) = scope.decls.get(name) { + return Some(*decl_id); + } + } + + for scope in self.permanent_state.scope.iter().rev() { + visibility.append(&scope.visibility); + + if let Some(decl_id) = scope.decls.get(name) { + if visibility.is_decl_id_visible(decl_id) { + return Some(*decl_id); + } + } + } + + None + } + + pub fn find_overlay(&self, name: &[u8]) -> Option { + for scope in self.delta.scope.iter().rev() { + if let Some(overlay_id) = scope.overlays.get(name) { + return Some(*overlay_id); + } + } + + for scope in self.permanent_state.scope.iter().rev() { + if let Some(overlay_id) = scope.overlays.get(name) { + return Some(*overlay_id); + } + } + + None + } + + // pub fn update_decl(&mut self, decl_id: usize, block: Option) { + // let decl = self.get_decl_mut(decl_id); + // decl.body = block; + // } + + pub fn contains_decl_partial_match(&self, name: &[u8]) -> bool { + for scope in self.delta.scope.iter().rev() { + for decl in &scope.decls { + if decl.0.starts_with(name) { + return true; + } + } + } + + for scope in self.permanent_state.scope.iter().rev() { + for decl in &scope.decls { + if decl.0.starts_with(name) { + return true; + } + } + } + + false + } + + pub fn next_var_id(&self) -> VarId { + let num_permanent_vars = self.permanent_state.num_vars(); + num_permanent_vars + self.delta.vars.len() + } + + pub fn find_variable(&self, name: &[u8]) -> Option { + for scope in self.delta.scope.iter().rev() { + if let Some(var_id) = scope.vars.get(name) { + return Some(*var_id); + } + } + + for scope in self.permanent_state.scope.iter().rev() { + if let Some(var_id) = scope.vars.get(name) { + return Some(*var_id); + } + } + + None + } + + pub fn find_alias(&self, name: &[u8]) -> Option<&[Span]> { + for scope in self.delta.scope.iter().rev() { + if let Some(spans) = scope.aliases.get(name) { + return Some(spans); + } + } + + for scope in self.permanent_state.scope.iter().rev() { + if let Some(spans) = scope.aliases.get(name) { + return Some(spans); + } + } + + None + } + + pub fn add_variable(&mut self, mut name: Vec, ty: Type) -> VarId { + let next_id = self.next_var_id(); + + // correct name if necessary + if !name.starts_with(b"$") { + name.insert(0, b'$'); + } + + let last = self + .delta + .scope + .last_mut() + .expect("internal error: missing stack frame"); + + last.vars.insert(name, next_id); + + self.delta.vars.push(ty); + + next_id + } + + pub fn add_alias(&mut self, name: Vec, replacement: Vec) { + let last = self + .delta + .scope + .last_mut() + .expect("internal error: missing stack frame"); + + last.aliases.insert(name, replacement); + } + + pub fn get_cwd(&self) -> String { + let pwd = self + .permanent_state + .env_vars + .get("PWD") + .expect("internal error: can't find PWD"); + pwd.as_string().expect("internal error: PWD not a string") + } + + pub fn set_variable_type(&mut self, var_id: VarId, ty: Type) { + let num_permanent_vars = self.permanent_state.num_vars(); + if var_id < num_permanent_vars { + panic!("Internal error: attempted to set into permanent state from working set") + } else { + self.delta.vars[var_id - num_permanent_vars] = ty; + } + } + + pub fn get_variable(&self, var_id: VarId) -> &Type { + let num_permanent_vars = self.permanent_state.num_vars(); + if var_id < num_permanent_vars { + self.permanent_state.get_var(var_id) + } else { + self.delta + .vars + .get(var_id - num_permanent_vars) + .expect("internal error: missing variable") + } + } + + #[allow(clippy::borrowed_box)] + pub fn get_decl(&self, decl_id: DeclId) -> &Box { + let num_permanent_decls = self.permanent_state.num_decls(); + if decl_id < num_permanent_decls { + self.permanent_state.get_decl(decl_id) + } else { + self.delta + .decls + .get(decl_id - num_permanent_decls) + .expect("internal error: missing declaration") + } + } + + pub fn get_decl_mut(&mut self, decl_id: DeclId) -> &mut Box { + let num_permanent_decls = self.permanent_state.num_decls(); + if decl_id < num_permanent_decls { + panic!("internal error: can only mutate declarations in working set") + } else { + self.delta + .decls + .get_mut(decl_id - num_permanent_decls) + .expect("internal error: missing declaration") + } + } + + pub fn find_commands_by_prefix(&self, name: &[u8]) -> Vec> { + let mut output = vec![]; + + for scope in self.delta.scope.iter().rev() { + for decl in &scope.decls { + if decl.0.starts_with(name) { + output.push(decl.0.clone()); + } + } + } + + let mut permanent = self.permanent_state.find_commands_by_prefix(name); + + output.append(&mut permanent); + + output + } + + pub fn get_block(&self, block_id: BlockId) -> &Block { + let num_permanent_blocks = self.permanent_state.num_blocks(); + if block_id < num_permanent_blocks { + self.permanent_state.get_block(block_id) + } else { + self.delta + .blocks + .get(block_id - num_permanent_blocks) + .expect("internal error: missing block") + } + } + + pub fn get_overlay(&self, overlay_id: OverlayId) -> &Overlay { + let num_permanent_overlays = self.permanent_state.num_overlays(); + if overlay_id < num_permanent_overlays { + self.permanent_state.get_overlay(overlay_id) + } else { + self.delta + .overlays + .get(overlay_id - num_permanent_overlays) + .expect("internal error: missing overlay") + } + } + + pub fn get_block_mut(&mut self, block_id: BlockId) -> &mut Block { + let num_permanent_blocks = self.permanent_state.num_blocks(); + if block_id < num_permanent_blocks { + panic!("Attempt to mutate a block that is in the permanent (immutable) state") + } else { + self.delta + .blocks + .get_mut(block_id - num_permanent_blocks) + .expect("internal error: missing block") + } + } + + pub fn render(self) -> StateDelta { + self.delta + } +} + +impl<'a> miette::SourceCode for &StateWorkingSet<'a> { + fn read_span<'b>( + &'b self, + span: &miette::SourceSpan, + context_lines_before: usize, + context_lines_after: usize, + ) -> Result, miette::MietteError> { + let debugging = std::env::var("MIETTE_DEBUG").is_ok(); + if debugging { + let finding_span = "Finding span in StateWorkingSet"; + dbg!(finding_span, span); + } + for (filename, start, end) in self.files() { + if debugging { + dbg!(&filename, start, end); + } + if span.offset() >= *start && span.offset() + span.len() <= *end { + if debugging { + let found_file = "Found matching file"; + dbg!(found_file); + } + let our_span = Span { + start: *start, + end: *end, + }; + // We need to move to a local span because we're only reading + // the specific file contents via self.get_span_contents. + let local_span = (span.offset() - *start, span.len()).into(); + if debugging { + dbg!(&local_span); + } + let span_contents = self.get_span_contents(our_span); + if debugging { + dbg!(String::from_utf8_lossy(span_contents)); + } + let span_contents = span_contents.read_span( + &local_span, + context_lines_before, + context_lines_after, + )?; + let content_span = span_contents.span(); + // Back to "global" indexing + let retranslated = (content_span.offset() + start, content_span.len()).into(); + if debugging { + dbg!(&retranslated); + } + + let data = span_contents.data(); + if filename == "" { + if debugging { + let success_cli = "Successfully read CLI span"; + dbg!(success_cli, String::from_utf8_lossy(data)); + } + return Ok(Box::new(miette::MietteSpanContents::new( + data, + retranslated, + span_contents.line(), + span_contents.column(), + span_contents.line_count(), + ))); + } else { + if debugging { + let success_file = "Successfully read file span"; + dbg!(success_file); + } + return Ok(Box::new(miette::MietteSpanContents::new_named( + filename.clone(), + data, + retranslated, + span_contents.line(), + span_contents.column(), + span_contents.line_count(), + ))); + } + } + } + Err(miette::MietteError::OutOfBounds) + } +} + +#[cfg(test)] +mod engine_state_tests { + use super::*; + + #[test] + fn add_file_gives_id() { + let engine_state = EngineState::new(); + let mut engine_state = StateWorkingSet::new(&engine_state); + let id = engine_state.add_file("test.nu".into(), &[]); + + assert_eq!(id, 0); + } + + #[test] + fn add_file_gives_id_including_parent() { + let mut engine_state = EngineState::new(); + let parent_id = engine_state.add_file("test.nu".into(), vec![]); + + let mut working_set = StateWorkingSet::new(&engine_state); + let working_set_id = working_set.add_file("child.nu".into(), &[]); + + assert_eq!(parent_id, 0); + assert_eq!(working_set_id, 1); + } + + #[test] + fn merge_states() -> Result<(), ShellError> { + let mut engine_state = EngineState::new(); + engine_state.add_file("test.nu".into(), vec![]); + + let delta = { + let mut working_set = StateWorkingSet::new(&engine_state); + working_set.add_file("child.nu".into(), &[]); + working_set.render() + }; + + let cwd = std::env::current_dir().expect("Could not get current working directory."); + engine_state.merge_delta(delta, None, &cwd)?; + + assert_eq!(engine_state.num_files(), 2); + assert_eq!(&engine_state.files[0].0, "test.nu"); + assert_eq!(&engine_state.files[1].0, "child.nu"); + + Ok(()) + } +} diff --git a/crates/nu-protocol/src/engine/mod.rs b/crates/nu-protocol/src/engine/mod.rs new file mode 100644 index 0000000000..296578b417 --- /dev/null +++ b/crates/nu-protocol/src/engine/mod.rs @@ -0,0 +1,11 @@ +mod call_info; +mod capture_block; +mod command; +mod engine_state; +mod stack; + +pub use call_info::*; +pub use capture_block::*; +pub use command::*; +pub use engine_state::*; +pub use stack::*; diff --git a/crates/nu-protocol/src/engine/stack.rs b/crates/nu-protocol/src/engine/stack.rs new file mode 100644 index 0000000000..f898abffa4 --- /dev/null +++ b/crates/nu-protocol/src/engine/stack.rs @@ -0,0 +1,249 @@ +use std::collections::{HashMap, HashSet}; + +use crate::engine::EngineState; +use crate::{Config, ShellError, Span, Value, VarId, CONFIG_VARIABLE_ID}; + +/// A runtime value stack used during evaluation +/// +/// A note on implementation: +/// +/// We previously set up the stack in a traditional way, where stack frames had parents which would +/// represent other frames that you might return to when exiting a function. +/// +/// While experimenting with blocks, we found that we needed to have closure captures of variables +/// seen outside of the blocks, so that they blocks could be run in a way that was both thread-safe +/// and followed the restrictions for closures applied to iterators. The end result left us with +/// closure-captured single stack frames that blocks could see. +/// +/// Blocks make up the only scope and stack definition abstraction in Nushell. As a result, we were +/// creating closure captures at any point we wanted to have a Block value we could safely evaluate +/// in any context. This meant that the parents were going largely unused, with captured variables +/// taking their place. The end result is this, where we no longer have separate frames, but instead +/// use the Stack as a way of representing the local and closure-captured state. +#[derive(Debug, Clone)] +pub struct Stack { + /// Variables + pub vars: HashMap, + /// Environment variables arranged as a stack to be able to recover values from parent scopes + pub env_vars: Vec>, + /// Tells which environment variables from engine state are hidden. We don't need to track the + /// env vars in the stack since we can just delete them. + pub env_hidden: HashSet, +} + +impl Default for Stack { + fn default() -> Self { + Self::new() + } +} + +impl Stack { + pub fn new() -> Stack { + Stack { + vars: HashMap::new(), + env_vars: vec![], + env_hidden: HashSet::new(), + } + } + + pub fn with_env(&mut self, env_vars: &[HashMap], env_hidden: &HashSet) { + // Do not clone the environment if it hasn't changed + if self.env_vars.iter().any(|scope| !scope.is_empty()) { + self.env_vars = env_vars.to_owned(); + } + + if !self.env_hidden.is_empty() { + self.env_hidden = env_hidden.clone(); + } + } + + pub fn get_var(&self, var_id: VarId, span: Span) -> Result { + if let Some(v) = self.vars.get(&var_id) { + return Ok(v.clone().with_span(span)); + } + + Err(ShellError::VariableNotFoundAtRuntime(span)) + } + + pub fn get_var_with_origin(&self, var_id: VarId, span: Span) -> Result { + if let Some(v) = self.vars.get(&var_id) { + return Ok(v.clone()); + } + + Err(ShellError::VariableNotFoundAtRuntime(span)) + } + + pub fn add_var(&mut self, var_id: VarId, value: Value) { + self.vars.insert(var_id, value); + } + + pub fn add_env_var(&mut self, var: String, value: Value) { + // if the env var was hidden, let's activate it again + self.env_hidden.remove(&var); + + if let Some(scope) = self.env_vars.last_mut() { + scope.insert(var, value); + } else { + self.env_vars.push(HashMap::from([(var, value)])); + } + } + + pub fn captures_to_stack(&self, captures: &HashMap) -> Stack { + let mut output = Stack::new(); + + output.vars = captures.clone(); + + // FIXME: this is probably slow + output.env_vars = self.env_vars.clone(); + output.env_vars.push(HashMap::new()); + + let config = self + .get_var(CONFIG_VARIABLE_ID, Span::new(0, 0)) + .expect("internal error: config is missing"); + output.vars.insert(CONFIG_VARIABLE_ID, config); + + output + } + + pub fn gather_captures(&self, captures: &[VarId]) -> Stack { + let mut output = Stack::new(); + + let fake_span = Span::new(0, 0); + + for capture in captures { + // Note: this assumes we have calculated captures correctly and that commands + // that take in a var decl will manually set this into scope when running the blocks + if let Ok(value) = self.get_var(*capture, fake_span) { + output.vars.insert(*capture, value); + } + } + + // FIXME: this is probably slow + output.env_vars = self.env_vars.clone(); + output.env_vars.push(HashMap::new()); + + let config = self + .get_var(CONFIG_VARIABLE_ID, fake_span) + .expect("internal error: config is missing"); + output.vars.insert(CONFIG_VARIABLE_ID, config); + + output + } + + /// Flatten the env var scope frames into one frame + pub fn get_env_vars(&self, engine_state: &EngineState) -> HashMap { + // TODO: We're collecting im::HashMap to HashMap here. It might make sense to make these + // the same data structure. + let mut result: HashMap = engine_state + .env_vars + .iter() + .filter(|(k, _)| !self.env_hidden.contains(*k)) + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + + for scope in &self.env_vars { + result.extend(scope.clone()); + } + + result + } + + /// Same as get_env_vars, but returns only the names as a HashSet + pub fn get_env_var_names(&self, engine_state: &EngineState) -> HashSet { + let mut result: HashSet = engine_state + .env_vars + .keys() + .filter(|k| !self.env_hidden.contains(*k)) + .cloned() + .collect(); + + for scope in &self.env_vars { + let scope_keys: HashSet = scope.keys().cloned().collect(); + result.extend(scope_keys); + } + + result + } + + pub fn get_env_var(&self, engine_state: &EngineState, name: &str) -> Option { + for scope in self.env_vars.iter().rev() { + if let Some(v) = scope.get(name) { + return Some(v.clone()); + } + } + + if self.env_hidden.contains(name) { + None + } else { + engine_state.env_vars.get(name).cloned() + } + } + + pub fn has_env_var(&self, engine_state: &EngineState, name: &str) -> bool { + for scope in self.env_vars.iter().rev() { + if scope.contains_key(name) { + return true; + } + } + + if self.env_hidden.contains(name) { + false + } else { + engine_state.env_vars.contains_key(name) + } + } + + pub fn remove_env_var(&mut self, engine_state: &EngineState, name: &str) -> Option { + for scope in self.env_vars.iter_mut().rev() { + if let Some(v) = scope.remove(name) { + return Some(v); + } + } + + if self.env_hidden.contains(name) { + // the environment variable is already hidden + None + } else if let Some(val) = engine_state.env_vars.get(name) { + // the environment variable was found in the engine state => mark it as hidden + self.env_hidden.insert(name.to_string()); + Some(val.clone()) + } else { + None + } + } + + pub fn get_config(&self) -> Result { + let config = self.get_var(CONFIG_VARIABLE_ID, Span::new(0, 0)); + + match config { + Ok(config) => config.into_config(), + Err(e) => Err(e), + } + } + + pub fn update_config(&mut self, name: &str, value: Value) { + if let Some(Value::Record { cols, vals, .. }) = self.vars.get_mut(&CONFIG_VARIABLE_ID) { + for col_val in cols.iter().zip(vals.iter_mut()) { + if col_val.0 == name { + *col_val.1 = value; + return; + } + } + cols.push(name.to_string()); + vals.push(value); + } + } + + pub fn print_stack(&self) { + println!("vars:"); + for (var, val) in &self.vars { + println!(" {}: {:?}", var, val); + } + for (i, scope) in self.env_vars.iter().rev().enumerate() { + println!("env vars, scope {} (from the last);", i); + for (var, val) in scope { + println!(" {}: {:?}", var, val.clone().debug_value()); + } + } + } +} diff --git a/crates/nu-protocol/src/example.rs b/crates/nu-protocol/src/example.rs new file mode 100644 index 0000000000..9d2c0b5e89 --- /dev/null +++ b/crates/nu-protocol/src/example.rs @@ -0,0 +1,8 @@ +use crate::Value; + +#[derive(Debug)] +pub struct Example { + pub example: &'static str, + pub description: &'static str, + pub result: Option, +} diff --git a/crates/nu-protocol/src/exportable.rs b/crates/nu-protocol/src/exportable.rs new file mode 100644 index 0000000000..5323d60bf9 --- /dev/null +++ b/crates/nu-protocol/src/exportable.rs @@ -0,0 +1,6 @@ +use crate::{BlockId, DeclId}; + +pub enum Exportable { + Decl(DeclId), + EnvVar(BlockId), +} diff --git a/crates/nu-protocol/src/id.rs b/crates/nu-protocol/src/id.rs new file mode 100644 index 0000000000..c40f1633dc --- /dev/null +++ b/crates/nu-protocol/src/id.rs @@ -0,0 +1,4 @@ +pub type VarId = usize; +pub type DeclId = usize; +pub type BlockId = usize; +pub type OverlayId = usize; diff --git a/crates/nu-protocol/src/lib.rs b/crates/nu-protocol/src/lib.rs index e9d7346359..8f01ee83e0 100644 --- a/crates/nu-protocol/src/lib.rs +++ b/crates/nu-protocol/src/lib.rs @@ -1,3 +1,4 @@ +<<<<<<< HEAD #[macro_use] mod macros; @@ -33,3 +34,37 @@ pub use crate::value::primitive::{format_date, format_duration, format_primitive pub use crate::value::range::{Range, RangeInclusion}; pub use crate::value::value_structure::{ValueResource, ValueStructure}; pub use crate::value::{merge_descriptors, UntaggedValue, Value}; +======= +pub mod ast; +mod config; +pub mod engine; +mod example; +mod exportable; +mod id; +mod overlay; +mod pipeline_data; +mod shell_error; +mod signature; +mod span; +mod syntax_shape; +mod ty; +mod value; +pub use value::Value; + +pub use config::*; +pub use engine::{ + CONFIG_VARIABLE_ID, ENV_VARIABLE_ID, IN_VARIABLE_ID, NU_VARIABLE_ID, SCOPE_VARIABLE_ID, +}; +pub use example::*; +pub use exportable::*; +pub use id::*; +pub use overlay::*; +pub use pipeline_data::*; +pub use shell_error::*; +pub use signature::*; +pub use span::*; +pub use syntax_shape::*; +pub use ty::*; +pub use value::CustomValue; +pub use value::*; +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce diff --git a/crates/nu-protocol/src/overlay.rs b/crates/nu-protocol/src/overlay.rs new file mode 100644 index 0000000000..81b7816113 --- /dev/null +++ b/crates/nu-protocol/src/overlay.rs @@ -0,0 +1,131 @@ +use crate::{BlockId, DeclId, Span}; + +use indexmap::IndexMap; + +// TODO: Move the import pattern matching logic here from use/hide commands and +// parse_use/parse_hide + +/// Collection of definitions that can be exported from a module +#[derive(Debug, Clone)] +pub struct Overlay { + pub decls: IndexMap, DeclId>, + pub env_vars: IndexMap, BlockId>, + pub span: Option, +} + +impl Overlay { + pub fn new() -> Self { + Overlay { + decls: IndexMap::new(), + env_vars: IndexMap::new(), + span: None, + } + } + + pub fn from_span(span: Span) -> Self { + Overlay { + decls: IndexMap::new(), + env_vars: IndexMap::new(), + span: Some(span), + } + } + + pub fn add_decl(&mut self, name: &[u8], decl_id: DeclId) -> Option { + self.decls.insert(name.to_vec(), decl_id) + } + + pub fn add_env_var(&mut self, name: &[u8], block_id: BlockId) -> Option { + self.env_vars.insert(name.to_vec(), block_id) + } + + pub fn extend(&mut self, other: &Overlay) { + self.decls.extend(other.decls.clone()); + self.env_vars.extend(other.env_vars.clone()); + } + + pub fn is_empty(&self) -> bool { + self.decls.is_empty() && self.env_vars.is_empty() + } + + pub fn get_decl_id(&self, name: &[u8]) -> Option { + self.decls.get(name).copied() + } + + pub fn has_decl(&self, name: &[u8]) -> bool { + self.decls.contains_key(name) + } + + pub fn decl_with_head(&self, name: &[u8], head: &[u8]) -> Option<(Vec, DeclId)> { + if let Some(id) = self.get_decl_id(name) { + let mut new_name = head.to_vec(); + new_name.push(b' '); + new_name.extend(name); + Some((new_name, id)) + } else { + None + } + } + + pub fn decls_with_head(&self, head: &[u8]) -> Vec<(Vec, DeclId)> { + self.decls + .iter() + .map(|(name, id)| { + let mut new_name = head.to_vec(); + new_name.push(b' '); + new_name.extend(name); + (new_name, *id) + }) + .collect() + } + + pub fn decls(&self) -> Vec<(Vec, DeclId)> { + self.decls + .iter() + .map(|(name, id)| (name.clone(), *id)) + .collect() + } + + pub fn get_env_var_id(&self, name: &[u8]) -> Option { + self.env_vars.get(name).copied() + } + + pub fn has_env_var(&self, name: &[u8]) -> bool { + self.env_vars.contains_key(name) + } + + pub fn env_var_with_head(&self, name: &[u8], head: &[u8]) -> Option<(Vec, BlockId)> { + if let Some(id) = self.get_env_var_id(name) { + let mut new_name = head.to_vec(); + new_name.push(b' '); + new_name.extend(name); + Some((new_name, id)) + } else { + None + } + } + + pub fn env_vars_with_head(&self, head: &[u8]) -> Vec<(Vec, BlockId)> { + self.env_vars + .iter() + .map(|(name, id)| { + let mut new_name = head.to_vec(); + new_name.push(b' '); + new_name.extend(name); + (new_name, *id) + }) + .collect() + } + + pub fn env_vars(&self) -> Vec<(Vec, BlockId)> { + self.env_vars + .iter() + .map(|(name, id)| (name.clone(), *id)) + .collect() + } +} + +impl Default for Overlay { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/nu-protocol/src/pipeline_data.rs b/crates/nu-protocol/src/pipeline_data.rs new file mode 100644 index 0000000000..c048bf5b21 --- /dev/null +++ b/crates/nu-protocol/src/pipeline_data.rs @@ -0,0 +1,470 @@ +use std::sync::{atomic::AtomicBool, Arc}; + +use crate::{ast::PathMember, Config, ListStream, RawStream, ShellError, Span, Value}; + +/// The foundational abstraction for input and output to commands +/// +/// This represents either a single Value or a stream of values coming into the command or leaving a command. +/// +/// A note on implementation: +/// +/// We've tried a few variations of this structure. Listing these below so we have a record. +/// +/// * We tried always assuming a stream in Nushell. This was a great 80% solution, but it had some rough edges. +/// Namely, how do you know the difference between a single string and a list of one string. How do you know +/// when to flatten the data given to you from a data source into the stream or to keep it as an unflattened +/// list? +/// +/// * We tried putting the stream into Value. This had some interesting properties as now commands "just worked +/// on values", but lead to a few unfortunate issues. +/// +/// The first is that you can't easily clone Values in a way that felt largely immutable. For example, if +/// you cloned a Value which contained a stream, and in one variable drained some part of it, then the second +/// variable would see different values based on what you did to the first. +/// +/// To make this kind of mutation thread-safe, we would have had to produce a lock for the stream, which in +/// practice would have meant always locking the stream before reading from it. But more fundamentally, it +/// felt wrong in practice that observation of a value at runtime could affect other values which happen to +/// alias the same stream. By separating these, we don't have this effect. Instead, variables could get +/// concrete list values rather than streams, and be able to view them without non-local effects. +/// +/// * A balance of the two approaches is what we've landed on: Values are thread-safe to pass, and we can stream +/// them into any sources. Streams are still available to model the infinite streams approach of original +/// Nushell. +#[derive(Debug)] +pub enum PipelineData { + Value(Value, Option), + ListStream(ListStream, Option), + RawStream(RawStream, Span, Option), +} + +#[derive(Debug, Clone)] +pub struct PipelineMetadata { + pub data_source: DataSource, +} + +#[derive(Debug, Clone)] +pub enum DataSource { + Ls, +} + +impl PipelineData { + pub fn new(span: Span) -> PipelineData { + PipelineData::Value(Value::Nothing { span }, None) + } + + pub fn new_with_metadata(metadata: Option, span: Span) -> PipelineData { + PipelineData::Value(Value::Nothing { span }, metadata) + } + + pub fn metadata(&self) -> Option { + match self { + PipelineData::ListStream(_, x) => x.clone(), + PipelineData::RawStream(_, _, x) => x.clone(), + PipelineData::Value(_, x) => x.clone(), + } + } + + pub fn set_metadata(mut self, metadata: Option) -> Self { + match &mut self { + PipelineData::ListStream(_, x) => *x = metadata, + PipelineData::RawStream(_, _, x) => *x = metadata, + PipelineData::Value(_, x) => *x = metadata, + } + + self + } + + pub fn is_nothing(&self) -> bool { + matches!(self, PipelineData::Value(Value::Nothing { .. }, ..)) + } + + pub fn into_value(self, span: Span) -> Value { + match self { + PipelineData::Value(Value::Nothing { .. }, ..) => Value::nothing(span), + PipelineData::Value(v, ..) => v, + PipelineData::ListStream(s, ..) => Value::List { + vals: s.collect(), + span, // FIXME? + }, + PipelineData::RawStream(mut s, ..) => { + let mut items = vec![]; + + for val in &mut s { + match val { + Ok(val) => { + items.push(val); + } + Err(e) => { + return Value::Error { error: e }; + } + } + } + + if s.is_binary { + let mut output = vec![]; + for item in items { + match item.as_binary() { + Ok(item) => { + output.extend(item); + } + Err(err) => { + return Value::Error { error: err }; + } + } + } + + Value::Binary { + val: output, + span, // FIXME? + } + } else { + let mut output = String::new(); + for item in items { + match item.as_string() { + Ok(s) => output.push_str(&s), + Err(err) => { + return Value::Error { error: err }; + } + } + } + Value::String { + val: output, + span, // FIXME? + } + } + } + } + } + + pub fn into_interruptible_iter(self, ctrlc: Option>) -> PipelineIterator { + let mut iter = self.into_iter(); + + if let PipelineIterator(PipelineData::ListStream(s, ..)) = &mut iter { + s.ctrlc = ctrlc; + } + + iter + } + + pub fn collect_string(self, separator: &str, config: &Config) -> Result { + match self { + PipelineData::Value(v, ..) => Ok(v.into_string(separator, config)), + PipelineData::ListStream(s, ..) => Ok(s.into_string(separator, config)), + PipelineData::RawStream(s, ..) => { + let mut items = vec![]; + + for val in s { + match val { + Ok(val) => { + items.push(val); + } + Err(e) => { + return Err(e); + } + } + } + + let mut output = String::new(); + for item in items { + match item.as_string() { + Ok(s) => output.push_str(&s), + Err(err) => { + return Err(err); + } + } + } + + Ok(output) + } + } + } + + pub fn follow_cell_path( + self, + cell_path: &[PathMember], + head: Span, + ) -> Result { + match self { + // FIXME: there are probably better ways of doing this + PipelineData::ListStream(stream, ..) => Value::List { + vals: stream.collect(), + span: head, + } + .follow_cell_path(cell_path), + PipelineData::Value(v, ..) => v.follow_cell_path(cell_path), + _ => Err(ShellError::IOError("can't follow stream paths".into())), + } + } + + pub fn update_cell_path( + &mut self, + cell_path: &[PathMember], + callback: Box Value>, + head: Span, + ) -> Result<(), ShellError> { + match self { + // FIXME: there are probably better ways of doing this + PipelineData::ListStream(stream, ..) => Value::List { + vals: stream.collect(), + span: head, + } + .update_cell_path(cell_path, callback), + PipelineData::Value(v, ..) => v.update_cell_path(cell_path, callback), + _ => Ok(()), + } + } + + /// Simplified mapper to help with simple values also. For full iterator support use `.into_iter()` instead + pub fn map( + self, + mut f: F, + ctrlc: Option>, + ) -> Result + where + Self: Sized, + F: FnMut(Value) -> Value + 'static + Send, + { + match self { + PipelineData::Value(Value::List { vals, .. }, ..) => { + Ok(vals.into_iter().map(f).into_pipeline_data(ctrlc)) + } + PipelineData::ListStream(stream, ..) => Ok(stream.map(f).into_pipeline_data(ctrlc)), + PipelineData::RawStream(stream, ..) => { + let collected = stream.into_bytes()?; + + if let Ok(st) = String::from_utf8(collected.clone().item) { + Ok(f(Value::String { + val: st, + span: collected.span, + }) + .into_pipeline_data()) + } else { + Ok(f(Value::Binary { + val: collected.item, + span: collected.span, + }) + .into_pipeline_data()) + } + } + + PipelineData::Value(Value::Range { val, .. }, ..) => { + Ok(val.into_range_iter()?.map(f).into_pipeline_data(ctrlc)) + } + PipelineData::Value(v, ..) => match f(v) { + Value::Error { error } => Err(error), + v => Ok(v.into_pipeline_data()), + }, + } + } + + /// Simplified flatmapper. For full iterator support use `.into_iter()` instead + pub fn flat_map( + self, + mut f: F, + ctrlc: Option>, + ) -> Result + where + Self: Sized, + U: IntoIterator, + ::IntoIter: 'static + Send, + F: FnMut(Value) -> U + 'static + Send, + { + match self { + PipelineData::Value(Value::List { vals, .. }, ..) => { + Ok(vals.into_iter().map(f).flatten().into_pipeline_data(ctrlc)) + } + PipelineData::ListStream(stream, ..) => { + Ok(stream.map(f).flatten().into_pipeline_data(ctrlc)) + } + PipelineData::RawStream(stream, ..) => { + let collected = stream.into_bytes()?; + + if let Ok(st) = String::from_utf8(collected.clone().item) { + Ok(f(Value::String { + val: st, + span: collected.span, + }) + .into_iter() + .into_pipeline_data(ctrlc)) + } else { + Ok(f(Value::Binary { + val: collected.item, + span: collected.span, + }) + .into_iter() + .into_pipeline_data(ctrlc)) + } + } + PipelineData::Value(Value::Range { val, .. }, ..) => match val.into_range_iter() { + Ok(iter) => Ok(iter.map(f).flatten().into_pipeline_data(ctrlc)), + Err(error) => Err(error), + }, + PipelineData::Value(v, ..) => Ok(f(v).into_iter().into_pipeline_data(ctrlc)), + } + } + + pub fn filter( + self, + mut f: F, + ctrlc: Option>, + ) -> Result + where + Self: Sized, + F: FnMut(&Value) -> bool + 'static + Send, + { + match self { + PipelineData::Value(Value::List { vals, .. }, ..) => { + Ok(vals.into_iter().filter(f).into_pipeline_data(ctrlc)) + } + PipelineData::ListStream(stream, ..) => Ok(stream.filter(f).into_pipeline_data(ctrlc)), + PipelineData::RawStream(stream, ..) => { + let collected = stream.into_bytes()?; + + if let Ok(st) = String::from_utf8(collected.clone().item) { + let v = Value::String { + val: st, + span: collected.span, + }; + + if f(&v) { + Ok(v.into_pipeline_data()) + } else { + Ok(PipelineData::new(collected.span)) + } + } else { + let v = Value::Binary { + val: collected.item, + span: collected.span, + }; + + if f(&v) { + Ok(v.into_pipeline_data()) + } else { + Ok(PipelineData::new(collected.span)) + } + } + } + PipelineData::Value(Value::Range { val, .. }, ..) => { + Ok(val.into_range_iter()?.filter(f).into_pipeline_data(ctrlc)) + } + PipelineData::Value(v, ..) => { + if f(&v) { + Ok(v.into_pipeline_data()) + } else { + Ok(Value::Nothing { span: v.span()? }.into_pipeline_data()) + } + } + } + } +} + +pub struct PipelineIterator(PipelineData); + +impl IntoIterator for PipelineData { + type Item = Value; + + type IntoIter = PipelineIterator; + + fn into_iter(self) -> Self::IntoIter { + match self { + PipelineData::Value(Value::List { vals, .. }, metadata) => { + PipelineIterator(PipelineData::ListStream( + ListStream { + stream: Box::new(vals.into_iter()), + ctrlc: None, + }, + metadata, + )) + } + PipelineData::Value(Value::Range { val, .. }, metadata) => { + match val.into_range_iter() { + Ok(iter) => PipelineIterator(PipelineData::ListStream( + ListStream { + stream: Box::new(iter), + ctrlc: None, + }, + metadata, + )), + Err(error) => PipelineIterator(PipelineData::ListStream( + ListStream { + stream: Box::new(std::iter::once(Value::Error { error })), + ctrlc: None, + }, + metadata, + )), + } + } + x => PipelineIterator(x), + } + } +} + +impl Iterator for PipelineIterator { + type Item = Value; + + fn next(&mut self) -> Option { + match &mut self.0 { + PipelineData::Value(Value::Nothing { .. }, ..) => None, + PipelineData::Value(v, ..) => Some(std::mem::take(v)), + PipelineData::ListStream(stream, ..) => stream.next(), + PipelineData::RawStream(stream, ..) => stream.next().map(|x| match x { + Ok(x) => x, + Err(err) => Value::Error { error: err }, + }), + } + } +} + +pub trait IntoPipelineData { + fn into_pipeline_data(self) -> PipelineData; +} + +impl IntoPipelineData for V +where + V: Into, +{ + fn into_pipeline_data(self) -> PipelineData { + PipelineData::Value(self.into(), None) + } +} + +pub trait IntoInterruptiblePipelineData { + fn into_pipeline_data(self, ctrlc: Option>) -> PipelineData; + fn into_pipeline_data_with_metadata( + self, + metadata: PipelineMetadata, + ctrlc: Option>, + ) -> PipelineData; +} + +impl IntoInterruptiblePipelineData for I +where + I: IntoIterator + Send + 'static, + I::IntoIter: Send + 'static, + ::Item: Into, +{ + fn into_pipeline_data(self, ctrlc: Option>) -> PipelineData { + PipelineData::ListStream( + ListStream { + stream: Box::new(self.into_iter().map(Into::into)), + ctrlc, + }, + None, + ) + } + + fn into_pipeline_data_with_metadata( + self, + metadata: PipelineMetadata, + ctrlc: Option>, + ) -> PipelineData { + PipelineData::ListStream( + ListStream { + stream: Box::new(self.into_iter().map(Into::into)), + ctrlc, + }, + Some(metadata), + ) + } +} diff --git a/crates/nu-protocol/src/shell_error.rs b/crates/nu-protocol/src/shell_error.rs new file mode 100644 index 0000000000..43f75cafd7 --- /dev/null +++ b/crates/nu-protocol/src/shell_error.rs @@ -0,0 +1,367 @@ +use miette::Diagnostic; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::{ast::Operator, Span, Type}; + +/// The fundamental error type for the evaluation engine. These cases represent different kinds of errors +/// the evaluator might face, along with helpful spans to label. An error renderer will take this error value +/// and pass it into an error viewer to display to the user. +#[derive(Debug, Clone, Error, Diagnostic, Serialize, Deserialize)] +pub enum ShellError { + #[error("Type mismatch during operation.")] + #[diagnostic(code(nu::shell::type_mismatch), url(docsrs))] + OperatorMismatch { + #[label = "type mismatch for operator"] + op_span: Span, + lhs_ty: Type, + #[label("{lhs_ty}")] + lhs_span: Span, + rhs_ty: Type, + #[label("{rhs_ty}")] + rhs_span: Span, + }, + + #[error("Operator overflow.")] + #[diagnostic(code(nu::shell::operator_overflow), url(docsrs))] + OperatorOverflow(String, #[label = "{0}"] Span), + + #[error("Pipeline mismatch.")] + #[diagnostic(code(nu::shell::pipeline_mismatch), url(docsrs))] + PipelineMismatch( + String, + #[label("expected: {0}")] Span, + #[label("value originates from here")] Span, + ), + + #[error("Type mismatch")] + #[diagnostic(code(nu::shell::type_mismatch), url(docsrs))] + TypeMismatch(String, #[label = "{0}"] Span), + + #[error("Unsupported operator: {0}.")] + #[diagnostic(code(nu::shell::unsupported_operator), url(docsrs))] + UnsupportedOperator(Operator, #[label = "unsupported operator"] Span), + + #[error("Unsupported operator: {0}.")] + #[diagnostic(code(nu::shell::unknown_operator), url(docsrs))] + UnknownOperator(String, #[label = "unsupported operator"] Span), + + #[error("Missing parameter: {0}.")] + #[diagnostic(code(nu::shell::missing_parameter), url(docsrs))] + MissingParameter(String, #[label = "missing parameter: {0}"] Span), + + // Be cautious, as flags can share the same span, resulting in a panic (ex: `rm -pt`) + #[error("Incompatible parameters.")] + #[diagnostic(code(nu::shell::incompatible_parameters), url(docsrs))] + IncompatibleParameters { + left_message: String, + #[label("{left_message}")] + left_span: Span, + right_message: String, + #[label("{right_message}")] + right_span: Span, + }, + + #[error("Delimiter error")] + #[diagnostic(code(nu::shell::delimiter_error), url(docsrs))] + DelimiterError(String, #[label("{0}")] Span), + + #[error("Incompatible parameters.")] + #[diagnostic(code(nu::shell::incompatible_parameters), url(docsrs))] + IncompatibleParametersSingle(String, #[label = "{0}"] Span), + + #[error("Feature not enabled.")] + #[diagnostic(code(nu::shell::feature_not_enabled), url(docsrs))] + FeatureNotEnabled(#[label = "feature not enabled"] Span), + + #[error("External commands not yet supported")] + #[diagnostic(code(nu::shell::external_commands), url(docsrs))] + ExternalNotSupported(#[label = "external not supported"] Span), + + #[error("Invalid Probability.")] + #[diagnostic(code(nu::shell::invalid_probability), url(docsrs))] + InvalidProbability(#[label = "invalid probability"] Span), + + #[error("Invalid range {0}..{1}")] + #[diagnostic(code(nu::shell::invalid_range), url(docsrs))] + InvalidRange(String, String, #[label = "expected a valid range"] Span), + + // Only use this one if we Nushell completely falls over and hits a state that isn't possible or isn't recoverable + #[error("Nushell failed: {0}.")] + #[diagnostic(code(nu::shell::nushell_failed), url(docsrs))] + NushellFailed(String), + + // Only use this one if we Nushell completely falls over and hits a state that isn't possible or isn't recoverable + #[error("Nushell failed: {0}.")] + #[diagnostic(code(nu::shell::nushell_failed), url(docsrs))] + NushellFailedSpanned(String, String, #[label = "{1}"] Span), + + #[error("Variable not found")] + #[diagnostic(code(nu::shell::variable_not_found), url(docsrs))] + VariableNotFoundAtRuntime(#[label = "variable not found"] Span), + + #[error("Environment variable '{0}' not found")] + #[diagnostic(code(nu::shell::env_variable_not_found), url(docsrs))] + EnvVarNotFoundAtRuntime(String, #[label = "environment variable not found"] Span), + + #[error("Not found.")] + #[diagnostic(code(nu::parser::not_found), url(docsrs))] + NotFound(#[label = "did not find anything under this name"] Span), + + #[error("Can't convert to {0}.")] + #[diagnostic(code(nu::shell::cant_convert), url(docsrs))] + CantConvert(String, String, #[label("can't convert {1} to {0}")] Span), + + #[error("Division by zero.")] + #[diagnostic(code(nu::shell::division_by_zero), url(docsrs))] + DivisionByZero(#[label("division by zero")] Span), + + #[error("Can't convert range to countable values")] + #[diagnostic(code(nu::shell::range_to_countable), url(docsrs))] + CannotCreateRange(#[label = "can't convert to countable values"] Span), + + #[error("Row number too large (max: {0}).")] + #[diagnostic(code(nu::shell::access_beyond_end), url(docsrs))] + AccessBeyondEnd(usize, #[label = "too large"] Span), + + #[error("Row number too large.")] + #[diagnostic(code(nu::shell::access_beyond_end_of_stream), url(docsrs))] + AccessBeyondEndOfStream(#[label = "too large"] Span), + + #[error("Data cannot be accessed with a cell path")] + #[diagnostic(code(nu::shell::incompatible_path_access), url(docsrs))] + IncompatiblePathAccess(String, #[label("{0} doesn't support cell paths")] Span), + + #[error("Cannot find column")] + #[diagnostic(code(nu::shell::column_not_found), url(docsrs))] + CantFindColumn( + #[label = "cannot find column"] Span, + #[label = "value originates here"] Span, + ), + + #[error("Not a list value")] + #[diagnostic(code(nu::shell::not_a_list), url(docsrs))] + NotAList( + #[label = "value not a list"] Span, + #[label = "value originates here"] Span, + ), + + #[error("External command")] + #[diagnostic(code(nu::shell::external_command), url(docsrs), help("{1}"))] + ExternalCommand(String, String, #[label("{0}")] Span), + + #[error("Unsupported input")] + #[diagnostic(code(nu::shell::unsupported_input), url(docsrs))] + UnsupportedInput(String, #[label("{0}")] Span), + + #[error("Network failure")] + #[diagnostic(code(nu::shell::network_failure), url(docsrs))] + NetworkFailure(String, #[label("{0}")] Span), + + #[error("Command not found")] + #[diagnostic(code(nu::shell::command_not_found), url(docsrs))] + CommandNotFound(#[label("command not found")] Span), + + #[error("Flag not found")] + #[diagnostic(code(nu::shell::flag_not_found), url(docsrs))] + FlagNotFound(String, #[label("{0} not found")] Span), + + #[error("File not found")] + #[diagnostic(code(nu::shell::file_not_found), url(docsrs))] + FileNotFound(#[label("file not found")] Span), + + #[error("File not found")] + #[diagnostic(code(nu::shell::file_not_found), url(docsrs))] + FileNotFoundCustom(String, #[label("{0}")] Span), + + #[error("Plugin failed to load: {0}")] + #[diagnostic(code(nu::shell::plugin_failed_to_load), url(docsrs))] + PluginFailedToLoad(String), + + #[error("Plugin failed to encode: {0}")] + #[diagnostic(code(nu::shell::plugin_failed_to_encode), url(docsrs))] + PluginFailedToEncode(String), + + #[error("Plugin failed to decode: {0}")] + #[diagnostic(code(nu::shell::plugin_failed_to_decode), url(docsrs))] + PluginFailedToDecode(String), + + #[error("I/O error")] + #[diagnostic(code(nu::shell::io_error), url(docsrs), help("{0}"))] + IOError(String), + + #[error("Cannot change to directory")] + #[diagnostic(code(nu::shell::cannot_cd_to_directory), url(docsrs))] + NotADirectory(#[label("is not a directory")] Span), + + #[error("Directory not found")] + #[diagnostic(code(nu::shell::directory_not_found), url(docsrs))] + DirectoryNotFound(#[label("directory not found")] Span), + + #[error("Directory not found")] + #[diagnostic(code(nu::shell::directory_not_found_custom), url(docsrs))] + DirectoryNotFoundCustom(String, #[label("{0}")] Span), + + #[error("Directory not found")] + #[diagnostic(code(nu::shell::directory_not_found_help), url(docsrs), help("{1}"))] + DirectoryNotFoundHelp(#[label("directory not found")] Span, String), + + #[error("Move not possible")] + #[diagnostic(code(nu::shell::move_not_possible), url(docsrs))] + MoveNotPossible { + source_message: String, + #[label("{source_message}")] + source_span: Span, + destination_message: String, + #[label("{destination_message}")] + destination_span: Span, + }, + + #[error("Move not possible")] + #[diagnostic(code(nu::shell::move_not_possible_single), url(docsrs))] + MoveNotPossibleSingle(String, #[label("{0}")] Span), + + #[error("Create not possible")] + #[diagnostic(code(nu::shell::create_not_possible), url(docsrs))] + CreateNotPossible(String, #[label("{0}")] Span), + + #[error("Remove not possible")] + #[diagnostic(code(nu::shell::remove_not_possible), url(docsrs))] + RemoveNotPossible(String, #[label("{0}")] Span), + + #[error("No file to be removed")] + NoFileToBeRemoved(), + #[error("No file to be moved")] + NoFileToBeMoved(), + #[error("No file to be copied")] + NoFileToBeCopied(), + + #[error("Name not found")] + #[diagnostic(code(nu::shell::name_not_found), url(docsrs))] + DidYouMean(String, #[label("did you mean '{0}'?")] Span), + + #[error("Non-UTF8 string")] + #[diagnostic(code(nu::parser::non_utf8), url(docsrs))] + NonUtf8(#[label = "non-UTF8 string"] Span), + + #[error("Casting error")] + #[diagnostic(code(nu::shell::downcast_not_possible), url(docsrs))] + DowncastNotPossible(String, #[label("{0}")] Span), + + #[error("Unsupported config value")] + #[diagnostic(code(nu::shell::unsupported_config_value), url(docsrs))] + UnsupportedConfigValue(String, String, #[label = "expected {0}, got {1}"] Span), + + #[error("Missing config value")] + #[diagnostic(code(nu::shell::missing_config_value), url(docsrs))] + MissingConfigValue(String, #[label = "missing {0}"] Span), + + #[error("{0}")] + #[diagnostic()] + SpannedLabeledError(String, String, #[label("{1}")] Span), + + #[error("{0}")] + #[diagnostic(help("{3}"))] + SpannedLabeledErrorHelp(String, String, #[label("{1}")] Span, String), + + #[error("{0}")] + #[diagnostic(help("{1}"))] + LabeledError(String, String), +} + +impl From for ShellError { + fn from(input: std::io::Error) -> ShellError { + ShellError::IOError(format!("{:?}", input)) + } +} + +impl std::convert::From> for ShellError { + fn from(input: Box) -> ShellError { + ShellError::IOError(input.to_string()) + } +} + +impl From> for ShellError { + fn from(input: Box) -> ShellError { + ShellError::IOError(format!("{:?}", input)) + } +} + +pub fn did_you_mean(possibilities: &[String], tried: &str) -> Option { + let mut possible_matches: Vec<_> = possibilities + .iter() + .map(|word| { + let edit_distance = levenshtein_distance(word, tried); + (edit_distance, word.to_owned()) + }) + .collect(); + + possible_matches.sort(); + + if let Some((_, first)) = possible_matches.into_iter().next() { + Some(first) + } else { + None + } +} + +// Borrowed from here https://github.com/wooorm/levenshtein-rs +pub fn levenshtein_distance(a: &str, b: &str) -> usize { + let mut result = 0; + + /* Shortcut optimizations / degenerate cases. */ + if a == b { + return result; + } + + let length_a = a.chars().count(); + let length_b = b.chars().count(); + + if length_a == 0 { + return length_b; + } + + if length_b == 0 { + return length_a; + } + + /* Initialize the vector. + * + * This is why it’s fast, normally a matrix is used, + * here we use a single vector. */ + let mut cache: Vec = (1..).take(length_a).collect(); + let mut distance_a; + let mut distance_b; + + /* Loop. */ + for (index_b, code_b) in b.chars().enumerate() { + result = index_b; + distance_a = index_b; + + for (index_a, code_a) in a.chars().enumerate() { + distance_b = if code_a == code_b { + distance_a + } else { + distance_a + 1 + }; + + distance_a = cache[index_a]; + + result = if distance_a > result { + if distance_b > result { + result + 1 + } else { + distance_b + } + } else if distance_b > distance_a { + distance_a + 1 + } else { + distance_b + }; + + cache[index_a] = result; + } + } + + result +} diff --git a/crates/nu-protocol/src/signature.rs b/crates/nu-protocol/src/signature.rs index 1969931241..c4996325ce 100644 --- a/crates/nu-protocol/src/signature.rs +++ b/crates/nu-protocol/src/signature.rs @@ -1,3 +1,4 @@ +<<<<<<< HEAD use crate::syntax_shape::SyntaxShape; use crate::type_shape::Type; use indexmap::IndexMap; @@ -156,13 +157,118 @@ pub struct Signature { pub input: Option, /// If the command is expected to filter data, or to consume it (as a sink) pub is_filter: bool, +======= +use serde::Deserialize; +use serde::Serialize; + +use crate::ast::Call; +use crate::engine::Command; +use crate::engine::EngineState; +use crate::engine::Stack; +use crate::BlockId; +use crate::PipelineData; +use crate::SyntaxShape; +use crate::VarId; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Flag { + pub long: String, + pub short: Option, + pub arg: Option, + pub required: bool, + pub desc: String, + // For custom commands + pub var_id: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PositionalArg { + pub name: String, + pub desc: String, + pub shape: SyntaxShape, + // For custom commands + pub var_id: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum Category { + Default, + Conversions, + Core, + Date, + Env, + Experimental, + FileSystem, + Filters, + Formats, + Math, + Network, + Random, + Platform, + Shells, + Strings, + System, + Viewers, + Hash, + Generators, + Custom(String), +} + +impl std::fmt::Display for Category { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let msg = match self { + Category::Default => "default", + Category::Conversions => "conversions", + Category::Core => "core", + Category::Date => "date", + Category::Env => "env", + Category::Experimental => "experimental", + Category::FileSystem => "filesystem", + Category::Filters => "filters", + Category::Formats => "formats", + Category::Math => "math", + Category::Network => "network", + Category::Random => "random", + Category::Platform => "platform", + Category::Shells => "shells", + Category::Strings => "strings", + Category::System => "system", + Category::Viewers => "viewers", + Category::Hash => "hash", + Category::Generators => "generators", + Category::Custom(name) => name, + }; + + write!(f, "{}", msg) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Signature { + pub name: String, + pub usage: String, + pub extra_usage: String, + pub required_positional: Vec, + pub optional_positional: Vec, + pub rest_positional: Option, + pub named: Vec, + pub is_filter: bool, + pub creates_scope: bool, + // Signature category used to classify commands stored in the list of declarations + pub category: Category, +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } impl PartialEq for Signature { fn eq(&self, other: &Self) -> bool { self.name == other.name && self.usage == other.usage +<<<<<<< HEAD && self.positional == other.positional +======= + && self.required_positional == other.required_positional + && self.optional_positional == other.optional_positional +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce && self.rest_positional == other.rest_positional && self.is_filter == other.is_filter } @@ -171,6 +277,7 @@ impl PartialEq for Signature { impl Eq for Signature {} impl Signature { +<<<<<<< HEAD pub fn shift_positional(&mut self) { self.positional = Vec::from(&self.positional[1..]); } @@ -224,10 +331,24 @@ impl PrettyDebugWithSource for Signature { impl Signature { /// Create a new command signature with the given name pub fn new(name: impl Into) -> Signature { +======= + pub fn new(name: impl Into) -> Signature { + // default help flag + let flag = Flag { + long: "help".into(), + short: Some('h'), + arg: None, + desc: "Display this help message".into(), + required: false, + var_id: None, + }; + +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce Signature { name: name.into(), usage: String::new(), extra_usage: String::new(), +<<<<<<< HEAD positional: vec![], rest_positional: None, named: indexmap::indexmap! {"help".into() => (NamedType::Switch(Some('h')), "Display this help message".into())}, @@ -238,6 +359,17 @@ impl Signature { } /// Create a new signature +======= + required_positional: vec![], + optional_positional: vec![], + rest_positional: None, + named: vec![flag], + is_filter: false, + creates_scope: false, + category: Category::Default, + } + } +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce pub fn build(name: impl Into) -> Signature { Signature::new(name.into()) } @@ -252,6 +384,7 @@ impl Signature { pub fn required( mut self, name: impl Into, +<<<<<<< HEAD ty: impl Into, desc: impl Into, ) -> Signature { @@ -259,10 +392,22 @@ impl Signature { PositionalType::Mandatory(name.into(), ty.into()), desc.into(), )); +======= + shape: impl Into, + desc: impl Into, + ) -> Signature { + self.required_positional.push(PositionalArg { + name: name.into(), + desc: desc.into(), + shape: shape.into(), + var_id: None, + }); +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce self } +<<<<<<< HEAD /// Add an optional positional argument to the signature pub fn optional( mut self, @@ -274,6 +419,37 @@ impl Signature { PositionalType::Optional(name.into(), ty.into()), desc.into(), )); +======= + /// Add a required positional argument to the signature + pub fn optional( + mut self, + name: impl Into, + shape: impl Into, + desc: impl Into, + ) -> Signature { + self.optional_positional.push(PositionalArg { + name: name.into(), + desc: desc.into(), + shape: shape.into(), + var_id: None, + }); + + self + } + + pub fn rest( + mut self, + name: &str, + shape: impl Into, + desc: impl Into, + ) -> Signature { + self.rest_positional = Some(PositionalArg { + name: name.into(), + desc: desc.into(), + shape: shape.into(), + var_id: None, + }); +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce self } @@ -282,6 +458,7 @@ impl Signature { pub fn named( mut self, name: impl Into, +<<<<<<< HEAD ty: impl Into, desc: impl Into, short: Option, @@ -294,6 +471,22 @@ impl Signature { name.into(), (NamedType::Optional(s, ty.into()), desc.into()), ); +======= + shape: impl Into, + desc: impl Into, + short: Option, + ) -> Signature { + let (name, s) = self.check_names(name, short); + + self.named.push(Flag { + long: name, + short: s, + arg: Some(shape.into()), + required: false, + desc: desc.into(), + var_id: None, + }); +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce self } @@ -302,6 +495,7 @@ impl Signature { pub fn required_named( mut self, name: impl Into, +<<<<<<< HEAD ty: impl Into, desc: impl Into, short: Option, @@ -316,6 +510,23 @@ impl Signature { (NamedType::Mandatory(s, ty.into()), desc.into()), ); +======= + shape: impl Into, + desc: impl Into, + short: Option, + ) -> Signature { + let (name, s) = self.check_names(name, short); + + self.named.push(Flag { + long: name, + short: s, + arg: Some(shape.into()), + required: true, + desc: desc.into(), + var_id: None, + }); + +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce self } @@ -326,6 +537,80 @@ impl Signature { desc: impl Into, short: Option, ) -> Signature { +<<<<<<< HEAD +======= + let (name, s) = self.check_names(name, short); + + self.named.push(Flag { + long: name, + short: s, + arg: None, + required: false, + desc: desc.into(), + var_id: None, + }); + + self + } + + /// Changes the signature category + pub fn category(mut self, category: Category) -> Signature { + self.category = category; + + self + } + + /// Sets that signature will create a scope as it parses + pub fn creates_scope(mut self) -> Signature { + self.creates_scope = true; + self + } + + pub fn call_signature(&self) -> String { + let mut one_liner = String::new(); + one_liner.push_str(&self.name); + one_liner.push(' '); + + // Note: the call signature needs flags first because on the nu commandline, + // flags will precede the script file name. Flags for internal commands can come + // either before or after (or around) positional parameters, so there isn't a strong + // preference, so we default to the more constrained example. + if self.named.len() > 1 { + one_liner.push_str("{flags} "); + } + + for positional in &self.required_positional { + one_liner.push_str(&get_positional_short_name(positional, true)); + } + for positional in &self.optional_positional { + one_liner.push_str(&get_positional_short_name(positional, false)); + } + + if let Some(rest) = &self.rest_positional { + one_liner.push_str(&format!("...{}", get_positional_short_name(rest, false))); + } + + // if !self.subcommands.is_empty() { + // one_liner.push_str(" "); + // } + + one_liner + } + + /// Get list of the short-hand flags + pub fn get_shorts(&self) -> Vec { + self.named.iter().filter_map(|f| f.short).collect() + } + + /// Get list of the long-hand flags + pub fn get_names(&self) -> Vec<&str> { + self.named.iter().map(|f| f.long.as_str()).collect() + } + + /// Checks if short or long are already present + /// Panics if one of them is found + fn check_names(&self, name: impl Into, short: Option) -> (String, Option) { +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce let s = short.map(|c| { debug_assert!( !self.get_shorts().contains(&c), @@ -334,9 +619,95 @@ impl Signature { c }); +<<<<<<< HEAD self.named .insert(name.into(), (NamedType::Switch(s), desc.into())); self +======= + let name = { + let name: String = name.into(); + debug_assert!( + !self.get_names().contains(&name.as_str()), + "There may be duplicate name flags, such as --help" + ); + name + }; + + (name, s) + } + + pub fn get_positional(&self, position: usize) -> Option { + if position < self.required_positional.len() { + self.required_positional.get(position).cloned() + } else if position < (self.required_positional.len() + self.optional_positional.len()) { + self.optional_positional + .get(position - self.required_positional.len()) + .cloned() + } else { + self.rest_positional.clone() + } + } + + pub fn num_positionals(&self) -> usize { + let mut total = self.required_positional.len() + self.optional_positional.len(); + + for positional in &self.required_positional { + if let SyntaxShape::Keyword(..) = positional.shape { + // Keywords have a required argument, so account for that + total += 1; + } + } + for positional in &self.optional_positional { + if let SyntaxShape::Keyword(..) = positional.shape { + // Keywords have a required argument, so account for that + total += 1; + } + } + total + } + + pub fn num_positionals_after(&self, idx: usize) -> usize { + let mut total = 0; + + for (curr, positional) in self.required_positional.iter().enumerate() { + match positional.shape { + SyntaxShape::Keyword(..) => { + // Keywords have a required argument, so account for that + if curr > idx { + total += 2; + } + } + _ => { + if curr > idx { + total += 1; + } + } + } + } + total + } + + /// Find the matching long flag + pub fn get_long_flag(&self, name: &str) -> Option { + for flag in &self.named { + if flag.long == name { + return Some(flag.clone()); + } + } + None + } + + /// Find the matching long flag + pub fn get_short_flag(&self, short: char) -> Option { + for flag in &self.named { + if let Some(short_flag) = &flag.short { + if *short_flag == short { + return Some(flag.clone()); + } + } + } + None +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } /// Set the filter flag for the signature @@ -345,6 +716,7 @@ impl Signature { self } +<<<<<<< HEAD /// Set the type for the "rest" of the positional arguments /// Note: Not naming the field in your struct holding the rest values "rest", can /// cause errors when deserializing @@ -379,5 +751,102 @@ impl Signature { } } shorts +======= + /// Create a placeholder implementation of Command as a way to predeclare a definition's + /// signature so other definitions can see it. This placeholder is later replaced with the + /// full definition in a second pass of the parser. + pub fn predeclare(self) -> Box { + Box::new(Predeclaration { signature: self }) + } + + /// Combines a signature and a block into a runnable block + pub fn into_block_command(self, block_id: BlockId) -> Box { + Box::new(BlockCommand { + signature: self, + block_id, + }) + } +} + +#[derive(Clone)] +struct Predeclaration { + signature: Signature, +} + +impl Command for Predeclaration { + fn name(&self) -> &str { + &self.signature.name + } + + fn signature(&self) -> Signature { + self.signature.clone() + } + + fn usage(&self) -> &str { + &self.signature.usage + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + panic!("Internal error: can't run a predeclaration without a body") + } +} + +fn get_positional_short_name(arg: &PositionalArg, is_required: bool) -> String { + match &arg.shape { + SyntaxShape::Keyword(name, ..) => { + if is_required { + format!("{} <{}> ", String::from_utf8_lossy(name), arg.name) + } else { + format!("({} <{}>) ", String::from_utf8_lossy(name), arg.name) + } + } + _ => { + if is_required { + format!("<{}> ", arg.name) + } else { + format!("({}) ", arg.name) + } + } + } +} + +#[derive(Clone)] +struct BlockCommand { + signature: Signature, + block_id: BlockId, +} + +impl Command for BlockCommand { + fn name(&self) -> &str { + &self.signature.name + } + + fn signature(&self) -> Signature { + self.signature.clone() + } + + fn usage(&self) -> &str { + &self.signature.usage + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + panic!("Internal error: can't run custom command with 'run', use block_id"); + } + + fn get_block_id(&self) -> Option { + Some(self.block_id) +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } } diff --git a/crates/nu-protocol/src/span.rs b/crates/nu-protocol/src/span.rs new file mode 100644 index 0000000000..3b383df0ed --- /dev/null +++ b/crates/nu-protocol/src/span.rs @@ -0,0 +1,76 @@ +use miette::SourceSpan; +use serde::{Deserialize, Serialize}; + +/// A spanned area of interest, generic over what kind of thing is of interest +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Spanned +where + T: Clone + std::fmt::Debug, +{ + pub item: T, + pub span: Span, +} + +/// Spans are a global offset across all seen files, which are cached in the engine's state. The start and +/// end offset together make the inclusive start/exclusive end pair for where to underline to highlight +/// a given point of interest. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +pub struct Span { + pub start: usize, + pub end: usize, +} + +impl From for SourceSpan { + fn from(s: Span) -> Self { + Self::new(s.start.into(), (s.end - s.start).into()) + } +} + +impl Span { + pub fn new(start: usize, end: usize) -> Span { + Span { start, end } + } + + /// Note: Only use this for test data, *not* live data, as it will point into unknown source + /// when used in errors. + pub fn test_data() -> Span { + Span { start: 0, end: 0 } + } + + pub fn offset(&self, offset: usize) -> Span { + Span { + start: self.start - offset, + end: self.end - offset, + } + } + + pub fn contains(&self, pos: usize) -> bool { + pos >= self.start && pos < self.end + } + + /// Point to the space just past this span, useful for missing + /// values + pub fn past(&self) -> Span { + Span { + start: self.end, + end: self.end, + } + } +} + +/// Used when you have a slice of spans of at least size 1 +pub fn span(spans: &[Span]) -> Span { + let length = spans.len(); + + if length == 0 { + // TODO: do this for now, but we might also want to protect against this case + Span { start: 0, end: 0 } + } else if length == 1 { + spans[0] + } else { + Span { + start: spans[0].start, + end: spans[length - 1].end, + } + } +} diff --git a/crates/nu-protocol/src/syntax_shape.rs b/crates/nu-protocol/src/syntax_shape.rs index 0cea4011b4..112042dc18 100644 --- a/crates/nu-protocol/src/syntax_shape.rs +++ b/crates/nu-protocol/src/syntax_shape.rs @@ -1,3 +1,4 @@ +<<<<<<< HEAD use nu_source::{DbgDocBldr, DebugDocBuilder, PrettyDebug}; use serde::{Deserialize, Serialize}; @@ -58,13 +59,171 @@ impl SyntaxShape { SyntaxShape::Operator => "operator", SyntaxShape::RowCondition => "condition", SyntaxShape::MathExpression => "math expression", +======= +use std::fmt::Display; + +use serde::{Deserialize, Serialize}; + +use crate::Type; + +/// The syntactic shapes that values must match to be passed into a command. You can think of this as the type-checking that occurs when you call a function. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum SyntaxShape { + /// A specific match to a word or symbol + Keyword(Vec, Box), + + /// Any syntactic form is allowed + Any, + + /// Strings and string-like bare words are allowed + String, + + /// A dotted path to navigate the table + CellPath, + + /// A dotted path to navigate the table (including variable) + FullCellPath, + + /// Only a numeric (integer or decimal) value is allowed + Number, + + /// A range is allowed (eg, `1..3`) + Range, + + /// Only an integer value is allowed + Int, + + /// A filepath is allowed + Filepath, + + /// A glob pattern is allowed, eg `foo*` + GlobPattern, + + /// A module path pattern used for imports + ImportPattern, + + /// A block is allowed, eg `{start this thing}` + Block(Option>), + + /// A table is allowed, eg `[[first, second]; [1, 2]]` + Table, + + /// A table is allowed, eg `[first second]` + List(Box), + + /// A filesize value is allowed, eg `10kb` + Filesize, + + /// A duration value is allowed, eg `19day` + Duration, + + /// An operator + Operator, + + /// A math expression which expands shorthand forms on the lefthand side, eg `foo > 1` + /// The shorthand allows us to more easily reach columns inside of the row being passed in + RowCondition, + + /// A general math expression, eg `1 + 2` + MathExpression, + + /// A variable name + Variable, + + /// A variable with optional type, `x` or `x: int` + VarWithOptType, + + /// A signature for a definition, `[x:int, --foo]` + Signature, + + /// A general expression, eg `1 + 2` or `foo --bar` + Expression, + + /// A boolean value + Boolean, + + /// A record value + Record, + + /// A custom shape with custom completion logic + Custom(Box, String), +} + +impl SyntaxShape { + pub fn to_type(&self) -> Type { + match self { + SyntaxShape::Any => Type::Unknown, + SyntaxShape::Block(_) => Type::Block, + SyntaxShape::CellPath => Type::Unknown, + SyntaxShape::Custom(custom, _) => custom.to_type(), + SyntaxShape::Duration => Type::Duration, + SyntaxShape::Expression => Type::Unknown, + SyntaxShape::Filepath => Type::String, + SyntaxShape::Filesize => Type::Filesize, + SyntaxShape::FullCellPath => Type::Unknown, + SyntaxShape::GlobPattern => Type::String, + SyntaxShape::ImportPattern => Type::Unknown, + SyntaxShape::Int => Type::Int, + SyntaxShape::List(x) => { + let contents = x.to_type(); + Type::List(Box::new(contents)) + } + SyntaxShape::Keyword(_, expr) => expr.to_type(), + SyntaxShape::MathExpression => Type::Unknown, + SyntaxShape::Number => Type::Number, + SyntaxShape::Operator => Type::Unknown, + SyntaxShape::Range => Type::Unknown, + SyntaxShape::Record => Type::Record(vec![]), // FIXME: Add actual record type + SyntaxShape::RowCondition => Type::Bool, + SyntaxShape::Boolean => Type::Bool, + SyntaxShape::Signature => Type::Signature, + SyntaxShape::String => Type::String, + SyntaxShape::Table => Type::List(Box::new(Type::Unknown)), // FIXME: Tables should have better types + SyntaxShape::VarWithOptType => Type::Unknown, + SyntaxShape::Variable => Type::Unknown, +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } } } +<<<<<<< HEAD impl PrettyDebug for SyntaxShape { /// Prepare SyntaxShape for pretty-printing fn pretty(&self) -> DebugDocBuilder { DbgDocBldr::kind(self.syntax_shape_name().to_string()) +======= +impl Display for SyntaxShape { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SyntaxShape::Keyword(kw, shape) => { + write!(f, "\"{}\" {}", String::from_utf8_lossy(kw), shape) + } + SyntaxShape::Any => write!(f, "any"), + SyntaxShape::String => write!(f, "string"), + SyntaxShape::CellPath => write!(f, "cellpath"), + SyntaxShape::FullCellPath => write!(f, "cellpath"), + SyntaxShape::Number => write!(f, "number"), + SyntaxShape::Range => write!(f, "range"), + SyntaxShape::Int => write!(f, "int"), + SyntaxShape::Filepath => write!(f, "path"), + SyntaxShape::GlobPattern => write!(f, "glob"), + SyntaxShape::ImportPattern => write!(f, "import"), + SyntaxShape::Block(_) => write!(f, "block"), + SyntaxShape::Table => write!(f, "table"), + SyntaxShape::List(x) => write!(f, "list<{}>", x), + SyntaxShape::Record => write!(f, "record"), + SyntaxShape::Filesize => write!(f, "filesize"), + SyntaxShape::Duration => write!(f, "duration"), + SyntaxShape::Operator => write!(f, "operator"), + SyntaxShape::RowCondition => write!(f, "condition"), + SyntaxShape::MathExpression => write!(f, "variable"), + SyntaxShape::Variable => write!(f, "var"), + SyntaxShape::VarWithOptType => write!(f, "vardecl"), + SyntaxShape::Signature => write!(f, "signature"), + SyntaxShape::Expression => write!(f, "expression"), + SyntaxShape::Boolean => write!(f, "bool"), + SyntaxShape::Custom(x, _) => write!(f, "custom<{}>", x), + } +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } } diff --git a/crates/nu-protocol/src/ty.rs b/crates/nu-protocol/src/ty.rs new file mode 100644 index 0000000000..568a09b6af --- /dev/null +++ b/crates/nu-protocol/src/ty.rs @@ -0,0 +1,64 @@ +use serde::{Deserialize, Serialize}; + +use std::fmt::Display; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum Type { + Int, + Float, + Range, + Bool, + String, + Block, + CellPath, + Duration, + Date, + Filesize, + List(Box), + Number, + Nothing, + Record(Vec<(String, Type)>), + Table, + ValueStream, + Unknown, + Error, + Binary, + Custom, + Signature, +} + +impl Display for Type { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Type::Block => write!(f, "block"), + Type::Bool => write!(f, "bool"), + Type::CellPath => write!(f, "cell path"), + Type::Date => write!(f, "date"), + Type::Duration => write!(f, "duration"), + Type::Filesize => write!(f, "filesize"), + Type::Float => write!(f, "float"), + Type::Int => write!(f, "int"), + Type::Range => write!(f, "range"), + Type::Record(fields) => write!( + f, + "record<{}>", + fields + .iter() + .map(|(x, y)| format!("{}: {}", x, y)) + .collect::>() + .join(", "), + ), + Type::Table => write!(f, "table"), + Type::List(l) => write!(f, "list<{}>", l), + Type::Nothing => write!(f, "nothing"), + Type::Number => write!(f, "number"), + Type::String => write!(f, "string"), + Type::ValueStream => write!(f, "value stream"), + Type::Unknown => write!(f, "unknown"), + Type::Error => write!(f, "error"), + Type::Binary => write!(f, "binary"), + Type::Custom => write!(f, "custom"), + Type::Signature => write!(f, "signature"), + } + } +} diff --git a/crates/nu-protocol/src/value/custom_value.rs b/crates/nu-protocol/src/value/custom_value.rs new file mode 100644 index 0000000000..35b4d34f84 --- /dev/null +++ b/crates/nu-protocol/src/value/custom_value.rs @@ -0,0 +1,60 @@ +use std::{cmp::Ordering, fmt}; + +use crate::{ast::Operator, ShellError, Span, Value}; + +// Trait definition for a custom value +#[typetag::serde(tag = "type")] +pub trait CustomValue: fmt::Debug + Send + Sync { + fn clone_value(&self, span: Span) -> Value; + + //fn category(&self) -> Category; + + // Define string representation of the custom value + fn value_string(&self) -> String; + + // Converts the custom value to a base nushell value + // This is used to represent the custom value using the table representations + // That already exist in nushell + fn to_base_value(&self, span: Span) -> Result; + + // Json representation of custom value + fn to_json(&self) -> nu_json::Value { + nu_json::Value::Null + } + + // Any representation used to downcast object to its original type + fn as_any(&self) -> &dyn std::any::Any; + + // Follow cell path functions + fn follow_path_int(&self, _count: usize, span: Span) -> Result { + Err(ShellError::IncompatiblePathAccess( + format!("{} does't support path access", self.value_string()), + span, + )) + } + + fn follow_path_string(&self, _column_name: String, span: Span) -> Result { + Err(ShellError::IncompatiblePathAccess( + format!("{} does't support path access", self.value_string()), + span, + )) + } + + // ordering with other value + fn partial_cmp(&self, _other: &Value) -> Option { + None + } + + // Definition of an operation between the object that implements the trait + // and another Value. + // The Operator enum is used to indicate the expected operation + fn operation( + &self, + _lhs_span: Span, + operator: Operator, + op: Span, + _right: &Value, + ) -> Result { + Err(ShellError::UnsupportedOperator(operator, op)) + } +} diff --git a/crates/nu-protocol/src/value/from.rs b/crates/nu-protocol/src/value/from.rs new file mode 100644 index 0000000000..ea3ff0ef7f --- /dev/null +++ b/crates/nu-protocol/src/value/from.rs @@ -0,0 +1,25 @@ +use crate::{ShellError, Value}; + +impl Value { + pub fn as_f64(&self) -> Result { + match self { + Value::Float { val, .. } => Ok(*val), + x => Err(ShellError::CantConvert( + "f64".into(), + x.get_type().to_string(), + self.span()?, + )), + } + } + + pub fn as_i64(&self) -> Result { + match self { + Value::Int { val, .. } => Ok(*val), + x => Err(ShellError::CantConvert( + "rf64".into(), + x.get_type().to_string(), + self.span()?, + )), + } + } +} diff --git a/crates/nu-protocol/src/value/from_value.rs b/crates/nu-protocol/src/value/from_value.rs new file mode 100644 index 0000000000..e96bc88044 --- /dev/null +++ b/crates/nu-protocol/src/value/from_value.rs @@ -0,0 +1,404 @@ +use std::path::PathBuf; +use std::str::FromStr; + +use crate::ast::{CellPath, PathMember}; +use crate::engine::CaptureBlock; +use crate::ShellError; +use crate::{Range, Spanned, Value}; +use chrono::{DateTime, FixedOffset}; + +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 Spanned { + fn from_value(v: &Value) -> Result { + match v { + Value::Int { val, span } => Ok(Spanned { + item: *val, + span: *span, + }), + Value::Filesize { val, span } => Ok(Spanned { + item: *val as i64, + span: *span, + }), + Value::Duration { val, span } => Ok(Spanned { + item: *val as i64, + span: *span, + }), + + v => Err(ShellError::CantConvert( + "integer".into(), + v.get_type().to_string(), + v.span()?, + )), + } + } +} + +impl FromValue for i64 { + fn from_value(v: &Value) -> Result { + match v { + Value::Int { val, .. } => Ok(*val), + Value::Filesize { val, .. } => Ok(*val as i64), + Value::Duration { val, .. } => Ok(*val as i64), + + v => Err(ShellError::CantConvert( + "integer".into(), + v.get_type().to_string(), + v.span()?, + )), + } + } +} + +impl FromValue for Spanned { + fn from_value(v: &Value) -> Result { + match v { + Value::Int { val, span } => Ok(Spanned { + item: *val as f64, + span: *span, + }), + Value::Float { val, span } => Ok(Spanned { + item: *val, + span: *span, + }), + + v => Err(ShellError::CantConvert( + "float".into(), + v.get_type().to_string(), + v.span()?, + )), + } + } +} + +impl FromValue for f64 { + fn from_value(v: &Value) -> Result { + match v { + Value::Float { val, .. } => Ok(*val), + Value::Int { val, .. } => Ok(*val as f64), + v => Err(ShellError::CantConvert( + "float".into(), + v.get_type().to_string(), + v.span()?, + )), + } + } +} + +impl FromValue for Spanned { + fn from_value(v: &Value) -> Result { + match v { + Value::Int { val, span } => Ok(Spanned { + item: *val as usize, + span: *span, + }), + Value::Filesize { val, span } => Ok(Spanned { + item: *val as usize, + span: *span, + }), + Value::Duration { val, span } => Ok(Spanned { + item: *val as usize, + span: *span, + }), + + v => Err(ShellError::CantConvert( + "integer".into(), + v.get_type().to_string(), + v.span()?, + )), + } + } +} + +impl FromValue for usize { + fn from_value(v: &Value) -> Result { + match v { + Value::Int { val, .. } => Ok(*val as usize), + Value::Filesize { val, .. } => Ok(*val as usize), + Value::Duration { val, .. } => Ok(*val as usize), + + v => Err(ShellError::CantConvert( + "integer".into(), + v.get_type().to_string(), + v.span()?, + )), + } + } +} + +impl FromValue for String { + fn from_value(v: &Value) -> Result { + // FIXME: we may want to fail a little nicer here + match v { + Value::CellPath { val, .. } => Ok(val.into_string()), + Value::String { val, .. } => Ok(val.clone()), + v => Err(ShellError::CantConvert( + "string".into(), + v.get_type().to_string(), + v.span()?, + )), + } + } +} + +impl FromValue for Spanned { + fn from_value(v: &Value) -> Result { + Ok(Spanned { + item: match v { + Value::CellPath { val, .. } => val.into_string(), + Value::String { val, .. } => val.clone(), + v => { + return Err(ShellError::CantConvert( + "string".into(), + v.get_type().to_string(), + v.span()?, + )) + } + }, + span: v.span()?, + }) + } +} + +impl FromValue for Vec { + fn from_value(v: &Value) -> Result { + // FIXME: we may want to fail a little nicer here + match v { + Value::List { vals, .. } => vals + .iter() + .map(|val| match val { + Value::String { val, .. } => Ok(val.clone()), + c => Err(ShellError::CantConvert( + "string".into(), + c.get_type().to_string(), + c.span()?, + )), + }) + .collect::, ShellError>>(), + v => Err(ShellError::CantConvert( + "string".into(), + v.get_type().to_string(), + v.span()?, + )), + } + } +} + +impl FromValue for CellPath { + fn from_value(v: &Value) -> Result { + let span = v.span()?; + match v { + Value::CellPath { val, .. } => Ok(val.clone()), + Value::String { val, .. } => Ok(CellPath { + members: vec![PathMember::String { + val: val.clone(), + span, + }], + }), + Value::Int { val, .. } => Ok(CellPath { + members: vec![PathMember::Int { + val: *val as usize, + span, + }], + }), + x => Err(ShellError::CantConvert( + "cell path".into(), + x.get_type().to_string(), + span, + )), + } + } +} + +impl FromValue for bool { + fn from_value(v: &Value) -> Result { + match v { + Value::Bool { val, .. } => Ok(*val), + v => Err(ShellError::CantConvert( + "bool".into(), + v.get_type().to_string(), + v.span()?, + )), + } + } +} + +impl FromValue for Spanned { + fn from_value(v: &Value) -> Result { + match v { + Value::Bool { val, span } => Ok(Spanned { + item: *val, + span: *span, + }), + v => Err(ShellError::CantConvert( + "bool".into(), + v.get_type().to_string(), + v.span()?, + )), + } + } +} + +impl FromValue for DateTime { + fn from_value(v: &Value) -> Result { + match v { + Value::Date { val, .. } => Ok(*val), + v => Err(ShellError::CantConvert( + "date".into(), + v.get_type().to_string(), + v.span()?, + )), + } + } +} + +impl FromValue for Spanned> { + fn from_value(v: &Value) -> Result { + match v { + Value::Date { val, span } => Ok(Spanned { + item: *val, + span: *span, + }), + v => Err(ShellError::CantConvert( + "date".into(), + v.get_type().to_string(), + v.span()?, + )), + } + } +} + +impl FromValue for Range { + fn from_value(v: &Value) -> Result { + match v { + Value::Range { val, .. } => Ok((**val).clone()), + v => Err(ShellError::CantConvert( + "range".into(), + v.get_type().to_string(), + v.span()?, + )), + } + } +} + +impl FromValue for Spanned { + fn from_value(v: &Value) -> Result { + match v { + Value::Range { val, span } => Ok(Spanned { + item: (**val).clone(), + span: *span, + }), + v => Err(ShellError::CantConvert( + "range".into(), + v.get_type().to_string(), + v.span()?, + )), + } + } +} + +impl FromValue for Vec { + fn from_value(v: &Value) -> Result { + match v { + Value::Binary { val, .. } => Ok(val.clone()), + Value::String { val, .. } => Ok(val.bytes().collect()), + v => Err(ShellError::CantConvert( + "binary data".into(), + v.get_type().to_string(), + v.span()?, + )), + } + } +} + +impl FromValue for Spanned { + fn from_value(v: &Value) -> Result { + match v { + Value::String { val, span } => Ok(Spanned { + item: PathBuf::from_str(val) + .map_err(|err| ShellError::FileNotFoundCustom(err.to_string(), *span))?, + span: *span, + }), + v => Err(ShellError::CantConvert( + "range".into(), + v.get_type().to_string(), + v.span()?, + )), + } + } +} + +impl FromValue for Vec { + fn from_value(v: &Value) -> Result { + // FIXME: we may want to fail a little nicer here + match v { + Value::List { vals, .. } => Ok(vals.clone()), + v => Err(ShellError::CantConvert( + "Vector of values".into(), + v.get_type().to_string(), + v.span()?, + )), + } + } +} + +// A record +impl FromValue for (Vec, Vec) { + fn from_value(v: &Value) -> Result { + match v { + Value::Record { cols, vals, .. } => Ok((cols.clone(), vals.clone())), + v => Err(ShellError::CantConvert( + "Record".into(), + v.get_type().to_string(), + v.span()?, + )), + } + } +} + +impl FromValue for CaptureBlock { + fn from_value(v: &Value) -> Result { + match v { + Value::Block { val, captures, .. } => Ok(CaptureBlock { + block_id: *val, + captures: captures.clone(), + }), + v => Err(ShellError::CantConvert( + "Block".into(), + v.get_type().to_string(), + v.span()?, + )), + } + } +} + +impl FromValue for Spanned { + fn from_value(v: &Value) -> Result { + match v { + Value::Block { + val, + captures, + span, + } => Ok(Spanned { + item: CaptureBlock { + block_id: *val, + captures: captures.clone(), + }, + span: *span, + }), + v => Err(ShellError::CantConvert( + "Block".into(), + v.get_type().to_string(), + v.span()?, + )), + } + } +} diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs new file mode 100644 index 0000000000..29e6a4653e --- /dev/null +++ b/crates/nu-protocol/src/value/mod.rs @@ -0,0 +1,1914 @@ +mod custom_value; +mod from; +mod from_value; +mod range; +mod stream; +mod unit; + +use byte_unit::ByteUnit; +use chrono::{DateTime, FixedOffset}; +use chrono_humanize::HumanTime; +pub use from_value::FromValue; +use indexmap::map::IndexMap; +use num_format::{Locale, ToFormattedString}; +pub use range::*; +use serde::{Deserialize, Serialize}; +pub use stream::*; +use sys_locale::get_locale; +pub use unit::*; + +use std::collections::HashMap; +use std::path::PathBuf; +use std::{cmp::Ordering, fmt::Debug}; + +use crate::ast::{CellPath, PathMember}; +use crate::{did_you_mean, span, BlockId, Config, Span, Spanned, Type, VarId}; + +use crate::ast::Operator; +pub use custom_value::CustomValue; + +use crate::ShellError; + +/// Core structured values that pass through the pipeline in engine-q +#[derive(Debug, Serialize, Deserialize)] +pub enum Value { + Bool { + val: bool, + span: Span, + }, + Int { + val: i64, + span: Span, + }, + Filesize { + val: i64, + span: Span, + }, + Duration { + val: i64, + span: Span, + }, + Date { + val: DateTime, + span: Span, + }, + Range { + val: Box, + span: Span, + }, + Float { + val: f64, + span: Span, + }, + String { + val: String, + span: Span, + }, + Record { + cols: Vec, + vals: Vec, + span: Span, + }, + List { + vals: Vec, + span: Span, + }, + Block { + val: BlockId, + captures: HashMap, + span: Span, + }, + Nothing { + span: Span, + }, + Error { + error: ShellError, + }, + Binary { + val: Vec, + span: Span, + }, + CellPath { + val: CellPath, + span: Span, + }, + CustomValue { + val: Box, + span: Span, + }, +} + +impl Clone for Value { + fn clone(&self) -> Self { + match self { + Value::Bool { val, span } => Value::Bool { + val: *val, + span: *span, + }, + Value::Int { val, span } => Value::Int { + val: *val, + span: *span, + }, + Value::Filesize { val, span } => Value::Filesize { + val: *val, + span: *span, + }, + Value::Duration { val, span } => Value::Duration { + val: *val, + span: *span, + }, + Value::Date { val, span } => Value::Date { + val: *val, + span: *span, + }, + Value::Range { val, span } => Value::Range { + val: val.clone(), + span: *span, + }, + Value::Float { val, span } => Value::Float { + val: *val, + span: *span, + }, + Value::String { val, span } => Value::String { + val: val.clone(), + span: *span, + }, + Value::Record { cols, vals, span } => Value::Record { + cols: cols.clone(), + vals: vals.clone(), + span: *span, + }, + Value::List { vals, span } => Value::List { + vals: vals.clone(), + span: *span, + }, + Value::Block { + val, + captures, + span, + } => Value::Block { + val: *val, + captures: captures.clone(), + span: *span, + }, + Value::Nothing { span } => Value::Nothing { span: *span }, + Value::Error { error } => Value::Error { + error: error.clone(), + }, + Value::Binary { val, span } => Value::Binary { + val: val.clone(), + span: *span, + }, + Value::CellPath { val, span } => Value::CellPath { + val: val.clone(), + span: *span, + }, + Value::CustomValue { val, span } => val.clone_value(*span), + } + } +} + +impl Value { + pub fn as_string(&self) -> Result { + match self { + Value::String { val, .. } => Ok(val.to_string()), + Value::Binary { val, .. } => Ok(match std::str::from_utf8(val) { + Ok(s) => s.to_string(), + Err(_) => { + // println!("{:?}", e); + // println!("bytes: {}", pretty_hex::pretty_hex(&val)); + // panic!("let's see it"); + return Err(ShellError::CantConvert( + "string".into(), + "binary".into(), + self.span()?, + )); + } + }), + x => Err(ShellError::CantConvert( + "string".into(), + x.get_type().to_string(), + self.span()?, + )), + } + } + + pub fn as_spanned_string(&self) -> Result, ShellError> { + match self { + Value::String { val, span } => Ok(Spanned { + item: val.to_string(), + span: *span, + }), + Value::Binary { val, span } => Ok(match std::str::from_utf8(val) { + Ok(s) => Spanned { + item: s.to_string(), + span: *span, + }, + Err(_) => { + return Err(ShellError::CantConvert( + "string".into(), + "binary".into(), + self.span()?, + )) + } + }), + x => Err(ShellError::CantConvert( + "string".into(), + x.get_type().to_string(), + self.span()?, + )), + } + } + + pub fn as_path(&self) -> Result { + match self { + Value::String { val, .. } => Ok(PathBuf::from(val)), + x => Err(ShellError::CantConvert( + "path".into(), + x.get_type().to_string(), + self.span()?, + )), + } + } + + pub fn as_block(&self) -> Result { + match self { + Value::Block { val, .. } => Ok(*val), + x => Err(ShellError::CantConvert( + "block".into(), + x.get_type().to_string(), + self.span()?, + )), + } + } + + pub fn as_binary(&self) -> Result<&[u8], ShellError> { + match self { + Value::Binary { val, .. } => Ok(val), + Value::String { val, .. } => Ok(val.as_bytes()), + x => Err(ShellError::CantConvert( + "binary".into(), + x.get_type().to_string(), + self.span()?, + )), + } + } + + pub fn as_record(&self) -> Result<(&[String], &[Value]), ShellError> { + match self { + Value::Record { cols, vals, .. } => Ok((cols, vals)), + x => Err(ShellError::CantConvert( + "record".into(), + x.get_type().to_string(), + self.span()?, + )), + } + } + + pub fn as_list(&self) -> Result<&[Value], ShellError> { + match self { + Value::List { vals, .. } => Ok(vals), + x => Err(ShellError::CantConvert( + "list".into(), + x.get_type().to_string(), + self.span()?, + )), + } + } + + pub fn as_bool(&self) -> Result { + match self { + Value::Bool { val, .. } => Ok(*val), + x => Err(ShellError::CantConvert( + "boolean".into(), + x.get_type().to_string(), + self.span()?, + )), + } + } + + pub fn as_float(&self) -> Result { + match self { + Value::Float { val, .. } => Ok(*val), + x => Err(ShellError::CantConvert( + "float".into(), + x.get_type().to_string(), + self.span()?, + )), + } + } + + pub fn as_integer(&self) -> Result { + match self { + Value::Int { val, .. } => Ok(*val), + x => Err(ShellError::CantConvert( + "integer".into(), + x.get_type().to_string(), + self.span()?, + )), + } + } + + /// Get the span for the current value + pub fn span(&self) -> Result { + match self { + Value::Error { error } => Err(error.clone()), + Value::Bool { span, .. } => Ok(*span), + Value::Int { span, .. } => Ok(*span), + Value::Float { span, .. } => Ok(*span), + Value::Filesize { span, .. } => Ok(*span), + Value::Duration { span, .. } => Ok(*span), + Value::Date { span, .. } => Ok(*span), + Value::Range { span, .. } => Ok(*span), + Value::String { span, .. } => Ok(*span), + Value::Record { span, .. } => Ok(*span), + Value::List { span, .. } => Ok(*span), + Value::Block { span, .. } => Ok(*span), + Value::Nothing { span, .. } => Ok(*span), + Value::Binary { span, .. } => Ok(*span), + Value::CellPath { span, .. } => Ok(*span), + Value::CustomValue { span, .. } => Ok(*span), + } + } + + /// Update the value with a new span + pub fn with_span(mut self, new_span: Span) -> Value { + match &mut self { + Value::Bool { span, .. } => *span = new_span, + Value::Int { span, .. } => *span = new_span, + Value::Float { span, .. } => *span = new_span, + Value::Filesize { span, .. } => *span = new_span, + Value::Duration { span, .. } => *span = new_span, + Value::Date { span, .. } => *span = new_span, + Value::Range { span, .. } => *span = new_span, + Value::String { span, .. } => *span = new_span, + Value::Record { span, .. } => *span = new_span, + Value::List { span, .. } => *span = new_span, + Value::Block { span, .. } => *span = new_span, + Value::Nothing { span, .. } => *span = new_span, + Value::Error { .. } => {} + Value::Binary { span, .. } => *span = new_span, + Value::CellPath { span, .. } => *span = new_span, + Value::CustomValue { span, .. } => *span = new_span, + } + + self + } + + /// Get the type of the current Value + pub fn get_type(&self) -> Type { + match self { + Value::Bool { .. } => Type::Bool, + Value::Int { .. } => Type::Int, + Value::Float { .. } => Type::Float, + Value::Filesize { .. } => Type::Filesize, + Value::Duration { .. } => Type::Duration, + Value::Date { .. } => Type::Date, + Value::Range { .. } => Type::Range, + Value::String { .. } => Type::String, + Value::Record { cols, vals, .. } => Type::Record( + cols.iter() + .zip(vals.iter()) + .map(|(x, y)| (x.clone(), y.get_type())) + .collect(), + ), + Value::List { .. } => Type::List(Box::new(Type::Unknown)), // FIXME + Value::Nothing { .. } => Type::Nothing, + Value::Block { .. } => Type::Block, + Value::Error { .. } => Type::Error, + Value::Binary { .. } => Type::Binary, + Value::CellPath { .. } => Type::CellPath, + Value::CustomValue { .. } => Type::Custom, + } + } + + pub fn get_data_by_key(&self, name: &str) -> Option { + match self { + Value::Record { cols, vals, .. } => cols + .iter() + .zip(vals.iter()) + .find(|(col, _)| col == &name) + .map(|(_, val)| val.clone()), + Value::List { vals, span } => { + let mut out = vec![]; + for item in vals { + match item { + Value::Record { .. } => match item.get_data_by_key(name) { + Some(v) => out.push(v), + None => out.push(Value::nothing(*span)), + }, + _ => out.push(Value::nothing(*span)), + } + } + + if !out.is_empty() { + Some(Value::List { + vals: out, + span: *span, + }) + } else { + None + } + } + _ => None, + } + } + + /// Convert Value into string. Note that Streams will be consumed. + pub fn into_string(&self, separator: &str, config: &Config) -> String { + match self { + Value::Bool { val, .. } => val.to_string(), + Value::Int { val, .. } => val.to_string(), + Value::Float { val, .. } => val.to_string(), + Value::Filesize { val, .. } => format_filesize(*val, config), + Value::Duration { val, .. } => format_duration(*val), + Value::Date { val, .. } => HumanTime::from(*val).to_string(), + Value::Range { val, .. } => { + format!( + "{}..{}", + val.from.into_string(", ", config), + val.to.into_string(", ", config) + ) + } + Value::String { val, .. } => val.clone(), + Value::List { vals: val, .. } => format!( + "[{}]", + val.iter() + .map(|x| x.into_string(", ", config)) + .collect::>() + .join(separator) + ), + Value::Record { cols, vals, .. } => format!( + "{{{}}}", + cols.iter() + .zip(vals.iter()) + .map(|(x, y)| format!("{}: {}", x, y.into_string(", ", config))) + .collect::>() + .join(separator) + ), + Value::Block { val, .. } => format!("", val), + Value::Nothing { .. } => String::new(), + Value::Error { error } => format!("{:?}", error), + Value::Binary { val, .. } => format!("{:?}", val), + Value::CellPath { val, .. } => val.into_string(), + Value::CustomValue { val, .. } => val.value_string(), + } + } + + /// Convert Value into string. Note that Streams will be consumed. + pub fn into_abbreviated_string(&self, config: &Config) -> String { + match self { + Value::Bool { val, .. } => val.to_string(), + Value::Int { val, .. } => val.to_string(), + Value::Float { val, .. } => val.to_string(), + Value::Filesize { val, .. } => format_filesize(*val, config), + Value::Duration { val, .. } => format_duration(*val), + Value::Date { val, .. } => HumanTime::from(*val).to_string(), + Value::Range { val, .. } => { + format!( + "{}..{}", + val.from.into_string(", ", config), + val.to.into_string(", ", config) + ) + } + Value::String { val, .. } => val.to_string(), + Value::List { ref vals, .. } => match &vals[..] { + [Value::Record { .. }, _end @ ..] => format!( + "[table {} row{}]", + vals.len(), + if vals.len() == 1 { "" } else { "s" } + ), + _ => format!( + "[list {} item{}]", + vals.len(), + if vals.len() == 1 { "" } else { "s" } + ), + }, + Value::Record { cols, .. } => format!( + "{{record {} field{}}}", + cols.len(), + if cols.len() == 1 { "" } else { "s" } + ), + Value::Block { val, .. } => format!("", val), + Value::Nothing { .. } => String::new(), + Value::Error { error } => format!("{:?}", error), + Value::Binary { val, .. } => format!("{:?}", val), + Value::CellPath { val, .. } => val.into_string(), + Value::CustomValue { val, .. } => val.value_string(), + } + } + + /// Convert Value into a debug string + pub fn debug_value(&self) -> String { + format!("{:#?}", self) + } + + /// Convert Value into string. Note that Streams will be consumed. + pub fn debug_string(&self, separator: &str, config: &Config) -> String { + match self { + Value::Bool { val, .. } => val.to_string(), + Value::Int { val, .. } => val.to_string(), + Value::Float { val, .. } => val.to_string(), + Value::Filesize { val, .. } => format_filesize(*val, config), + Value::Duration { val, .. } => format_duration(*val), + Value::Date { val, .. } => format!("{:?}", val), + Value::Range { val, .. } => { + format!( + "{}..{}", + val.from.into_string(", ", config), + val.to.into_string(", ", config) + ) + } + Value::String { val, .. } => val.clone(), + Value::List { vals: val, .. } => format!( + "[{}]", + val.iter() + .map(|x| x.into_string(", ", config)) + .collect::>() + .join(separator) + ), + Value::Record { cols, vals, .. } => format!( + "{{{}}}", + cols.iter() + .zip(vals.iter()) + .map(|(x, y)| format!("{}: {}", x, y.into_string(", ", config))) + .collect::>() + .join(separator) + ), + Value::Block { val, .. } => format!("", val), + Value::Nothing { .. } => String::new(), + Value::Error { error } => format!("{:?}", error), + Value::Binary { val, .. } => format!("{:?}", val), + Value::CellPath { val, .. } => val.into_string(), + Value::CustomValue { val, .. } => val.value_string(), + } + } + + /// Check if the content is empty + pub fn is_empty(&self) -> bool { + match self { + Value::String { val, .. } => val.is_empty(), + Value::List { vals, .. } => { + vals.is_empty() && vals.iter().all(|v| v.clone().is_empty()) + } + Value::Record { cols, vals, .. } => { + cols.iter().all(|v| v.is_empty()) && vals.iter().all(|v| v.clone().is_empty()) + } + Value::Nothing { .. } => true, + _ => false, + } + } + + /// Create a new `Nothing` value + pub fn nothing(span: Span) -> Value { + Value::Nothing { span } + } + + /// Follow a given column path into the value: for example accessing nth elements in a stream or list + pub fn follow_cell_path(self, cell_path: &[PathMember]) -> Result { + let mut current = self; + for member in cell_path { + // FIXME: this uses a few extra clones for simplicity, but there may be a way + // to traverse the path without them + match member { + PathMember::Int { + val: count, + span: origin_span, + } => { + // Treat a numeric path member as `nth ` + match &mut current { + Value::List { vals: val, .. } => { + if let Some(item) = val.get(*count) { + current = item.clone(); + } else { + return Err(ShellError::AccessBeyondEnd(val.len(), *origin_span)); + } + } + Value::Binary { val, .. } => { + if let Some(item) = val.get(*count) { + current = Value::Int { + val: *item as i64, + span: *origin_span, + }; + } else { + return Err(ShellError::AccessBeyondEnd(val.len(), *origin_span)); + } + } + Value::Range { val, .. } => { + if let Some(item) = val.clone().into_range_iter()?.nth(*count) { + current = item.clone(); + } else { + return Err(ShellError::AccessBeyondEndOfStream(*origin_span)); + } + } + Value::CustomValue { val, .. } => { + current = val.follow_path_int(*count, *origin_span)?; + } + x => { + return Err(ShellError::IncompatiblePathAccess( + format!("{}", x.get_type()), + *origin_span, + )) + } + } + } + PathMember::String { + val: column_name, + span: origin_span, + } => match &mut current { + Value::Record { cols, vals, span } => { + let cols = cols.clone(); + let span = *span; + + if let Some(found) = + cols.iter().zip(vals.iter()).find(|x| x.0 == column_name) + { + current = found.1.clone(); + } else if let Some(suggestion) = did_you_mean(&cols, column_name) { + return Err(ShellError::DidYouMean(suggestion, *origin_span)); + } else { + return Err(ShellError::CantFindColumn(*origin_span, span)); + } + } + Value::List { vals, span } => { + let mut output = vec![]; + let mut hasvalue = false; + let mut temp: Result = Err(ShellError::NotFound(*span)); + for val in vals { + temp = val.clone().follow_cell_path(&[PathMember::String { + val: column_name.clone(), + span: *origin_span, + }]); + if let Ok(result) = temp.clone() { + hasvalue = true; + output.push(result); + } else { + output.push(Value::Nothing { span: *span }); + } + } + if hasvalue { + current = Value::List { + vals: output, + span: *span, + }; + } else { + return temp; + } + } + Value::CustomValue { val, .. } => { + current = val.follow_path_string(column_name.clone(), *origin_span)?; + } + x => { + return Err(ShellError::IncompatiblePathAccess( + format!("{}", x.get_type()), + *origin_span, + )) + } + }, + } + } + + Ok(current) + } + + /// Follow a given column path into the value: for example accessing nth elements in a stream or list + pub fn update_cell_path( + &mut self, + cell_path: &[PathMember], + callback: Box Value>, + ) -> Result<(), ShellError> { + let orig = self.clone(); + + let new_val = callback(&orig.follow_cell_path(cell_path)?); + + match new_val { + Value::Error { error } => Err(error), + new_val => self.replace_data_at_cell_path(cell_path, new_val), + } + } + + pub fn replace_data_at_cell_path( + &mut self, + cell_path: &[PathMember], + new_val: Value, + ) -> Result<(), ShellError> { + match cell_path.first() { + Some(path_member) => match path_member { + PathMember::String { + val: col_name, + span, + } => match self { + Value::List { vals, .. } => { + for val in vals.iter_mut() { + match val { + Value::Record { cols, vals, .. } => { + let mut found = false; + for col in cols.iter().zip(vals.iter_mut()) { + if col.0 == col_name { + found = true; + col.1.replace_data_at_cell_path( + &cell_path[1..], + new_val.clone(), + )? + } + } + if !found { + if cell_path.len() == 1 { + cols.push(col_name.clone()); + vals.push(new_val); + break; + } else { + let mut new_col = Value::Record { + cols: vec![], + vals: vec![], + span: new_val.span()?, + }; + new_col.replace_data_at_cell_path( + &cell_path[1..], + new_val, + )?; + vals.push(new_col); + break; + } + } + } + v => return Err(ShellError::CantFindColumn(*span, v.span()?)), + } + } + } + Value::Record { cols, vals, .. } => { + let mut found = false; + + for col in cols.iter().zip(vals.iter_mut()) { + if col.0 == col_name { + found = true; + + col.1 + .replace_data_at_cell_path(&cell_path[1..], new_val.clone())? + } + } + if !found { + if cell_path.len() == 1 { + cols.push(col_name.clone()); + vals.push(new_val); + } else { + let mut new_col = Value::Record { + cols: vec![], + vals: vec![], + span: new_val.span()?, + }; + new_col.replace_data_at_cell_path(&cell_path[1..], new_val)?; + vals.push(new_col); + } + } + } + v => return Err(ShellError::CantFindColumn(*span, v.span()?)), + }, + PathMember::Int { val: row_num, span } => match self { + Value::List { vals, .. } => { + if let Some(v) = vals.get_mut(*row_num) { + v.replace_data_at_cell_path(&cell_path[1..], new_val)? + } else { + return Err(ShellError::AccessBeyondEnd(vals.len(), *span)); + } + } + v => return Err(ShellError::NotAList(*span, v.span()?)), + }, + }, + None => { + *self = new_val; + } + } + Ok(()) + } + + pub fn is_true(&self) -> bool { + matches!(self, Value::Bool { val: true, .. }) + } + + pub fn columns(&self) -> Vec { + match self { + Value::Record { cols, .. } => cols.clone(), + _ => vec![], + } + } + + pub fn string(val: impl Into, span: Span) -> Value { + Value::String { + val: val.into(), + span, + } + } + + pub fn int(val: i64, span: Span) -> Value { + Value::Int { val, span } + } + + pub fn float(val: f64, span: Span) -> Value { + Value::Float { val, span } + } + + pub fn boolean(val: bool, span: Span) -> Value { + Value::Bool { val, span } + } + + /// Note: Only use this for test data, *not* live data, as it will point into unknown source + /// when used in errors. + pub fn test_string(s: impl Into) -> Value { + Value::String { + val: s.into(), + span: Span::test_data(), + } + } + + /// Note: Only use this for test data, *not* live data, as it will point into unknown source + /// when used in errors. + pub fn test_int(val: i64) -> Value { + Value::Int { + val, + span: Span::test_data(), + } + } + + /// Note: Only use this for test data, *not* live data, as it will point into unknown source + /// when used in errors. + pub fn test_float(val: f64) -> Value { + Value::Float { + val, + span: Span::test_data(), + } + } + + /// Note: Only use this for test data, *not* live data, as it will point into unknown source + /// when used in errors. + pub fn test_bool(val: bool) -> Value { + Value::Bool { + val, + span: Span::test_data(), + } + } + + /// Note: Only use this for test data, *not* live data, as it will point into unknown source + /// when used in errors. + pub fn test_filesize(val: i64) -> Value { + Value::Filesize { + val, + span: Span::test_data(), + } + } + + /// Note: Only use this for test data, *not* live data, as it will point into unknown source + /// when used in errors. + pub fn test_nothing() -> Value { + Value::Nothing { + span: Span::test_data(), + } + } + + /// Note: Only use this for test data, *not* live data, as it will point into unknown source + /// when used in errors. + pub fn test_record(cols: Vec>, vals: Vec) -> Value { + Value::Record { + cols: cols.into_iter().map(|s| s.into()).collect(), + vals, + + span: Span::test_data(), + } + } +} + +impl Default for Value { + fn default() -> Self { + Value::Nothing { + span: Span { start: 0, end: 0 }, + } + } +} + +impl PartialOrd for Value { + fn partial_cmp(&self, other: &Self) -> Option { + // Compare two floating point numbers. The decision interval for equality is dynamically + // scaled as the value being compared increases in magnitude. + fn compare_floats(val: f64, other: f64) -> Option { + let prec = f64::EPSILON.max(val.abs() * f64::EPSILON); + + if (other - val).abs() < prec { + return Some(Ordering::Equal); + } + + val.partial_cmp(&other) + } + + match (self, other) { + (Value::Bool { val: lhs, .. }, Value::Bool { val: rhs, .. }) => lhs.partial_cmp(rhs), + (Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => lhs.partial_cmp(rhs), + (Value::Float { val: lhs, .. }, Value::Float { val: rhs, .. }) => { + compare_floats(*lhs, *rhs) + } + (Value::Date { val: lhs, .. }, Value::Date { val: rhs, .. }) => lhs.partial_cmp(rhs), + (Value::String { val: lhs, .. }, Value::String { val: rhs, .. }) => { + lhs.partial_cmp(rhs) + } + (Value::Int { val: lhs, .. }, Value::Float { val: rhs, .. }) => { + compare_floats(*lhs as f64, *rhs) + } + (Value::Float { val: lhs, .. }, Value::Int { val: rhs, .. }) => { + compare_floats(*lhs, *rhs as f64) + } + (Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => { + lhs.partial_cmp(rhs) + } + (Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => { + lhs.partial_cmp(rhs) + } + (Value::Block { val: b1, .. }, Value::Block { val: b2, .. }) if b1 == b2 => { + Some(Ordering::Equal) + } + (Value::List { vals: lhs, .. }, Value::List { vals: rhs, .. }) => lhs.partial_cmp(rhs), + ( + Value::Record { + vals: lhs, + cols: lhs_headers, + .. + }, + Value::Record { + vals: rhs, + cols: rhs_headers, + .. + }, + ) if lhs_headers == rhs_headers && lhs == rhs => Some(Ordering::Equal), + (Value::Binary { val: lhs, .. }, Value::Binary { val: rhs, .. }) => { + lhs.partial_cmp(rhs) + } + (Value::CustomValue { val: lhs, .. }, rhs) => lhs.partial_cmp(rhs), + (Value::Nothing { .. }, Value::Nothing { .. }) => Some(Ordering::Equal), + (_, _) => None, + } + } +} + +impl PartialEq for Value { + fn eq(&self, other: &Self) -> bool { + self.partial_cmp(other).map_or(false, Ordering::is_eq) + } +} + +impl Value { + pub fn add(&self, op: Span, rhs: &Value) -> Result { + let span = span(&[self.span()?, rhs.span()?]); + + match (self, rhs) { + (Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => { + if let Some(val) = lhs.checked_add(*rhs) { + Ok(Value::Int { val, span }) + } else { + Err(ShellError::OperatorOverflow( + "add operation overflowed".into(), + span, + )) + } + } + (Value::Int { val: lhs, .. }, Value::Float { val: rhs, .. }) => Ok(Value::Float { + val: *lhs as f64 + *rhs, + span, + }), + (Value::Float { val: lhs, .. }, Value::Int { val: rhs, .. }) => Ok(Value::Float { + val: *lhs + *rhs as f64, + span, + }), + (Value::Float { val: lhs, .. }, Value::Float { val: rhs, .. }) => Ok(Value::Float { + val: lhs + rhs, + span, + }), + (Value::String { val: lhs, .. }, Value::String { val: rhs, .. }) => Ok(Value::String { + val: lhs.to_string() + rhs, + span, + }), + (Value::Date { val: lhs, .. }, Value::Duration { val: rhs, .. }) => { + match lhs.checked_add_signed(chrono::Duration::nanoseconds(*rhs)) { + Some(val) => Ok(Value::Date { val, span }), + _ => Err(ShellError::OperatorOverflow( + "addition operation overflowed".into(), + span, + )), + } + } + (Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => { + if let Some(val) = lhs.checked_add(*rhs) { + Ok(Value::Duration { val, span }) + } else { + Err(ShellError::OperatorOverflow( + "add operation overflowed".into(), + span, + )) + } + } + (Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => { + if let Some(val) = lhs.checked_add(*rhs) { + Ok(Value::Filesize { val, span }) + } else { + Err(ShellError::OperatorOverflow( + "add operation overflowed".into(), + span, + )) + } + } + + (Value::CustomValue { val: lhs, span }, rhs) => { + lhs.operation(*span, Operator::Plus, op, rhs) + } + + _ => Err(ShellError::OperatorMismatch { + op_span: op, + lhs_ty: self.get_type(), + lhs_span: self.span()?, + rhs_ty: rhs.get_type(), + rhs_span: rhs.span()?, + }), + } + } + pub fn sub(&self, op: Span, rhs: &Value) -> Result { + let span = span(&[self.span()?, rhs.span()?]); + + match (self, rhs) { + (Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => { + if let Some(val) = lhs.checked_sub(*rhs) { + Ok(Value::Int { val, span }) + } else { + Err(ShellError::OperatorOverflow( + "subtraction operation overflowed".into(), + span, + )) + } + } + (Value::Int { val: lhs, .. }, Value::Float { val: rhs, .. }) => Ok(Value::Float { + val: *lhs as f64 - *rhs, + span, + }), + (Value::Float { val: lhs, .. }, Value::Int { val: rhs, .. }) => Ok(Value::Float { + val: *lhs - *rhs as f64, + span, + }), + (Value::Float { val: lhs, .. }, Value::Float { val: rhs, .. }) => Ok(Value::Float { + val: lhs - rhs, + span, + }), + (Value::Date { val: lhs, .. }, Value::Duration { val: rhs, .. }) => { + match lhs.checked_sub_signed(chrono::Duration::nanoseconds(*rhs)) { + Some(val) => Ok(Value::Date { val, span }), + _ => Err(ShellError::OperatorOverflow( + "subtraction operation overflowed".into(), + span, + )), + } + } + (Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => { + if let Some(val) = lhs.checked_sub(*rhs) { + Ok(Value::Duration { val, span }) + } else { + Err(ShellError::OperatorOverflow( + "subtraction operation overflowed".into(), + span, + )) + } + } + (Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => { + if let Some(val) = lhs.checked_sub(*rhs) { + Ok(Value::Filesize { val, span }) + } else { + Err(ShellError::OperatorOverflow( + "add operation overflowed".into(), + span, + )) + } + } + + (Value::CustomValue { val: lhs, span }, rhs) => { + lhs.operation(*span, Operator::Minus, op, rhs) + } + + _ => Err(ShellError::OperatorMismatch { + op_span: op, + lhs_ty: self.get_type(), + lhs_span: self.span()?, + rhs_ty: rhs.get_type(), + rhs_span: rhs.span()?, + }), + } + } + pub fn mul(&self, op: Span, rhs: &Value) -> Result { + let span = span(&[self.span()?, rhs.span()?]); + + match (self, rhs) { + (Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => { + if let Some(val) = lhs.checked_mul(*rhs) { + Ok(Value::Int { val, span }) + } else { + Err(ShellError::OperatorOverflow( + "multiply operation overflowed".into(), + span, + )) + } + } + (Value::Int { val: lhs, .. }, Value::Float { val: rhs, .. }) => Ok(Value::Float { + val: *lhs as f64 * *rhs, + span, + }), + (Value::Float { val: lhs, .. }, Value::Int { val: rhs, .. }) => Ok(Value::Float { + val: *lhs * *rhs as f64, + span, + }), + (Value::Float { val: lhs, .. }, Value::Float { val: rhs, .. }) => Ok(Value::Float { + val: lhs * rhs, + span, + }), + (Value::Int { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => { + Ok(Value::Filesize { + val: *lhs * *rhs, + span, + }) + } + (Value::Filesize { val: lhs, .. }, Value::Int { val: rhs, .. }) => { + Ok(Value::Filesize { + val: *lhs * *rhs, + span, + }) + } + (Value::Int { val: lhs, .. }, Value::Duration { val: rhs, .. }) => { + Ok(Value::Duration { + val: *lhs * *rhs, + span, + }) + } + (Value::Duration { val: lhs, .. }, Value::Int { val: rhs, .. }) => { + Ok(Value::Duration { + val: *lhs * *rhs, + span, + }) + } + (Value::CustomValue { val: lhs, span }, rhs) => { + lhs.operation(*span, Operator::Multiply, op, rhs) + } + + _ => Err(ShellError::OperatorMismatch { + op_span: op, + lhs_ty: self.get_type(), + lhs_span: self.span()?, + rhs_ty: rhs.get_type(), + rhs_span: rhs.span()?, + }), + } + } + pub fn div(&self, op: Span, rhs: &Value) -> Result { + let span = span(&[self.span()?, rhs.span()?]); + + match (self, rhs) { + (Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => { + if *rhs != 0 { + if lhs % rhs == 0 { + Ok(Value::Int { + val: lhs / rhs, + span, + }) + } else { + Ok(Value::Float { + val: (*lhs as f64) / (*rhs as f64), + span, + }) + } + } else { + Err(ShellError::DivisionByZero(op)) + } + } + (Value::Int { val: lhs, .. }, Value::Float { val: rhs, .. }) => { + if *rhs != 0.0 { + Ok(Value::Float { + val: *lhs as f64 / *rhs, + span, + }) + } else { + Err(ShellError::DivisionByZero(op)) + } + } + (Value::Float { val: lhs, .. }, Value::Int { val: rhs, .. }) => { + if *rhs != 0 { + Ok(Value::Float { + val: *lhs / *rhs as f64, + span, + }) + } else { + Err(ShellError::DivisionByZero(op)) + } + } + (Value::Float { val: lhs, .. }, Value::Float { val: rhs, .. }) => { + if *rhs != 0.0 { + Ok(Value::Float { + val: lhs / rhs, + span, + }) + } else { + Err(ShellError::DivisionByZero(op)) + } + } + (Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => { + if *rhs != 0 { + if lhs % rhs == 0 { + Ok(Value::Int { + val: lhs / rhs, + span, + }) + } else { + Ok(Value::Float { + val: (*lhs as f64) / (*rhs as f64), + span, + }) + } + } else { + Err(ShellError::DivisionByZero(op)) + } + } + (Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => { + if *rhs != 0 { + if lhs % rhs == 0 { + Ok(Value::Int { + val: lhs / rhs, + span, + }) + } else { + Ok(Value::Float { + val: (*lhs as f64) / (*rhs as f64), + span, + }) + } + } else { + Err(ShellError::DivisionByZero(op)) + } + } + (Value::Filesize { val: lhs, .. }, Value::Int { val: rhs, .. }) => { + if *rhs != 0 { + Ok(Value::Filesize { + val: lhs / rhs, + span, + }) + } else { + Err(ShellError::DivisionByZero(op)) + } + } + (Value::Duration { val: lhs, .. }, Value::Int { val: rhs, .. }) => { + if *rhs != 0 { + Ok(Value::Duration { + val: lhs / rhs, + span, + }) + } else { + Err(ShellError::DivisionByZero(op)) + } + } + (Value::CustomValue { val: lhs, span }, rhs) => { + lhs.operation(*span, Operator::Divide, op, rhs) + } + + _ => Err(ShellError::OperatorMismatch { + op_span: op, + lhs_ty: self.get_type(), + lhs_span: self.span()?, + rhs_ty: rhs.get_type(), + rhs_span: rhs.span()?, + }), + } + } + pub fn lt(&self, op: Span, rhs: &Value) -> Result { + let span = span(&[self.span()?, rhs.span()?]); + + if let (Value::CustomValue { val: lhs, span }, rhs) = (self, rhs) { + return lhs.operation(*span, Operator::LessThan, op, rhs); + } + + match self.partial_cmp(rhs) { + Some(ordering) => Ok(Value::Bool { + val: matches!(ordering, Ordering::Less), + span, + }), + None => Err(ShellError::OperatorMismatch { + op_span: op, + lhs_ty: self.get_type(), + lhs_span: self.span()?, + rhs_ty: rhs.get_type(), + rhs_span: rhs.span()?, + }), + } + } + pub fn lte(&self, op: Span, rhs: &Value) -> Result { + let span = span(&[self.span()?, rhs.span()?]); + + if let (Value::CustomValue { val: lhs, span }, rhs) = (self, rhs) { + return lhs.operation(*span, Operator::LessThanOrEqual, op, rhs); + } + + match self.partial_cmp(rhs) { + Some(ordering) => Ok(Value::Bool { + val: matches!(ordering, Ordering::Less | Ordering::Equal), + span, + }), + None => Err(ShellError::OperatorMismatch { + op_span: op, + lhs_ty: self.get_type(), + lhs_span: self.span()?, + rhs_ty: rhs.get_type(), + rhs_span: rhs.span()?, + }), + } + } + pub fn gt(&self, op: Span, rhs: &Value) -> Result { + let span = span(&[self.span()?, rhs.span()?]); + + if let (Value::CustomValue { val: lhs, span }, rhs) = (self, rhs) { + return lhs.operation(*span, Operator::GreaterThan, op, rhs); + } + + match self.partial_cmp(rhs) { + Some(ordering) => Ok(Value::Bool { + val: matches!(ordering, Ordering::Greater), + span, + }), + None => Err(ShellError::OperatorMismatch { + op_span: op, + lhs_ty: self.get_type(), + lhs_span: self.span()?, + rhs_ty: rhs.get_type(), + rhs_span: rhs.span()?, + }), + } + } + pub fn gte(&self, op: Span, rhs: &Value) -> Result { + let span = span(&[self.span()?, rhs.span()?]); + + if let (Value::CustomValue { val: lhs, span }, rhs) = (self, rhs) { + return lhs.operation(*span, Operator::GreaterThanOrEqual, op, rhs); + } + + match self.partial_cmp(rhs) { + Some(ordering) => Ok(Value::Bool { + val: matches!(ordering, Ordering::Greater | Ordering::Equal), + span, + }), + None => Err(ShellError::OperatorMismatch { + op_span: op, + lhs_ty: self.get_type(), + lhs_span: self.span()?, + rhs_ty: rhs.get_type(), + rhs_span: rhs.span()?, + }), + } + } + pub fn eq(&self, op: Span, rhs: &Value) -> Result { + let span = span(&[self.span()?, rhs.span()?]); + + if let (Value::CustomValue { val: lhs, span }, rhs) = (self, rhs) { + return lhs.operation(*span, Operator::Equal, op, rhs); + } + + match self.partial_cmp(rhs) { + Some(ordering) => Ok(Value::Bool { + val: matches!(ordering, Ordering::Equal), + span, + }), + None => match (self, rhs) { + (Value::Nothing { .. }, _) | (_, Value::Nothing { .. }) => { + Ok(Value::Bool { val: false, span }) + } + _ => Err(ShellError::OperatorMismatch { + op_span: op, + lhs_ty: self.get_type(), + lhs_span: self.span()?, + rhs_ty: rhs.get_type(), + rhs_span: rhs.span()?, + }), + }, + } + } + pub fn ne(&self, op: Span, rhs: &Value) -> Result { + let span = span(&[self.span()?, rhs.span()?]); + + if let (Value::CustomValue { val: lhs, span }, rhs) = (self, rhs) { + return lhs.operation(*span, Operator::NotEqual, op, rhs); + } + + match self.partial_cmp(rhs) { + Some(ordering) => Ok(Value::Bool { + val: !matches!(ordering, Ordering::Equal), + span, + }), + None => match (self, rhs) { + (Value::Nothing { .. }, _) | (_, Value::Nothing { .. }) => { + Ok(Value::Bool { val: true, span }) + } + _ => Err(ShellError::OperatorMismatch { + op_span: op, + lhs_ty: self.get_type(), + lhs_span: self.span()?, + rhs_ty: rhs.get_type(), + rhs_span: rhs.span()?, + }), + }, + } + } + + pub fn r#in(&self, op: Span, rhs: &Value) -> Result { + let span = span(&[self.span()?, rhs.span()?]); + + match (self, rhs) { + (lhs, Value::Range { val: rhs, .. }) => Ok(Value::Bool { + val: rhs.contains(lhs), + span, + }), + (Value::String { val: lhs, .. }, Value::String { val: rhs, .. }) => Ok(Value::Bool { + val: rhs.contains(lhs), + span, + }), + (lhs, Value::List { vals: rhs, .. }) => Ok(Value::Bool { + val: rhs.contains(lhs), + span, + }), + (Value::String { val: lhs, .. }, Value::Record { cols: rhs, .. }) => Ok(Value::Bool { + val: rhs.contains(lhs), + span, + }), + (Value::String { .. } | Value::Int { .. }, Value::CellPath { val: rhs, .. }) => { + let val = rhs.members.iter().any(|member| match (self, member) { + (Value::Int { val: lhs, .. }, PathMember::Int { val: rhs, .. }) => { + *lhs == *rhs as i64 + } + (Value::String { val: lhs, .. }, PathMember::String { val: rhs, .. }) => { + lhs == rhs + } + (Value::String { .. }, PathMember::Int { .. }) + | (Value::Int { .. }, PathMember::String { .. }) => false, + _ => unreachable!( + "outer match arm ensures `self` is either a `String` or `Int` variant" + ), + }); + + Ok(Value::Bool { val, span }) + } + (Value::CellPath { val: lhs, .. }, Value::CellPath { val: rhs, .. }) => { + Ok(Value::Bool { + val: rhs + .members + .windows(lhs.members.len()) + .any(|member_window| member_window == rhs.members), + span, + }) + } + (Value::CustomValue { val: lhs, span }, rhs) => { + lhs.operation(*span, Operator::In, op, rhs) + } + _ => Err(ShellError::OperatorMismatch { + op_span: op, + lhs_ty: self.get_type(), + lhs_span: self.span()?, + rhs_ty: rhs.get_type(), + rhs_span: rhs.span()?, + }), + } + } + + pub fn not_in(&self, op: Span, rhs: &Value) -> Result { + let span = span(&[self.span()?, rhs.span()?]); + + match (self, rhs) { + (lhs, Value::Range { val: rhs, .. }) => Ok(Value::Bool { + val: !rhs.contains(lhs), + span, + }), + (Value::String { val: lhs, .. }, Value::String { val: rhs, .. }) => Ok(Value::Bool { + val: !rhs.contains(lhs), + span, + }), + (lhs, Value::List { vals: rhs, .. }) => Ok(Value::Bool { + val: !rhs.contains(lhs), + span, + }), + (Value::String { val: lhs, .. }, Value::Record { cols: rhs, .. }) => Ok(Value::Bool { + val: !rhs.contains(lhs), + span, + }), + (Value::String { .. } | Value::Int { .. }, Value::CellPath { val: rhs, .. }) => { + let val = rhs.members.iter().any(|member| match (self, member) { + (Value::Int { val: lhs, .. }, PathMember::Int { val: rhs, .. }) => { + *lhs != *rhs as i64 + } + (Value::String { val: lhs, .. }, PathMember::String { val: rhs, .. }) => { + lhs != rhs + } + (Value::String { .. }, PathMember::Int { .. }) + | (Value::Int { .. }, PathMember::String { .. }) => true, + _ => unreachable!( + "outer match arm ensures `self` is either a `String` or `Int` variant" + ), + }); + + Ok(Value::Bool { val, span }) + } + (Value::CellPath { val: lhs, .. }, Value::CellPath { val: rhs, .. }) => { + Ok(Value::Bool { + val: rhs + .members + .windows(lhs.members.len()) + .all(|member_window| member_window != rhs.members), + span, + }) + } + (Value::CustomValue { val: lhs, span }, rhs) => { + lhs.operation(*span, Operator::NotIn, op, rhs) + } + _ => Err(ShellError::OperatorMismatch { + op_span: op, + lhs_ty: self.get_type(), + lhs_span: self.span()?, + rhs_ty: rhs.get_type(), + rhs_span: rhs.span()?, + }), + } + } + + pub fn contains(&self, op: Span, rhs: &Value) -> Result { + let span = span(&[self.span()?, rhs.span()?]); + + match (self, rhs) { + (Value::String { val: lhs, .. }, Value::String { val: rhs, .. }) => Ok(Value::Bool { + val: lhs.contains(rhs), + span, + }), + (Value::CustomValue { val: lhs, span }, rhs) => { + lhs.operation(*span, Operator::Contains, op, rhs) + } + _ => Err(ShellError::OperatorMismatch { + op_span: op, + lhs_ty: self.get_type(), + lhs_span: self.span()?, + rhs_ty: rhs.get_type(), + rhs_span: rhs.span()?, + }), + } + } + + pub fn not_contains(&self, op: Span, rhs: &Value) -> Result { + let span = span(&[self.span()?, rhs.span()?]); + + match (self, rhs) { + (Value::String { val: lhs, .. }, Value::String { val: rhs, .. }) => Ok(Value::Bool { + val: !lhs.contains(rhs), + span, + }), + (Value::CustomValue { val: lhs, span }, rhs) => { + lhs.operation(*span, Operator::NotContains, op, rhs) + } + _ => Err(ShellError::OperatorMismatch { + op_span: op, + lhs_ty: self.get_type(), + lhs_span: self.span()?, + rhs_ty: rhs.get_type(), + rhs_span: rhs.span()?, + }), + } + } + + pub fn modulo(&self, op: Span, rhs: &Value) -> Result { + let span = span(&[self.span()?, rhs.span()?]); + + match (self, rhs) { + (Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => { + if *rhs != 0 { + Ok(Value::Int { + val: lhs % rhs, + span, + }) + } else { + Err(ShellError::DivisionByZero(op)) + } + } + (Value::Int { val: lhs, .. }, Value::Float { val: rhs, .. }) => { + if *rhs != 0.0 { + Ok(Value::Float { + val: *lhs as f64 % *rhs, + span, + }) + } else { + Err(ShellError::DivisionByZero(op)) + } + } + (Value::Float { val: lhs, .. }, Value::Int { val: rhs, .. }) => { + if *rhs != 0 { + Ok(Value::Float { + val: *lhs % *rhs as f64, + span, + }) + } else { + Err(ShellError::DivisionByZero(op)) + } + } + (Value::Float { val: lhs, .. }, Value::Float { val: rhs, .. }) => { + if *rhs != 0.0 { + Ok(Value::Float { + val: lhs % rhs, + span, + }) + } else { + Err(ShellError::DivisionByZero(op)) + } + } + (Value::CustomValue { val: lhs, span }, rhs) => { + lhs.operation(*span, Operator::Modulo, op, rhs) + } + + _ => Err(ShellError::OperatorMismatch { + op_span: op, + lhs_ty: self.get_type(), + lhs_span: self.span()?, + rhs_ty: rhs.get_type(), + rhs_span: rhs.span()?, + }), + } + } + + pub fn and(&self, op: Span, rhs: &Value) -> Result { + let span = span(&[self.span()?, rhs.span()?]); + + match (self, rhs) { + (Value::Bool { val: lhs, .. }, Value::Bool { val: rhs, .. }) => Ok(Value::Bool { + val: *lhs && *rhs, + span, + }), + (Value::CustomValue { val: lhs, span }, rhs) => { + lhs.operation(*span, Operator::And, op, rhs) + } + _ => Err(ShellError::OperatorMismatch { + op_span: op, + lhs_ty: self.get_type(), + lhs_span: self.span()?, + rhs_ty: rhs.get_type(), + rhs_span: rhs.span()?, + }), + } + } + + pub fn or(&self, op: Span, rhs: &Value) -> Result { + let span = span(&[self.span()?, rhs.span()?]); + + match (self, rhs) { + (Value::Bool { val: lhs, .. }, Value::Bool { val: rhs, .. }) => Ok(Value::Bool { + val: *lhs || *rhs, + span, + }), + (Value::CustomValue { val: lhs, span }, rhs) => { + lhs.operation(*span, Operator::Or, op, rhs) + } + _ => Err(ShellError::OperatorMismatch { + op_span: op, + lhs_ty: self.get_type(), + lhs_span: self.span()?, + rhs_ty: rhs.get_type(), + rhs_span: rhs.span()?, + }), + } + } + + pub fn pow(&self, op: Span, rhs: &Value) -> Result { + let span = span(&[self.span()?, rhs.span()?]); + + match (self, rhs) { + (Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => { + if let Some(val) = lhs.checked_pow(*rhs as u32) { + Ok(Value::Int { val, span }) + } else { + Err(ShellError::OperatorOverflow( + "pow operation overflowed".into(), + span, + )) + } + } + (Value::Int { val: lhs, .. }, Value::Float { val: rhs, .. }) => Ok(Value::Float { + val: (*lhs as f64).powf(*rhs), + span, + }), + (Value::Float { val: lhs, .. }, Value::Int { val: rhs, .. }) => Ok(Value::Float { + val: lhs.powf(*rhs as f64), + span, + }), + (Value::Float { val: lhs, .. }, Value::Float { val: rhs, .. }) => Ok(Value::Float { + val: lhs.powf(*rhs), + span, + }), + (Value::CustomValue { val: lhs, span }, rhs) => { + lhs.operation(*span, Operator::Pow, op, rhs) + } + + _ => Err(ShellError::OperatorMismatch { + op_span: op, + lhs_ty: self.get_type(), + lhs_span: self.span()?, + rhs_ty: rhs.get_type(), + rhs_span: rhs.span()?, + }), + } + } +} + +/// Create a Value::Record from a spanned hashmap +impl From>> for Value { + fn from(input: Spanned>) -> Self { + let span = input.span; + let (cols, vals) = input + .item + .into_iter() + .fold((vec![], vec![]), |mut acc, (k, v)| { + acc.0.push(k); + acc.1.push(v); + acc + }); + + Value::Record { cols, vals, span } + } +} + +/// Create a Value::Record from a spanned indexmap +impl From>> for Value { + fn from(input: Spanned>) -> Self { + let span = input.span; + let (cols, vals) = input + .item + .into_iter() + .fold((vec![], vec![]), |mut acc, (k, v)| { + acc.0.push(k); + acc.1.push(v); + acc + }); + + Value::Record { cols, vals, span } + } +} + +/// Format a duration in nanoseconds into a string +pub fn format_duration(duration: i64) -> String { + let (sign, duration) = if duration >= 0 { + (1, duration) + } else { + (-1, -duration) + }; + let (micros, nanos): (i64, i64) = (duration / 1000, duration % 1000); + let (millis, micros): (i64, i64) = (micros / 1000, micros % 1000); + let (secs, millis): (i64, i64) = (millis / 1000, millis % 1000); + let (mins, secs): (i64, i64) = (secs / 60, secs % 60); + let (hours, mins): (i64, i64) = (mins / 60, mins % 60); + let (days, hours): (i64, i64) = (hours / 24, hours % 24); + + let mut output_prep = vec![]; + + if days != 0 { + output_prep.push(format!("{}day", days)); + } + + if hours != 0 { + output_prep.push(format!("{}hr", hours)); + } + + if mins != 0 { + output_prep.push(format!("{}min", mins)); + } + // output 0sec for zero duration + if duration == 0 || secs != 0 { + output_prep.push(format!("{}sec", secs)); + } + + if millis != 0 { + output_prep.push(format!("{}ms", millis)); + } + + if micros != 0 { + output_prep.push(format!("{}us", micros)); + } + + if nanos != 0 { + output_prep.push(format!("{}ns", nanos)); + } + + format!( + "{}{}", + if sign == -1 { "-" } else { "" }, + output_prep.join(" ") + ) +} + +fn format_filesize(num_bytes: i64, config: &Config) -> String { + // Allow the user to specify how they want their numbers formatted + let filesize_format_var = get_config_filesize_format(config); + + let byte = byte_unit::Byte::from_bytes(num_bytes as u128); + let adj_byte = + if filesize_format_var.0 == byte_unit::ByteUnit::B && filesize_format_var.1 == "auto" { + byte.get_appropriate_unit(!config.filesize_metric) + } else { + byte.get_adjusted_unit(filesize_format_var.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_var.1 == "auto" { + format!("{} B", locale_byte_string) + } else { + locale_byte_string + } + } + _ => adj_byte.format(1), + } +} + +fn get_config_filesize_format(config: &Config) -> (ByteUnit, &str) { + // We need to take into account config.filesize_metric so, if someone asks for KB + // filesize_metric is true, return KiB + let filesize_format = match config.filesize_format.as_str() { + "b" => (byte_unit::ByteUnit::B, ""), + "kb" => { + if config.filesize_metric { + (byte_unit::ByteUnit::KiB, "") + } else { + (byte_unit::ByteUnit::KB, "") + } + } + "kib" => (byte_unit::ByteUnit::KiB, ""), + "mb" => { + if config.filesize_metric { + (byte_unit::ByteUnit::MiB, "") + } else { + (byte_unit::ByteUnit::MB, "") + } + } + "mib" => (byte_unit::ByteUnit::MiB, ""), + "gb" => { + if config.filesize_metric { + (byte_unit::ByteUnit::GiB, "") + } else { + (byte_unit::ByteUnit::GB, "") + } + } + "gib" => (byte_unit::ByteUnit::GiB, ""), + "tb" => { + if config.filesize_metric { + (byte_unit::ByteUnit::TiB, "") + } else { + (byte_unit::ByteUnit::TB, "") + } + } + "tib" => (byte_unit::ByteUnit::TiB, ""), + "pb" => { + if config.filesize_metric { + (byte_unit::ByteUnit::PiB, "") + } else { + (byte_unit::ByteUnit::PB, "") + } + } + "pib" => (byte_unit::ByteUnit::PiB, ""), + "eb" => { + if config.filesize_metric { + (byte_unit::ByteUnit::EiB, "") + } else { + (byte_unit::ByteUnit::EB, "") + } + } + "eib" => (byte_unit::ByteUnit::EiB, ""), + "zb" => { + if config.filesize_metric { + (byte_unit::ByteUnit::ZiB, "") + } else { + (byte_unit::ByteUnit::ZB, "") + } + } + "zib" => (byte_unit::ByteUnit::ZiB, ""), + _ => (byte_unit::ByteUnit::B, "auto"), + }; + + filesize_format +} diff --git a/crates/nu-protocol/src/value/range.rs b/crates/nu-protocol/src/value/range.rs index 77b27b9965..4f404f411e 100644 --- a/crates/nu-protocol/src/value/range.rs +++ b/crates/nu-protocol/src/value/range.rs @@ -1,3 +1,4 @@ +<<<<<<< HEAD use crate::value::Primitive; use derive_new::new; use nu_errors::ShellError; @@ -150,5 +151,225 @@ impl Range { } // How would inclusive vs. exclusive range work here? +======= +use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; + +/// A Range is an iterator over integers. +use crate::{ + ast::{RangeInclusion, RangeOperator}, + *, +}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Range { + pub from: Value, + pub incr: Value, + pub to: Value, + pub inclusion: RangeInclusion, +} + +impl Range { + pub fn new( + expr_span: Span, + from: Value, + next: Value, + to: Value, + operator: &RangeOperator, + ) -> Result { + // Select from & to values if they're not specified + // TODO: Replace the placeholder values with proper min/max for range based on data type + let from = if let Value::Nothing { .. } = from { + Value::Int { + val: 0i64, + span: expr_span, + } + } else { + from + }; + + let to = if let Value::Nothing { .. } = to { + if let Ok(Value::Bool { val: true, .. }) = next.lt(expr_span, &from) { + Value::Int { + val: -100i64, + span: expr_span, + } + } else { + Value::Int { + val: 100i64, + span: expr_span, + } + } + } else { + to + }; + + // Check if the range counts up or down + let moves_up = matches!(from.lte(expr_span, &to), Ok(Value::Bool { val: true, .. })); + + // Convert the next value into the inctement + let incr = if let Value::Nothing { .. } = next { + if moves_up { + Value::Int { + val: 1i64, + span: expr_span, + } + } else { + Value::Int { + val: -1i64, + span: expr_span, + } + } + } else { + next.sub(operator.next_op_span, &from)? + }; + + let zero = Value::Int { + val: 0i64, + span: expr_span, + }; + + // Increment must be non-zero, otherwise we iterate forever + if matches!(incr.eq(expr_span, &zero), Ok(Value::Bool { val: true, .. })) { + return Err(ShellError::CannotCreateRange(expr_span)); + } + + // If to > from, then incr > 0, otherwise we iterate forever + if let (Value::Bool { val: true, .. }, Value::Bool { val: false, .. }) = ( + to.gt(operator.span, &from)?, + incr.gt(operator.next_op_span, &zero)?, + ) { + return Err(ShellError::CannotCreateRange(expr_span)); + } + + // If to < from, then incr < 0, otherwise we iterate forever + if let (Value::Bool { val: true, .. }, Value::Bool { val: false, .. }) = ( + to.lt(operator.span, &from)?, + incr.lt(operator.next_op_span, &zero)?, + ) { + return Err(ShellError::CannotCreateRange(expr_span)); + } + + Ok(Range { + from, + incr, + to, + inclusion: operator.inclusion, + }) + } + + #[inline] + fn moves_up(&self) -> bool { + self.from <= self.to + } + + #[inline] + fn is_end_inclusive(&self) -> bool { + matches!(self.inclusion, RangeInclusion::Inclusive) + } + + pub fn contains(&self, item: &Value) -> bool { + match (item.partial_cmp(&self.from), item.partial_cmp(&self.to)) { + (Some(Ordering::Greater | Ordering::Equal), Some(Ordering::Less)) => self.moves_up(), + (Some(Ordering::Less | Ordering::Equal), Some(Ordering::Greater)) => !self.moves_up(), + (Some(_), Some(Ordering::Equal)) => self.is_end_inclusive(), + (_, _) => false, + } + } + + pub fn into_range_iter(self) -> Result { + let span = self.from.span()?; + + Ok(RangeIterator::new(self, span)) + } +} + +pub struct RangeIterator { + curr: Value, + end: Value, + span: Span, + is_end_inclusive: bool, + moves_up: bool, + incr: Value, + done: bool, +} + +impl RangeIterator { + pub fn new(range: Range, span: Span) -> RangeIterator { + let moves_up = range.moves_up(); + let is_end_inclusive = range.is_end_inclusive(); + + let start = match range.from { + Value::Nothing { .. } => Value::Int { val: 0, span }, + x => x, + }; + + let end = match range.to { + Value::Nothing { .. } => Value::Int { + val: i64::MAX, + span, + }, + x => x, + }; + + RangeIterator { + moves_up, + curr: start, + end, + span, + is_end_inclusive, + done: false, + incr: range.incr, + } + } +} + +impl Iterator for RangeIterator { + type Item = Value; + fn next(&mut self) -> Option { + if self.done { + return None; + } + + let ordering = if matches!(self.end, Value::Nothing { .. }) { + Some(Ordering::Less) + } else { + self.curr.partial_cmp(&self.end) + }; + + let ordering = if let Some(ord) = ordering { + ord + } else { + self.done = true; + return Some(Value::Error { + error: ShellError::CannotCreateRange(self.span), + }); + }; + + let desired_ordering = if self.moves_up { + Ordering::Less + } else { + Ordering::Greater + }; + + if (ordering == desired_ordering) || (self.is_end_inclusive && ordering == Ordering::Equal) + { + let next_value = self.curr.add(self.span, &self.incr); + + let mut next = match next_value { + Ok(result) => result, + + Err(error) => { + self.done = true; + return Some(Value::Error { error }); + } + }; + std::mem::swap(&mut self.curr, &mut next); + + Some(next) + } else { + None + } +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } } diff --git a/crates/nu-protocol/src/value/stream.rs b/crates/nu-protocol/src/value/stream.rs new file mode 100644 index 0000000000..6f00299eb7 --- /dev/null +++ b/crates/nu-protocol/src/value/stream.rs @@ -0,0 +1,204 @@ +use crate::*; +use std::{ + fmt::Debug, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, +}; + +pub struct RawStream { + pub stream: Box, ShellError>> + Send + 'static>, + pub leftover: Vec, + pub ctrlc: Option>, + pub is_binary: bool, + pub span: Span, +} + +impl RawStream { + pub fn new( + stream: Box, ShellError>> + Send + 'static>, + ctrlc: Option>, + span: Span, + ) -> Self { + Self { + stream, + leftover: vec![], + ctrlc, + is_binary: false, + span, + } + } + + pub fn into_bytes(self) -> Result>, ShellError> { + let mut output = vec![]; + + for item in self.stream { + output.extend(item?); + } + + Ok(Spanned { + item: output, + span: self.span, + }) + } + + pub fn into_string(self) -> Result, ShellError> { + let mut output = String::new(); + let span = self.span; + + for item in self { + output.push_str(&item?.as_string()?); + } + + Ok(Spanned { item: output, span }) + } +} +impl Debug for RawStream { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RawStream").finish() + } +} +impl Iterator for RawStream { + type Item = Result; + + fn next(&mut self) -> Option { + // If we know we're already binary, just output that + if self.is_binary { + match self.stream.next() { + Some(buffer) => match buffer { + Ok(mut v) => { + if !self.leftover.is_empty() { + while let Some(b) = self.leftover.pop() { + v.insert(0, b); + } + } + Some(Ok(Value::Binary { + val: v, + span: self.span, + })) + } + Err(e) => Some(Err(e)), + }, + None => None, + } + } else { + // We *may* be text. We're only going to try utf-8. Other decodings + // needs to be taken as binary first, then passed through `decode`. + match self.stream.next() { + Some(buffer) => match buffer { + Ok(mut v) => { + if !self.leftover.is_empty() { + while let Some(b) = self.leftover.pop() { + v.insert(0, b); + } + } + + match String::from_utf8(v.clone()) { + Ok(s) => { + // Great, we have a complete string, let's output it + Some(Ok(Value::String { + val: s, + span: self.span, + })) + } + Err(err) => { + // Okay, we *might* have a string but we've also got some errors + if v.is_empty() { + // We can just end here + None + } else if v.len() > 3 + && (v.len() - err.utf8_error().valid_up_to() > 3) + { + // As UTF-8 characters are max 4 bytes, if we have more than that in error we know + // that it's not just a character spanning two frames. + // We now know we are definitely binary, so switch to binary and stay there. + self.is_binary = true; + Some(Ok(Value::Binary { + val: v, + span: self.span, + })) + } else { + // Okay, we have a tiny bit of error at the end of the buffer. This could very well be + // a character that spans two frames. Since this is the case, remove the error from + // the current frame an dput it in the leftover buffer. + self.leftover = v[err.utf8_error().valid_up_to()..].to_vec(); + + let buf = v[0..err.utf8_error().valid_up_to()].to_vec(); + + match String::from_utf8(buf) { + Ok(s) => Some(Ok(Value::String { + val: s, + span: self.span, + })), + Err(_) => { + // Something is definitely wrong. Switch to binary, and stay there + self.is_binary = true; + Some(Ok(Value::Binary { + val: v, + span: self.span, + })) + } + } + } + } + } + } + Err(e) => Some(Err(e)), + }, + None => None, + } + } + } +} + +/// A potentially infinite stream of values, optinally with a mean to send a Ctrl-C signal to stop +/// the stream from continuing. +/// +/// In practice, a "stream" here means anything which can be iterated and produce Values as it iterates. +/// Like other iterators in Rust, observing values from this stream will drain the items as you view them +/// and the stream cannot be replayed. +pub struct ListStream { + pub stream: Box + Send + 'static>, + pub ctrlc: Option>, +} + +impl ListStream { + pub fn into_string(self, separator: &str, config: &Config) -> String { + self.map(|x: Value| x.into_string(", ", config)) + .collect::>() + .join(separator) + } + + pub fn from_stream( + input: impl Iterator + Send + 'static, + ctrlc: Option>, + ) -> ListStream { + ListStream { + stream: Box::new(input), + ctrlc, + } + } +} + +impl Debug for ListStream { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ValueStream").finish() + } +} + +impl Iterator for ListStream { + type Item = Value; + + fn next(&mut self) -> Option { + if let Some(ctrlc) = &self.ctrlc { + if ctrlc.load(Ordering::SeqCst) { + None + } else { + self.stream.next() + } + } else { + self.stream.next() + } + } +} diff --git a/crates/nu-protocol/src/value/unit.rs b/crates/nu-protocol/src/value/unit.rs index e69de29bb2..27fd893014 100644 --- a/crates/nu-protocol/src/value/unit.rs +++ b/crates/nu-protocol/src/value/unit.rs @@ -0,0 +1,27 @@ +#[derive(Debug, Clone, Copy)] +pub enum Unit { + // Filesize units: metric + Byte, + Kilobyte, + Megabyte, + Gigabyte, + Terabyte, + Petabyte, + + // Filesize units: ISO/IEC 80000 + Kibibyte, + Mebibyte, + Gibibyte, + Tebibyte, + Pebibyte, + + // Duration units + Nanosecond, + Microsecond, + Millisecond, + Second, + Minute, + Hour, + Day, + Week, +} diff --git a/crates/nu-protocol/tests/test_signature.rs b/crates/nu-protocol/tests/test_signature.rs new file mode 100644 index 0000000000..3a9048824f --- /dev/null +++ b/crates/nu-protocol/tests/test_signature.rs @@ -0,0 +1,170 @@ +use nu_protocol::{Flag, PositionalArg, Signature, SyntaxShape}; + +#[test] +fn test_signature() { + let signature = Signature::new("new_signature"); + let from_build = Signature::build("new_signature"); + + // asserting partial eq implementation + assert_eq!(signature, from_build); + + // constructing signature with description + let signature = Signature::new("signature").desc("example usage"); + assert_eq!(signature.usage, "example usage".to_string()) +} + +#[test] +fn test_signature_chained() { + let signature = Signature::new("new_signature") + .desc("description") + .required("required", SyntaxShape::String, "required description") + .optional("optional", SyntaxShape::String, "optional description") + .required_named( + "req_named", + SyntaxShape::String, + "required named description", + Some('r'), + ) + .named("named", SyntaxShape::String, "named description", Some('n')) + .switch("switch", "switch description", None) + .rest("rest", SyntaxShape::String, "rest description"); + + assert_eq!(signature.required_positional.len(), 1); + assert_eq!(signature.optional_positional.len(), 1); + assert_eq!(signature.named.len(), 4); // The 3 above + help + assert!(signature.rest_positional.is_some()); + assert_eq!(signature.get_shorts(), vec!['h', 'r', 'n']); + assert_eq!( + signature.get_names(), + vec!["help", "req_named", "named", "switch"] + ); + assert_eq!(signature.num_positionals(), 2); + + assert_eq!( + signature.get_positional(0), + Some(PositionalArg { + name: "required".to_string(), + desc: "required description".to_string(), + shape: SyntaxShape::String, + var_id: None + }) + ); + assert_eq!( + signature.get_positional(1), + Some(PositionalArg { + name: "optional".to_string(), + desc: "optional description".to_string(), + shape: SyntaxShape::String, + var_id: None + }) + ); + assert_eq!( + signature.get_positional(2), + Some(PositionalArg { + name: "rest".to_string(), + desc: "rest description".to_string(), + shape: SyntaxShape::String, + var_id: None + }) + ); + + assert_eq!( + signature.get_long_flag("req_named"), + Some(Flag { + long: "req_named".to_string(), + short: Some('r'), + arg: Some(SyntaxShape::String), + required: true, + desc: "required named description".to_string(), + var_id: None + }) + ); + + assert_eq!( + signature.get_short_flag('r'), + Some(Flag { + long: "req_named".to_string(), + short: Some('r'), + arg: Some(SyntaxShape::String), + required: true, + desc: "required named description".to_string(), + var_id: None + }) + ); +} + +#[test] +#[should_panic(expected = "There may be duplicate short flags, such as -h")] +fn test_signature_same_short() { + // Creating signature with same short name should panic + Signature::new("new_signature") + .required_named( + "required_named", + SyntaxShape::String, + "required named description", + Some('n'), + ) + .named("named", SyntaxShape::String, "named description", Some('n')); +} + +#[test] +#[should_panic(expected = "There may be duplicate name flags, such as --help")] +fn test_signature_same_name() { + // Creating signature with same short name should panic + Signature::new("new_signature") + .required_named( + "name", + SyntaxShape::String, + "required named description", + Some('r'), + ) + .named("name", SyntaxShape::String, "named description", Some('n')); +} + +#[test] +fn test_signature_round_trip() { + let signature = Signature::new("new_signature") + .desc("description") + .required("first", SyntaxShape::String, "first required") + .required("second", SyntaxShape::Int, "second required") + .optional("optional", SyntaxShape::String, "optional description") + .required_named( + "req_named", + SyntaxShape::String, + "required named description", + Some('r'), + ) + .named("named", SyntaxShape::String, "named description", Some('n')) + .switch("switch", "switch description", None) + .rest("rest", SyntaxShape::String, "rest description") + .category(nu_protocol::Category::Conversions); + + let string = serde_json::to_string_pretty(&signature).unwrap(); + let returned: Signature = serde_json::from_str(&string).unwrap(); + + assert_eq!(signature.name, returned.name); + assert_eq!(signature.usage, returned.usage); + assert_eq!(signature.extra_usage, returned.extra_usage); + assert_eq!(signature.is_filter, returned.is_filter); + assert_eq!(signature.category, returned.category); + + signature + .required_positional + .iter() + .zip(returned.required_positional.iter()) + .for_each(|(lhs, rhs)| assert_eq!(lhs, rhs)); + + signature + .optional_positional + .iter() + .zip(returned.optional_positional.iter()) + .for_each(|(lhs, rhs)| assert_eq!(lhs, rhs)); + + signature + .named + .iter() + .zip(returned.named.iter()) + .for_each(|(lhs, rhs)| assert_eq!(lhs, rhs)); + + assert_eq!(signature.rest_positional, returned.rest_positional,); +} diff --git a/crates/nu-protocol/tests/test_value.rs b/crates/nu-protocol/tests/test_value.rs new file mode 100644 index 0000000000..65cbc3e33f --- /dev/null +++ b/crates/nu-protocol/tests/test_value.rs @@ -0,0 +1,45 @@ +use nu_protocol::{Span, Value}; + +#[test] +fn test_comparison_nothing() { + let values = vec![ + Value::Int { + val: 1, + span: Span::test_data(), + }, + Value::String { + val: "string".into(), + span: Span::test_data(), + }, + Value::Float { + val: 1.0, + span: Span::test_data(), + }, + ]; + + let nothing = Value::Nothing { + span: Span::test_data(), + }; + + for value in values { + assert!(matches!( + value.eq(Span::test_data(), ¬hing), + Ok(Value::Bool { val: false, .. }) + )); + + assert!(matches!( + value.ne(Span::test_data(), ¬hing), + Ok(Value::Bool { val: true, .. }) + )); + + assert!(matches!( + nothing.eq(Span::test_data(), &value), + Ok(Value::Bool { val: false, .. }) + )); + + assert!(matches!( + nothing.ne(Span::test_data(), &value), + Ok(Value::Bool { val: true, .. }) + )); + } +} diff --git a/crates/nu-system/.gitignore b/crates/nu-system/.gitignore new file mode 100644 index 0000000000..ea8c4bf7f3 --- /dev/null +++ b/crates/nu-system/.gitignore @@ -0,0 +1 @@ +/target diff --git a/crates/nu-system/Cargo.lock b/crates/nu-system/Cargo.lock new file mode 100644 index 0000000000..47bce2f73b --- /dev/null +++ b/crates/nu-system/Cargo.lock @@ -0,0 +1,253 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cc" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time", + "winapi", +] + +[[package]] +name = "crc32fast" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "738c290dfaea84fc1ca15ad9c168d083b05a714e1efddd8edaab678dc28d2836" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "flate2" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" +dependencies = [ + "cfg-if", + "crc32fast", + "libc", + "miniz_oxide", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" + +[[package]] +name = "libproc" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6466fc1f834276563fbbd4be1c24236ef92bb9efdbd4691e07f1cf85a0b407f0" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + +[[package]] +name = "nu-system" +version = "0.1.0" +dependencies = [ + "errno", + "libproc", + "procfs", + "users", + "which", + "winapi", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "procfs" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0941606b9934e2d98a3677759a971756eb821f75764d0e0d26946d08e74d9104" +dependencies = [ + "bitflags", + "byteorder", + "chrono", + "flate2", + "hex", + "lazy_static", + "libc", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi", + "winapi", +] + +[[package]] +name = "users" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" +dependencies = [ + "libc", + "log", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "which" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea187a8ef279bc014ec368c27a920da2024d2a711109bfbe3440585d5cf27ad9" +dependencies = [ + "either", + "lazy_static", + "libc", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/crates/nu-system/Cargo.toml b/crates/nu-system/Cargo.toml new file mode 100644 index 0000000000..9dd4278e1a --- /dev/null +++ b/crates/nu-system/Cargo.toml @@ -0,0 +1,35 @@ +[package] +authors = ["The Nu Project Contributors", "procs creators"] +description = "Nushell system querying" +name = "nu-system" +version = "0.60.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[[bin]] +name = "ps" +path = "src/main.rs" + +[dependencies] + + +[target.'cfg(target_os = "linux")'.dependencies] +procfs = "0.12.0" +users = "0.11" +which = "4" + +[target.'cfg(target_os = "macos")'.dependencies] +libproc = "0.10" +errno = "0.2" +users = "0.11" +which = "4" +libc = "0.2" + +[target.'cfg(target_os = "windows")'.dependencies] +# winapi = { version = "0.3", features = ["handleapi", "minwindef", "psapi", "securitybaseapi", "tlhelp32", "winbase", "winnt"] } +winapi = { version = "0.3.9", features = ["tlhelp32", "fileapi", "handleapi", "ifdef", "ioapiset", "minwindef", "pdh", "psapi", "synchapi", "sysinfoapi", "winbase", "winerror", "winioctl", "winnt", "oleauto", "wbemcli", "rpcdce", "combaseapi", "objidl", "powerbase", "netioapi", "lmcons", "lmaccess", "lmapibuf", "memoryapi", "shellapi", "std"] } +chrono = "0.4" +libc = "0.2" +ntapi = "0.3" +once_cell = "1.0" \ No newline at end of file diff --git a/crates/nu-system/LICENSE b/crates/nu-system/LICENSE new file mode 100644 index 0000000000..e0e33baa83 --- /dev/null +++ b/crates/nu-system/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 procs developers and Nushell developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/nu-system/src/lib.rs b/crates/nu-system/src/lib.rs new file mode 100644 index 0000000000..a8e9728d1f --- /dev/null +++ b/crates/nu-system/src/lib.rs @@ -0,0 +1,13 @@ +#[cfg(target_os = "linux")] +mod linux; +#[cfg(target_os = "macos")] +mod macos; +#[cfg(target_os = "windows")] +mod windows; + +#[cfg(target_os = "linux")] +pub use self::linux::*; +#[cfg(target_os = "macos")] +pub use self::macos::*; +#[cfg(target_os = "windows")] +pub use self::windows::*; diff --git a/crates/nu-system/src/linux.rs b/crates/nu-system/src/linux.rs new file mode 100644 index 0000000000..5432eed07b --- /dev/null +++ b/crates/nu-system/src/linux.rs @@ -0,0 +1,253 @@ +use procfs::process::{FDInfo, Io, Process, Stat, Status, TasksIter}; +use procfs::{ProcError, ProcessCgroup}; +use std::collections::HashMap; +use std::thread; +use std::time::{Duration, Instant}; + +pub enum ProcessTask { + Process(Process), + Task { stat: Stat, owner: u32 }, +} + +impl ProcessTask { + pub fn stat(&self) -> &Stat { + match self { + ProcessTask::Process(x) => &x.stat, + ProcessTask::Task { stat: x, owner: _ } => x, + } + } + + pub fn cmdline(&self) -> Result, ProcError> { + match self { + ProcessTask::Process(x) => x.cmdline(), + _ => Err(ProcError::Other("not supported".to_string())), + } + } + + pub fn cgroups(&self) -> Result, ProcError> { + match self { + ProcessTask::Process(x) => x.cgroups(), + _ => Err(ProcError::Other("not supported".to_string())), + } + } + + pub fn fd(&self) -> Result, ProcError> { + match self { + ProcessTask::Process(x) => x.fd(), + _ => Err(ProcError::Other("not supported".to_string())), + } + } + + pub fn loginuid(&self) -> Result { + match self { + ProcessTask::Process(x) => x.loginuid(), + _ => Err(ProcError::Other("not supported".to_string())), + } + } + + pub fn owner(&self) -> u32 { + match self { + ProcessTask::Process(x) => x.owner, + ProcessTask::Task { stat: _, owner: x } => *x, + } + } + + pub fn wchan(&self) -> Result { + match self { + ProcessTask::Process(x) => x.wchan(), + _ => Err(ProcError::Other("not supported".to_string())), + } + } +} + +pub struct ProcessInfo { + pub pid: i32, + pub ppid: i32, + pub curr_proc: ProcessTask, + pub prev_proc: ProcessTask, + pub curr_io: Option, + pub prev_io: Option, + pub curr_status: Option, + pub interval: Duration, +} + +pub fn collect_proc(interval: Duration, with_thread: bool) -> Vec { + let mut base_procs = Vec::new(); + let mut base_tasks = HashMap::new(); + let mut ret = Vec::new(); + + if let Ok(all_proc) = procfs::process::all_processes() { + for proc in all_proc { + let io = proc.io().ok(); + let time = Instant::now(); + if with_thread { + if let Ok(iter) = proc.tasks() { + collect_task(iter, &mut base_tasks); + } + } + base_procs.push((proc.pid(), proc, io, time)); + } + } + + thread::sleep(interval); + + for (pid, prev_proc, prev_io, prev_time) in base_procs { + let curr_proc = if let Ok(proc) = Process::new(pid) { + proc + } else { + prev_proc.clone() + }; + let curr_io = curr_proc.io().ok(); + let curr_status = curr_proc.status().ok(); + let curr_time = Instant::now(); + let interval = curr_time - prev_time; + let ppid = curr_proc.stat.ppid; + let owner = curr_proc.owner; + + let mut curr_tasks = HashMap::new(); + if with_thread { + if let Ok(iter) = curr_proc.tasks() { + collect_task(iter, &mut curr_tasks); + } + } + + let curr_proc = ProcessTask::Process(curr_proc); + let prev_proc = ProcessTask::Process(prev_proc); + + let proc = ProcessInfo { + pid, + ppid, + curr_proc, + prev_proc, + curr_io, + prev_io, + curr_status, + interval, + }; + + ret.push(proc); + + for (tid, (pid, curr_stat, curr_status, curr_io)) in curr_tasks { + if let Some((_, prev_stat, _, prev_io)) = base_tasks.remove(&tid) { + let proc = ProcessInfo { + pid: tid, + ppid: pid, + curr_proc: ProcessTask::Task { + stat: curr_stat, + owner, + }, + prev_proc: ProcessTask::Task { + stat: prev_stat, + owner, + }, + curr_io, + prev_io, + curr_status, + interval, + }; + ret.push(proc); + } + } + } + + ret +} + +#[allow(clippy::type_complexity)] +fn collect_task(iter: TasksIter, map: &mut HashMap, Option)>) { + for task in iter { + let task = if let Ok(x) = task { + x + } else { + continue; + }; + if task.tid != task.pid { + let stat = if let Ok(x) = task.stat() { + x + } else { + continue; + }; + let status = task.status().ok(); + let io = task.io().ok(); + map.insert(task.tid, (task.pid, stat, status, io)); + } + } +} + +impl ProcessInfo { + /// PID of process + pub fn pid(&self) -> i32 { + self.pid + } + + /// Name of command + pub fn name(&self) -> String { + self.command() + .split(' ') + .collect::>() + .first() + .map(|x| x.to_string()) + .unwrap_or_default() + } + + /// Full name of command, with arguments + pub fn command(&self) -> String { + if let Ok(cmd) = &self.curr_proc.cmdline() { + if !cmd.is_empty() { + let mut cmd = cmd + .iter() + .cloned() + .map(|mut x| { + x.push(' '); + x + }) + .collect::(); + cmd.pop(); + cmd = cmd.replace("\n", " ").replace("\t", " "); + cmd + } else { + self.curr_proc.stat().comm.clone() + } + } else { + self.curr_proc.stat().comm.clone() + } + } + + /// Get the status of the process + pub fn status(&self) -> String { + match self.curr_proc.stat().state { + 'S' => "Sleeping".into(), + 'R' => "Running".into(), + 'D' => "Disk sleep".into(), + 'Z' => "Zombie".into(), + 'T' => "Stopped".into(), + 't' => "Tracing".into(), + 'X' => "Dead".into(), + 'x' => "Dead".into(), + 'K' => "Wakekill".into(), + 'W' => "Waking".into(), + 'P' => "Parked".into(), + _ => "Unknown".into(), + } + } + + /// CPU usage as a percent of total + pub fn cpu_usage(&self) -> f64 { + let curr_time = self.curr_proc.stat().utime + self.curr_proc.stat().stime; + let prev_time = self.prev_proc.stat().utime + self.prev_proc.stat().stime; + let usage_ms = + (curr_time - prev_time) * 1000 / procfs::ticks_per_second().unwrap_or(100) as u64; + let interval_ms = self.interval.as_secs() * 1000 + u64::from(self.interval.subsec_millis()); + usage_ms as f64 * 100.0 / interval_ms as f64 + } + + /// Memory size in number of bytes + pub fn mem_size(&self) -> u64 { + self.curr_proc.stat().rss_bytes().unwrap_or(0) as u64 + } + + /// Virtual memory size in bytes + pub fn virtual_size(&self) -> u64 { + self.curr_proc.stat().vsize + } +} diff --git a/crates/nu-system/src/macos.rs b/crates/nu-system/src/macos.rs new file mode 100644 index 0000000000..db72450347 --- /dev/null +++ b/crates/nu-system/src/macos.rs @@ -0,0 +1,405 @@ +use libc::{c_int, c_void, size_t}; +use libproc::libproc::bsd_info::BSDInfo; +use libproc::libproc::file_info::{pidfdinfo, ListFDs, ProcFDType}; +use libproc::libproc::net_info::{InSockInfo, SocketFDInfo, SocketInfoKind, TcpSockInfo}; +use libproc::libproc::pid_rusage::{pidrusage, RUsageInfoV2}; +use libproc::libproc::proc_pid::{listpidinfo, listpids, pidinfo, ListThreads, ProcType}; +use libproc::libproc::task_info::{TaskAllInfo, TaskInfo}; +use libproc::libproc::thread_info::ThreadInfo; +use std::cmp; +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; +use std::thread; +use std::time::{Duration, Instant}; + +pub struct ProcessInfo { + pub pid: i32, + pub ppid: i32, + pub curr_task: TaskAllInfo, + pub prev_task: TaskAllInfo, + pub curr_path: Option, + pub curr_threads: Vec, + pub curr_udps: Vec, + pub curr_tcps: Vec, + pub curr_res: Option, + pub prev_res: Option, + pub interval: Duration, +} + +#[cfg_attr(tarpaulin, skip)] +pub fn collect_proc(interval: Duration, _with_thread: bool) -> Vec { + let mut base_procs = Vec::new(); + let mut ret = Vec::new(); + let arg_max = get_arg_max(); + + if let Ok(procs) = listpids(ProcType::ProcAllPIDS) { + for p in procs { + if let Ok(task) = pidinfo::(p as i32, 0) { + let res = pidrusage::(p as i32).ok(); + let time = Instant::now(); + base_procs.push((p as i32, task, res, time)); + } + } + } + + thread::sleep(interval); + + for (pid, prev_task, prev_res, prev_time) in base_procs { + let curr_task = if let Ok(task) = pidinfo::(pid, 0) { + task + } else { + clone_task_all_info(&prev_task) + }; + + let curr_path = get_path_info(pid, arg_max); + + let threadids = listpidinfo::(pid, curr_task.ptinfo.pti_threadnum as usize); + let mut curr_threads = Vec::new(); + if let Ok(threadids) = threadids { + for t in threadids { + if let Ok(thread) = pidinfo::(pid, t) { + curr_threads.push(thread); + } + } + } + + let mut curr_tcps = Vec::new(); + let mut curr_udps = Vec::new(); + + let fds = listpidinfo::(pid, curr_task.pbsd.pbi_nfiles as usize); + if let Ok(fds) = fds { + for fd in fds { + if let ProcFDType::Socket = fd.proc_fdtype.into() { + if let Ok(socket) = pidfdinfo::(pid, fd.proc_fd) { + match socket.psi.soi_kind.into() { + SocketInfoKind::In => { + if socket.psi.soi_protocol == libc::IPPROTO_UDP { + let info = unsafe { socket.psi.soi_proto.pri_in }; + curr_udps.push(info); + } + } + SocketInfoKind::Tcp => { + let info = unsafe { socket.psi.soi_proto.pri_tcp }; + curr_tcps.push(info); + } + _ => (), + } + } + } + } + } + + let curr_res = pidrusage::(pid).ok(); + + let curr_time = Instant::now(); + let interval = curr_time - prev_time; + let ppid = curr_task.pbsd.pbi_ppid as i32; + + let proc = ProcessInfo { + pid, + ppid, + curr_task, + prev_task, + curr_path, + curr_threads, + curr_udps, + curr_tcps, + curr_res, + prev_res, + interval, + }; + + ret.push(proc); + } + + ret +} + +#[cfg_attr(tarpaulin, skip)] +fn get_arg_max() -> size_t { + let mut mib: [c_int; 2] = [libc::CTL_KERN, libc::KERN_ARGMAX]; + let mut arg_max = 0i32; + let mut size = ::std::mem::size_of::(); + unsafe { + while libc::sysctl( + mib.as_mut_ptr(), + 2, + (&mut arg_max) as *mut i32 as *mut c_void, + &mut size, + ::std::ptr::null_mut(), + 0, + ) == -1 + {} + } + arg_max as size_t +} + +pub struct PathInfo { + pub name: String, + pub exe: PathBuf, + pub root: PathBuf, + pub cmd: Vec, + pub env: Vec, +} + +#[cfg_attr(tarpaulin, skip)] +unsafe fn get_unchecked_str(cp: *mut u8, start: *mut u8) -> String { + let len = cp as usize - start as usize; + let part = Vec::from_raw_parts(start, len, len); + let tmp = String::from_utf8_unchecked(part.clone()); + ::std::mem::forget(part); + tmp +} + +#[cfg_attr(tarpaulin, skip)] +fn get_path_info(pid: i32, mut size: size_t) -> Option { + let mut proc_args = Vec::with_capacity(size as usize); + let ptr: *mut u8 = proc_args.as_mut_slice().as_mut_ptr(); + + let mut mib: [c_int; 3] = [libc::CTL_KERN, libc::KERN_PROCARGS2, pid as c_int]; + + unsafe { + let ret = libc::sysctl( + mib.as_mut_ptr(), + 3, + ptr as *mut c_void, + &mut size, + ::std::ptr::null_mut(), + 0, + ); + if ret != -1 { + let mut n_args: c_int = 0; + libc::memcpy( + (&mut n_args) as *mut c_int as *mut c_void, + ptr as *const c_void, + ::std::mem::size_of::(), + ); + let mut cp = ptr.add(::std::mem::size_of::()); + let mut start = cp; + if cp < ptr.add(size) { + while cp < ptr.add(size) && *cp != 0 { + cp = cp.offset(1); + } + let exe = Path::new(get_unchecked_str(cp, start).as_str()).to_path_buf(); + let name = exe + .file_name() + .unwrap_or_else(|| OsStr::new("")) + .to_str() + .unwrap_or("") + .to_owned(); + let mut need_root = true; + let mut root = Default::default(); + if exe.is_absolute() { + if let Some(parent) = exe.parent() { + root = parent.to_path_buf(); + need_root = false; + } + } + while cp < ptr.add(size) && *cp == 0 { + cp = cp.offset(1); + } + start = cp; + let mut c = 0; + let mut cmd = Vec::new(); + while c < n_args && cp < ptr.add(size) { + if *cp == 0 { + c += 1; + cmd.push(get_unchecked_str(cp, start)); + start = cp.offset(1); + } + cp = cp.offset(1); + } + start = cp; + let mut env = Vec::new(); + while cp < ptr.add(size) { + if *cp == 0 { + if cp == start { + break; + } + env.push(get_unchecked_str(cp, start)); + start = cp.offset(1); + } + cp = cp.offset(1); + } + if need_root { + for env in env.iter() { + if env.starts_with("PATH=") { + root = Path::new(&env[6..]).to_path_buf(); + break; + } + } + } + + Some(PathInfo { + exe, + name, + root, + cmd, + env, + }) + } else { + None + } + } else { + None + } + } +} + +#[cfg_attr(tarpaulin, skip)] +fn clone_task_all_info(src: &TaskAllInfo) -> TaskAllInfo { + let pbsd = BSDInfo { + pbi_flags: src.pbsd.pbi_flags, + pbi_status: src.pbsd.pbi_status, + pbi_xstatus: src.pbsd.pbi_xstatus, + pbi_pid: src.pbsd.pbi_pid, + pbi_ppid: src.pbsd.pbi_ppid, + pbi_uid: src.pbsd.pbi_uid, + pbi_gid: src.pbsd.pbi_gid, + pbi_ruid: src.pbsd.pbi_ruid, + pbi_rgid: src.pbsd.pbi_rgid, + pbi_svuid: src.pbsd.pbi_svuid, + pbi_svgid: src.pbsd.pbi_svgid, + rfu_1: src.pbsd.rfu_1, + pbi_comm: src.pbsd.pbi_comm, + pbi_name: src.pbsd.pbi_name, + pbi_nfiles: src.pbsd.pbi_nfiles, + pbi_pgid: src.pbsd.pbi_pgid, + pbi_pjobc: src.pbsd.pbi_pjobc, + e_tdev: src.pbsd.e_tdev, + e_tpgid: src.pbsd.e_tpgid, + pbi_nice: src.pbsd.pbi_nice, + pbi_start_tvsec: src.pbsd.pbi_start_tvsec, + pbi_start_tvusec: src.pbsd.pbi_start_tvusec, + }; + let ptinfo = TaskInfo { + pti_virtual_size: src.ptinfo.pti_virtual_size, + pti_resident_size: src.ptinfo.pti_resident_size, + pti_total_user: src.ptinfo.pti_total_user, + pti_total_system: src.ptinfo.pti_total_system, + pti_threads_user: src.ptinfo.pti_threads_user, + pti_threads_system: src.ptinfo.pti_threads_system, + pti_policy: src.ptinfo.pti_policy, + pti_faults: src.ptinfo.pti_faults, + pti_pageins: src.ptinfo.pti_pageins, + pti_cow_faults: src.ptinfo.pti_cow_faults, + pti_messages_sent: src.ptinfo.pti_messages_sent, + pti_messages_received: src.ptinfo.pti_messages_received, + pti_syscalls_mach: src.ptinfo.pti_syscalls_mach, + pti_syscalls_unix: src.ptinfo.pti_syscalls_unix, + pti_csw: src.ptinfo.pti_csw, + pti_threadnum: src.ptinfo.pti_threadnum, + pti_numrunning: src.ptinfo.pti_numrunning, + pti_priority: src.ptinfo.pti_priority, + }; + TaskAllInfo { pbsd, ptinfo } +} + +impl ProcessInfo { + /// PID of process + pub fn pid(&self) -> i32 { + self.pid + } + + /// Name of command + pub fn name(&self) -> String { + // self.command() + // .split(' ') + // .collect::>() + // .first() + // .map(|x| x.to_string()) + // .unwrap_or_default() + self.command_only() + } + + /// Full name of command, with arguments + pub fn command(&self) -> String { + if let Some(path) = &self.curr_path { + if !path.cmd.is_empty() { + let mut cmd = path + .cmd + .iter() + .cloned() + .map(|mut x| { + x.push(' '); + x + }) + .collect::(); + cmd.pop(); + cmd = cmd.replace("\n", " ").replace("\t", " "); + cmd + } else { + String::from("") + } + } else { + String::from("") + } + } + + /// Full name of comand only + pub fn command_only(&self) -> String { + if let Some(path) = &self.curr_path { + if !path.cmd.is_empty() { + path.exe.to_string_lossy().to_string() + } else { + String::from("") + } + } else { + String::from("") + } + } + + /// Get the status of the process + pub fn status(&self) -> String { + let mut state = 7; + for t in &self.curr_threads { + let s = match t.pth_run_state { + 1 => 1, // TH_STATE_RUNNING + 2 => 5, // TH_STATE_STOPPED + 3 => { + if t.pth_sleep_time > 20 { + 4 + } else { + 3 + } + } // TH_STATE_WAITING + 4 => 2, // TH_STATE_UNINTERRUPTIBLE + 5 => 6, // TH_STATE_HALTED + _ => 7, + }; + state = cmp::min(s, state); + } + let state = match state { + 0 => "", + 1 => "Running", + 2 => "Uninterruptible", + 3 => "Sleep", + 4 => "Waiting", + 5 => "Stopped", + 6 => "Halted", + _ => "?", + }; + state.to_string() + } + + /// CPU usage as a percent of total + pub fn cpu_usage(&self) -> f64 { + let curr_time = + self.curr_task.ptinfo.pti_total_user + self.curr_task.ptinfo.pti_total_system; + let prev_time = + self.prev_task.ptinfo.pti_total_user + self.prev_task.ptinfo.pti_total_system; + let usage_ms = (curr_time - prev_time) / 1000000u64; + let interval_ms = self.interval.as_secs() * 1000 + u64::from(self.interval.subsec_millis()); + usage_ms as f64 * 100.0 / interval_ms as f64 + } + + /// Memory size in number of bytes + pub fn mem_size(&self) -> u64 { + self.curr_task.ptinfo.pti_resident_size + } + + /// Virtual memory size in bytes + pub fn virtual_size(&self) -> u64 { + self.curr_task.ptinfo.pti_virtual_size + } +} diff --git a/crates/nu-system/src/main.rs b/crates/nu-system/src/main.rs new file mode 100644 index 0000000000..f0ad96ee68 --- /dev/null +++ b/crates/nu-system/src/main.rs @@ -0,0 +1,17 @@ +use std::time::Duration; + +fn main() { + for proc in nu_system::collect_proc(Duration::from_millis(100), false) { + // if proc.cpu_usage() > 0.1 { + println!( + "{} - {} - {} - {:.1} - {}M - {}M", + proc.pid(), + proc.name(), + proc.status(), + proc.cpu_usage(), + proc.mem_size() / (1024 * 1024), + proc.virtual_size() / (1024 * 1024), + ) + // } + } +} diff --git a/crates/nu-system/src/windows.rs b/crates/nu-system/src/windows.rs new file mode 100644 index 0000000000..70b85b3333 --- /dev/null +++ b/crates/nu-system/src/windows.rs @@ -0,0 +1,1070 @@ +// Attribution: a lot of this came from procs https://github.com/dalance/procs +// and sysinfo https://github.com/GuillaumeGomez/sysinfo + +use chrono::offset::TimeZone; +use chrono::{Local, NaiveDate}; +use libc::c_void; +use ntapi::ntpebteb::PEB; +use ntapi::ntpsapi::{ + NtQueryInformationProcess, ProcessBasicInformation, ProcessCommandLineInformation, + ProcessWow64Information, PROCESSINFOCLASS, PROCESS_BASIC_INFORMATION, +}; +use ntapi::ntrtl::{RtlGetVersion, PRTL_USER_PROCESS_PARAMETERS, RTL_USER_PROCESS_PARAMETERS}; +use ntapi::ntwow64::{PEB32, PRTL_USER_PROCESS_PARAMETERS32, RTL_USER_PROCESS_PARAMETERS32}; +use once_cell::sync::Lazy; +use std::cell::RefCell; +use std::collections::HashMap; +use std::ffi::OsString; +use std::mem::{size_of, zeroed, MaybeUninit}; +use std::os::windows::ffi::OsStringExt; +use std::path::PathBuf; +use std::ptr; +use std::ptr::null_mut; +use std::thread; +use std::time::{Duration, Instant}; +use winapi::shared::basetsd::SIZE_T; +use winapi::shared::minwindef::{DWORD, FALSE, FILETIME, LPVOID, MAX_PATH, TRUE, ULONG}; +use winapi::shared::ntdef::{NT_SUCCESS, UNICODE_STRING}; +use winapi::shared::ntstatus::{ + STATUS_BUFFER_OVERFLOW, STATUS_BUFFER_TOO_SMALL, STATUS_INFO_LENGTH_MISMATCH, +}; +use winapi::um::handleapi::CloseHandle; +use winapi::um::memoryapi::{ReadProcessMemory, VirtualQueryEx}; +use winapi::um::processthreadsapi::{ + GetCurrentProcess, GetPriorityClass, GetProcessTimes, OpenProcess, OpenProcessToken, +}; +use winapi::um::psapi::{ + GetModuleBaseNameW, GetProcessMemoryInfo, K32EnumProcesses, PROCESS_MEMORY_COUNTERS, + PROCESS_MEMORY_COUNTERS_EX, +}; +use winapi::um::securitybaseapi::{AdjustTokenPrivileges, GetTokenInformation}; +use winapi::um::tlhelp32::{ + CreateToolhelp32Snapshot, Process32First, Process32Next, PROCESSENTRY32, TH32CS_SNAPPROCESS, +}; +use winapi::um::winbase::{GetProcessIoCounters, LookupAccountSidW, LookupPrivilegeValueW}; +use winapi::um::winnt::{ + TokenGroups, TokenUser, HANDLE, IO_COUNTERS, MEMORY_BASIC_INFORMATION, + PROCESS_QUERY_INFORMATION, PROCESS_VM_READ, PSID, RTL_OSVERSIONINFOEXW, SE_DEBUG_NAME, + SE_PRIVILEGE_ENABLED, SID, TOKEN_ADJUST_PRIVILEGES, TOKEN_GROUPS, TOKEN_PRIVILEGES, + TOKEN_QUERY, TOKEN_USER, +}; + +pub struct ProcessInfo { + pub pid: i32, + pub command: String, + pub ppid: i32, + pub start_time: chrono::DateTime, + pub cpu_info: CpuInfo, + pub memory_info: MemoryInfo, + pub disk_info: DiskInfo, + pub user: SidName, + pub groups: Vec, + pub priority: u32, + pub thread: i32, + pub interval: Duration, + pub cmd: Vec, + pub environ: Vec, + pub cwd: PathBuf, +} + +#[derive(Default)] +pub struct MemoryInfo { + pub page_fault_count: u64, + pub peak_working_set_size: u64, + pub working_set_size: u64, + pub quota_peak_paged_pool_usage: u64, + pub quota_paged_pool_usage: u64, + pub quota_peak_non_paged_pool_usage: u64, + pub quota_non_paged_pool_usage: u64, + pub page_file_usage: u64, + pub peak_page_file_usage: u64, + pub private_usage: u64, +} + +#[derive(Default)] +pub struct DiskInfo { + pub prev_read: u64, + pub prev_write: u64, + pub curr_read: u64, + pub curr_write: u64, +} + +#[derive(Default)] +pub struct CpuInfo { + pub prev_sys: u64, + pub prev_user: u64, + pub curr_sys: u64, + pub curr_user: u64, +} + +#[cfg_attr(tarpaulin, skip)] +pub fn collect_proc(interval: Duration, _with_thread: bool) -> Vec { + let mut base_procs = Vec::new(); + let mut ret = Vec::new(); + + let _ = set_privilege(); + + for pid in get_pids() { + let handle = get_handle(pid); + + if let Some(handle) = handle { + let times = get_times(handle); + let io = get_io(handle); + + let time = Instant::now(); + + if let (Some((_, _, sys, user)), Some((read, write))) = (times, io) { + base_procs.push((pid, sys, user, read, write, time)); + } + } + } + + thread::sleep(interval); + + let (mut ppids, mut threads) = get_ppid_threads(); + + for (pid, prev_sys, prev_user, prev_read, prev_write, prev_time) in base_procs { + let ppid = ppids.remove(&pid); + let thread = threads.remove(&pid); + let handle = get_handle(pid); + + if let Some(handle) = handle { + let command = get_command(handle); + let memory_info = get_memory_info(handle); + let times = get_times(handle); + let io = get_io(handle); + + let start_time = if let Some((start, _, _, _)) = times { + let time = chrono::Duration::seconds(start as i64 / 10_000_000); + let base = NaiveDate::from_ymd(1600, 1, 1).and_hms(0, 0, 0); + let time = base + time; + Local.from_utc_datetime(&time) + } else { + Local.from_utc_datetime(&NaiveDate::from_ymd(1600, 1, 1).and_hms(0, 0, 0)) + }; + + let cpu_info = if let Some((_, _, curr_sys, curr_user)) = times { + Some(CpuInfo { + prev_sys, + prev_user, + curr_sys, + curr_user, + }) + } else { + None + }; + + let disk_info = if let Some((curr_read, curr_write)) = io { + Some(DiskInfo { + prev_read, + prev_write, + curr_read, + curr_write, + }) + } else { + None + }; + + let user = get_user(handle); + let groups = get_groups(handle); + + let priority = get_priority(handle); + + let curr_time = Instant::now(); + let interval = curr_time - prev_time; + + let mut all_ok = true; + all_ok &= command.is_some(); + all_ok &= cpu_info.is_some(); + all_ok &= memory_info.is_some(); + all_ok &= disk_info.is_some(); + all_ok &= user.is_some(); + all_ok &= groups.is_some(); + all_ok &= thread.is_some(); + + if all_ok { + // let process_params = unsafe { get_process_params(handle) }; + // match process_params { + // Ok((pp_cmd, pp_env, pp_cwd)) => { + // eprintln!( + // "cmd: {:?}, env: {:?}, cwd: {:?}", + // pp_cmd, + // "noop".to_string(), + // pp_cwd + // ); + // } + // Err(_) => {} + // } + let (proc_cmd, proc_env, proc_cwd) = match unsafe { get_process_params(handle) } { + Ok(pp) => (pp.0, pp.1, pp.2), + Err(_) => (vec![], vec![], PathBuf::new()), + }; + let command = command.unwrap_or_default(); + let ppid = ppid.unwrap_or(0); + let cpu_info = cpu_info.unwrap_or_default(); + let memory_info = memory_info.unwrap_or_default(); + let disk_info = disk_info.unwrap_or_default(); + let user = user.unwrap_or_else(|| SidName { + sid: vec![], + name: None, + domainname: None, + }); + let groups = groups.unwrap_or_else(Vec::new); + let thread = thread.unwrap_or_default(); + + let proc = ProcessInfo { + pid, + command, + ppid, + start_time, + cpu_info, + memory_info, + disk_info, + user, + groups, + priority, + thread, + interval, + cmd: proc_cmd, + environ: proc_env, + cwd: proc_cwd, + }; + + ret.push(proc); + } + + unsafe { + CloseHandle(handle); + } + } + } + + ret +} + +#[cfg_attr(tarpaulin, skip)] +fn set_privilege() -> bool { + unsafe { + let handle = GetCurrentProcess(); + let mut token: HANDLE = zeroed(); + let ret = OpenProcessToken(handle, TOKEN_ADJUST_PRIVILEGES, &mut token); + if ret == 0 { + return false; + } + + let mut tps: TOKEN_PRIVILEGES = zeroed(); + let se_debug_name: Vec = format!("{}\0", SE_DEBUG_NAME).encode_utf16().collect(); + tps.PrivilegeCount = 1; + let ret = LookupPrivilegeValueW( + ptr::null(), + se_debug_name.as_ptr(), + &mut tps.Privileges[0].Luid, + ); + if ret == 0 { + return false; + } + + tps.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + let ret = AdjustTokenPrivileges( + token, + FALSE, + &mut tps, + 0, + ptr::null::() as *mut TOKEN_PRIVILEGES, + ptr::null::() as *mut u32, + ); + if ret == 0 { + return false; + } + + true + } +} + +#[cfg_attr(tarpaulin, skip)] +fn get_pids() -> Vec { + let dword_size = size_of::(); + let mut pids: Vec = Vec::with_capacity(10192); + let mut cb_needed = 0; + + unsafe { + pids.set_len(10192); + let result = K32EnumProcesses( + pids.as_mut_ptr(), + (dword_size * pids.len()) as DWORD, + &mut cb_needed, + ); + if result == 0 { + return Vec::new(); + } + let pids_len = cb_needed / dword_size as DWORD; + pids.set_len(pids_len as usize); + } + + pids.iter().map(|x| *x as i32).collect() +} + +#[cfg_attr(tarpaulin, skip)] +fn get_ppid_threads() -> (HashMap, HashMap) { + let mut ppids = HashMap::new(); + let mut threads = HashMap::new(); + + unsafe { + let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + let mut entry: PROCESSENTRY32 = zeroed(); + entry.dwSize = size_of::() as u32; + let mut not_the_end = Process32First(snapshot, &mut entry); + + while not_the_end != 0 { + ppids.insert(entry.th32ProcessID as i32, entry.th32ParentProcessID as i32); + threads.insert(entry.th32ProcessID as i32, entry.cntThreads as i32); + not_the_end = Process32Next(snapshot, &mut entry); + } + + CloseHandle(snapshot); + } + + (ppids, threads) +} + +#[cfg_attr(tarpaulin, skip)] +fn get_handle(pid: i32) -> Option { + if pid == 0 { + return None; + } + + let handle = unsafe { + OpenProcess( + PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, + FALSE, + pid as DWORD, + ) + }; + + if handle.is_null() { + None + } else { + Some(handle) + } +} + +#[cfg_attr(tarpaulin, skip)] +fn get_times(handle: HANDLE) -> Option<(u64, u64, u64, u64)> { + unsafe { + let mut start: FILETIME = zeroed(); + let mut exit: FILETIME = zeroed(); + let mut sys: FILETIME = zeroed(); + let mut user: FILETIME = zeroed(); + + let ret = GetProcessTimes( + handle, + &mut start as *mut FILETIME, + &mut exit as *mut FILETIME, + &mut sys as *mut FILETIME, + &mut user as *mut FILETIME, + ); + + let start = u64::from(start.dwHighDateTime) << 32 | u64::from(start.dwLowDateTime); + let exit = u64::from(exit.dwHighDateTime) << 32 | u64::from(exit.dwLowDateTime); + let sys = u64::from(sys.dwHighDateTime) << 32 | u64::from(sys.dwLowDateTime); + let user = u64::from(user.dwHighDateTime) << 32 | u64::from(user.dwLowDateTime); + + if ret != 0 { + Some((start, exit, sys, user)) + } else { + None + } + } +} + +#[cfg_attr(tarpaulin, skip)] +fn get_memory_info(handle: HANDLE) -> Option { + unsafe { + let mut pmc: PROCESS_MEMORY_COUNTERS_EX = zeroed(); + let ret = GetProcessMemoryInfo( + handle, + &mut pmc as *mut PROCESS_MEMORY_COUNTERS_EX as *mut c_void + as *mut PROCESS_MEMORY_COUNTERS, + size_of::() as DWORD, + ); + + if ret != 0 { + let info = MemoryInfo { + page_fault_count: u64::from(pmc.PageFaultCount), + peak_working_set_size: pmc.PeakWorkingSetSize as u64, + working_set_size: pmc.WorkingSetSize as u64, + quota_peak_paged_pool_usage: pmc.QuotaPeakPagedPoolUsage as u64, + quota_paged_pool_usage: pmc.QuotaPagedPoolUsage as u64, + quota_peak_non_paged_pool_usage: pmc.QuotaPeakNonPagedPoolUsage as u64, + quota_non_paged_pool_usage: pmc.QuotaNonPagedPoolUsage as u64, + page_file_usage: pmc.PagefileUsage as u64, + peak_page_file_usage: pmc.PeakPagefileUsage as u64, + private_usage: pmc.PrivateUsage as u64, + }; + Some(info) + } else { + None + } + } +} + +#[cfg_attr(tarpaulin, skip)] +fn get_command(handle: HANDLE) -> Option { + unsafe { + let mut exe_buf = [0u16; MAX_PATH + 1]; + let h_mod = std::ptr::null_mut(); + + let ret = GetModuleBaseNameW( + handle, + h_mod as _, + exe_buf.as_mut_ptr(), + MAX_PATH as DWORD + 1, + ); + + let mut pos = 0; + for x in exe_buf.iter() { + if *x == 0 { + break; + } + pos += 1; + } + + if ret != 0 { + Some(String::from_utf16_lossy(&exe_buf[..pos])) + } else { + None + } + } +} + +trait RtlUserProcessParameters { + fn get_cmdline(&self, handle: HANDLE) -> Result, &'static str>; + fn get_cwd(&self, handle: HANDLE) -> Result, &'static str>; + fn get_environ(&self, handle: HANDLE) -> Result, &'static str>; +} + +macro_rules! impl_RtlUserProcessParameters { + ($t:ty) => { + impl RtlUserProcessParameters for $t { + fn get_cmdline(&self, handle: HANDLE) -> Result, &'static str> { + let ptr = self.CommandLine.Buffer; + let size = self.CommandLine.Length; + unsafe { get_process_data(handle, ptr as _, size as _) } + } + fn get_cwd(&self, handle: HANDLE) -> Result, &'static str> { + let ptr = self.CurrentDirectory.DosPath.Buffer; + let size = self.CurrentDirectory.DosPath.Length; + unsafe { get_process_data(handle, ptr as _, size as _) } + } + fn get_environ(&self, handle: HANDLE) -> Result, &'static str> { + let ptr = self.Environment; + unsafe { + let size = get_region_size(handle, ptr as LPVOID)?; + get_process_data(handle, ptr as _, size as _) + } + } + } + }; +} + +impl_RtlUserProcessParameters!(RTL_USER_PROCESS_PARAMETERS32); +impl_RtlUserProcessParameters!(RTL_USER_PROCESS_PARAMETERS); + +unsafe fn null_terminated_wchar_to_string(slice: &[u16]) -> String { + match slice.iter().position(|&x| x == 0) { + Some(pos) => OsString::from_wide(&slice[..pos]) + .to_string_lossy() + .into_owned(), + None => OsString::from_wide(slice).to_string_lossy().into_owned(), + } +} + +#[allow(clippy::uninit_vec)] +unsafe fn get_process_data( + handle: HANDLE, + ptr: LPVOID, + size: usize, +) -> Result, &'static str> { + let mut buffer: Vec = Vec::with_capacity(size / 2 + 1); + buffer.set_len(size / 2); + if ReadProcessMemory( + handle, + ptr as *mut _, + buffer.as_mut_ptr() as *mut _, + size, + std::ptr::null_mut(), + ) != TRUE + { + return Err("Unable to read process data"); + } + Ok(buffer) +} + +unsafe fn get_region_size(handle: HANDLE, ptr: LPVOID) -> Result { + let mut meminfo = MaybeUninit::::uninit(); + if VirtualQueryEx( + handle, + ptr, + meminfo.as_mut_ptr() as *mut _, + size_of::(), + ) == 0 + { + return Err("Unable to read process memory information"); + } + let meminfo = meminfo.assume_init(); + Ok((meminfo.RegionSize as isize - ptr.offset_from(meminfo.BaseAddress)) as usize) +} + +#[allow(clippy::uninit_vec)] +unsafe fn ph_query_process_variable_size( + process_handle: HANDLE, + process_information_class: PROCESSINFOCLASS, +) -> Option> { + let mut return_length = MaybeUninit::::uninit(); + + let mut status = NtQueryInformationProcess( + process_handle, + process_information_class, + std::ptr::null_mut(), + 0, + return_length.as_mut_ptr() as *mut _, + ); + + if status != STATUS_BUFFER_OVERFLOW + && status != STATUS_BUFFER_TOO_SMALL + && status != STATUS_INFO_LENGTH_MISMATCH + { + return None; + } + + let mut return_length = return_length.assume_init(); + let buf_len = (return_length as usize) / 2; + let mut buffer: Vec = Vec::with_capacity(buf_len + 1); + buffer.set_len(buf_len); + + status = NtQueryInformationProcess( + process_handle, + process_information_class, + buffer.as_mut_ptr() as *mut _, + return_length, + &mut return_length as *mut _, + ); + if !NT_SUCCESS(status) { + return None; + } + buffer.push(0); + Some(buffer) +} + +unsafe fn get_cmdline_from_buffer(buffer: *const u16) -> Vec { + // Get argc and argv from the command line + let mut argc = MaybeUninit::::uninit(); + let argv_p = winapi::um::shellapi::CommandLineToArgvW(buffer, argc.as_mut_ptr()); + if argv_p.is_null() { + return Vec::new(); + } + let argc = argc.assume_init(); + let argv = std::slice::from_raw_parts(argv_p, argc as usize); + + let mut res = Vec::new(); + for arg in argv { + let len = libc::wcslen(*arg); + let str_slice = std::slice::from_raw_parts(*arg, len); + res.push(String::from_utf16_lossy(str_slice)); + } + + winapi::um::winbase::LocalFree(argv_p as *mut _); + + res +} + +unsafe fn get_process_params( + handle: HANDLE, +) -> Result<(Vec, Vec, PathBuf), &'static str> { + if !cfg!(target_pointer_width = "64") { + return Err("Non 64 bit targets are not supported"); + } + + // First check if target process is running in wow64 compatibility emulator + let mut pwow32info = MaybeUninit::::uninit(); + let result = NtQueryInformationProcess( + handle, + ProcessWow64Information, + pwow32info.as_mut_ptr() as *mut _, + size_of::() as u32, + null_mut(), + ); + if !NT_SUCCESS(result) { + return Err("Unable to check WOW64 information about the process"); + } + let pwow32info = pwow32info.assume_init(); + + if pwow32info.is_null() { + // target is a 64 bit process + + let mut pbasicinfo = MaybeUninit::::uninit(); + let result = NtQueryInformationProcess( + handle, + ProcessBasicInformation, + pbasicinfo.as_mut_ptr() as *mut _, + size_of::() as u32, + null_mut(), + ); + if !NT_SUCCESS(result) { + return Err("Unable to get basic process information"); + } + let pinfo = pbasicinfo.assume_init(); + + let mut peb = MaybeUninit::::uninit(); + if ReadProcessMemory( + handle, + pinfo.PebBaseAddress as *mut _, + peb.as_mut_ptr() as *mut _, + size_of::() as SIZE_T, + std::ptr::null_mut(), + ) != TRUE + { + return Err("Unable to read process PEB"); + } + + let peb = peb.assume_init(); + + let mut proc_params = MaybeUninit::::uninit(); + if ReadProcessMemory( + handle, + peb.ProcessParameters as *mut PRTL_USER_PROCESS_PARAMETERS as *mut _, + proc_params.as_mut_ptr() as *mut _, + size_of::() as SIZE_T, + std::ptr::null_mut(), + ) != TRUE + { + return Err("Unable to read process parameters"); + } + + let proc_params = proc_params.assume_init(); + return Ok(( + get_cmd_line(&proc_params, handle), + get_proc_env(&proc_params, handle), + get_cwd(&proc_params, handle), + )); + } + // target is a 32 bit process in wow64 mode + + let mut peb32 = MaybeUninit::::uninit(); + if ReadProcessMemory( + handle, + pwow32info, + peb32.as_mut_ptr() as *mut _, + size_of::() as SIZE_T, + std::ptr::null_mut(), + ) != TRUE + { + return Err("Unable to read PEB32"); + } + let peb32 = peb32.assume_init(); + + let mut proc_params = MaybeUninit::::uninit(); + if ReadProcessMemory( + handle, + peb32.ProcessParameters as *mut PRTL_USER_PROCESS_PARAMETERS32 as *mut _, + proc_params.as_mut_ptr() as *mut _, + size_of::() as SIZE_T, + std::ptr::null_mut(), + ) != TRUE + { + return Err("Unable to read 32 bit process parameters"); + } + let proc_params = proc_params.assume_init(); + Ok(( + get_cmd_line(&proc_params, handle), + get_proc_env(&proc_params, handle), + get_cwd(&proc_params, handle), + )) +} + +static WINDOWS_8_1_OR_NEWER: Lazy = Lazy::new(|| { + let mut version_info: RTL_OSVERSIONINFOEXW = unsafe { MaybeUninit::zeroed().assume_init() }; + + version_info.dwOSVersionInfoSize = std::mem::size_of::() as u32; + if !NT_SUCCESS(unsafe { + RtlGetVersion(&mut version_info as *mut RTL_OSVERSIONINFOEXW as *mut _) + }) { + return true; + } + + // Windows 8.1 is 6.3 + version_info.dwMajorVersion > 6 + || version_info.dwMajorVersion == 6 && version_info.dwMinorVersion >= 3 +}); + +fn get_cmd_line(params: &T, handle: HANDLE) -> Vec { + if *WINDOWS_8_1_OR_NEWER { + get_cmd_line_new(handle) + } else { + get_cmd_line_old(params, handle) + } +} + +#[allow(clippy::cast_ptr_alignment)] +fn get_cmd_line_new(handle: HANDLE) -> Vec { + unsafe { + if let Some(buffer) = ph_query_process_variable_size(handle, ProcessCommandLineInformation) + { + let buffer = (*(buffer.as_ptr() as *const UNICODE_STRING)).Buffer; + + get_cmdline_from_buffer(buffer) + } else { + vec![] + } + } +} + +fn get_cmd_line_old(params: &T, handle: HANDLE) -> Vec { + match params.get_cmdline(handle) { + Ok(buffer) => unsafe { get_cmdline_from_buffer(buffer.as_ptr()) }, + Err(_e) => { + // sysinfo_debug!("get_cmd_line_old failed to get data: {}", _e); + Vec::new() + } + } +} + +fn get_proc_env(params: &T, handle: HANDLE) -> Vec { + match params.get_environ(handle) { + Ok(buffer) => { + let equals = "=" + .encode_utf16() + .next() + .expect("unable to get next utf16 value"); + let raw_env = buffer; + let mut result = Vec::new(); + let mut begin = 0; + while let Some(offset) = raw_env[begin..].iter().position(|&c| c == 0) { + let end = begin + offset; + if raw_env[begin..end].iter().any(|&c| c == equals) { + result.push( + OsString::from_wide(&raw_env[begin..end]) + .to_string_lossy() + .into_owned(), + ); + begin = end + 1; + } else { + break; + } + } + result + } + Err(_e) => { + // sysinfo_debug!("get_proc_env failed to get data: {}", _e); + Vec::new() + } + } +} + +fn get_cwd(params: &T, handle: HANDLE) -> PathBuf { + match params.get_cwd(handle) { + Ok(buffer) => unsafe { PathBuf::from(null_terminated_wchar_to_string(buffer.as_slice())) }, + Err(_e) => { + // sysinfo_debug!("get_cwd failed to get data: {}", _e); + PathBuf::new() + } + } +} + +#[cfg_attr(tarpaulin, skip)] +fn get_io(handle: HANDLE) -> Option<(u64, u64)> { + unsafe { + let mut io: IO_COUNTERS = zeroed(); + let ret = GetProcessIoCounters(handle, &mut io); + + if ret != 0 { + Some((io.ReadTransferCount, io.WriteTransferCount)) + } else { + None + } + } +} + +pub struct SidName { + pub sid: Vec, + pub name: Option, + pub domainname: Option, +} + +#[cfg_attr(tarpaulin, skip)] +fn get_user(handle: HANDLE) -> Option { + unsafe { + let mut token: HANDLE = zeroed(); + let ret = OpenProcessToken(handle, TOKEN_QUERY, &mut token); + + if ret == 0 { + return None; + } + + let mut cb_needed = 0; + let _ = GetTokenInformation( + token, + TokenUser, + ptr::null::() as *mut c_void, + 0, + &mut cb_needed, + ); + + let mut buf: Vec = Vec::with_capacity(cb_needed as usize); + + let ret = GetTokenInformation( + token, + TokenUser, + buf.as_mut_ptr() as *mut c_void, + cb_needed, + &mut cb_needed, + ); + buf.set_len(cb_needed as usize); + + if ret == 0 { + return None; + } + + #[allow(clippy::cast_ptr_alignment)] + let token_user = buf.as_ptr() as *const TOKEN_USER; + let psid = (*token_user).User.Sid; + + let sid = get_sid(psid); + let (name, domainname) = if let Some((x, y)) = get_name_cached(psid) { + (Some(x), Some(y)) + } else { + (None, None) + }; + + Some(SidName { + sid, + name, + domainname, + }) + } +} + +#[cfg_attr(tarpaulin, skip)] +fn get_groups(handle: HANDLE) -> Option> { + unsafe { + let mut token: HANDLE = zeroed(); + let ret = OpenProcessToken(handle, TOKEN_QUERY, &mut token); + + if ret == 0 { + return None; + } + + let mut cb_needed = 0; + let _ = GetTokenInformation( + token, + TokenGroups, + ptr::null::() as *mut c_void, + 0, + &mut cb_needed, + ); + + let mut buf: Vec = Vec::with_capacity(cb_needed as usize); + + let ret = GetTokenInformation( + token, + TokenGroups, + buf.as_mut_ptr() as *mut c_void, + cb_needed, + &mut cb_needed, + ); + buf.set_len(cb_needed as usize); + + if ret == 0 { + return None; + } + + #[allow(clippy::cast_ptr_alignment)] + let token_groups = buf.as_ptr() as *const TOKEN_GROUPS; + + let mut ret = Vec::new(); + let sa = (*token_groups).Groups.as_ptr(); + for i in 0..(*token_groups).GroupCount { + let psid = (*sa.offset(i as isize)).Sid; + let sid = get_sid(psid); + let (name, domainname) = if let Some((x, y)) = get_name_cached(psid) { + (Some(x), Some(y)) + } else { + (None, None) + }; + + let sid_name = SidName { + sid, + name, + domainname, + }; + ret.push(sid_name); + } + + Some(ret) + } +} + +#[cfg_attr(tarpaulin, skip)] +fn get_sid(psid: PSID) -> Vec { + unsafe { + let mut ret = Vec::new(); + let psid = psid as *const SID; + + let mut ia = 0; + ia |= u64::from((*psid).IdentifierAuthority.Value[0]) << 40; + ia |= u64::from((*psid).IdentifierAuthority.Value[1]) << 32; + ia |= u64::from((*psid).IdentifierAuthority.Value[2]) << 24; + ia |= u64::from((*psid).IdentifierAuthority.Value[3]) << 16; + ia |= u64::from((*psid).IdentifierAuthority.Value[4]) << 8; + ia |= u64::from((*psid).IdentifierAuthority.Value[5]); + + ret.push(u64::from((*psid).Revision)); + ret.push(ia); + let cnt = (*psid).SubAuthorityCount; + let sa = (*psid).SubAuthority.as_ptr(); + for i in 0..cnt { + ret.push(u64::from(*sa.offset(i as isize))); + } + + ret + } +} + +thread_local!( + pub static NAME_CACHE: RefCell>> = + RefCell::new(HashMap::new()); +); + +#[cfg_attr(tarpaulin, skip)] +fn get_name_cached(psid: PSID) -> Option<(String, String)> { + NAME_CACHE.with(|c| { + let mut c = c.borrow_mut(); + if let Some(x) = c.get(&psid) { + x.clone() + } else { + let x = get_name(psid); + c.insert(psid, x.clone()); + x + } + }) +} + +#[cfg_attr(tarpaulin, skip)] +fn get_name(psid: PSID) -> Option<(String, String)> { + unsafe { + let mut cc_name = 0; + let mut cc_domainname = 0; + let mut pe_use = 0; + let _ = LookupAccountSidW( + ptr::null::() as *mut u16, + psid, + ptr::null::() as *mut u16, + &mut cc_name, + ptr::null::() as *mut u16, + &mut cc_domainname, + &mut pe_use, + ); + + if cc_name == 0 || cc_domainname == 0 { + return None; + } + + let mut name: Vec = Vec::with_capacity(cc_name as usize); + let mut domainname: Vec = Vec::with_capacity(cc_domainname as usize); + name.set_len(cc_name as usize); + domainname.set_len(cc_domainname as usize); + let ret = LookupAccountSidW( + ptr::null::() as *mut u16, + psid, + name.as_mut_ptr() as *mut u16, + &mut cc_name, + domainname.as_mut_ptr() as *mut u16, + &mut cc_domainname, + &mut pe_use, + ); + + if ret == 0 { + return None; + } + + let name = from_wide_ptr(name.as_ptr()); + let domainname = from_wide_ptr(domainname.as_ptr()); + Some((name, domainname)) + } +} + +#[cfg_attr(tarpaulin, skip)] +fn from_wide_ptr(ptr: *const u16) -> String { + // use std::ffi::OsString; + // use std::os::windows::ffi::OsStringExt; + unsafe { + assert!(!ptr.is_null()); + let len = (0..std::isize::MAX) + .position(|i| *ptr.offset(i) == 0) + .unwrap_or_default(); + let slice = std::slice::from_raw_parts(ptr, len); + OsString::from_wide(slice).to_string_lossy().into_owned() + } +} + +#[cfg_attr(tarpaulin, skip)] +fn get_priority(handle: HANDLE) -> u32 { + unsafe { GetPriorityClass(handle) } +} + +impl ProcessInfo { + /// PID of process + pub fn pid(&self) -> i32 { + self.pid + } + + /// Name of command + pub fn name(&self) -> String { + // self.command() + // .split(' ') + // .collect::>() + // .first() + // .map(|x| x.to_string()) + // .unwrap_or_default() + self.command.clone() + } + + /// Full name of command, with arguments + pub fn command(&self) -> String { + // self.command.clone() + self.cmd.join(" ") + } + + pub fn environ(&self) -> Vec { + self.environ.clone() + } + + pub fn cwd(&self) -> String { + self.cwd.display().to_string() + } + + /// Get the status of the process + pub fn status(&self) -> String { + "unknown".to_string() + } + + /// CPU usage as a percent of total + pub fn cpu_usage(&self) -> f64 { + let curr_time = self.cpu_info.curr_sys + self.cpu_info.curr_user; + let prev_time = self.cpu_info.prev_sys + self.cpu_info.prev_user; + + let usage_ms = (curr_time - prev_time) / 10000u64; + let interval_ms = self.interval.as_secs() * 1000 + u64::from(self.interval.subsec_millis()); + usage_ms as f64 * 100.0 / interval_ms as f64 + } + + /// Memory size in number of bytes + pub fn mem_size(&self) -> u64 { + self.memory_info.working_set_size + } + + /// Virtual memory size in bytes + pub fn virtual_size(&self) -> u64 { + self.memory_info.private_usage + } +} diff --git a/crates/nu-table/Cargo.toml b/crates/nu-table/Cargo.toml index 1a6e2565a0..d3a198ca4b 100644 --- a/crates/nu-table/Cargo.toml +++ b/crates/nu-table/Cargo.toml @@ -1,10 +1,17 @@ [package] authors = ["The Nu Project Contributors"] description = "Nushell table printing" +<<<<<<< HEAD edition = "2018" license = "MIT" name = "nu-table" version = "0.43.0" +======= +edition = "2021" +license = "MIT" +name = "nu-table" +version = "0.36.0" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [[bin]] @@ -12,9 +19,20 @@ name = "table" path = "src/main.rs" [dependencies] +<<<<<<< HEAD atty = "0.2.14" nu-ansi-term = { version = "0.43.0", path="../nu-ansi-term" } regex = "1.4" strip-ansi-escapes = "0.1.1" unicode-width = "0.1.8" +======= +# nu-ansi-term = { path = "../nu-ansi-term" } +nu-ansi-term = "0.42.0" +nu-protocol = { path = "../nu-protocol"} +regex = "1.4" +unicode-width = "0.1.8" +strip-ansi-escapes = "0.1.1" +ansi-cut = "0.2.0" +atty = "0.2.14" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce diff --git a/crates/nu-table/src/main.rs b/crates/nu-table/src/main.rs index bb464ca069..ccc23a5ea2 100644 --- a/crates/nu-table/src/main.rs +++ b/crates/nu-table/src/main.rs @@ -1,3 +1,7 @@ +<<<<<<< HEAD +======= +use nu_protocol::Config; +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce use nu_table::{draw_table, StyledString, Table, TextStyle, Theme}; use std::collections::HashMap; @@ -25,6 +29,7 @@ fn main() { let table = Table::new(headers, vec![rows; 3], Theme::rounded()); // FIXME: Config isn't available from here so just put these here to compile let color_hm: HashMap = HashMap::new(); +<<<<<<< HEAD // Capture the table as a string let output_table = draw_table(&table, width, &color_hm); @@ -39,6 +44,14 @@ fn main() { println!("{}", output_table) } } +======= + // get the default config + let config = Config::default(); + // Capture the table as a string + let output_table = draw_table(&table, width, &color_hm, &config); + // Draw the table + println!("{}", output_table) +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } fn make_table_data() -> (Vec<&'static str>, Vec<&'static str>) { diff --git a/crates/nu-table/src/table.rs b/crates/nu-table/src/table.rs index 38f826552b..e64c201b31 100644 --- a/crates/nu-table/src/table.rs +++ b/crates/nu-table/src/table.rs @@ -1,5 +1,9 @@ use crate::wrap::{column_width, split_sublines, wrap, Alignment, Subline, WrappedCell}; use nu_ansi_term::{Color, Style}; +<<<<<<< HEAD +======= +use nu_protocol::{Config, FooterMode}; +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce use std::collections::HashMap; use std::fmt::Write; @@ -240,6 +244,13 @@ impl TextStyle { .bold(Some(true)) } +<<<<<<< HEAD +======= + pub fn default_field() -> TextStyle { + TextStyle::new().fg(Color::Green).bold(Some(true)) + } + +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce pub fn with_attributes(bo: bool, al: Alignment, co: Color) -> TextStyle { TextStyle::new().alignment(al).fg(co).bold(Some(bo)) } @@ -607,15 +618,26 @@ impl Table { } #[derive(Debug)] +<<<<<<< HEAD pub struct ProcessedTable<'a> { pub headers: Vec>, pub data: Vec>>, +======= +pub struct ProcessedTable { + pub headers: Vec, + pub data: Vec>, +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce pub theme: Theme, } #[derive(Debug)] +<<<<<<< HEAD pub struct ProcessedCell<'a> { pub contents: Vec>>, +======= +pub struct ProcessedCell { + pub contents: Vec>, +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce pub style: TextStyle, } @@ -625,6 +647,10 @@ pub struct WrappedTable { pub headers: Vec, pub data: Vec>, pub theme: Theme, +<<<<<<< HEAD +======= + pub footer: Vec, +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } impl WrappedTable { @@ -636,7 +662,11 @@ impl WrappedTable { let column_count = self.column_widths.len(); let mut output = String::new(); let sep_color = color_hm +<<<<<<< HEAD .get("separator_color") +======= + .get("separator") +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce .unwrap_or(&Style::default()) .to_owned(); @@ -685,6 +715,10 @@ impl WrappedTable { ); } } +<<<<<<< HEAD +======= + output.push('\n'); +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } SeparatorPosition::Middle => { for column in self.column_widths.iter().enumerate() { @@ -728,6 +762,10 @@ impl WrappedTable { .push_str(&sep_color.paint(&self.theme.center.to_string()).to_string()); } } +<<<<<<< HEAD +======= + output.push('\n'); +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } SeparatorPosition::Bottom => { for column in self.column_widths.iter().enumerate() { @@ -774,7 +812,10 @@ impl WrappedTable { } } } +<<<<<<< HEAD output.push('\n'); +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce output } @@ -784,7 +825,11 @@ impl WrappedTable { color_hm: &HashMap, ) -> String { let sep_color = color_hm +<<<<<<< HEAD .get("separator_color") +======= + .get("separator") +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce .unwrap_or(&Style::default()) .to_owned(); @@ -868,11 +913,16 @@ impl WrappedTable { break; } +<<<<<<< HEAD writeln!(&mut total_output, "{}", output).unwrap(); +======= + writeln!(&mut total_output, "{}", output).expect("writing should be done to buffer"); +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } total_output } +<<<<<<< HEAD fn print_table(&self, color_hm: &HashMap) -> String { let mut output = String::new(); @@ -880,15 +930,33 @@ impl WrappedTable { { let _ = nu_ansi_term::enable_ansi_support(); } +======= + fn print_table(&self, color_hm: &HashMap, config: &Config) -> String { + let mut output = String::new(); + + // TODO: This may be unnecessary after JTs changes. Let's remove it and see. + // #[cfg(windows)] + // { + // let _ = nu_ansi_term::enable_ansi_support(); + // } +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce if self.data.is_empty() { return output; } +<<<<<<< HEAD +======= + // The top border +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce if self.theme.print_top_border { output.push_str(&self.print_separator(SeparatorPosition::Top, color_hm)); } +<<<<<<< HEAD +======= + // The header +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce let skip_headers = (self.headers.len() == 2 && self.headers[1].max_width == 0) || (self.headers.len() == 1 && self.headers[0].max_width == 0); @@ -896,8 +964,13 @@ impl WrappedTable { output.push_str(&self.print_cell_contents(&self.headers, color_hm)); } +<<<<<<< HEAD let mut first_row = true; +======= + // The middle section + let mut first_row = true; +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce for row in &self.data { if !first_row { if self.theme.separate_rows { @@ -914,20 +987,59 @@ impl WrappedTable { output.push_str(&self.print_cell_contents(row, color_hm)); } +<<<<<<< HEAD +======= + match config.footer_mode { + FooterMode::Always => { + if self.theme.separate_header && !self.headers.is_empty() && !skip_headers { + output.push_str(&self.print_separator(SeparatorPosition::Middle, color_hm)); + } + + if !self.headers.is_empty() && !skip_headers { + output.push_str(&self.print_cell_contents(&self.footer, color_hm)); + } + } + FooterMode::RowCount(r) => { + if self.data.len() as u64 > r { + if self.theme.separate_header && !self.headers.is_empty() && !skip_headers { + output.push_str(&self.print_separator(SeparatorPosition::Middle, color_hm)); + } + + if !self.headers.is_empty() && !skip_headers { + output.push_str(&self.print_cell_contents(&self.footer, color_hm)); + } + } + } + _ => {} // Never and Auto aka auto get eaten and nothing happens + } + + // The table finish +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce if self.theme.print_bottom_border { output.push_str(&self.print_separator(SeparatorPosition::Bottom, color_hm)); } +<<<<<<< HEAD if atty::is(atty::Stream::Stdout) { // Draw the table with ansi colors output } else { +======= + // the atty is for when people do ls from vim, there should be no coloring there + if !config.use_ansi_coloring || !atty::is(atty::Stream::Stdout) { +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce // Draw the table without ansi colors if let Ok(bytes) = strip_ansi_escapes::strip(&output) { String::from_utf8_lossy(&bytes).to_string() } else { output } +<<<<<<< HEAD +======= + } else { + // Draw the table with ansi colors + output +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } } } @@ -1000,7 +1112,11 @@ pub fn maybe_truncate_columns(termwidth: usize, processed_table: &mut ProcessedT processed_table.headers.push(ProcessedCell { contents: vec![vec![Subline { +<<<<<<< HEAD subline: "...", +======= + subline: "...".to_string(), +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce width: 3, }]], style: TextStyle::basic_center(), @@ -1009,7 +1125,11 @@ pub fn maybe_truncate_columns(termwidth: usize, processed_table: &mut ProcessedT for entry in processed_table.data.iter_mut() { entry.push(ProcessedCell { contents: vec![vec![Subline { +<<<<<<< HEAD subline: "...", +======= + subline: "...".to_string(), +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce width: 3, }]], style: TextStyle::basic_center(), @@ -1018,10 +1138,22 @@ pub fn maybe_truncate_columns(termwidth: usize, processed_table: &mut ProcessedT } } +<<<<<<< HEAD pub fn draw_table(table: &Table, termwidth: usize, color_hm: &HashMap) -> String { // Remove the edges, if used let termwidth = if table.theme.print_left_border && table.theme.print_right_border { termwidth - 2 +======= +pub fn draw_table( + table: &Table, + termwidth: usize, + color_hm: &HashMap, + config: &Config, +) -> String { + // Remove the edges, if used + let termwidth = if table.theme.print_left_border && table.theme.print_right_border { + termwidth - 3 +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } else if table.theme.print_left_border || table.theme.print_right_border { termwidth - 1 } else { @@ -1078,7 +1210,11 @@ pub fn draw_table(table: &Table, termwidth: usize, color_hm: &HashMap>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } fn wrap_cells( @@ -1156,11 +1292,30 @@ fn wrap_cells( output_data.push(output_row); } +<<<<<<< HEAD +======= + let mut footer = vec![ + WrappedCell { + lines: vec![], + max_width: 0, + style: TextStyle { + ..Default::default() + }, + }; + output_headers.len() + ]; + footer.clone_from_slice(&output_headers[..]); + +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce WrappedTable { column_widths, headers: output_headers, data: output_data, theme: processed_table.theme, +<<<<<<< HEAD +======= + footer, +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } } diff --git a/crates/nu-table/src/wrap.rs b/crates/nu-table/src/wrap.rs index 2bdb97edaa..0938babff5 100644 --- a/crates/nu-table/src/wrap.rs +++ b/crates/nu-table/src/wrap.rs @@ -1,8 +1,16 @@ use crate::table::TextStyle; +<<<<<<< HEAD use nu_ansi_term::Style; use std::collections::HashMap; use std::{fmt::Display, iter::Iterator}; use unicode_width::UnicodeWidthStr; +======= +use ansi_cut::AnsiCut; +use nu_ansi_term::Style; +use std::collections::HashMap; +use std::{fmt::Display, iter::Iterator}; +use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce #[derive(Debug, Clone, Copy)] pub enum Alignment { @@ -12,24 +20,42 @@ pub enum Alignment { } #[derive(Debug)] +<<<<<<< HEAD pub struct Subline<'a> { pub subline: &'a str, +======= +pub struct Subline { + pub subline: String, +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce pub width: usize, } #[derive(Debug)] +<<<<<<< HEAD pub struct Line<'a> { pub sublines: Vec>, pub width: usize, } #[derive(Debug)] +======= +pub struct Line { + pub sublines: Vec, + pub width: usize, +} + +#[derive(Debug, Clone)] +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce pub struct WrappedLine { pub line: String, pub width: usize, } +<<<<<<< HEAD #[derive(Debug)] +======= +#[derive(Debug, Clone)] +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce pub struct WrappedCell { pub lines: Vec, pub max_width: usize, @@ -37,7 +63,11 @@ pub struct WrappedCell { pub style: TextStyle, } +<<<<<<< HEAD impl<'a> Display for Line<'a> { +======= +impl Display for Line { +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut first = true; for subline in &self.sublines { @@ -52,20 +82,76 @@ impl<'a> Display for Line<'a> { } } +<<<<<<< HEAD +======= +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 unicode_width_strip_ansi(astring: &str) -> usize { + let stripped_string: String = { + if let Ok(bytes) = strip_ansi_escapes::strip(astring) { + String::from_utf8_lossy(&bytes).to_string() + } else { + astring.to_string() + } + }; + + UnicodeWidthStr::width(&stripped_string[..]) +} + +// fn special_width(astring: &str) -> usize { +// // remove the zwj's '\u{200d}' +// // remove the fe0f's +// let stripped_string: String = { +// if let Ok(bytes) = strip_ansi_escapes::strip(astring) { +// String::from_utf8_lossy(&bytes).to_string() +// } else { +// astring.to_string() +// } +// }; + +// let no_zwj = stripped_string.replace('\u{200d}', ""); +// let no_fe0f = no_zwj.replace('\u{fe0f}', ""); +// UnicodeWidthStr::width(&no_fe0f[..]) +// } + +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce pub fn split_sublines(input: &str) -> Vec> { input .split_terminator('\n') .map(|line| { line.split_terminator(' ') .map(|x| Subline { +<<<<<<< HEAD subline: x, +======= + subline: x.to_string(), +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce width: { // We've tried UnicodeWidthStr::width(x), UnicodeSegmentation::graphemes(x, true).count() // and x.chars().count() with all types of combinations. Currently, it appears that // getting the max of char count and Unicode width seems to produce the best layout. // However, it's not perfect. +<<<<<<< HEAD let c = x.chars().count(); let u = UnicodeWidthStr::width(x); +======= + // let c = x.chars().count(); + // let u = UnicodeWidthStr::width(x); + // std::cmp::min(c, u) + + // let c = strip_ansi(x).chars().count(); + // let u = special_width(x); + // std::cmp::max(c, u) + + let c = strip_ansi(x).chars().count(); + let u = unicode_width_strip_ansi(x); +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce std::cmp::max(c, u) }, }) @@ -101,19 +187,31 @@ pub fn column_width(input: &[Vec]) -> usize { } fn split_word(cell_width: usize, word: &str) -> Vec { +<<<<<<< HEAD use unicode_width::UnicodeWidthChar; +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce let mut output = vec![]; let mut current_width = 0; let mut start_index = 0; let mut end_index; +<<<<<<< HEAD for c in word.char_indices() { +======= + let word_no_ansi = strip_ansi(word); + for c in word_no_ansi.char_indices() { +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce if let Some(width) = c.1.width() { end_index = c.0; if current_width + width > cell_width { output.push(Subline { +<<<<<<< HEAD subline: &word[start_index..end_index], +======= + subline: word.cut(start_index..end_index), +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce width: current_width, }); @@ -125,9 +223,15 @@ fn split_word(cell_width: usize, word: &str) -> Vec { } } +<<<<<<< HEAD if start_index != word.len() { output.push(Subline { subline: &word[start_index..], +======= + if start_index != word_no_ansi.len() { + output.push(Subline { + subline: word.cut(start_index..), +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce width: current_width, }); } @@ -135,9 +239,15 @@ fn split_word(cell_width: usize, word: &str) -> Vec { output } +<<<<<<< HEAD pub fn wrap<'a>( cell_width: usize, mut input: impl Iterator>, +======= +pub fn wrap( + cell_width: usize, + mut input: impl Iterator, +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce color_hm: &HashMap, re_leading: ®ex::Regex, re_trailing: ®ex::Regex, @@ -165,7 +275,11 @@ pub fn wrap<'a>( // If this is a really long single word, we need to split the word if current_line.len() == 1 && current_width > cell_width { max_width = cell_width; +<<<<<<< HEAD let sublines = split_word(cell_width, current_line[0].subline); +======= + let sublines = split_word(cell_width, ¤t_line[0].subline); +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce for subline in sublines { let width = subline.width; lines.push(Line { @@ -200,7 +314,11 @@ pub fn wrap<'a>( None => { if current_width > cell_width { // We need to break up the last word +<<<<<<< HEAD let sublines = split_word(cell_width, current_line[0].subline); +======= + let sublines = split_word(cell_width, ¤t_line[0].subline); +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce for subline in sublines { let width = subline.width; lines.push(Line { @@ -235,7 +353,11 @@ pub fn wrap<'a>( first = false; current_line_width = subline.width; } +<<<<<<< HEAD current_line.push_str(subline.subline); +======= + current_line.push_str(&subline.subline); +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } if current_line_width > current_max { diff --git a/crates/nu-table/.gitignore b/crates/nu-term-grid/.gitignore similarity index 100% rename from crates/nu-table/.gitignore rename to crates/nu-term-grid/.gitignore diff --git a/crates/nu-term-grid/Cargo.toml b/crates/nu-term-grid/Cargo.toml new file mode 100644 index 0000000000..c524284fe1 --- /dev/null +++ b/crates/nu-term-grid/Cargo.toml @@ -0,0 +1,16 @@ +[package] +authors = ["The Nu Project Contributors"] +description = "Nushell grid printing" +edition = "2021" +license = "MIT" +name = "nu-term-grid" +version = "0.36.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[[bin]] +name = "grid" +path = "src/main.rs" + +[dependencies] +unicode-width = "0.1.9" +strip-ansi-escapes = "0.1.1" diff --git a/crates/nu-term-grid/src/grid.rs b/crates/nu-term-grid/src/grid.rs new file mode 100644 index 0000000000..11c50c3b85 --- /dev/null +++ b/crates/nu-term-grid/src/grid.rs @@ -0,0 +1,768 @@ +// Thanks to https://github.com/ogham/rust-term-grid for making this available + +//! This library arranges textual data in a grid format suitable for +//! fixed-width fonts, using an algorithm to minimise the amount of space +//! needed. For example: +//! +//! ```rust +//! use nu_term_grid::grid::{Grid, GridOptions, Direction, Filling, Cell}; +//! +//! let mut grid = Grid::new(GridOptions { +//! filling: Filling::Spaces(1), +//! direction: Direction::LeftToRight, +//! }); +//! +//! for s in &["one", "two", "three", "four", "five", "six", "seven", +//! "eight", "nine", "ten", "eleven", "twelve"] +//! { +//! grid.add(Cell::from(*s)); +//! } +//! +//! println!("{}", grid.fit_into_width(24).unwrap()); +//! ``` +//! +//! Produces the following tabular result: +//! +//! ```text +//! one two three four +//! five six seven eight +//! nine ten eleven twelve +//! ``` +//! +//! +//! ## Creating a grid +//! +//! To add data to a grid, first create a new [`Grid`] value, and then add +//! cells to them with the `add` function. +//! +//! There are two options that must be specified in the [`GridOptions`] value +//! that dictate how the grid is formatted: +//! +//! - `filling`: what to put in between two columns — either a number of +//! spaces, or a text string; +//! - `direction`, which specifies whether the cells should go along +//! rows, or columns: +//! - `Direction::LeftToRight` starts them in the top left and +//! moves *rightwards*, going to the start of a new row after reaching the +//! final column; +//! - `Direction::TopToBottom` starts them in the top left and moves +//! *downwards*, going to the top of a new column after reaching the final +//! row. +//! +//! +//! ## Displaying a grid +//! +//! When display a grid, you can either specify the number of columns in advance, +//! or try to find the maximum number of columns that can fit in an area of a +//! given width. +//! +//! Splitting a series of cells into columns — or, in other words, starting a new +//! row every n cells — is achieved with the [`fit_into_columns`] function +//! on a `Grid` value. It takes as its argument the number of columns. +//! +//! Trying to fit as much data onto one screen as possible is the main use case +//! for specifying a maximum width instead. This is achieved with the +//! [`fit_into_width`] function. It takes the maximum allowed width, including +//! separators, as its argument. However, it returns an *optional* [`Display`] +//! value, depending on whether any of the cells actually had a width greater than +//! the maximum width! If this is the case, your best bet is to just output the +//! cells with one per line. +//! +//! +//! ## Cells and data +//! +//! Grids to not take `String`s or `&str`s — they take [`Cell`] values. +//! +//! A **Cell** is a struct containing an individual cell’s contents, as a string, +//! and its pre-computed length, which gets used when calculating a grid’s final +//! dimensions. Usually, you want the *Unicode width* of the string to be used for +//! this, so you can turn a `String` into a `Cell` with the `.into()` function. +//! +//! However, you may also want to supply your own width: when you already know the +//! width in advance, or when you want to change the measurement, such as skipping +//! over terminal control characters. For cases like these, the fields on the +//! `Cell` values are public, meaning you can construct your own instances as +//! necessary. +//! +//! [`Cell`]: ./struct.Cell.html +//! [`Display`]: ./struct.Display.html +//! [`Grid`]: ./struct.Grid.html +//! [`fit_into_columns`]: ./struct.Grid.html#method.fit_into_columns +//! [`fit_into_width`]: ./struct.Grid.html#method.fit_into_width +//! [`GridOptions`]: ./struct.GridOptions.html + +use std::cmp::max; +use std::fmt; +use std::iter::repeat; +use strip_ansi_escapes::strip; +use unicode_width::UnicodeWidthStr; + +fn unicode_width_strip_ansi(astring: &str) -> usize { + let stripped_string: String = { + if let Ok(bytes) = strip(astring) { + String::from_utf8_lossy(&bytes).to_string() + } else { + astring.to_string() + } + }; + + UnicodeWidthStr::width(&stripped_string[..]) +} + +/// Alignment indicate on which side the content should stick if some filling +/// is required. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Alignment { + /// The content will stick to the left. + Left, + + /// The content will stick to the right. + Right, +} + +/// A **Cell** is the combination of a string and its pre-computed length. +/// +/// The easiest way to create a Cell is just by using `string.into()`, which +/// uses the **unicode width** of the string (see the `unicode_width` crate). +/// However, the fields are public, if you wish to provide your own length. +#[derive(PartialEq, Debug, Clone)] +pub struct Cell { + /// The string to display when this cell gets rendered. + pub contents: String, + + /// The pre-computed length of the string. + pub width: Width, + + /// The side (left/right) to align the content if some filling is required. + pub alignment: Alignment, +} + +impl From for Cell { + fn from(string: String) -> Self { + Self { + width: unicode_width_strip_ansi(&*string), + contents: string, + alignment: Alignment::Left, + } + } +} + +impl<'a> From<&'a str> for Cell { + fn from(string: &'a str) -> Self { + Self { + width: unicode_width_strip_ansi(&*string), + contents: string.into(), + alignment: Alignment::Left, + } + } +} + +/// Direction cells should be written in — either across, or downwards. +#[derive(PartialEq, Debug, Copy, Clone)] +pub enum Direction { + /// Starts at the top left and moves rightwards, going back to the first + /// column for a new row, like a typewriter. + LeftToRight, + + /// Starts at the top left and moves downwards, going back to the first + /// row for a new column, like how `ls` lists files by default. + TopToBottom, +} + +/// The width of a cell, in columns. +pub type Width = usize; + +/// The text to put in between each pair of columns. +/// This does not include any spaces used when aligning cells. +#[derive(PartialEq, Debug)] +pub enum Filling { + /// A certain number of spaces should be used as the separator. + Spaces(Width), + + /// An arbitrary string. + /// `"|"` is a common choice. + Text(String), +} + +impl Filling { + fn width(&self) -> Width { + match *self { + Filling::Spaces(w) => w, + Filling::Text(ref t) => unicode_width_strip_ansi(&t[..]), + } + } +} + +/// The user-assignable options for a grid view that should be passed to +/// [`Grid::new()`](struct.Grid.html#method.new). +#[derive(PartialEq, Debug)] +pub struct GridOptions { + /// The direction that the cells should be written in — either + /// across, or downwards. + pub direction: Direction, + + /// The number of spaces to put in between each column of cells. + pub filling: Filling, +} + +#[derive(PartialEq, Debug)] +struct Dimensions { + /// The number of lines in the grid. + num_lines: Width, + + /// The width of each column in the grid. The length of this vector serves + /// as the number of columns. + widths: Vec, +} + +impl Dimensions { + fn total_width(&self, separator_width: Width) -> Width { + if self.widths.is_empty() { + 0 + } else { + let values = self.widths.iter().sum::(); + let separators = separator_width * (self.widths.len() - 1); + values + separators + } + } +} + +/// Everything needed to format the cells with the grid options. +/// +/// For more information, see the [`grid` crate documentation](index.html). +#[derive(PartialEq, Debug)] +pub struct Grid { + options: GridOptions, + cells: Vec, + widest_cell_length: Width, + width_sum: Width, + cell_count: usize, +} + +impl Grid { + /// Creates a new grid view with the given options. + pub fn new(options: GridOptions) -> Self { + let cells = Vec::new(); + Self { + options, + cells, + widest_cell_length: 0, + width_sum: 0, + cell_count: 0, + } + } + + /// Reserves space in the vector for the given number of additional cells + /// to be added. (See the `Vec::reserve` function.) + pub fn reserve(&mut self, additional: usize) { + self.cells.reserve(additional) + } + + /// Adds another cell onto the vector. + pub fn add(&mut self, cell: Cell) { + if cell.width > self.widest_cell_length { + self.widest_cell_length = cell.width; + } + self.width_sum += cell.width; + self.cell_count += 1; + self.cells.push(cell) + } + + /// Returns a displayable grid that’s been packed to fit into the given + /// width in the fewest number of rows. + /// + /// Returns `None` if any of the cells has a width greater than the + /// maximum width. + pub fn fit_into_width(&self, maximum_width: Width) -> Option> { + self.width_dimensions(maximum_width).map(|dims| Display { + grid: self, + dimensions: dims, + }) + } + + /// Returns a displayable grid with the given number of columns, and no + /// maximum width. + pub fn fit_into_columns(&self, num_columns: usize) -> Display<'_> { + Display { + grid: self, + dimensions: self.columns_dimensions(num_columns), + } + } + + fn columns_dimensions(&self, num_columns: usize) -> Dimensions { + let mut num_lines = self.cells.len() / num_columns; + if self.cells.len() % num_columns != 0 { + num_lines += 1; + } + + self.column_widths(num_lines, num_columns) + } + + fn column_widths(&self, num_lines: usize, num_columns: usize) -> Dimensions { + let mut widths: Vec = repeat(0).take(num_columns).collect(); + for (index, cell) in self.cells.iter().enumerate() { + let index = match self.options.direction { + Direction::LeftToRight => index % num_columns, + Direction::TopToBottom => index / num_lines, + }; + widths[index] = max(widths[index], cell.width); + } + + Dimensions { num_lines, widths } + } + + fn theoretical_max_num_lines(&self, maximum_width: usize) -> usize { + let mut theoretical_min_num_cols = 0; + let mut col_total_width_so_far = 0; + + let mut cells = self.cells.clone(); + cells.sort_unstable_by(|a, b| b.width.cmp(&a.width)); // Sort in reverse order + + for cell in &cells { + if cell.width + col_total_width_so_far <= maximum_width { + theoretical_min_num_cols += 1; + col_total_width_so_far += cell.width; + } else { + let mut theoretical_max_num_lines = self.cell_count / theoretical_min_num_cols; + if self.cell_count % theoretical_min_num_cols != 0 { + theoretical_max_num_lines += 1; + } + return theoretical_max_num_lines; + } + col_total_width_so_far += self.options.filling.width() + } + + // If we make it to this point, we have exhausted all cells before + // reaching the maximum width; the theoretical max number of lines + // needed to display all cells is 1. + 1 + } + + fn width_dimensions(&self, maximum_width: Width) -> Option { + if self.widest_cell_length > maximum_width { + // Largest cell is wider than maximum width; it is impossible to fit. + return None; + } + + if self.cell_count == 0 { + return Some(Dimensions { + num_lines: 0, + widths: Vec::new(), + }); + } + + if self.cell_count == 1 { + let the_cell = &self.cells[0]; + return Some(Dimensions { + num_lines: 1, + widths: vec![the_cell.width], + }); + } + + let theoretical_max_num_lines = self.theoretical_max_num_lines(maximum_width); + if theoretical_max_num_lines == 1 { + // This if—statement is neccesary for the function to work correctly + // for small inputs. + return Some(Dimensions { + num_lines: 1, + // I clone self.cells twice. Once here, and once in + // self.theoretical_max_num_lines. Perhaps not the best for + // performance? + widths: self + .cells + .clone() + .into_iter() + .map(|cell| cell.width) + .collect(), + }); + } + // Instead of numbers of columns, try to find the fewest number of *lines* + // that the output will fit in. + let mut smallest_dimensions_yet = None; + for num_lines in (1..=theoretical_max_num_lines).rev() { + // The number of columns is the number of cells divided by the number + // of lines, *rounded up*. + let mut num_columns = self.cell_count / num_lines; + if self.cell_count % num_lines != 0 { + num_columns += 1; + } + // Early abort: if there are so many columns that the width of the + // *column separators* is bigger than the width of the screen, then + // don’t even try to tabulate it. + // This is actually a necessary check, because the width is stored as + // a usize, and making it go negative makes it huge instead, but it + // also serves as a speed-up. + let total_separator_width = (num_columns - 1) * self.options.filling.width(); + if maximum_width < total_separator_width { + continue; + } + + // Remove the separator width from the available space. + let adjusted_width = maximum_width - total_separator_width; + let potential_dimensions = self.column_widths(num_lines, num_columns); + if potential_dimensions.widths.iter().sum::() < adjusted_width { + smallest_dimensions_yet = Some(potential_dimensions); + } else { + return smallest_dimensions_yet; + } + } + + None + } +} + +/// A displayable representation of a [`Grid`](struct.Grid.html). +/// +/// This type implements `Display`, so you can get the textual version +/// of the grid by calling `.to_string()`. +#[derive(PartialEq, Debug)] +pub struct Display<'grid> { + /// The grid to display. + grid: &'grid Grid, + + /// The pre-computed column widths for this grid. + dimensions: Dimensions, +} + +impl Display<'_> { + /// Returns how many columns this display takes up, based on the separator + /// width and the number and width of the columns. + pub fn width(&self) -> Width { + self.dimensions + .total_width(self.grid.options.filling.width()) + } + + /// Returns how many rows this display takes up. + pub fn row_count(&self) -> usize { + self.dimensions.num_lines + } + + /// Returns whether this display takes up as many columns as were allotted + /// to it. + /// + /// It’s possible to construct tables that don’t actually use up all the + /// columns that they could, such as when there are more columns than + /// cells! In this case, a column would have a width of zero. This just + /// checks for that. + pub fn is_complete(&self) -> bool { + self.dimensions.widths.iter().all(|&x| x > 0) + } +} + +impl fmt::Display for Display<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + for y in 0..self.dimensions.num_lines { + for x in 0..self.dimensions.widths.len() { + let num = match self.grid.options.direction { + Direction::LeftToRight => y * self.dimensions.widths.len() + x, + Direction::TopToBottom => y + self.dimensions.num_lines * x, + }; + + // Abandon a line mid-way through if that’s where the cells end + if num >= self.grid.cells.len() { + continue; + } + + let cell = &self.grid.cells[num]; + if x == self.dimensions.widths.len() - 1 { + match cell.alignment { + Alignment::Left => { + // The final column doesn’t need to have trailing spaces, + // as long as it’s left-aligned. + write!(f, "{}", cell.contents)?; + } + Alignment::Right => { + let extra_spaces = self.dimensions.widths[x] - cell.width; + write!( + f, + "{}", + pad_string(&cell.contents, extra_spaces, Alignment::Right) + )?; + } + } + } else { + assert!(self.dimensions.widths[x] >= cell.width); + match (&self.grid.options.filling, cell.alignment) { + (Filling::Spaces(n), Alignment::Left) => { + let extra_spaces = self.dimensions.widths[x] - cell.width + n; + write!( + f, + "{}", + pad_string(&cell.contents, extra_spaces, cell.alignment) + )?; + } + (Filling::Spaces(n), Alignment::Right) => { + let s = spaces(*n); + let extra_spaces = self.dimensions.widths[x] - cell.width; + write!( + f, + "{}{}", + pad_string(&cell.contents, extra_spaces, cell.alignment), + s + )?; + } + (Filling::Text(ref t), _) => { + let extra_spaces = self.dimensions.widths[x] - cell.width; + write!( + f, + "{}{}", + pad_string(&cell.contents, extra_spaces, cell.alignment), + t + )?; + } + } + } + } + + writeln!(f)?; + } + + Ok(()) + } +} + +/// Pad a string with the given number of spaces. +fn spaces(length: usize) -> String { + " ".repeat(length) +} + +/// Pad a string with the given alignment and number of spaces. +/// +/// This doesn’t take the width the string *should* be, rather the number +/// of spaces to add. +fn pad_string(string: &str, padding: usize, alignment: Alignment) -> String { + if alignment == Alignment::Left { + format!("{}{}", string, spaces(padding)) + } else { + format!("{}{}", spaces(padding), string) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn no_items() { + let grid = Grid::new(GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Spaces(2), + }); + + let display = grid.fit_into_width(40).unwrap(); + + assert_eq!(display.dimensions.num_lines, 0); + assert!(display.dimensions.widths.is_empty()); + + assert_eq!(display.width(), 0); + } + + #[test] + fn one_item() { + let mut grid = Grid::new(GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Spaces(2), + }); + + grid.add(Cell::from("1")); + + let display = grid.fit_into_width(40).unwrap(); + + assert_eq!(display.dimensions.num_lines, 1); + assert_eq!(display.dimensions.widths, vec![1]); + + assert_eq!(display.width(), 1); + } + + #[test] + fn one_item_exact_width() { + let mut grid = Grid::new(GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Spaces(2), + }); + + grid.add(Cell::from("1234567890")); + + let display = grid.fit_into_width(10).unwrap(); + + assert_eq!(display.dimensions.num_lines, 1); + assert_eq!(display.dimensions.widths, vec![10]); + + assert_eq!(display.width(), 10); + } + + #[test] + fn one_item_just_over() { + let mut grid = Grid::new(GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Spaces(2), + }); + + grid.add(Cell::from("1234567890!")); + + assert_eq!(grid.fit_into_width(10), None); + } + + #[test] + fn two_small_items() { + let mut grid = Grid::new(GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Spaces(2), + }); + + grid.add(Cell::from("1")); + grid.add(Cell::from("2")); + + let display = grid.fit_into_width(40).unwrap(); + + assert_eq!(display.dimensions.num_lines, 1); + assert_eq!(display.dimensions.widths, vec![1, 1]); + + assert_eq!(display.width(), 1 + 2 + 1); + } + + #[test] + fn two_medium_size_items() { + let mut grid = Grid::new(GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Spaces(2), + }); + + grid.add(Cell::from("hello there")); + grid.add(Cell::from("how are you today?")); + + let display = grid.fit_into_width(40).unwrap(); + + assert_eq!(display.dimensions.num_lines, 1); + assert_eq!(display.dimensions.widths, vec![11, 18]); + + assert_eq!(display.width(), 11 + 2 + 18); + } + + #[test] + fn two_big_items() { + let mut grid = Grid::new(GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Spaces(2), + }); + + grid.add(Cell::from( + "nuihuneihsoenhisenouiuteinhdauisdonhuisudoiosadiuohnteihaosdinhteuieudi", + )); + grid.add(Cell::from( + "oudisnuthasuouneohbueobaugceoduhbsauglcobeuhnaeouosbubaoecgueoubeohubeo", + )); + + assert_eq!(grid.fit_into_width(40), None); + } + + #[test] + fn that_example_from_earlier() { + let mut grid = Grid::new(GridOptions { + filling: Filling::Spaces(1), + direction: Direction::LeftToRight, + }); + + for s in &[ + "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", + "eleven", "twelve", + ] { + grid.add(Cell::from(*s)); + } + + let bits = "one two three four\nfive six seven eight\nnine ten eleven twelve\n"; + assert_eq!(grid.fit_into_width(24).unwrap().to_string(), bits); + assert_eq!(grid.fit_into_width(24).unwrap().row_count(), 3); + } + + #[test] + fn number_grid_with_pipe() { + let mut grid = Grid::new(GridOptions { + filling: Filling::Text("|".into()), + direction: Direction::LeftToRight, + }); + + for s in &[ + "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", + "eleven", "twelve", + ] { + grid.add(Cell::from(*s)); + } + + let bits = "one |two|three |four\nfive|six|seven |eight\nnine|ten|eleven|twelve\n"; + assert_eq!(grid.fit_into_width(24).unwrap().to_string(), bits); + assert_eq!(grid.fit_into_width(24).unwrap().row_count(), 3); + } + + #[test] + fn numbers_right() { + let mut grid = Grid::new(GridOptions { + filling: Filling::Spaces(1), + direction: Direction::LeftToRight, + }); + + for s in &[ + "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", + "eleven", "twelve", + ] { + let mut cell = Cell::from(*s); + cell.alignment = Alignment::Right; + grid.add(cell); + } + + let bits = " one two three four\nfive six seven eight\nnine ten eleven twelve\n"; + assert_eq!(grid.fit_into_width(24).unwrap().to_string(), bits); + assert_eq!(grid.fit_into_width(24).unwrap().row_count(), 3); + } + + #[test] + fn numbers_right_pipe() { + let mut grid = Grid::new(GridOptions { + filling: Filling::Text("|".into()), + direction: Direction::LeftToRight, + }); + + for s in &[ + "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", + "eleven", "twelve", + ] { + let mut cell = Cell::from(*s); + cell.alignment = Alignment::Right; + grid.add(cell); + } + + let bits = " one|two| three| four\nfive|six| seven| eight\nnine|ten|eleven|twelve\n"; + assert_eq!(grid.fit_into_width(24).unwrap().to_string(), bits); + assert_eq!(grid.fit_into_width(24).unwrap().row_count(), 3); + } + + #[test] + fn huge_separator() { + let mut grid = Grid::new(GridOptions { + filling: Filling::Spaces(100), + direction: Direction::LeftToRight, + }); + + grid.add("a".into()); + grid.add("b".into()); + + assert_eq!(grid.fit_into_width(99), None); + } + + #[test] + fn huge_yet_unused_separator() { + let mut grid = Grid::new(GridOptions { + filling: Filling::Spaces(100), + direction: Direction::LeftToRight, + }); + + grid.add("abcd".into()); + + let display = grid.fit_into_width(99).unwrap(); + + assert_eq!(display.dimensions.num_lines, 1); + assert_eq!(display.dimensions.widths, vec![4]); + + assert_eq!(display.width(), 4); + } +} diff --git a/crates/nu-term-grid/src/lib.rs b/crates/nu-term-grid/src/lib.rs new file mode 100644 index 0000000000..79b146593f --- /dev/null +++ b/crates/nu-term-grid/src/lib.rs @@ -0,0 +1,3 @@ +pub mod grid; + +pub use grid::Grid; diff --git a/crates/nu-term-grid/src/main.rs b/crates/nu-term-grid/src/main.rs new file mode 100644 index 0000000000..a9ef426790 --- /dev/null +++ b/crates/nu-term-grid/src/main.rs @@ -0,0 +1,30 @@ +use nu_term_grid::grid::{Alignment, Cell, Direction, Filling, Grid, GridOptions}; + +// This produces: +// +// 1 | 128 | 16384 | 2097152 | 268435456 | 34359738368 | 4398046511104 +// 2 | 256 | 32768 | 4194304 | 536870912 | 68719476736 | 8796093022208 +// 4 | 512 | 65536 | 8388608 | 1073741824 | 137438953472 | 17592186044416 +// 8 | 1024 | 131072 | 16777216 | 2147483648 | 274877906944 | 35184372088832 +// 16 | 2048 | 262144 | 33554432 | 4294967296 | 549755813888 | 70368744177664 +// 32 | 4096 | 524288 | 67108864 | 8589934592 | 1099511627776 | 140737488355328 +// 64 | 8192 | 1048576 | 134217728 | 17179869184 | 2199023255552 | + +fn main() { + let mut grid = Grid::new(GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Text(" | ".into()), + }); + + for i in 0..48 { + let mut cell = Cell::from(format!("{}", 2_isize.pow(i))); + cell.alignment = Alignment::Right; + grid.add(cell) + } + + if let Some(grid_display) = grid.fit_into_width(80) { + println!("{}", grid_display); + } else { + println!("Couldn't fit grid into 80 columns!"); + } +} diff --git a/crates/nu-test-support/Cargo.toml b/crates/nu-test-support/Cargo.toml index d53734b91e..6871226158 100644 --- a/crates/nu-test-support/Cargo.toml +++ b/crates/nu-test-support/Cargo.toml @@ -10,10 +10,15 @@ version = "0.43.0" doctest = false [dependencies] +<<<<<<< HEAD nu-errors = { version = "0.43.0", path="../nu-errors" } nu-path = { version = "0.43.0", path="../nu-path" } nu-protocol = { path="../nu-protocol", version = "0.43.0" } nu-source = { path="../nu-source", version = "0.43.0" } +======= +nu-path = { path="../nu-path" } +nu-protocol = { path="../nu-protocol" } +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce bigdecimal = { package = "bigdecimal", version = "0.3.0", features = ["serde"] } chrono = "0.4.19" diff --git a/crates/nu-test-support/src/commands.rs b/crates/nu-test-support/src/commands.rs index 18ec952641..569bd822b0 100644 --- a/crates/nu-test-support/src/commands.rs +++ b/crates/nu-test-support/src/commands.rs @@ -1,3 +1,4 @@ +<<<<<<< HEAD use nu_protocol::hir::{Expression, ExternalArgs, ExternalCommand, SpannedExpression}; use nu_source::{Span, SpannedItem, Tag}; @@ -44,3 +45,58 @@ impl ExternalBuilder { } } } +======= +// use nu_protocol::{ +// ast::{Expr, Expression}, +// Span, Spanned, Type, +// }; + +// pub struct ExternalBuilder { +// name: String, +// args: Vec, +// } + +// impl ExternalBuilder { +// pub fn for_name(name: &str) -> ExternalBuilder { +// ExternalBuilder { +// name: name.to_string(), +// args: vec![], +// } +// } + +// pub fn arg(&mut self, value: &str) -> &mut Self { +// self.args.push(value.to_string()); +// self +// } + +// pub fn build(&mut self) -> ExternalCommand { +// let mut path = crate::fs::binaries(); +// path.push(&self.name); + +// let name = Spanned { +// item: path.to_string_lossy().to_string(), +// span: Span::new(0, 0), +// }; + +// let args = self +// .args +// .iter() +// .map(|arg| Expression { +// expr: Expr::String(arg.to_string()), +// span: Span::new(0, 0), +// ty: Type::Unknown, +// custom_completion: None, +// }) +// .collect::>(); + +// ExternalCommand { +// name: name.to_string(), +// name_tag: Tag::unknown(), +// args: ExternalArgs { +// list: args, +// span: name.span, +// }, +// } +// } +// } +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce diff --git a/crates/nu-test-support/src/lib.rs b/crates/nu-test-support/src/lib.rs index 3e4988c4df..f75185e85d 100644 --- a/crates/nu-test-support/src/lib.rs +++ b/crates/nu-test-support/src/lib.rs @@ -2,7 +2,10 @@ pub mod commands; pub mod fs; pub mod macros; pub mod playground; +<<<<<<< HEAD pub mod value; +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce pub struct Outcome { pub out: String, @@ -57,7 +60,11 @@ mod tests { open los_tres_amigos.txt | from-csv | get rusty_luck +<<<<<<< HEAD | str to-int +======= + | into int +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce | math sum | echo "$it" "#, @@ -65,7 +72,11 @@ mod tests { assert_eq!( actual, +<<<<<<< HEAD r#"open los_tres_amigos.txt | from-csv | get rusty_luck | str to-int | math sum | echo "$it""# +======= + r#"open los_tres_amigos.txt | from-csv | get rusty_luck | into int | math sum | echo "$it""# +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce ); } } diff --git a/crates/nu-test-support/src/macros.rs b/crates/nu-test-support/src/macros.rs index 268148405a..02b30ae624 100644 --- a/crates/nu-test-support/src/macros.rs +++ b/crates/nu-test-support/src/macros.rs @@ -15,11 +15,16 @@ macro_rules! nu { }}; ($cwd:expr, $path:expr) => {{ +<<<<<<< HEAD +======= + pub use itertools::Itertools; +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce pub use std::error::Error; pub use std::io::prelude::*; pub use std::process::{Command, Stdio}; pub use $crate::NATIVE_PATH_ENV_VAR; +<<<<<<< HEAD let commands = &*format!( " cd \"{}\" @@ -31,6 +36,21 @@ macro_rules! nu { let test_bins = $crate::fs::binaries(); let test_bins = nu_path::canonicalize(&test_bins).unwrap_or_else(|e| { +======= + // let commands = &*format!( + // " + // cd \"{}\" + // {} + // exit", + // $crate::fs::in_directory($cwd), + // $crate::fs::DisplayPath::display_path(&$path) + // ); + + let test_bins = $crate::fs::binaries(); + + let cwd = std::env::current_dir().expect("Could not get current working directory."); + let test_bins = nu_path::canonicalize_with(&test_bins, cwd).unwrap_or_else(|e| { +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce panic!( "Couldn't canonicalize dummy binaries path {}: {:?}", test_bins.display(), @@ -41,6 +61,11 @@ macro_rules! nu { let mut paths = $crate::shell_os_paths(); paths.insert(0, test_bins); +<<<<<<< HEAD +======= + let path = $path.lines().collect::>().join("; "); + +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce let paths_joined = match std::env::join_paths(paths) { Ok(all) => all, Err(_) => panic!("Couldn't join paths for PATH var."), @@ -48,16 +73,27 @@ macro_rules! nu { let mut process = match Command::new($crate::fs::executable_path()) .env(NATIVE_PATH_ENV_VAR, paths_joined) +<<<<<<< HEAD .arg("--skip-plugins") .arg("--no-history") .arg("--config-file") .arg($crate::fs::DisplayPath::display_path(&$crate::fs::fixtures().join("playground/config/default.toml"))) .stdout(Stdio::piped()) .stdin(Stdio::piped()) +======= + // .arg("--skip-plugins") + // .arg("--no-history") + // .arg("--config-file") + // .arg($crate::fs::DisplayPath::display_path(&$crate::fs::fixtures().join("playground/config/default.toml"))) + .arg(format!("-c 'cd {}; {}'", $crate::fs::in_directory($cwd), $crate::fs::DisplayPath::display_path(&path))) + .stdout(Stdio::piped()) + // .stdin(Stdio::piped()) +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce .stderr(Stdio::piped()) .spawn() { Ok(child) => child, +<<<<<<< HEAD Err(why) => panic!("Can't run test {}", why.to_string()), }; @@ -65,6 +101,15 @@ macro_rules! nu { stdin .write_all(commands.as_bytes()) .expect("couldn't write to stdin"); +======= + Err(why) => panic!("Can't run test {:?} {}", $crate::fs::executable_path(), why.to_string()), + }; + + // let stdin = process.stdin.as_mut().expect("couldn't open stdin"); + // stdin + // .write_all(b"exit\n") + // .expect("couldn't write to stdin"); +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce let output = process .wait_with_output() diff --git a/crates/nu-test-support/src/playground/director.rs b/crates/nu-test-support/src/playground/director.rs index ba8b9922f4..75f82454cb 100644 --- a/crates/nu-test-support/src/playground/director.rs +++ b/crates/nu-test-support/src/playground/director.rs @@ -84,22 +84,45 @@ impl Director { impl Executable for Director { fn execute(&mut self) -> NuResult { +<<<<<<< HEAD use std::io::Write; +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce use std::process::Stdio; match self.executable() { Some(binary) => { +<<<<<<< HEAD let mut process = match binary .construct() .stdout(Stdio::piped()) .stdin(Stdio::piped()) .stderr(Stdio::piped()) +======= + let mut commands = String::new(); + if let Some(pipelines) = &self.pipeline { + for pipeline in pipelines { + if !commands.is_empty() { + commands.push_str("| "); + } + commands.push_str(&format!("{}\n", pipeline)); + } + } + + let process = match binary + .construct() + .stdout(Stdio::piped()) + // .stdin(Stdio::piped()) + .stderr(Stdio::piped()) + .arg(format!("-c '{}'", commands)) +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce .spawn() { Ok(child) => child, Err(why) => panic!("Can't run test {}", why), }; +<<<<<<< HEAD if let Some(pipelines) = &self.pipeline { let child = process.stdin.as_mut().expect("Failed to open stdin"); @@ -112,6 +135,8 @@ impl Executable for Director { child.write_all(b"exit\n").expect("Could not write to"); } +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce process .wait_with_output() .map_err(|_| { diff --git a/crates/nu-test-support/src/playground/play.rs b/crates/nu-test-support/src/playground/play.rs index e75f02b6b7..d9184bf679 100644 --- a/crates/nu-test-support/src/playground/play.rs +++ b/crates/nu-test-support/src/playground/play.rs @@ -78,7 +78,12 @@ impl<'a> Playground<'a> { std::fs::create_dir(PathBuf::from(&nuplay_dir)).expect("can not create directory"); let fixtures = fs::fixtures(); +<<<<<<< HEAD let fixtures = nu_path::canonicalize(fixtures.clone()).unwrap_or_else(|e| { +======= + let cwd = std::env::current_dir().expect("Could not get current working directory."); + let fixtures = nu_path::canonicalize_with(fixtures.clone(), cwd).unwrap_or_else(|e| { +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce panic!( "Couldn't canonicalize fixtures path {}: {:?}", fixtures.display(), @@ -97,6 +102,7 @@ impl<'a> Playground<'a> { let playground_root = playground.root.path(); +<<<<<<< HEAD let test = nu_path::canonicalize(playground_root.join(topic)).unwrap_or_else(|e| { panic!( "Couldn't canonicalize test path {}: {:?}", @@ -106,6 +112,20 @@ impl<'a> Playground<'a> { }); let root = nu_path::canonicalize(playground_root).unwrap_or_else(|e| { +======= + let cwd = std::env::current_dir().expect("Could not get current working directory."); + let test = + nu_path::canonicalize_with(playground_root.join(topic), cwd).unwrap_or_else(|e| { + panic!( + "Couldn't canonicalize test path {}: {:?}", + playground_root.join(topic).display(), + e + ) + }); + + let cwd = std::env::current_dir().expect("Could not get current working directory."); + let root = nu_path::canonicalize_with(playground_root, cwd).unwrap_or_else(|e| { +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce panic!( "Couldn't canonicalize tests root path {}: {:?}", playground_root.display(), diff --git a/crates/nu-test-support/src/playground/tests.rs b/crates/nu-test-support/src/playground/tests.rs index 4786cccc1d..9acfa44344 100644 --- a/crates/nu-test-support/src/playground/tests.rs +++ b/crates/nu-test-support/src/playground/tests.rs @@ -1,16 +1,23 @@ use crate::playground::Playground; use std::path::{Path, PathBuf}; +<<<<<<< HEAD use super::matchers::says; use hamcrest2::assert_that; use hamcrest2::prelude::*; fn path(p: &Path) -> PathBuf { nu_path::canonicalize(p) +======= +fn path(p: &Path) -> PathBuf { + let cwd = std::env::current_dir().expect("Could not get current working directory."); + nu_path::canonicalize_with(p, cwd) +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce .unwrap_or_else(|e| panic!("Couldn't canonicalize path {}: {:?}", p.display(), e)) } #[test] +<<<<<<< HEAD fn asserts_standard_out_expectation_from_nu_executable() { Playground::setup("topic", |_, nu| { assert_that!(nu.cococo("andres"), says().stdout("andres")); @@ -25,6 +32,8 @@ fn asserts_standard_out_expectation_from_nu_executable_pipeline_fed() { } #[test] +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce fn current_working_directory_in_sandbox_directory_created() { Playground::setup("topic", |dirs, nu| { let original_cwd = dirs.test(); diff --git a/crates/nu_plugin_example/Cargo.toml b/crates/nu_plugin_example/Cargo.toml new file mode 100644 index 0000000000..89cea551c8 --- /dev/null +++ b/crates/nu_plugin_example/Cargo.toml @@ -0,0 +1,11 @@ +[package] +authors = ["The Nu Project Contributors"] +description = "A version incrementer plugin for Nushell" +edition = "2021" +license = "MIT" +name = "nu_plugin_example" +version = "0.1.0" + +[dependencies] +nu-plugin = { path="../nu-plugin", version = "0.1.0" } +nu-protocol = { path="../nu-protocol", version = "0.1.0", features = ["plugin"]} diff --git a/crates/nu_plugin_example/README.md b/crates/nu_plugin_example/README.md new file mode 100644 index 0000000000..54df1602b8 --- /dev/null +++ b/crates/nu_plugin_example/README.md @@ -0,0 +1,4 @@ +# Plugin Example + +Crate with a simple example of the Plugin trait that needs to be implemented +in order to create a binary that can be registered into nushell declaration list diff --git a/crates/nu_plugin_example/src/example.rs b/crates/nu_plugin_example/src/example.rs new file mode 100644 index 0000000000..856f04b707 --- /dev/null +++ b/crates/nu_plugin_example/src/example.rs @@ -0,0 +1,94 @@ +use nu_plugin::{EvaluatedCall, LabeledError}; +use nu_protocol::Value; +pub struct Example; + +impl Example { + fn print_values( + &self, + index: u32, + call: &EvaluatedCall, + input: &Value, + ) -> Result<(), LabeledError> { + // Note. When debugging your plugin, you may want to print something to the console + // Use the eprintln macro to print your messages. Trying to print to stdout will + // cause a decoding error for your message + eprintln!("Calling test {} signature", index); + eprintln!("value received {:?}", input); + + // To extract the arguments from the Call object you can use the functions req, has_flag, + // opt, rest, and get_flag + // + // Note that plugin calls only accept simple arguments, this means that you can + // pass to the plug in Int and String. This should be improved when the plugin has + // the ability to call back to NuShell to extract more information + // Keep this in mind when designing your plugin signatures + let a: i64 = call.req(0)?; + let b: String = call.req(1)?; + let flag = call.has_flag("flag"); + let opt: Option = call.opt(2)?; + let named: Option = call.get_flag("named")?; + let rest: Vec = call.rest(3)?; + + eprintln!("Required values"); + eprintln!("a: {:}", a); + eprintln!("b: {:}", b); + eprintln!("flag: {:}", flag); + eprintln!("rest: {:?}", rest); + + match opt { + Some(v) => eprintln!("Found optional value opt: {:}", v), + None => eprintln!("No optional value found"), + } + + match named { + Some(v) => eprintln!("Named value: {:?}", v), + None => eprintln!("No named value found"), + } + + Ok(()) + } + + pub fn test1(&self, call: &EvaluatedCall, input: &Value) -> Result { + self.print_values(1, call, input)?; + + Ok(Value::Nothing { span: call.head }) + } + + pub fn test2(&self, call: &EvaluatedCall, input: &Value) -> Result { + self.print_values(2, call, input)?; + + let cols = vec!["one".to_string(), "two".to_string(), "three".to_string()]; + + let vals = (0..10i64) + .map(|i| { + let vals = (0..3) + .map(|v| Value::Int { + val: v * i, + span: call.head, + }) + .collect::>(); + + Value::Record { + cols: cols.clone(), + vals, + span: call.head, + } + }) + .collect::>(); + + Ok(Value::List { + vals, + span: call.head, + }) + } + + pub fn test3(&self, call: &EvaluatedCall, input: &Value) -> Result { + self.print_values(3, call, input)?; + + Err(LabeledError { + label: "ERROR from plugin".into(), + msg: "error message pointing to call head span".into(), + span: Some(call.head), + }) + } +} diff --git a/crates/nu_plugin_example/src/lib.rs b/crates/nu_plugin_example/src/lib.rs new file mode 100644 index 0000000000..995d09e8e1 --- /dev/null +++ b/crates/nu_plugin_example/src/lib.rs @@ -0,0 +1,4 @@ +mod example; +mod nu; + +pub use example::Example; diff --git a/crates/nu_plugin_example/src/main.rs b/crates/nu_plugin_example/src/main.rs new file mode 100644 index 0000000000..9542d75ad3 --- /dev/null +++ b/crates/nu_plugin_example/src/main.rs @@ -0,0 +1,30 @@ +use nu_plugin::{serve_plugin, CapnpSerializer}; +use nu_plugin_example::Example; + +fn main() { + // When defining your plugin, you can select the Serializer that could be + // used to encode and decode the messages. The available options are + // CapnpSerializer and JsonSerializer. Both are defined in the serializer + // folder in nu-plugin. + serve_plugin(&mut Example {}, CapnpSerializer {}) + + // Note + // When creating plugins in other languages one needs to consider how a plugin + // is added and used in nushell. + // The steps are: + // - The plugin is register. In this stage nushell calls the binary file of + // the plugin sending information using the encoded PluginCall::Signature object. + // Use this encoded data in your plugin to design the logic that will return + // the encoded signatures. + // Nushell is expecting and encoded PluginResponse::Signature with all the + // plugin signatures + // - When calling the plugin, nushell sends to the binary file the encoded + // PluginCall::CallInfo which has all the call information, such as the + // values of the arguments, the name of the signature called and the input + // from the pipeline. + // Use this data to design your plugin login and to create the value that + // will be sent to nushell + // Nushell expects an encoded PluginResponse::Value from the plugin + // - If an error needs to be sent back to nushell, one can encode PluginResponse::Error. + // This is a labeled error that nushell can format for pretty printing +} diff --git a/crates/nu_plugin_example/src/nu/mod.rs b/crates/nu_plugin_example/src/nu/mod.rs new file mode 100644 index 0000000000..7a87e3f840 --- /dev/null +++ b/crates/nu_plugin_example/src/nu/mod.rs @@ -0,0 +1,59 @@ +use crate::Example; +use nu_plugin::{EvaluatedCall, LabeledError, Plugin}; +use nu_protocol::{Category, Signature, SyntaxShape, Value}; + +impl Plugin for Example { + fn signature(&self) -> Vec { + // It is possible to declare multiple signature in a plugin + // Each signature will be converted to a command declaration once the + // plugin is registered to nushell + vec![ + Signature::build("nu-example-1") + .desc("Signature test 1 for plugin. Returns Value::Nothing") + .required("a", SyntaxShape::Int, "required integer value") + .required("b", SyntaxShape::String, "required string value") + .switch("flag", "a flag for the signature", Some('f')) + .optional("opt", SyntaxShape::Int, "Optional number") + .named("named", SyntaxShape::String, "named string", Some('n')) + .rest("rest", SyntaxShape::String, "rest value string") + .category(Category::Experimental), + Signature::build("nu-example-2") + .desc("Signature test 2 for plugin. Returns list of records") + .required("a", SyntaxShape::Int, "required integer value") + .required("b", SyntaxShape::String, "required string value") + .switch("flag", "a flag for the signature", Some('f')) + .optional("opt", SyntaxShape::Int, "Optional number") + .named("named", SyntaxShape::String, "named string", Some('n')) + .rest("rest", SyntaxShape::String, "rest value string") + .category(Category::Experimental), + Signature::build("nu-example-3") + .desc("Signature test 3 for plugin. Returns labeled error") + .required("a", SyntaxShape::Int, "required integer value") + .required("b", SyntaxShape::String, "required string value") + .switch("flag", "a flag for the signature", Some('f')) + .optional("opt", SyntaxShape::Int, "Optional number") + .named("named", SyntaxShape::String, "named string", Some('n')) + .rest("rest", SyntaxShape::String, "rest value string") + .category(Category::Experimental), + ] + } + + fn run( + &mut self, + name: &str, + call: &EvaluatedCall, + input: &Value, + ) -> Result { + // You can use the name to identify what plugin signature was called + match name { + "nu-example-1" => self.test1(call, input), + "nu-example-2" => self.test2(call, input), + "nu-example-3" => self.test3(call, input), + _ => Err(LabeledError { + label: "Plugin call with wrong name signature".into(), + msg: "the signature used to call the plugin does not match any name in the plugin signature vector".into(), + span: Some(call.head), + }), + } + } +} diff --git a/crates/nu_plugin_gstat/Cargo.toml b/crates/nu_plugin_gstat/Cargo.toml new file mode 100644 index 0000000000..bd862b0911 --- /dev/null +++ b/crates/nu_plugin_gstat/Cargo.toml @@ -0,0 +1,17 @@ +[package] +authors = ["The Nu Project Contributors"] +description = "A git status plugin for Nushell" +edition = "2021" +license = "MIT" +name = "nu_plugin_gstat" +version = "0.1.0" + +[lib] +doctest = false + +[dependencies] +nu-plugin = { path="../nu-plugin", version = "0.1.0" } +nu-protocol = { path="../nu-protocol", version = "0.1.0" } +nu-engine = { path="../nu-engine", version = "0.1.0" } + +git2 = "0.13.24" diff --git a/crates/nu_plugin_gstat/src/gstat.rs b/crates/nu_plugin_gstat/src/gstat.rs new file mode 100644 index 0000000000..7fc4c5a067 --- /dev/null +++ b/crates/nu_plugin_gstat/src/gstat.rs @@ -0,0 +1,578 @@ +use git2::{Branch, BranchType, DescribeOptions, Repository}; +use nu_plugin::LabeledError; +use nu_protocol::{Span, Spanned, Value}; +use std::fmt::Write; +use std::ops::BitAnd; +use std::path::PathBuf; + +// git status +// https://github.com/git/git/blob/9875c515535860450bafd1a177f64f0a478900fa/Documentation/git-status.txt + +// git status borrowed from here and tweaked +// https://github.com/glfmn/glitter/blob/master/lib/git.rs + +#[derive(Default)] +pub struct GStat; + +impl GStat { + pub fn new() -> Self { + Default::default() + } + + pub fn usage() -> &'static str { + "Usage: gstat" + } + + pub fn gstat( + &self, + value: &Value, + path: Option>, + span: &Span, + ) -> Result { + // use std::any::Any; + // eprintln!("input type: {:?} value: {:#?}", &value.type_id(), &value); + // eprintln!("path type: {:?} value: {:#?}", &path.type_id(), &path); + + // This is a flag to let us know if we're using the input value (value) + // or using the path specified (path) + let mut using_input_value = false; + + // let's get the input value as a string + let piped_value = match value.as_string() { + Ok(s) => { + using_input_value = true; + s + } + _ => String::new(), + }; + + // now let's get the path string + let mut a_path = match path { + Some(p) => { + // should we check for input and path? nah. + using_input_value = false; + p + } + None => Spanned { + item: ".".to_string(), + span: *span, + }, + }; + + // If there was no path specified and there is a piped in value, let's use the piped in value + if a_path.item == "." && piped_value.chars().count() > 0 { + a_path.item = piped_value; + } + + // This path has to exist + // TODO: If the path is relative, it will be expanded using `std::env::current_dir` and not + // the "PWD" environment variable. We would need a way to read the engine's environment + // variables here. + if !std::path::Path::new(&a_path.item).exists() { + return Err(LabeledError { + label: "error with path".to_string(), + msg: format!("path does not exist [{}]", &a_path.item), + span: if using_input_value { + Some(value.span().expect("unable to get value span")) + } else { + Some(a_path.span) + }, + }); + } + + let metadata = match std::fs::metadata(&a_path.item) { + Ok(md) => md, + Err(e) => { + return Err(LabeledError { + label: "error with metadata".to_string(), + msg: format!( + "unable to get metadata for [{}], error: {}", + &a_path.item, e + ), + span: if using_input_value { + Some(value.span().expect("unable to get value span")) + } else { + Some(a_path.span) + }, + }); + } + }; + + // This path has to be a directory + if !metadata.is_dir() { + return Err(LabeledError { + label: "error with directory".to_string(), + msg: format!("path is not a directory [{}]", &a_path.item), + span: if using_input_value { + Some(value.span().expect("unable to get value span")) + } else { + Some(a_path.span) + }, + }); + } + + let repo_path = match PathBuf::from(&a_path.item).canonicalize() { + Ok(p) => p, + Err(e) => { + return Err(LabeledError { + label: format!("error canonicalizing [{}]", a_path.item), + msg: e.to_string(), + span: if using_input_value { + Some(value.span().expect("unable to get value span")) + } else { + Some(a_path.span) + }, + }); + } + }; + + let (stats, repo) = if let Ok(mut repo) = Repository::discover(repo_path) { + (Stats::new(&mut repo), repo) + } else { + return Ok(self.create_empty_git_status(span)); + }; + + let repo_name = repo + .path() + .parent() + .and_then(|p| p.file_name()) + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or_else(|| "".to_string()); + + let mut desc_opts = DescribeOptions::new(); + desc_opts.describe_tags(); + + let tag = if let Ok(Ok(s)) = repo.describe(&desc_opts).map(|d| d.format(None)) { + s + } else { + "no_tag".to_string() + }; + + let mut cols = vec![]; + let mut vals = vec![]; + + cols.push("idx_added_staged".into()); + vals.push(Value::Int { + val: stats.idx_added_staged as i64, + span: *span, + }); + cols.push("idx_modified_staged".into()); + vals.push(Value::Int { + val: stats.idx_modified_staged as i64, + span: *span, + }); + cols.push("idx_deleted_staged".into()); + vals.push(Value::Int { + val: stats.idx_deleted_staged as i64, + span: *span, + }); + cols.push("idx_renamed".into()); + vals.push(Value::Int { + val: stats.idx_renamed as i64, + span: *span, + }); + cols.push("idx_type_changed".into()); + vals.push(Value::Int { + val: stats.idx_type_changed as i64, + span: *span, + }); + cols.push("wt_untracked".into()); + vals.push(Value::Int { + val: stats.wt_untracked as i64, + span: *span, + }); + cols.push("wt_modified".into()); + vals.push(Value::Int { + val: stats.wt_modified as i64, + span: *span, + }); + cols.push("wt_deleted".into()); + vals.push(Value::Int { + val: stats.wt_deleted as i64, + span: *span, + }); + cols.push("wt_type_changed".into()); + vals.push(Value::Int { + val: stats.wt_type_changed as i64, + span: *span, + }); + cols.push("wt_renamed".into()); + vals.push(Value::Int { + val: stats.wt_renamed as i64, + span: *span, + }); + cols.push("ignored".into()); + vals.push(Value::Int { + val: stats.ignored as i64, + span: *span, + }); + cols.push("conflicts".into()); + vals.push(Value::Int { + val: stats.conflicts as i64, + span: *span, + }); + cols.push("ahead".into()); + vals.push(Value::Int { + val: stats.ahead as i64, + span: *span, + }); + cols.push("behind".into()); + vals.push(Value::Int { + val: stats.behind as i64, + span: *span, + }); + cols.push("stashes".into()); + vals.push(Value::Int { + val: stats.stashes as i64, + span: *span, + }); + cols.push("repo_name".into()); + vals.push(Value::String { + val: repo_name, + span: *span, + }); + cols.push("tag".into()); + vals.push(Value::String { + val: tag, + span: *span, + }); + cols.push("branch".into()); + vals.push(Value::String { + val: stats.branch, + span: *span, + }); + cols.push("remote".into()); + vals.push(Value::String { + val: stats.remote, + span: *span, + }); + + // Leave this in case we want to turn it into a table instead of a list + // Ok(Value::List { + // vals: vec![Value::Record { + // cols, + // vals, + // span: *span, + // }], + // span: *span, + // }) + + Ok(Value::Record { + cols, + vals, + span: *span, + }) + } + + fn create_empty_git_status(&self, span: &Span) -> Value { + let mut cols = vec![]; + let mut vals = vec![]; + + cols.push("idx_added_staged".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("idx_modified_staged".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("idx_deleted_staged".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("idx_renamed".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("idx_type_changed".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("wt_untracked".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("wt_modified".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("wt_deleted".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("wt_type_changed".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("wt_renamed".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("ignored".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("conflicts".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("ahead".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("behind".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("stashes".into()); + vals.push(Value::Int { + val: -1, + span: *span, + }); + cols.push("repo_name".into()); + vals.push(Value::String { + val: "no_repository".to_string(), + span: *span, + }); + cols.push("tag".into()); + vals.push(Value::String { + val: "no_tag".to_string(), + span: *span, + }); + cols.push("branch".into()); + vals.push(Value::String { + val: "no_branch".to_string(), + span: *span, + }); + cols.push("remote".into()); + vals.push(Value::String { + val: "no_remote".to_string(), + span: *span, + }); + + Value::Record { + cols, + vals, + span: *span, + } + } +} + +/// Stats which the interpreter uses to populate the gist expression +#[derive(Debug, PartialEq, Eq, Default, Clone)] +pub struct Stats { + /// Number of files to be added + pub idx_added_staged: u16, + /// Number of staged changes to files + pub idx_modified_staged: u16, + /// Number of staged deletions + pub idx_deleted_staged: u16, + /// Number of renamed files + pub idx_renamed: u16, + /// Index file type change + pub idx_type_changed: u16, + + /// Number of untracked files which are new to the repository + pub wt_untracked: u16, + /// Number of modified files which have not yet been staged + pub wt_modified: u16, + /// Number of deleted files + pub wt_deleted: u16, + /// Working tree file type change + pub wt_type_changed: u16, + /// Working tree renamed + pub wt_renamed: u16, + + // Ignored files + pub ignored: u16, + /// Number of unresolved conflicts in the repository + pub conflicts: u16, + + /// Number of commits ahead of the upstream branch + pub ahead: u16, + /// Number of commits behind the upstream branch + pub behind: u16, + /// Number of stashes on the current branch + pub stashes: u16, + /// The branch name or other stats of the HEAD pointer + pub branch: String, + /// The of the upstream branch + pub remote: String, +} + +impl Stats { + /// Populate stats with the status of the given repository + pub fn new(repo: &mut Repository) -> Stats { + let mut st: Stats = Default::default(); + + st.read_branch(repo); + + let mut opts = git2::StatusOptions::new(); + + opts.include_untracked(true) + .recurse_untracked_dirs(true) + .renames_head_to_index(true); + + if let Ok(statuses) = repo.statuses(Some(&mut opts)) { + for status in statuses.iter() { + let flags = status.status(); + + if check(flags, git2::Status::INDEX_NEW) { + st.idx_added_staged += 1; + } + if check(flags, git2::Status::INDEX_MODIFIED) { + st.idx_modified_staged += 1; + } + if check(flags, git2::Status::INDEX_DELETED) { + st.idx_deleted_staged += 1; + } + if check(flags, git2::Status::INDEX_RENAMED) { + st.idx_renamed += 1; + } + if check(flags, git2::Status::INDEX_TYPECHANGE) { + st.idx_type_changed += 1; + } + + if check(flags, git2::Status::WT_NEW) { + st.wt_untracked += 1; + } + if check(flags, git2::Status::WT_MODIFIED) { + st.wt_modified += 1; + } + if check(flags, git2::Status::WT_DELETED) { + st.wt_deleted += 1; + } + if check(flags, git2::Status::WT_TYPECHANGE) { + st.wt_type_changed += 1; + } + if check(flags, git2::Status::WT_RENAMED) { + st.wt_renamed += 1; + } + + if check(flags, git2::Status::IGNORED) { + st.ignored += 1; + } + if check(flags, git2::Status::CONFLICTED) { + st.conflicts += 1; + } + } + } + + let _ = repo.stash_foreach(|_, &_, &_| { + st.stashes += 1; + true + }); + + st + } + + /// Read the branch-name of the repository + /// + /// If in detached head, grab the first few characters of the commit ID if possible, otherwise + /// simply provide HEAD as the branch name. This is to mimic the behaviour of `git status`. + fn read_branch(&mut self, repo: &Repository) { + self.branch = match repo.head() { + Ok(head) => { + if let Some(name) = head.shorthand() { + // try to use first 8 characters or so of the ID in detached HEAD + if name == "HEAD" { + if let Ok(commit) = head.peel_to_commit() { + let mut id = String::new(); + for byte in &commit.id().as_bytes()[..4] { + write!(&mut id, "{:x}", byte).unwrap(); + } + id + } else { + "HEAD".to_string() + } + // Grab the branch from the reference + } else { + let branch = name.to_string(); + // Since we have a branch name, look for the name of the upstream branch + self.read_upstream_name(repo, &branch); + branch + } + } else { + "HEAD".to_string() + } + } + Err(ref err) if err.code() == git2::ErrorCode::BareRepo => "master".to_string(), + Err(_) if repo.is_empty().unwrap_or(false) => "master".to_string(), + Err(_) => "HEAD".to_string(), + }; + } + + /// Read name of the upstream branch + fn read_upstream_name(&mut self, repo: &Repository, branch: &str) { + // First grab branch from the name + self.remote = match repo.find_branch(branch, BranchType::Local) { + Ok(branch) => { + // Grab the upstream from the branch + match branch.upstream() { + // Grab the name of the upstream if it's valid UTF-8 + Ok(upstream) => { + // While we have the upstream branch, traverse the graph and count + // ahead-behind commits. + self.read_ahead_behind(repo, &branch, &upstream); + + match upstream.name() { + Ok(Some(name)) => name.to_string(), + _ => String::new(), + } + } + _ => String::new(), + } + } + _ => String::new(), + }; + } + + /// Read ahead-behind information between the local and upstream branches + fn read_ahead_behind(&mut self, repo: &Repository, local: &Branch, upstream: &Branch) { + if let (Some(local), Some(upstream)) = (local.get().target(), upstream.get().target()) { + if let Ok((ahead, behind)) = repo.graph_ahead_behind(local, upstream) { + self.ahead = ahead as u16; + self.behind = behind as u16; + } + } + } +} + +// impl AddAssign for Stats { +// fn add_assign(&mut self, rhs: Self) { +// self.untracked += rhs.untracked; +// self.added_staged += rhs.added_staged; +// self.modified += rhs.modified; +// self.modified_staged += rhs.modified_staged; +// self.renamed += rhs.renamed; +// self.deleted += rhs.deleted; +// self.deleted_staged += rhs.deleted_staged; +// self.ahead += rhs.ahead; +// self.behind += rhs.behind; +// self.conflicts += rhs.conflicts; +// self.stashes += rhs.stashes; +// } +// } + +/// Check the bits of a flag against the value to see if they are set +#[inline] +fn check(val: B, flag: B) -> bool +where + B: BitAnd + PartialEq + Copy, +{ + val & flag == flag +} diff --git a/crates/nu_plugin_gstat/src/lib.rs b/crates/nu_plugin_gstat/src/lib.rs new file mode 100644 index 0000000000..c13f882478 --- /dev/null +++ b/crates/nu_plugin_gstat/src/lib.rs @@ -0,0 +1,4 @@ +mod gstat; +mod nu; + +pub use gstat::GStat; diff --git a/crates/nu_plugin_gstat/src/main.rs b/crates/nu_plugin_gstat/src/main.rs new file mode 100644 index 0000000000..b8fbc0e55e --- /dev/null +++ b/crates/nu_plugin_gstat/src/main.rs @@ -0,0 +1,6 @@ +use nu_plugin::{serve_plugin, CapnpSerializer}; +use nu_plugin_gstat::GStat; + +fn main() { + serve_plugin(&mut GStat::new(), CapnpSerializer {}) +} diff --git a/crates/nu_plugin_gstat/src/nu/mod.rs b/crates/nu_plugin_gstat/src/nu/mod.rs new file mode 100644 index 0000000000..fd626d7109 --- /dev/null +++ b/crates/nu_plugin_gstat/src/nu/mod.rs @@ -0,0 +1,27 @@ +use crate::GStat; +use nu_plugin::{EvaluatedCall, LabeledError, Plugin}; +use nu_protocol::{Category, Signature, Spanned, SyntaxShape, Value}; + +impl Plugin for GStat { + fn signature(&self) -> Vec { + vec![Signature::build("gstat") + .desc("Get the git status of a repo") + .optional("path", SyntaxShape::String, "path to repo") + .category(Category::Custom("Prompt".to_string()))] + } + + fn run( + &mut self, + name: &str, + call: &EvaluatedCall, + input: &Value, + ) -> Result { + if name != "gstat" { + return Ok(Value::Nothing { span: call.head }); + } + + let repo_path: Option> = call.opt(0)?; + // eprintln!("input value: {:#?}", &input); + self.gstat(input, repo_path, &call.head) + } +} diff --git a/crates/nu_plugin_inc/Cargo.toml b/crates/nu_plugin_inc/Cargo.toml index 7f4c05dec7..b2dcbf24ff 100644 --- a/crates/nu_plugin_inc/Cargo.toml +++ b/crates/nu_plugin_inc/Cargo.toml @@ -1,15 +1,23 @@ [package] authors = ["The Nu Project Contributors"] description = "A version incrementer plugin for Nushell" +<<<<<<< HEAD edition = "2018" license = "MIT" name = "nu_plugin_inc" version = "0.43.0" +======= +edition = "2021" +license = "MIT" +name = "nu_plugin_inc" +version = "0.1.0" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce [lib] doctest = false [dependencies] +<<<<<<< HEAD nu-errors = { path="../nu-errors", version = "0.43.0" } nu-plugin = { path="../nu-plugin", version = "0.43.0" } nu-protocol = { path="../nu-protocol", version = "0.43.0" } @@ -20,3 +28,9 @@ nu-value-ext = { path="../nu-value-ext", version = "0.43.0" } semver = "0.11.0" [build-dependencies] +======= +nu-plugin = { path="../nu-plugin", version = "0.1.0" } +nu-protocol = { path="../nu-protocol", version = "0.1.0", features = ["plugin"]} + +semver = "0.11.0" +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce diff --git a/crates/nu_plugin_inc/src/inc.rs b/crates/nu_plugin_inc/src/inc.rs index 0b6f322fa5..e648caa024 100644 --- a/crates/nu_plugin_inc/src/inc.rs +++ b/crates/nu_plugin_inc/src/inc.rs @@ -1,7 +1,12 @@ +<<<<<<< HEAD use nu_errors::ShellError; use nu_protocol::{did_you_mean, ColumnPath, Primitive, ShellTypeName, UntaggedValue, Value}; use nu_source::{span_for_spanned_list, HasSpan, SpannedItem, Tagged}; use nu_value_ext::{get_data_by_column_path, ValueExt}; +======= +use nu_plugin::LabeledError; +use nu_protocol::{Span, Value}; +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce #[derive(Debug, Eq, PartialEq)] pub enum Action { @@ -18,7 +23,10 @@ pub enum SemVerAction { #[derive(Default)] pub struct Inc { +<<<<<<< HEAD pub field: Option>, +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce pub error: Option, pub action: Option, } @@ -28,12 +36,25 @@ impl Inc { Default::default() } +<<<<<<< HEAD fn apply(&self, input: &str) -> UntaggedValue { +======= + fn apply(&self, input: &str, head: Span) -> Value { +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce match &self.action { Some(Action::SemVerAction(act_on)) => { let mut ver = match semver::Version::parse(input) { Ok(parsed_ver) => parsed_ver, +<<<<<<< HEAD Err(_) => return UntaggedValue::string(input.to_string()), +======= + Err(_) => { + return Value::String { + val: input.to_string(), + span: head, + } + } +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce }; match act_on { @@ -42,11 +63,28 @@ impl Inc { SemVerAction::Patch => ver.increment_patch(), } +<<<<<<< HEAD UntaggedValue::string(ver.to_string()) } Some(Action::Default) | None => match input.parse::() { Ok(v) => UntaggedValue::string((v + 1).to_string()), Err(_) => UntaggedValue::string(input), +======= + Value::String { + val: ver.to_string(), + span: head, + } + } + Some(Action::Default) | None => match input.parse::() { + Ok(v) => Value::String { + val: (v + 1).to_string(), + span: head, + }, + Err(_) => Value::String { + val: input.to_string(), + span: head, + }, +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce }, } } @@ -71,6 +109,7 @@ impl Inc { "Usage: inc field [--major|--minor|--patch]" } +<<<<<<< HEAD pub fn inc(&self, value: Value) -> Result { match &value.value { UntaggedValue::Primitive(Primitive::Int(i)) => { @@ -139,6 +178,28 @@ impl Inc { "incrementable value", value.type_name().spanned(value.span()), )), +======= + pub fn inc(&self, head: Span, value: &Value) -> Result { + match value { + Value::Int { val, span } => Ok(Value::Int { + val: val + 1, + span: *span, + }), + Value::String { val, .. } => Ok(self.apply(val, head)), + x => { + let msg = x.as_string().map_err(|e| LabeledError { + label: "Unable to extract string".into(), + msg: format!("value cannot be converted to string {:?} - {}", x, e), + span: Some(head), + })?; + + Err(LabeledError { + label: "Incorrect value".into(), + msg, + span: Some(head), + }) + } +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } } } @@ -146,6 +207,7 @@ impl Inc { #[cfg(test)] mod tests { mod semver { +<<<<<<< HEAD use crate::inc::SemVerAction; use crate::Inc; use nu_test_support::value::string; @@ -155,20 +217,56 @@ mod tests { let mut inc = Inc::new(); inc.for_semver(SemVerAction::Major); assert_eq!(inc.apply("0.1.3"), string("1.0.0").value); +======= + use nu_protocol::{Span, Value}; + + use crate::inc::SemVerAction; + use crate::Inc; + + #[test] + fn major() { + let expected = Value::String { + val: "1.0.0".to_string(), + span: Span::test_data(), + }; + let mut inc = Inc::new(); + inc.for_semver(SemVerAction::Major); + assert_eq!(inc.apply("0.1.3", Span::test_data()), expected) +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } #[test] fn minor() { +<<<<<<< HEAD let mut inc = Inc::new(); inc.for_semver(SemVerAction::Minor); assert_eq!(inc.apply("0.1.3"), string("0.2.0").value); +======= + let expected = Value::String { + val: "0.2.0".to_string(), + span: Span::test_data(), + }; + let mut inc = Inc::new(); + inc.for_semver(SemVerAction::Minor); + assert_eq!(inc.apply("0.1.3", Span::test_data()), expected) +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } #[test] fn patch() { +<<<<<<< HEAD let mut inc = Inc::new(); inc.for_semver(SemVerAction::Patch); assert_eq!(inc.apply("0.1.3"), string("0.1.4").value); +======= + let expected = Value::String { + val: "0.1.4".to_string(), + span: Span::test_data(), + }; + let mut inc = Inc::new(); + inc.for_semver(SemVerAction::Patch); + assert_eq!(inc.apply("0.1.3", Span::test_data()), expected) +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } } } diff --git a/crates/nu_plugin_inc/src/lib.rs b/crates/nu_plugin_inc/src/lib.rs index 9167c0be44..ad747ca042 100644 --- a/crates/nu_plugin_inc/src/lib.rs +++ b/crates/nu_plugin_inc/src/lib.rs @@ -2,6 +2,7 @@ mod inc; mod nu; pub use inc::Inc; +<<<<<<< HEAD #[cfg(test)] mod tests { @@ -33,3 +34,5 @@ mod tests { } } } +======= +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce diff --git a/crates/nu_plugin_inc/src/main.rs b/crates/nu_plugin_inc/src/main.rs index 7245f1fbca..e1d727fd66 100644 --- a/crates/nu_plugin_inc/src/main.rs +++ b/crates/nu_plugin_inc/src/main.rs @@ -1,6 +1,14 @@ +<<<<<<< HEAD use nu_plugin::serve_plugin; use nu_plugin_inc::Inc; fn main() { serve_plugin(&mut Inc::new()) +======= +use nu_plugin::{serve_plugin, CapnpSerializer}; +use nu_plugin_inc::Inc; + +fn main() { + serve_plugin(&mut Inc::new(), CapnpSerializer {}) +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } diff --git a/crates/nu_plugin_inc/src/nu/mod.rs b/crates/nu_plugin_inc/src/nu/mod.rs index b7c48fbd5f..4571c7e626 100644 --- a/crates/nu_plugin_inc/src/nu/mod.rs +++ b/crates/nu_plugin_inc/src/nu/mod.rs @@ -1,3 +1,4 @@ +<<<<<<< HEAD #[cfg(test)] mod tests; @@ -15,6 +16,16 @@ use nu_value_ext::ValueExt; impl Plugin for Inc { fn config(&mut self) -> Result { Ok(Signature::build("inc") +======= +use crate::inc::SemVerAction; +use crate::Inc; +use nu_plugin::{EvaluatedCall, LabeledError, Plugin}; +use nu_protocol::{Signature, Value}; + +impl Plugin for Inc { + fn signature(&self) -> Vec { + vec![Signature::build("inc") +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce .desc("Increment a value or version. Optionally use the column of a table.") .switch( "major", @@ -30,6 +41,7 @@ impl Plugin for Inc { "patch", "increment the patch version (eg 1.2.1 -> 1.2.2)", Some('p'), +<<<<<<< HEAD ) .rest("rest", SyntaxShape::ColumnPath, "the column(s) to update") .filter()) @@ -81,5 +93,31 @@ impl Plugin for Inc { fn filter(&mut self, input: Value) -> Result, ShellError> { Ok(vec![ReturnSuccess::value(self.inc(input)?)]) +======= + )] + } + + fn run( + &mut self, + name: &str, + call: &EvaluatedCall, + input: &Value, + ) -> Result { + if name != "inc" { + return Ok(Value::Nothing { span: call.head }); + } + + if call.has_flag("major") { + self.for_semver(SemVerAction::Major); + } + if call.has_flag("minor") { + self.for_semver(SemVerAction::Minor); + } + if call.has_flag("patch") { + self.for_semver(SemVerAction::Patch); + } + + self.inc(call.head, input) +>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce } } diff --git a/crates/nu_plugin_python/plugin.py b/crates/nu_plugin_python/plugin.py new file mode 100644 index 0000000000..ec19614e08 --- /dev/null +++ b/crates/nu_plugin_python/plugin.py @@ -0,0 +1,417 @@ +# Example of using python as script to create plugins for nushell +# +# The example uses JSON encoding but it should be a similar process using +# capnp proto to move data betwee nushell and the plugin. The only difference +# would be that you need to compile the schema file in order have the objects +# that decode and encode information that is read and written to stdin and stdour +# +# To register the plugin use: +# register -e json +# +# Be carefull with the spans. Miette will crash if a span is outside the +# size of the contents vector. For this example we are using 0 and 1, which will +# point to the beginning of the contents vector. We strongly suggest using the span +# found in the plugin call head +# +# The plugin will be run using the active python implementation. If you are in +# a python environment, that is the python version that is used +# +# Note: To keep the plugin simple and without dependencies, the dictionaries that +# represent the data transferred between nushell and the plugin are kept as +# native python dictionaries. The encoding and decoding process could be improved +# by using libraries like pydantic and marshmallow +# +# This plugin uses python3 +# Note: To debug plugins write to stderr using sys.stderr.write +import sys +import json + + +def signatures(): + """ + Multiple signatures can be sent to nushell. Each signature will be registered + as a different plugin function in nushell. + + In your plugin logic you can use the name of the signature to indicate what + operation should be done with the plugin + """ + return { + "Signature": [ + { + "name": "nu-python", + "usage": "Signature test for python", + "extra_usage": "", + "required_positional": [ + { + "name": "a", + "desc": "required integer value", + "shape": "Int", + "var_id": None, + }, + { + "name": "b", + "desc": "required string value", + "shape": "String", + "var_id": None, + }, + ], + "optional_positional": [ + { + "name": "opt", + "desc": "Optional number", + "shape": "Int", + "var_id": None, + } + ], + "rest_positional": { + "name": "rest", + "desc": "rest value string", + "shape": "String", + "var_id": None, + }, + "named": [ + { + "long": "help", + "short": "h", + "arg": None, + "required": False, + "desc": "Display this help message", + "var_id": None + }, + { + "long": "flag", + "short": "f", + "arg": None, + "required": False, + "desc": "a flag for the signature", + "var_id": None, + }, + { + "long": "named", + "short": "n", + "arg": "String", + "required": False, + "desc": "named string", + "var_id": None, + }, + ], + "is_filter": False, + "creates_scope": False, + "category": "Experimental", + } + ] + } + + +def process_call(plugin_call): + """ + plugin_call is a dictionary with the information from the call + It should contain: + - The name of the call + - The call data which includes the positional and named values + - The input from the pippeline + + Use this information to implement your plugin logic + """ + # Pretty printing the call to stderr + sys.stderr.write(json.dumps(plugin_call, indent=4)) + sys.stderr.write("\n") + + # Creates a Value of type List that will be encoded and sent to nushell + return { + "Value": { + "List": { + "vals": [ + { + "Record": { + "cols": ["one", "two", "three"], + "vals": [ + { + "Int": { + "val": 0, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 0, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 0, + "span": {"start": 0, "end": 1}, + } + }, + ], + "span": {"start": 0, "end": 1}, + } + }, + { + "Record": { + "cols": ["one", "two", "three"], + "vals": [ + { + "Int": { + "val": 0, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 1, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 2, + "span": {"start": 0, "end": 1}, + } + }, + ], + "span": {"start": 0, "end": 1}, + } + }, + { + "Record": { + "cols": ["one", "two", "three"], + "vals": [ + { + "Int": { + "val": 0, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 2, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 4, + "span": {"start": 0, "end": 1}, + } + }, + ], + "span": {"start": 0, "end": 1}, + } + }, + { + "Record": { + "cols": ["one", "two", "three"], + "vals": [ + { + "Int": { + "val": 0, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 3, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 6, + "span": {"start": 0, "end": 1}, + } + }, + ], + "span": {"start": 0, "end": 1}, + } + }, + { + "Record": { + "cols": ["one", "two", "three"], + "vals": [ + { + "Int": { + "val": 0, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 4, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 8, + "span": {"start": 0, "end": 1}, + } + }, + ], + "span": {"start": 0, "end": 1}, + } + }, + { + "Record": { + "cols": ["one", "two", "three"], + "vals": [ + { + "Int": { + "val": 0, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 5, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 10, + "span": {"start": 0, "end": 1}, + } + }, + ], + "span": {"start": 0, "end": 1}, + } + }, + { + "Record": { + "cols": ["one", "two", "three"], + "vals": [ + { + "Int": { + "val": 0, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 6, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 12, + "span": {"start": 0, "end": 1}, + } + }, + ], + "span": {"start": 0, "end": 1}, + } + }, + { + "Record": { + "cols": ["one", "two", "three"], + "vals": [ + { + "Int": { + "val": 0, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 7, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 14, + "span": {"start": 0, "end": 1}, + } + }, + ], + "span": {"start": 0, "end": 1}, + } + }, + { + "Record": { + "cols": ["one", "two", "three"], + "vals": [ + { + "Int": { + "val": 0, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 8, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 16, + "span": {"start": 0, "end": 1}, + } + }, + ], + "span": {"start": 0, "end": 1}, + } + }, + { + "Record": { + "cols": ["one", "two", "three"], + "vals": [ + { + "Int": { + "val": 0, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 9, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 18, + "span": {"start": 0, "end": 1}, + } + }, + ], + "span": {"start": 0, "end": 1}, + } + }, + ], + "span": {"start": 0, "end": 1}, + } + } + } + + +def plugin(): + call_str = ",".join(sys.stdin.readlines()) + plugin_call = json.loads(call_str) + + if plugin_call == "Signature": + signature = json.dumps(signatures()) + sys.stdout.write(signature) + + elif "CallInfo" in plugin_call: + response = process_call(plugin_call) + sys.stdout.write(json.dumps(response)) + + else: + # Use this error format if you want to return an error back to nushell + error = { + "Error": { + "label": "ERROR from plugin", + "msg": "error message pointing to call head span", + "span": {"start": 0, "end": 1}, + } + } + sys.stdout.write(json.dumps(error)) + + +if __name__ == "__main__": + plugin() diff --git a/crates/nu_plugin_query/Cargo.toml b/crates/nu_plugin_query/Cargo.toml new file mode 100644 index 0000000000..188dce7b3f --- /dev/null +++ b/crates/nu_plugin_query/Cargo.toml @@ -0,0 +1,19 @@ +[package] +authors = ["The Nu Project Contributors"] +description = "A set of query commands for Nushell" +edition = "2021" +license = "MIT" +name = "nu_plugin_query" +version = "0.1.0" + +[lib] +doctest = false + +[dependencies] +nu-plugin = { path="../nu-plugin", version = "0.1.0" } +nu-protocol = { path="../nu-protocol", version = "0.1.0" } +nu-engine = { path="../nu-engine", version = "0.1.0" } +gjson = "0.8.0" +scraper = "0.12.0" +sxd-document = "0.3.2" +sxd-xpath = "0.4.2" diff --git a/crates/nu_plugin_query/src/lib.rs b/crates/nu_plugin_query/src/lib.rs new file mode 100644 index 0000000000..4b8ebba399 --- /dev/null +++ b/crates/nu_plugin_query/src/lib.rs @@ -0,0 +1,12 @@ +mod nu; +mod query; +mod query_json; +mod query_web; +mod query_xml; +mod web_tables; + +pub use query::Query; +pub use query_json::execute_json_query; +pub use query_web::parse_selector_params; +pub use query_xml::execute_xpath_query; +pub use web_tables::WebTable; diff --git a/crates/nu_plugin_query/src/main.rs b/crates/nu_plugin_query/src/main.rs new file mode 100644 index 0000000000..c43203a8be --- /dev/null +++ b/crates/nu_plugin_query/src/main.rs @@ -0,0 +1,6 @@ +use nu_plugin::{serve_plugin, CapnpSerializer}; +use nu_plugin_query::Query; + +fn main() { + serve_plugin(&mut Query {}, CapnpSerializer {}) +} diff --git a/crates/nu_plugin_query/src/nu/mod.rs b/crates/nu_plugin_query/src/nu/mod.rs new file mode 100644 index 0000000000..8322f877a5 --- /dev/null +++ b/crates/nu_plugin_query/src/nu/mod.rs @@ -0,0 +1,70 @@ +use crate::Query; +use nu_plugin::{EvaluatedCall, LabeledError, Plugin}; +use nu_protocol::{Category, Signature, Spanned, SyntaxShape, Value}; + +impl Plugin for Query { + fn signature(&self) -> Vec { + vec![ + Signature::build("query") + .desc("Show all the query commands") + .category(Category::Filters), + + Signature::build("query json") + .desc("execute json query on json file (open --raw | query json 'query string')") + .required("query", SyntaxShape::String, "json query") + .category(Category::Filters), + + Signature::build("query xml") + .desc("execute xpath query on xml") + .required("query", SyntaxShape::String, "xpath query") + .category(Category::Filters), + + Signature::build("query web") + .desc("execute selector query on html/web") + .named("query", SyntaxShape::String, "selector query", Some('q')) + .switch("as_html", "return the query output as html", Some('m')) + .named( + "attribute", + SyntaxShape::String, + "downselect based on the given attribute", + Some('a'), + ) + .named( + "as_table", + SyntaxShape::Table, + "find table based on column header list", + Some('t'), + ) + .switch( + "inspect", + "run in inspect mode to provide more information for determining column headers", + Some('i'), + ) + .category(Category::Network), + ] + } + + fn run( + &mut self, + name: &str, + call: &EvaluatedCall, + input: &Value, + ) -> Result { + // You can use the name to identify what plugin signature was called + let path: Option> = call.opt(0)?; + + match name { + "query" => { + self.query(name, call, input, path) + } + "query json" => self.query_json( name, call, input, path), + "query web" => self.query_web(name, call, input, path), + "query xml" => self.query_xml(name, call, input, path), + _ => Err(LabeledError { + label: "Plugin call with wrong name signature".into(), + msg: "the signature used to call the plugin does not match any name in the plugin signature vector".into(), + span: Some(call.head), + }), + } + } +} diff --git a/crates/nu_plugin_query/src/query.rs b/crates/nu_plugin_query/src/query.rs new file mode 100644 index 0000000000..41e22d5514 --- /dev/null +++ b/crates/nu_plugin_query/src/query.rs @@ -0,0 +1,75 @@ +use crate::query_json::execute_json_query; +use crate::query_web::parse_selector_params; +use crate::query_xml::execute_xpath_query; +use nu_engine::documentation::get_flags_section; +use nu_plugin::{EvaluatedCall, LabeledError, Plugin}; +use nu_protocol::{Signature, Spanned, Value}; + +#[derive(Default)] +pub struct Query; + +impl Query { + pub fn new() -> Self { + Default::default() + } + + pub fn usage() -> &'static str { + "Usage: query" + } + + pub fn query( + &self, + _name: &str, + call: &EvaluatedCall, + _value: &Value, + _path: Option>, + ) -> Result { + let help = get_brief_subcommand_help(&Query.signature()); + Ok(Value::string(help, call.head)) + } + + pub fn query_json( + &self, + name: &str, + call: &EvaluatedCall, + input: &Value, + query: Option>, + ) -> Result { + execute_json_query(name, call, input, query) + } + pub fn query_web( + &self, + _name: &str, + call: &EvaluatedCall, + input: &Value, + _rest: Option>, + ) -> Result { + parse_selector_params(call, input) + } + pub fn query_xml( + &self, + name: &str, + call: &EvaluatedCall, + input: &Value, + query: Option>, + ) -> Result { + execute_xpath_query(name, call, input, query) + } +} + +pub fn get_brief_subcommand_help(sigs: &[Signature]) -> String { + let mut help = String::new(); + help.push_str(&format!("{}\n\n", sigs[0].usage)); + help.push_str(&format!("Usage:\n > {}\n\n", sigs[0].name)); + help.push_str("Subcommands:\n"); + + for x in sigs.iter().enumerate() { + if x.0 == 0 { + continue; + } + help.push_str(&format!(" {} - {}\n", x.1.name, x.1.usage)); + } + + help.push_str(&get_flags_section(&sigs[0])); + help +} diff --git a/crates/nu_plugin_query/src/query_json.rs b/crates/nu_plugin_query/src/query_json.rs new file mode 100644 index 0000000000..616fd18c21 --- /dev/null +++ b/crates/nu_plugin_query/src/query_json.rs @@ -0,0 +1,151 @@ +use gjson::Value as gjValue; +use nu_plugin::{EvaluatedCall, LabeledError}; +use nu_protocol::{Span, Spanned, Value}; + +pub fn execute_json_query( + _name: &str, + call: &EvaluatedCall, + input: &Value, + query: Option>, +) -> Result { + let input_string = match &input.as_string() { + Ok(s) => s.clone(), + Err(e) => { + return Err(LabeledError { + span: Some(call.head), + msg: e.to_string(), + label: "problem with input data".to_string(), + }) + } + }; + + let query_string = match &query { + Some(v) => &v.item, + None => { + return Err(LabeledError { + msg: "problem with input data".to_string(), + label: "problem with input data".to_string(), + span: Some(call.head), + }) + } + }; + + // Validate the json before trying to query it + let is_valid_json = gjson::valid(&input_string); + + if !is_valid_json { + return Err(LabeledError { + msg: "invalid json".to_string(), + label: "invalid json".to_string(), + span: Some(call.head), + }); + } + + let val: gjValue = gjson::get(&input_string, query_string); + + if query_contains_modifiers(query_string) { + let json_str = val.json(); + Ok(Value::string(json_str, Span::test_data())) + } else { + Ok(convert_gjson_value_to_nu_value(&val, &call.head)) + } +} + +fn query_contains_modifiers(query: &str) -> bool { + // https://github.com/tidwall/gjson.rs documents 7 modifiers as of 4/19/21 + // Some of these modifiers mean we really need to output the data as a string + // instead of tabular data. Others don't matter. + + // Output as String + // @ugly: Remove all whitespace from a json document. + // @pretty: Make the json document more human readable. + query.contains("@ugly") || query.contains("@pretty") + + // Output as Tablular + // Since it's output as tabular, which is our default, we can just ignore these + // @reverse: Reverse an array or the members of an object. + // @this: Returns the current element. It can be used to retrieve the root element. + // @valid: Ensure the json document is valid. + // @flatten: Flattens an array. + // @join: Joins multiple objects into a single object. +} + +fn convert_gjson_value_to_nu_value(v: &gjValue, span: &Span) -> Value { + match v.kind() { + gjson::Kind::Array => { + let mut vals = vec![]; + v.each(|_k, v| { + vals.push(convert_gjson_value_to_nu_value(&v, span)); + true + }); + + Value::List { vals, span: *span } + } + gjson::Kind::Null => Value::nothing(*span), + gjson::Kind::False => Value::boolean(false, *span), + gjson::Kind::Number => { + let str_value = v.str(); + if str_value.contains('.') { + Value::float(v.f64(), *span) + } else { + Value::int(v.i64(), *span) + } + } + gjson::Kind::String => Value::string(v.str(), *span), + gjson::Kind::True => Value::boolean(true, *span), + gjson::Kind::Object => { + let mut cols = vec![]; + let mut vals = vec![]; + v.each(|k, v| { + cols.push(k.to_string()); + vals.push(convert_gjson_value_to_nu_value(&v, span)); + true + }); + Value::Record { + cols, + vals, + span: *span, + } + } + } +} + +#[cfg(test)] +mod tests { + use gjson::{valid, Value as gjValue}; + + #[test] + fn validate_string() { + let json = r#"{ "name": { "first": "Tom", "last": "Anderson" }, "age": 37, "children": ["Sara", "Alex", "Jack"], "friends": [ { "first": "James", "last": "Murphy" }, { "first": "Roger", "last": "Craig" } ] }"#; + let val = valid(json); + assert!(val); + } + + #[test] + fn answer_from_get_age() { + let json = r#"{ "name": { "first": "Tom", "last": "Anderson" }, "age": 37, "children": ["Sara", "Alex", "Jack"], "friends": [ { "first": "James", "last": "Murphy" }, { "first": "Roger", "last": "Craig" } ] }"#; + let val: gjValue = gjson::get(json, "age"); + assert_eq!(val.str(), "37"); + } + + #[test] + fn answer_from_get_children() { + let json = r#"{ "name": { "first": "Tom", "last": "Anderson" }, "age": 37, "children": ["Sara", "Alex", "Jack"], "friends": [ { "first": "James", "last": "Murphy" }, { "first": "Roger", "last": "Craig" } ] }"#; + let val: gjValue = gjson::get(json, "children"); + assert_eq!(val.str(), r#"["Sara", "Alex", "Jack"]"#); + } + + #[test] + fn answer_from_get_children_count() { + let json = r#"{ "name": { "first": "Tom", "last": "Anderson" }, "age": 37, "children": ["Sara", "Alex", "Jack"], "friends": [ { "first": "James", "last": "Murphy" }, { "first": "Roger", "last": "Craig" } ] }"#; + let val: gjValue = gjson::get(json, "children.#"); + assert_eq!(val.str(), "3"); + } + + #[test] + fn answer_from_get_friends_first_name() { + let json = r#"{ "name": { "first": "Tom", "last": "Anderson" }, "age": 37, "children": ["Sara", "Alex", "Jack"], "friends": [ { "first": "James", "last": "Murphy" }, { "first": "Roger", "last": "Craig" } ] }"#; + let val: gjValue = gjson::get(json, "friends.#.first"); + assert_eq!(val.str(), r#"["James","Roger"]"#); + } +} diff --git a/crates/nu_plugin_query/src/query_web.rs b/crates/nu_plugin_query/src/query_web.rs new file mode 100644 index 0000000000..422f7e8e7a --- /dev/null +++ b/crates/nu_plugin_query/src/query_web.rs @@ -0,0 +1,303 @@ +use crate::web_tables::WebTable; +use nu_plugin::{EvaluatedCall, LabeledError}; +use nu_protocol::{Span, Value}; +use scraper::{Html, Selector as ScraperSelector}; + +pub struct Selector { + pub query: String, + pub as_html: bool, + pub attribute: String, + pub as_table: Value, + pub inspect: bool, +} + +impl Selector { + pub fn new() -> Selector { + Selector { + query: String::new(), + as_html: false, + attribute: String::new(), + as_table: Value::string("".to_string(), Span::test_data()), + inspect: false, + } + } +} + +impl Default for Selector { + fn default() -> Self { + Self::new() + } +} + +pub fn parse_selector_params(call: &EvaluatedCall, input: &Value) -> Result { + let head = call.head; + let query: String = match call.get_flag("query")? { + Some(q2) => q2, + None => "".to_string(), + }; + let as_html = call.has_flag("as_html"); + let attribute: String = match call.get_flag("attribute")? { + Some(a) => a, + None => "".to_string(), + }; + let as_table: Value = match call.get_flag("as_table")? { + Some(v) => v, + None => Value::nothing(head), + }; + + let inspect = call.has_flag("inspect"); + + if !&query.is_empty() && ScraperSelector::parse(&query).is_err() { + return Err(LabeledError { + msg: "Cannot parse this query as a valid css selector".to_string(), + label: "Parse error".to_string(), + span: Some(head), + }); + } + + let selector = Selector { + query, + as_html, + attribute, + as_table, + inspect, + }; + + match input { + Value::String { val, span } => Ok(begin_selector_query(val.to_string(), selector, *span)), + _ => Err(LabeledError { + label: "requires text input".to_string(), + msg: "Expected text from pipeline".to_string(), + span: Some(input.span()?), + }), + } +} + +fn begin_selector_query(input_html: String, selector: Selector, span: Span) -> Value { + if let Value::List { .. } = selector.as_table { + return retrieve_tables( + input_html.as_str(), + &selector.as_table, + selector.inspect, + span, + ); + } else { + match selector.attribute.is_empty() { + true => execute_selector_query( + input_html.as_str(), + selector.query.as_str(), + selector.as_html, + span, + ), + false => execute_selector_query_with_attribute( + input_html.as_str(), + selector.query.as_str(), + selector.attribute.as_str(), + span, + ), + } + } +} + +pub fn retrieve_tables( + input_string: &str, + columns: &Value, + inspect_mode: bool, + span: Span, +) -> Value { + let html = input_string; + let mut cols: Vec = Vec::new(); + if let Value::List { vals, .. } = &columns { + for x in vals { + // TODO Find a way to get the Config object here + if let Value::String { val, .. } = x { + cols.push(val.to_string()) + } + } + } + + if inspect_mode { + eprintln!("Passed in Column Headers = {:#?}", &cols,); + } + + let tables = match WebTable::find_by_headers(html, &cols) { + Some(t) => { + if inspect_mode { + eprintln!("Table Found = {:#?}", &t); + } + t + } + None => vec![WebTable::empty()], + }; + + if tables.len() == 1 { + return retrieve_table( + tables + .into_iter() + .next() + .expect("This should never trigger"), + columns, + span, + ); + } + + let vals = tables + .into_iter() + .map(move |table| retrieve_table(table, columns, span)) + .collect(); + + Value::List { vals, span } +} + +fn retrieve_table(mut table: WebTable, columns: &Value, span: Span) -> Value { + let mut cols: Vec = Vec::new(); + if let Value::List { vals, .. } = &columns { + for x in vals { + // TODO Find a way to get the Config object here + if let Value::String { val, .. } = x { + cols.push(val.to_string()) + } + } + } + + if cols.is_empty() && !table.headers().is_empty() { + for col in table.headers().keys() { + cols.push(col.to_string()); + } + } + + let mut table_out = Vec::new(); + // sometimes there are tables where the first column is the headers, kind of like + // a table has ben rotated ccw 90 degrees, in these cases all columns will be missing + // we keep track of this with this variable so we can deal with it later + let mut at_least_one_row_filled = false; + // if columns are still empty, let's just make a single column table with the data + if cols.is_empty() { + at_least_one_row_filled = true; + let table_with_no_empties: Vec<_> = table.iter().filter(|item| !item.is_empty()).collect(); + + let mut cols = vec![]; + let mut vals = vec![]; + for row in &table_with_no_empties { + for (counter, cell) in row.iter().enumerate() { + cols.push(format!("Column{}", counter)); + vals.push(Value::string(cell.to_string(), span)) + } + } + table_out.push(Value::Record { cols, vals, span }) + } else { + for row in &table { + let mut vals = vec![]; + let record_cols = &cols; + for col in &cols { + let val = row + .get(col) + .unwrap_or(&format!("Missing column: '{}'", &col)) + .to_string(); + + if !at_least_one_row_filled && val != format!("Missing column: '{}'", &col) { + at_least_one_row_filled = true; + } + vals.push(Value::string(val, span)); + } + table_out.push(Value::Record { + cols: record_cols.to_vec(), + vals, + span, + }) + } + } + if !at_least_one_row_filled { + let mut data2 = Vec::new(); + for x in &table.data { + data2.push(x.join(", ")); + } + table.data = vec![data2]; + return retrieve_table(table, columns, span); + } + // table_out + + Value::List { + vals: table_out, + span, + } +} + +fn execute_selector_query_with_attribute( + input_string: &str, + query_string: &str, + attribute: &str, + span: Span, +) -> Value { + let doc = Html::parse_fragment(input_string); + + let vals: Vec = doc + .select(&css(query_string)) + .map(|selection| { + Value::string( + selection.value().attr(attribute).unwrap_or("").to_string(), + span, + ) + }) + .collect(); + Value::List { vals, span } +} + +fn execute_selector_query( + input_string: &str, + query_string: &str, + as_html: bool, + span: Span, +) -> Value { + let doc = Html::parse_fragment(input_string); + + let vals: Vec = match as_html { + true => doc + .select(&css(query_string)) + .map(|selection| Value::string(selection.html(), span)) + .collect(), + false => doc + .select(&css(query_string)) + .map(|selection| { + Value::string( + selection + .text() + .fold("".to_string(), |acc, x| format!("{}{}", acc, x)), + span, + ) + }) + .collect(), + }; + + Value::List { vals, span } +} + +pub fn css(selector: &str) -> ScraperSelector { + ScraperSelector::parse(selector).expect("this should never trigger") +} + +// #[cfg(test)] +// mod tests { +// use super::*; + +// const SIMPLE_LIST: &str = r#" +//
    +//
  • Coffee
  • +//
  • Tea
  • +//
  • Milk
  • +//
+// "#; + +// #[test] +// fn test_first_child_is_not_empty() { +// assert!(!execute_selector_query(SIMPLE_LIST, "li:first-child", false).is_empty()) +// } + +// #[test] +// fn test_first_child() { +// assert_eq!( +// vec!["Coffee".to_string()], +// execute_selector_query(SIMPLE_LIST, "li:first-child", false) +// ) +// } +// } diff --git a/crates/nu_plugin_query/src/query_xml.rs b/crates/nu_plugin_query/src/query_xml.rs new file mode 100644 index 0000000000..75e08f56ab --- /dev/null +++ b/crates/nu_plugin_query/src/query_xml.rs @@ -0,0 +1,188 @@ +use nu_plugin::{EvaluatedCall, LabeledError}; +use nu_protocol::{Span, Spanned, Value}; +use sxd_document::parser; +use sxd_xpath::{Context, Factory}; + +pub fn execute_xpath_query( + _name: &str, + call: &EvaluatedCall, + input: &Value, + query: Option>, +) -> Result { + let (query_string, span) = match &query { + Some(v) => (&v.item, &v.span), + None => { + return Err(LabeledError { + msg: "problem with input data".to_string(), + label: "problem with input data".to_string(), + span: Some(call.head), + }) + } + }; + + let xpath = build_xpath(query_string, span)?; + let input_string = input.as_string()?; + let package = parser::parse(&input_string); + + if package.is_err() { + return Err(LabeledError { + label: "invalid xml document".to_string(), + msg: "invalid xml document".to_string(), + span: Some(call.head), + }); + } + + let package = package.expect("invalid xml document"); + + let document = package.as_document(); + let context = Context::new(); + + // leaving this here for augmentation at some point + // build_variables(&arguments, &mut context); + // build_namespaces(&arguments, &mut context); + let res = xpath.evaluate(&context, document.root()); + + // Some xpath statements can be long, so let's truncate it with ellipsis + let mut key = query_string.clone(); + if query_string.len() >= 20 { + key.truncate(17); + key += "..."; + } else { + key = query_string.to_string(); + }; + + match res { + Ok(r) => { + let mut cols: Vec = vec![]; + let mut vals: Vec = vec![]; + let mut records: Vec = vec![]; + + match r { + sxd_xpath::Value::Nodeset(ns) => { + for n in ns.into_iter() { + cols.push(key.to_string()); + vals.push(Value::string(n.string_value(), Span::test_data())); + } + } + sxd_xpath::Value::Boolean(b) => { + cols.push(key.to_string()); + vals.push(Value::boolean(b, Span::test_data())); + } + sxd_xpath::Value::Number(n) => { + cols.push(key.to_string()); + vals.push(Value::float(n, Span::test_data())); + } + sxd_xpath::Value::String(s) => { + cols.push(key.to_string()); + vals.push(Value::string(s, Span::test_data())); + } + }; + + // convert the cols and vecs to a table by creating individual records + // for each item so we can then use a list to make a table + for (k, v) in cols.iter().zip(vals.iter()) { + records.push(Value::Record { + cols: vec![k.to_string()], + vals: vec![v.clone()], + span: Span::test_data(), + }) + } + + Ok(Value::List { + vals: records, + span: Span::test_data(), + }) + } + Err(_) => Err(LabeledError { + label: "xpath query error".to_string(), + msg: "xpath query error".to_string(), + span: Some(Span::test_data()), + }), + } +} + +fn build_xpath(xpath_str: &str, span: &Span) -> Result { + let factory = Factory::new(); + + match factory.build(xpath_str) { + Ok(xpath) => xpath.ok_or_else(|| LabeledError { + label: "invalid xpath query".to_string(), + msg: "invalid xpath query".to_string(), + span: Some(*span), + }), + Err(_) => Err(LabeledError { + label: "expected valid xpath query".to_string(), + msg: "expected valid xpath query".to_string(), + span: Some(*span), + }), + } +} + +#[cfg(test)] +mod tests { + use super::execute_xpath_query as query; + use nu_plugin::EvaluatedCall; + use nu_protocol::{Span, Spanned, Value}; + + #[test] + fn position_function_in_predicate() { + let call = EvaluatedCall { + head: Span::test_data(), + positional: vec![], + named: vec![], + }; + + let text = Value::string( + r#""#, + Span::test_data(), + ); + + let spanned_str: Spanned = Spanned { + item: "count(//a/*[position() = 2])".to_string(), + span: Span::test_data(), + }; + + let actual = query("", &call, &text, Some(spanned_str)).expect("test should not fail"); + let expected = Value::List { + vals: vec![Value::Record { + cols: vec!["count(//a/*[posit...".to_string()], + vals: vec![Value::float(1.0, Span::test_data())], + span: Span::test_data(), + }], + span: Span::test_data(), + }; + + assert_eq!(actual, expected); + } + + #[test] + fn functions_implicitly_coerce_argument_types() { + let call = EvaluatedCall { + head: Span::test_data(), + positional: vec![], + named: vec![], + }; + + let text = Value::string( + r#"true"#, + Span::test_data(), + ); + + let spanned_str: Spanned = Spanned { + item: "count(//*[contains(., true)])".to_string(), + span: Span::test_data(), + }; + + let actual = query("", &call, &text, Some(spanned_str)).expect("test should not fail"); + let expected = Value::List { + vals: vec![Value::Record { + cols: vec!["count(//*[contain...".to_string()], + vals: vec![Value::float(1.0, Span::test_data())], + span: Span::test_data(), + }], + span: Span::test_data(), + }; + + assert_eq!(actual, expected); + } +} diff --git a/crates/nu_plugin_query/src/web_tables.rs b/crates/nu_plugin_query/src/web_tables.rs new file mode 100644 index 0000000000..a2809dbd26 --- /dev/null +++ b/crates/nu_plugin_query/src/web_tables.rs @@ -0,0 +1,1227 @@ +use crate::query_web::css; +use scraper::{element_ref::ElementRef, Html, Selector as ScraperSelector}; +use std::collections::HashMap; + +pub type Headers = HashMap; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct WebTable { + headers: Headers, + pub data: Vec>, +} + +impl WebTable { + /// Finds the first table in `html`. + pub fn find_first(html: &str) -> Option { + let html = Html::parse_fragment(html); + html.select(&css("table")).next().map(WebTable::new) + } + + pub fn find_all_tables(html: &str) -> Option> { + let html = Html::parse_fragment(html); + let iter: Vec = html.select(&css("table")).map(WebTable::new).collect(); + if iter.is_empty() { + return None; + } + Some(iter) + } + + /// Finds the table in `html` with an id of `id`. + pub fn find_by_id(html: &str, id: &str) -> Option { + let html = Html::parse_fragment(html); + let selector = format!("table#{}", id); + ScraperSelector::parse(&selector) + .ok() + .as_ref() + .map(|s| html.select(s)) + .and_then(|mut s| s.next()) + .map(WebTable::new) + } + + /// Finds the table in `html` whose first row contains all of the headers + /// specified in `headers`. The order does not matter. + /// + /// If `headers` is empty, this is the same as + /// [`find_first`](#method.find_first). + pub fn find_by_headers(html: &str, headers: &[T]) -> Option> + where + T: AsRef, + { + if headers.is_empty() { + return WebTable::find_all_tables(html); + } + + let sel_table = css("table"); + let sel_tr = css("tr"); + let sel_th = css("th"); + + let html = Html::parse_fragment(html); + let mut tables = html + .select(&sel_table) + .filter(|table| { + table.select(&sel_tr).next().map_or(false, |tr| { + let cells = select_cells(tr, &sel_th, true); + headers.iter().all(|h| contains_str(&cells, h.as_ref())) + }) + }) + .peekable(); + tables.peek()?; + Some(tables.map(WebTable::new).collect()) + } + + /// Returns the headers of the table. + /// + /// This will be empty if the table had no `` tags in its first row. See + /// [`Headers`](type.Headers.html) for more. + pub fn headers(&self) -> &Headers { + &self.headers + } + + /// Returns an iterator over the [`Row`](struct.Row.html)s of the table. + /// + /// Only `` cells are considered when generating rows. If the first row + /// of the table is a header row, meaning it contains at least one `` + /// cell, the iterator will start on the second row. Use + /// [`headers`](#method.headers) to access the header row in that case. + pub fn iter(&self) -> Iter { + Iter { + headers: &self.headers, + iter: self.data.iter(), + } + } + + pub fn empty() -> WebTable { + WebTable { + headers: HashMap::new(), + data: vec![vec!["".to_string()]], + } + } + + // fn new(element: ElementRef) -> Table { + // let sel_tr = css("tr"); + // let sel_th = css("th"); + // let sel_td = css("td"); + + // let mut headers = HashMap::new(); + // let mut rows = element.select(&sel_tr).peekable(); + // if let Some(tr) = rows.peek() { + // for (i, th) in tr.select(&sel_th).enumerate() { + // headers.insert(cell_content(th), i); + // } + // } + // if !headers.is_empty() { + // rows.next(); + // } + // let data = rows.map(|tr| select_cells(tr, &sel_td, true)).collect(); + // Table { headers, data } + // } + + fn new(element: ElementRef) -> WebTable { + let sel_tr = css("tr"); + let sel_th = css("th"); + let sel_td = css("td"); + + let mut headers = HashMap::new(); + let mut rows = element.select(&sel_tr).peekable(); + if let Some(tr) = rows.clone().peek() { + for (i, th) in tr.select(&sel_th).enumerate() { + headers.insert(cell_content(th), i); + } + } + if !headers.is_empty() { + rows.next(); + } + + if headers.is_empty() { + // try looking for data as headers i.e. they're row headers not column headers + for (i, d) in rows + .clone() + .map(|tr| select_cells(tr, &sel_th, true)) + .enumerate() + { + headers.insert(d.join(", "), i); + } + // check if headers are there but empty + let mut empty_headers = true; + for (h, _i) in headers.clone() { + if !h.is_empty() { + empty_headers = false; + break; + } + } + if empty_headers { + headers = HashMap::new(); + } + let data = rows.map(|tr| select_cells(tr, &sel_td, true)).collect(); + WebTable { headers, data } + } else { + let data = rows.map(|tr| select_cells(tr, &sel_td, true)).collect(); + WebTable { headers, data } + } + } +} + +impl<'a> IntoIterator for &'a WebTable { + type Item = Row<'a>; + type IntoIter = Iter<'a>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +/// An iterator over the rows in a [`Table`](struct.Table.html). +pub struct Iter<'a> { + headers: &'a Headers, + iter: std::slice::Iter<'a, Vec>, +} + +impl<'a> Iterator for Iter<'a> { + type Item = Row<'a>; + + fn next(&mut self) -> Option { + let headers = self.headers; + self.iter.next().map(|cells| Row { headers, cells }) + } +} + +/// A row in a [`Table`](struct.Table.html). +/// +/// A row consists of a number of data cells stored as strings. If the row +/// contains the same number of cells as the table's header row, its cells can +/// be safely accessed by header names using [`get`](#method.get). Otherwise, +/// the data should be accessed via [`as_slice`](#method.as_slice) or by +/// iterating over the row. +/// +/// This struct can be thought of as a lightweight reference into a table. As +/// such, it implements the `Copy` trait. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Row<'a> { + headers: &'a Headers, + cells: &'a [String], +} + +impl<'a> Row<'a> { + /// Returns the number of cells in the row. + pub fn len(&self) -> usize { + self.cells.len() + } + + /// Returns `true` if the row contains no cells. + pub fn is_empty(&self) -> bool { + self.cells.is_empty() + } + + /// Returns the cell underneath `header`. + /// + /// Returns `None` if there is no such header, or if there is no cell at + /// that position in the row. + pub fn get(&self, header: &str) -> Option<&'a str> { + // eprintln!( + // "header={}, headers={:?}, cells={:?}", + // &header, &self.headers, &self.cells + // ); + self.headers.get(header).and_then(|&i| { + // eprintln!("i={}", i); + self.cells.get(i).map(String::as_str) + }) + } + + pub fn get_header_at(&self, index: usize) -> Option<&'a str> { + let mut a_match = ""; + for (key, val) in self.headers { + if *val == index { + a_match = key; + break; + } + } + if a_match.is_empty() { + None + } else { + Some(a_match) + } + } + + /// Returns a slice containing all the cells. + pub fn as_slice(&self) -> &'a [String] { + self.cells + } + + /// Returns an iterator over the cells of the row. + pub fn iter(&self) -> std::slice::Iter { + self.cells.iter() + } +} + +impl<'a> IntoIterator for Row<'a> { + type Item = &'a String; + type IntoIter = std::slice::Iter<'a, String>; + + fn into_iter(self) -> Self::IntoIter { + self.cells.iter() + } +} + +fn select_cells( + element: ElementRef, + selector: &ScraperSelector, + remove_html_tags: bool, +) -> Vec { + if remove_html_tags { + let scraped = element.select(selector).map(cell_content); + let mut dehtmlized: Vec = Vec::new(); + for item in scraped { + let frag = Html::parse_fragment(&item); + for node in frag.tree { + if let scraper::node::Node::Text(text) = node { + dehtmlized.push(text.text.to_string()); + } + } + } + dehtmlized + } else { + element.select(selector).map(cell_content).collect() + } +} + +fn cell_content(element: ElementRef) -> String { + // element.inner_html().trim().to_string() + let mut dehtmlize = String::new(); + let element = element.inner_html().trim().to_string(); + let frag = Html::parse_fragment(&element); + for node in frag.tree { + if let scraper::node::Node::Text(text) = node { + dehtmlize.push_str(&text.text.to_string()) + } + } + + // eprintln!("element={} dehtmlize={}", &element, &dehtmlize); + + if dehtmlize.is_empty() { + dehtmlize = element; + } + + dehtmlize +} + +fn contains_str(slice: &[String], item: &str) -> bool { + // slice.iter().any(|s| s == item) + + let mut dehtmlized = String::new(); + let frag = Html::parse_fragment(item); + for node in frag.tree { + if let scraper::node::Node::Text(text) = node { + dehtmlized.push_str(&text.text.to_string()); + } + } + + if dehtmlized.is_empty() { + dehtmlized = item.to_string(); + } + + slice.iter().any(|s| { + // eprintln!( + // "\ns={} item={} contains={}\n", + // &s, + // &dehtmlized, + // &dehtmlized.contains(s) + // ); + // s.starts_with(item) + dehtmlized.contains(s) + }) +} + +// #[cfg(test)] +// mod tests { +// use super::*; +// use crate::query_web::retrieve_tables; +// use indexmap::indexmap; +// use nu_protocol::Value; + +// const TABLE_EMPTY: &str = r#" +//
+// "#; + +// const TABLE_TH: &str = r#" +// +// +//
NameAge
+// "#; + +// const TABLE_TD: &str = r#" +// +// +//
NameAge
+// "#; + +// const TWO_TABLES_TD: &str = r#" +// +// +//
NameAge
+// +// +//
ProfessionCivil State
+// "#; + +// const TABLE_TH_TD: &str = r#" +// +// +// +//
NameAge
John20
+// "#; + +// const TWO_TABLES_TH_TD: &str = r#" +// +// +// +//
NameAge
John20
+// +// +// +//
ProfessionCivil State
MechanicSingle
+// "#; + +// const TABLE_TD_TD: &str = r#" +// +// +// +//
NameAge
John20
+// "#; + +// const TABLE_TH_TH: &str = r#" +// +// +// +//
NameAge
John20
+// "#; + +// const TABLE_COMPLEX: &str = r#" +// +// +// +// +// +// +//
NameAgeExtra
John20
May30foo
abcd
+// "#; + +// const TWO_TABLES_COMPLEX: &str = r#" +// +// +// foo +// +// +// +// +// +// +// +//
NameAgeExtra
John20
May30foo
abcd
+// +// +// +// +// +// +//
ProfessionCivil StateExtra
CarpenterSingle
MechanicMarriedbar
efgh
+// +// +// "#; + +// const HTML_NO_TABLE: &str = r#" +// +// +// foo +//

Hi.

+// +// "#; + +// const HTML_TWO_TABLES: &str = r#" +// +// +// foo +// +// +// +// +//
NameAge
John20
+// +// +// +//
NameWeight
John150
+// +// +// "#; + +// const HTML_TABLE_FRAGMENT: &str = r#" +// +// +// +//
NameAge
John20
+// +// +// "#; + +// const HTML_TABLE_WIKIPEDIA_WITH_COLUMN_NAMES: &str = r#" +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +//
Excel 2007 formats +//
Format +// Extension +// Description +//
Excel Workbook +// .xlsx +// The default Excel 2007 and later workbook format. In reality, a Zip compressed archive with a directory structure of XML text documents. +//Functions as the primary replacement for the former binary .xls format, although it does not support Excel macros for security reasons. Saving as .xlsx offers file size reduction over .xls[38] +//
Excel Macro-enabled Workbook +// .xlsm +// As Excel Workbook, but with macro support. +//
Excel Binary Workbook +// .xlsb +// As Excel Macro-enabled Workbook, but storing information in binary form rather than XML documents for opening and saving documents more quickly and efficiently. Intended especially for very large documents with tens of thousands of rows, and/or several hundreds +//of columns. This format is very useful for shrinking large Excel files as is often the case when doing data analysis. +//
Excel Macro-enabled Template +// .xltm +// A template document that forms a basis for actual workbooks, with macro support. The replacement for the old .xlt format. +//
Excel Add-in +// .xlam +// Excel add-in to add extra functionality and tools. Inherent macro support because of the file purpose. +//
+// "#; + +// const HTML_TABLE_WIKIPEDIA_COLUMNS_AS_ROWS: &str = r#" +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +//
+// Microsoft Excel +//
+// Microsoft Office Excel (2019–present).svg +//
+// Microsoft Excel.png +//
+// A simple +// line chart being +// created in Excel, running on +// Windows 10 +//
+//
+// Developer(s) +// +// Microsoft +//
+// Initial release +// +// 1987; 34 years ago (1987) +//
+// Stable release +// +//
+// 2103 (16.0.13901.20400) / April 13, 2021; 4 months ago (2021-04-13)[1] +//
+//
+// Operating system +// +// Microsoft Windows +//
+// Type +// +// Spreadsheet +//
+// License +// +// Trialware[2] +//
+// Website +// +// products.office.com/en-us/excel +//
+// "#; + +// #[test] +// fn test_find_first_none() { +// assert_eq!(None, Table::find_first("")); +// assert_eq!(None, Table::find_first("foo")); +// assert_eq!(None, Table::find_first(HTML_NO_TABLE)); +// } + +// #[test] +// fn test_find_first_empty() { +// let empty = Table { +// headers: HashMap::new(), +// data: Vec::new(), +// }; +// assert_eq!(Some(empty), Table::find_first(TABLE_EMPTY)); +// } + +// #[test] +// fn test_find_first_some() { +// assert!(Table::find_first(TABLE_TH).is_some()); +// assert!(Table::find_first(TABLE_TD).is_some()); +// } + +// #[test] +// fn test_find_by_id_none() { +// assert_eq!(None, Table::find_by_id("", "")); +// assert_eq!(None, Table::find_by_id("foo", "id")); +// assert_eq!(None, Table::find_by_id(HTML_NO_TABLE, "id")); + +// assert_eq!(None, Table::find_by_id(TABLE_EMPTY, "id")); +// assert_eq!(None, Table::find_by_id(TABLE_TH, "id")); +// assert_eq!(None, Table::find_by_id(TABLE_TH, "")); +// assert_eq!(None, Table::find_by_id(HTML_TWO_TABLES, "id")); +// } + +// #[test] +// fn test_find_by_id_some() { +// assert!(Table::find_by_id(HTML_TWO_TABLES, "first").is_some()); +// assert!(Table::find_by_id(HTML_TWO_TABLES, "second").is_some()); +// } + +// #[test] +// fn test_find_by_headers_empty() { +// let headers: [&str; 0] = []; + +// assert_eq!(None, Table::find_by_headers("", &headers)); +// assert_eq!(None, Table::find_by_headers("foo", &headers)); +// assert_eq!(None, Table::find_by_headers(HTML_NO_TABLE, &headers)); + +// assert!(Table::find_by_headers(TABLE_EMPTY, &headers).is_some()); +// assert!(Table::find_by_headers(HTML_TWO_TABLES, &headers).is_some()); +// } + +// #[test] +// fn test_find_by_headers_none() { +// let headers = ["Name", "Age"]; +// let bad_headers = ["Name", "BAD"]; + +// assert_eq!(None, Table::find_by_headers("", &headers)); +// assert_eq!(None, Table::find_by_headers("foo", &headers)); +// assert_eq!(None, Table::find_by_headers(HTML_NO_TABLE, &headers)); + +// assert_eq!(None, Table::find_by_headers(TABLE_EMPTY, &bad_headers)); +// assert_eq!(None, Table::find_by_headers(TABLE_TH, &bad_headers)); + +// assert_eq!(None, Table::find_by_headers(TABLE_TD, &headers)); +// assert_eq!(None, Table::find_by_headers(TABLE_TD, &bad_headers)); +// } + +// #[test] +// fn test_find_by_headers_some() { +// let headers: [&str; 0] = []; +// assert!(Table::find_by_headers(TABLE_TH, &headers).is_some()); +// assert!(Table::find_by_headers(TABLE_TH_TD, &headers).is_some()); +// assert!(Table::find_by_headers(HTML_TWO_TABLES, &headers).is_some()); + +// let headers = ["Name"]; +// assert!(Table::find_by_headers(TABLE_TH, &headers).is_some()); +// assert!(Table::find_by_headers(TABLE_TH_TD, &headers).is_some()); +// assert!(Table::find_by_headers(HTML_TWO_TABLES, &headers).is_some()); + +// let headers = ["Age", "Name"]; +// assert!(Table::find_by_headers(TABLE_TH, &headers).is_some()); +// assert!(Table::find_by_headers(TABLE_TH_TD, &headers).is_some()); +// assert!(Table::find_by_headers(HTML_TWO_TABLES, &headers).is_some()); +// } + +// #[test] +// fn test_find_first_incomplete_fragment() { +// assert!(Table::find_first(HTML_TABLE_FRAGMENT).is_some()); +// } + +// #[test] +// fn test_headers_empty() { +// let empty = HashMap::new(); +// assert_eq!(&empty, Table::find_first(TABLE_TD).unwrap().headers()); +// assert_eq!(&empty, Table::find_first(TABLE_TD_TD).unwrap().headers()); +// } + +// #[test] +// fn test_headers_nonempty() { +// let mut headers = HashMap::new(); +// headers.insert("Name".to_string(), 0); +// headers.insert("Age".to_string(), 1); + +// assert_eq!(&headers, Table::find_first(TABLE_TH).unwrap().headers()); +// assert_eq!(&headers, Table::find_first(TABLE_TH_TD).unwrap().headers()); +// assert_eq!(&headers, Table::find_first(TABLE_TH_TH).unwrap().headers()); + +// headers.insert("Extra".to_string(), 2); +// assert_eq!( +// &headers, +// Table::find_first(TABLE_COMPLEX).unwrap().headers() +// ); +// } + +// #[test] +// fn test_iter_empty() { +// assert_eq!(0, Table::find_first(TABLE_EMPTY).unwrap().iter().count()); +// assert_eq!(0, Table::find_first(TABLE_TH).unwrap().iter().count()); +// } + +// #[test] +// fn test_iter_nonempty() { +// assert_eq!(1, Table::find_first(TABLE_TD).unwrap().iter().count()); +// assert_eq!(1, Table::find_first(TABLE_TH_TD).unwrap().iter().count()); +// assert_eq!(2, Table::find_first(TABLE_TD_TD).unwrap().iter().count()); +// assert_eq!(1, Table::find_first(TABLE_TH_TH).unwrap().iter().count()); +// assert_eq!(4, Table::find_first(TABLE_COMPLEX).unwrap().iter().count()); +// } + +// #[test] +// fn test_row_is_empty() { +// let table = Table::find_first(TABLE_TD).unwrap(); +// assert_eq!( +// vec![false], +// table.iter().map(|r| r.is_empty()).collect::>() +// ); + +// let table = Table::find_first(TABLE_COMPLEX).unwrap(); +// assert_eq!( +// vec![false, false, true, false], +// table.iter().map(|r| r.is_empty()).collect::>() +// ); +// } + +// #[test] +// fn test_row_len() { +// let table = Table::find_first(TABLE_TD).unwrap(); +// assert_eq!(vec![2], table.iter().map(|r| r.len()).collect::>()); + +// let table = Table::find_first(TABLE_COMPLEX).unwrap(); +// assert_eq!( +// vec![2, 3, 0, 4], +// table.iter().map(|r| r.len()).collect::>() +// ); +// } + +// #[test] +// fn test_row_len_two_tables() { +// let tables = Table::find_all_tables(HTML_TWO_TABLES).unwrap(); +// let mut tables_iter = tables.iter(); +// let table_1 = tables_iter.next().unwrap(); +// let table_2 = tables_iter.next().unwrap(); +// assert_eq!(vec![2], table_1.iter().map(|r| r.len()).collect::>()); +// assert_eq!(vec![2], table_2.iter().map(|r| r.len()).collect::>()); + +// let tables = Table::find_all_tables(TWO_TABLES_COMPLEX).unwrap(); +// let mut tables_iter = tables.iter(); +// let table_1 = tables_iter.next().unwrap(); +// let table_2 = tables_iter.next().unwrap(); +// assert_eq!( +// vec![2, 3, 0, 4], +// table_1.iter().map(|r| r.len()).collect::>() +// ); +// assert_eq!( +// vec![2, 3, 0, 4], +// table_2.iter().map(|r| r.len()).collect::>() +// ); +// } + +// #[test] +// fn test_row_get_without_headers() { +// let table = Table::find_first(TABLE_TD).unwrap(); +// let mut iter = table.iter(); +// let row = iter.next().unwrap(); + +// assert_eq!(None, row.get("")); +// assert_eq!(None, row.get("foo")); +// assert_eq!(None, row.get("Name")); +// assert_eq!(None, row.get("Age")); + +// assert_eq!(None, iter.next()); +// } + +// #[test] +// fn test_row_get_with_headers() { +// let table = Table::find_first(TABLE_TH_TD).unwrap(); +// let mut iter = table.iter(); +// let row = iter.next().unwrap(); + +// assert_eq!(None, row.get("")); +// assert_eq!(None, row.get("foo")); +// assert_eq!(Some("John"), row.get("Name")); +// assert_eq!(Some("20"), row.get("Age")); + +// assert_eq!(None, iter.next()); +// } + +// #[test] +// fn test_row_get_complex() { +// let table = Table::find_first(TABLE_COMPLEX).unwrap(); +// let mut iter = table.iter(); + +// let row = iter.next().unwrap(); +// assert_eq!(Some("John"), row.get("Name")); +// assert_eq!(Some("20"), row.get("Age")); +// assert_eq!(None, row.get("Extra")); + +// let row = iter.next().unwrap(); +// assert_eq!(Some("May"), row.get("Name")); +// assert_eq!(Some("30"), row.get("Age")); +// assert_eq!(Some("foo"), row.get("Extra")); + +// let row = iter.next().unwrap(); +// assert_eq!(None, row.get("Name")); +// assert_eq!(None, row.get("Age")); +// assert_eq!(None, row.get("Extra")); + +// let row = iter.next().unwrap(); +// assert_eq!(Some("a"), row.get("Name")); +// assert_eq!(Some("b"), row.get("Age")); +// assert_eq!(Some("c"), row.get("Extra")); + +// assert_eq!(None, iter.next()); +// } + +// #[test] +// fn test_two_tables_row_get_complex() { +// let tables = Table::find_all_tables(TWO_TABLES_COMPLEX).unwrap(); +// let mut tables_iter = tables.iter(); +// let table_1 = tables_iter.next().unwrap(); +// let table_2 = tables_iter.next().unwrap(); +// let mut iter_1 = table_1.iter(); +// let mut iter_2 = table_2.iter(); + +// let row_table_1 = iter_1.next().unwrap(); +// let row_table_2 = iter_2.next().unwrap(); +// assert_eq!(Some("John"), row_table_1.get("Name")); +// assert_eq!(Some("20"), row_table_1.get("Age")); +// assert_eq!(None, row_table_1.get("Extra")); +// assert_eq!(Some("Carpenter"), row_table_2.get("Profession")); +// assert_eq!(Some("Single"), row_table_2.get("Civil State")); +// assert_eq!(None, row_table_2.get("Extra")); + +// let row_table_1 = iter_1.next().unwrap(); +// let row_table_2 = iter_2.next().unwrap(); +// assert_eq!(Some("May"), row_table_1.get("Name")); +// assert_eq!(Some("30"), row_table_1.get("Age")); +// assert_eq!(Some("foo"), row_table_1.get("Extra")); +// assert_eq!(Some("Mechanic"), row_table_2.get("Profession")); +// assert_eq!(Some("Married"), row_table_2.get("Civil State")); +// assert_eq!(Some("bar"), row_table_2.get("Extra")); + +// let row_table_1 = iter_1.next().unwrap(); +// let row_table_2 = iter_2.next().unwrap(); +// assert_eq!(None, row_table_1.get("Name")); +// assert_eq!(None, row_table_1.get("Age")); +// assert_eq!(None, row_table_1.get("Extra")); +// assert_eq!(None, row_table_2.get("Name")); +// assert_eq!(None, row_table_2.get("Age")); +// assert_eq!(None, row_table_2.get("Extra")); + +// let row_table_1 = iter_1.next().unwrap(); +// let row_table_2 = iter_2.next().unwrap(); +// assert_eq!(Some("a"), row_table_1.get("Name")); +// assert_eq!(Some("b"), row_table_1.get("Age")); +// assert_eq!(Some("c"), row_table_1.get("Extra")); +// assert_eq!(Some("e"), row_table_2.get("Profession")); +// assert_eq!(Some("f"), row_table_2.get("Civil State")); +// assert_eq!(Some("g"), row_table_2.get("Extra")); + +// assert_eq!(None, iter_1.next()); +// assert_eq!(None, iter_2.next()); +// } + +// #[test] +// fn test_row_as_slice_without_headers() { +// let table = Table::find_first(TABLE_TD).unwrap(); +// let mut iter = table.iter(); + +// assert_eq!(&["Name", "Age"], iter.next().unwrap().as_slice()); +// assert_eq!(None, iter.next()); +// } + +// #[test] +// fn test_row_as_slice_without_headers_two_tables() { +// let tables = Table::find_all_tables(TWO_TABLES_TD).unwrap(); +// let mut tables_iter = tables.iter(); +// let table_1 = tables_iter.next().unwrap(); +// let table_2 = tables_iter.next().unwrap(); +// let mut iter_1 = table_1.iter(); +// let mut iter_2 = table_2.iter(); + +// assert_eq!(&["Name", "Age"], iter_1.next().unwrap().as_slice()); +// assert_eq!( +// &["Profession", "Civil State"], +// iter_2.next().unwrap().as_slice() +// ); +// assert_eq!(None, iter_1.next()); +// assert_eq!(None, iter_2.next()); +// } + +// #[test] +// fn test_row_as_slice_with_headers() { +// let table = Table::find_first(TABLE_TH_TD).unwrap(); +// let mut iter = table.iter(); + +// assert_eq!(&["John", "20"], iter.next().unwrap().as_slice()); +// assert_eq!(None, iter.next()); +// } + +// #[test] +// fn test_row_as_slice_with_headers_two_tables() { +// let tables = Table::find_all_tables(TWO_TABLES_TH_TD).unwrap(); +// let mut tables_iter = tables.iter(); +// let table_1 = tables_iter.next().unwrap(); +// let table_2 = tables_iter.next().unwrap(); +// let mut iter_1 = table_1.iter(); +// let mut iter_2 = table_2.iter(); + +// assert_eq!(&["John", "20"], iter_1.next().unwrap().as_slice()); +// assert_eq!(&["Mechanic", "Single"], iter_2.next().unwrap().as_slice()); +// assert_eq!(None, iter_1.next()); +// assert_eq!(None, iter_2.next()); +// } + +// #[test] +// fn test_row_as_slice_complex() { +// let table = Table::find_first(TABLE_COMPLEX).unwrap(); +// let mut iter = table.iter(); +// let empty: [&str; 0] = []; + +// assert_eq!(&["John", "20"], iter.next().unwrap().as_slice()); +// assert_eq!(&["May", "30", "foo"], iter.next().unwrap().as_slice()); +// assert_eq!(&empty, iter.next().unwrap().as_slice()); +// assert_eq!(&["a", "b", "c", "d"], iter.next().unwrap().as_slice()); +// assert_eq!(None, iter.next()); +// } + +// #[test] +// fn test_row_as_slice_complex_two_tables() { +// let tables = Table::find_all_tables(TWO_TABLES_COMPLEX).unwrap(); +// let mut tables_iter = tables.iter(); +// let table_1 = tables_iter.next().unwrap(); +// let table_2 = tables_iter.next().unwrap(); +// let mut iter_1 = table_1.iter(); +// let mut iter_2 = table_2.iter(); +// let empty: [&str; 0] = []; + +// assert_eq!(&["John", "20"], iter_1.next().unwrap().as_slice()); +// assert_eq!(&["May", "30", "foo"], iter_1.next().unwrap().as_slice()); +// assert_eq!(&empty, iter_1.next().unwrap().as_slice()); +// assert_eq!(&["a", "b", "c", "d"], iter_1.next().unwrap().as_slice()); +// assert_eq!(None, iter_1.next()); +// assert_eq!(&["Carpenter", "Single"], iter_2.next().unwrap().as_slice()); +// assert_eq!( +// &["Mechanic", "Married", "bar"], +// iter_2.next().unwrap().as_slice() +// ); +// assert_eq!(&empty, iter_2.next().unwrap().as_slice()); +// assert_eq!(&["e", "f", "g", "h"], iter_2.next().unwrap().as_slice()); +// assert_eq!(None, iter_2.next()); +// } + +// #[test] +// fn test_row_iter_simple() { +// let table = Table::find_first(TABLE_TD).unwrap(); +// let row = table.iter().next().unwrap(); +// let mut iter = row.iter(); + +// assert_eq!(Some("Name"), iter.next().map(String::as_str)); +// assert_eq!(Some("Age"), iter.next().map(String::as_str)); +// assert_eq!(None, iter.next()); +// } + +// #[test] +// fn test_row_iter_simple_two_tables() { +// let tables = Table::find_all_tables(TWO_TABLES_TD).unwrap(); +// let mut tables_iter = tables.iter(); +// let table_1 = tables_iter.next().unwrap(); +// let table_2 = tables_iter.next().unwrap(); +// let row_1 = table_1.iter().next().unwrap(); +// let row_2 = table_2.iter().next().unwrap(); +// let mut iter_1 = row_1.iter(); +// let mut iter_2 = row_2.iter(); + +// assert_eq!(Some("Name"), iter_1.next().map(String::as_str)); +// assert_eq!(Some("Age"), iter_1.next().map(String::as_str)); +// assert_eq!(None, iter_1.next()); +// assert_eq!(Some("Profession"), iter_2.next().map(String::as_str)); +// assert_eq!(Some("Civil State"), iter_2.next().map(String::as_str)); +// assert_eq!(None, iter_2.next()); +// } + +// #[test] +// fn test_row_iter_complex() { +// let table = Table::find_first(TABLE_COMPLEX).unwrap(); +// let mut table_iter = table.iter(); + +// let row = table_iter.next().unwrap(); +// let mut iter = row.iter(); +// assert_eq!(Some("John"), iter.next().map(String::as_str)); +// assert_eq!(Some("20"), iter.next().map(String::as_str)); +// assert_eq!(None, iter.next()); + +// let row = table_iter.next().unwrap(); +// let mut iter = row.iter(); +// assert_eq!(Some("May"), iter.next().map(String::as_str)); +// assert_eq!(Some("30"), iter.next().map(String::as_str)); +// assert_eq!(Some("foo"), iter.next().map(String::as_str)); +// assert_eq!(None, iter.next()); + +// let row = table_iter.next().unwrap(); +// let mut iter = row.iter(); +// assert_eq!(None, iter.next()); + +// let row = table_iter.next().unwrap(); +// let mut iter = row.iter(); +// assert_eq!(Some("a"), iter.next().map(String::as_str)); +// assert_eq!(Some("b"), iter.next().map(String::as_str)); +// assert_eq!(Some("c"), iter.next().map(String::as_str)); +// assert_eq!(Some("d"), iter.next().map(String::as_str)); +// assert_eq!(None, iter.next()); +// } + +// #[test] +// fn test_row_iter_complex_two_tables() { +// let tables = Table::find_all_tables(TWO_TABLES_COMPLEX).unwrap(); +// let mut tables_iter = tables.iter(); +// let mut table_1 = tables_iter.next().unwrap().iter(); +// let mut table_2 = tables_iter.next().unwrap().iter(); + +// let row_1 = table_1.next().unwrap(); +// let row_2 = table_2.next().unwrap(); +// let mut iter_1 = row_1.iter(); +// let mut iter_2 = row_2.iter(); +// assert_eq!(Some("John"), iter_1.next().map(String::as_str)); +// assert_eq!(Some("20"), iter_1.next().map(String::as_str)); +// assert_eq!(None, iter_1.next()); +// assert_eq!(Some("Carpenter"), iter_2.next().map(String::as_str)); +// assert_eq!(Some("Single"), iter_2.next().map(String::as_str)); +// assert_eq!(None, iter_2.next()); + +// let row_1 = table_1.next().unwrap(); +// let row_2 = table_2.next().unwrap(); +// let mut iter_1 = row_1.iter(); +// let mut iter_2 = row_2.iter(); +// assert_eq!(Some("May"), iter_1.next().map(String::as_str)); +// assert_eq!(Some("30"), iter_1.next().map(String::as_str)); +// assert_eq!(Some("foo"), iter_1.next().map(String::as_str)); +// assert_eq!(None, iter_1.next()); +// assert_eq!(Some("Mechanic"), iter_2.next().map(String::as_str)); +// assert_eq!(Some("Married"), iter_2.next().map(String::as_str)); +// assert_eq!(Some("bar"), iter_2.next().map(String::as_str)); +// assert_eq!(None, iter_2.next()); + +// let row_1 = table_1.next().unwrap(); +// let row_2 = table_2.next().unwrap(); +// let mut iter_1 = row_1.iter(); +// let mut iter_2 = row_2.iter(); +// assert_eq!(None, iter_1.next()); +// assert_eq!(None, iter_2.next()); + +// let row_1 = table_1.next().unwrap(); +// let row_2 = table_2.next().unwrap(); +// let mut iter_1 = row_1.iter(); +// let mut iter_2 = row_2.iter(); +// assert_eq!(Some("a"), iter_1.next().map(String::as_str)); +// assert_eq!(Some("b"), iter_1.next().map(String::as_str)); +// assert_eq!(Some("c"), iter_1.next().map(String::as_str)); +// assert_eq!(Some("d"), iter_1.next().map(String::as_str)); +// assert_eq!(None, iter_1.next()); +// assert_eq!(Some("e"), iter_2.next().map(String::as_str)); +// assert_eq!(Some("f"), iter_2.next().map(String::as_str)); +// assert_eq!(Some("g"), iter_2.next().map(String::as_str)); +// assert_eq!(Some("h"), iter_2.next().map(String::as_str)); +// assert_eq!(None, iter_2.next()); +// } + +// #[test] +// fn test_wikipedia_swapped_rows_columns() { +// // empty columns +// let cols = nu_protocol::value::Value { +// value: nu_protocol::UntaggedValue::Primitive(nu_protocol::Primitive::String( +// "".to_string(), +// )), +// tag: nu_source::Tag::unknown(), +// }; + +// // this table is taken straight from wikipedia with no changes +// let table = retrieve_tables(HTML_TABLE_WIKIPEDIA_COLUMNS_AS_ROWS, &cols, true); + +// let expected = vec![UntaggedValue::row(indexmap! { +// "Stable release".to_string() => UntaggedValue::string("\n 2103 (16.0.13901.20400) / April\u{a0}13, 2021; 4 months ago\u{a0}(2021-04-13)[1]\n ").into(), +// "Developer(s)".to_string() => UntaggedValue::string("Microsoft").into(), +// "Operating system".to_string() => UntaggedValue::string("Microsoft Windows").into(), +// "Type".to_string() => UntaggedValue::string("Spreadsheet").into(), +// "License".to_string() => UntaggedValue::string("Trialware[2]").into(), +// "".to_string() => UntaggedValue::string("").into(), +// "Website".to_string() => UntaggedValue::string("products.office.com/en-us/excel").into(), +// "Initial release".to_string() => UntaggedValue::string("1987; 34\u{a0}years ago\u{a0}(1987)").into(), +// }).into()]; + +// assert_eq!(table, expected); +// } + +// #[test] +// fn test_wikipedia_table_with_column_headers() { +// let cols = UntaggedValue::table(&[ +// UntaggedValue::string("Format".to_string()).into(), +// UntaggedValue::string("Extension".to_string()).into(), +// UntaggedValue::string("Description".to_string()).into(), +// ]) +// .into(); + +// // this table is taken straight from wikipedia with no changes +// let table = retrieve_tables(HTML_TABLE_WIKIPEDIA_WITH_COLUMN_NAMES, &cols, true); +// let expected = vec![ +// UntaggedValue::row(indexmap! { +// "Format".to_string() => UntaggedValue::string("Excel Workbook").into(), +// "Extension".to_string() => UntaggedValue::string(".xlsx").into(), +// "Description".to_string() => UntaggedValue::string("The default Excel 2007 and later workbook format. In reality, a Zip compressed archive with a directory structure of XML text documents. Functions as the primary +// +//replacement for the former binary .xls format, although it does not support Excel macros for security reasons. Saving as .xlsx offers file size reduction over .xls[38]").into(), +// }).into(), +// UntaggedValue::row(indexmap! { +// "Format".to_string() => UntaggedValue::string("Excel Macro-enabled Workbook").into(), +// "Extension".to_string() => UntaggedValue::string(".xlsm").into(), +// "Description".to_string() => UntaggedValue::string("As Excel Workbook, but with macro support.").into(), +// }).into(), +// UntaggedValue::row(indexmap! { +// "Format".to_string() => UntaggedValue::string("Excel Binary Workbook").into(), +// "Extension".to_string() => UntaggedValue::string(".xlsb").into(), +// "Description".to_string() => UntaggedValue::string("As Excel Macro-enabled Workbook, but storing information in binary form rather than XML documents for opening and saving documents more quickly and efficiently. Intended especially for very large documents with tens of thousands of rows, and/or several hundreds of columns. This format is very useful for shrinking large Excel files as is often the case when doing data analysis.").into(), +// }).into(), +// UntaggedValue::row(indexmap! { +// "Format".to_string() => UntaggedValue::string("Excel Macro-enabled Template").into(), +// "Extension".to_string() => UntaggedValue::string(".xltm").into(), +// "Description".to_string() => UntaggedValue::string("A template document that forms a basis for actual workbooks, with macro support. The replacement for the old .xlt format.").into(), +// }).into(), +// UntaggedValue::row(indexmap! { +// "Format".to_string() => UntaggedValue::string("Excel Add-in").into(), +// "Extension".to_string() => UntaggedValue::string(".xlam").into(), +// "Description".to_string() => UntaggedValue::string("Excel add-in to add extra functionality and tools. Inherent macro support because of the file purpose.").into(), +// }).into(), +// ]; + +// assert_eq!(table, expected); +// } +// } diff --git a/docs/3rd_Party_Prompts.md b/docs/3rd_Party_Prompts.md new file mode 100644 index 0000000000..1c6bb9aea2 --- /dev/null +++ b/docs/3rd_Party_Prompts.md @@ -0,0 +1,37 @@ +# How to configure 3rd party prompts + +## nerdfonts + +nerdfonts are not required but they make the presentation much better. + +[site](https://www.nerdfonts.com) + +[repo](https://github.com/ryanoasis/nerd-fonts) + + +## oh-my-posh + +[site](ttps://ohmyposh.dev/) + +[repo](https://github.com/JanDeDobbeleer/oh-my-posh) + + +If you like [oh-my-posh](https://ohmyposh.dev/), you can use oh-my-posh with engine-q with few steps. It's works great with engine-q. There is how to setup oh-my-posh with engine-q: + +1. Install Oh My Posh and download oh-my-posh's themes following [guide](https://ohmyposh.dev/docs/linux#installation) +2. Download and Install a [nerd font](https://github.com/ryanoasis/nerd-fonts) +3. Set the PROMPT_COMMAND in ~/.config/nushell/config.nu, change `M365Princess.omp.json` to whatever you like [Themes demo](https://ohmyposh.dev/docs/themes) +``` +let-env PROMPT_COMMAND = { oh-my-posh --config ~/.poshthemes/M365Princess.omp.json } +``` +4. Restart engine-q. + +## Starship + +[site](https://starship.rs/) + +[repo](https://github.com/starship/starship) + +## Purs + +[repo](https://github.com/xcambar/purs) diff --git a/docs/Environment_Variables.md b/docs/Environment_Variables.md new file mode 100644 index 0000000000..8fb34879e9 --- /dev/null +++ b/docs/Environment_Variables.md @@ -0,0 +1,103 @@ +# Environment Variables (addition explaining the Values part) + +(this is supposed to go to the Environment book chapter) + +## Environment Variables Are Values + +Since Nushell extends the idea of classic shells "Everything is a text" into "Everything is a structured data", it feels natural to apply this philosophy to environment variables as well. +In Nushell, environment variables can hold any value, not just a string. + +Since the host environment (i.e., the OS Nushell is running in) treats environment variables as strings, we need a way to convert them between strings and arbitrary values seamlessly. +In general, there are two places when Nushell needs to interact with the host environment: +1. On startup, Nushell inherits the host environment variables. +2. When running an external program that is not part of Nushell's commands, the program expects environment variables to be strings. +3. When an environment variable is passed to an extenal library in the Nushell's codebase (this includes plugins as well). These variables are listed later in this section. + +## Configuration + +By default, if you do not configure anything, all environment variables are imported as strings on startup and then directly passed to any external program we might be spawning. +However, you can configure selected any environment variable to be converted to/from any value: + +``` +# config.nu + +let config = { + ... other config ... + env_conversions: { + FOO: { + from_string: {|s| $s | split row ':' } + to_string: {|v| $v | str collect ':' } + } + } +} +``` + +The above snippet will configure Nushell to run the `from_string` block with the `FOO` environment variable value as an argument on startup. +Whenever we run some external tool, the `to_string` block will be called with `FOO` as the argument and the result passed to the tool. +You can test the conversions by manually calling them: + +``` +> let-env FOO = "a:b:c" + +> let list = (do $config.env_conversions.from_string $env.FOO) + +> $list +╭───┬───╮ +│ 0 │ a │ +│ 1 │ b │ +│ 2 │ c │ +╰───┴───╯ + +> do $config.env_conversions.to_string $list +a:b:c +``` + +To verify the conversion works on startup, you can first set up `FOO`, then launch a new instance of Nushell (denoted as `>>`): +``` +> let-env FOO = "a:b:c" + +> nu + +>> $env.FOO +╭───┬───╮ +│ 0 │ a │ +│ 1 │ b │ +│ 2 │ c │ +╰───┴───╯ +``` + +To verify we're sending the correct value to an external tool, we would need to make a small program or script that prints its environment variables. +This is not hard, but we have a built-in command `env` to help. +Let's continue the previous session: + +``` +>> env +╭────┬───────────────────┬───────────────┬───────────────────┬───────────────────╮ +│ # │ name │ type │ value │ raw │ +├────┼───────────────────┼───────────────┼───────────────────┼───────────────────┤ +│ 0 │ ... │ ... │ ... │ ... │ +│ X │ FOO │ list │ [list 3 items] │ a:b:c │ +│ Y │ ... │ ... │ ... │ ... │ +╰────┴───────────────────┴───────────────┴───────────────────┴───────────────────╯ +``` + +The `env` command will print every environment variable, its value and a type and also the translated value as a string under the `raw` column. +The `raw` values is the values external tools will see when spawned from Nushell. + +## Special Variables + +Out of the box, Nushell ships with several environment variables serving a special purpose: +* `PROMPT_COMMAND` (block): To set the prompt. Every time Nushell REPL enters a new line, it will run the block stored as its value and set the result as the prompt. +* `PATH`/`Path`: Not yet used except passthrough to externals but is planned to support both its string and list forms. +* `LS_COLORS`: Sets up file coloring rules when running `ls` or `grid`. Supports `env_conversions` settings. + + +## Breaking Changes + +* Setting environment variable to `$nothing` will no longer remove it -- it will be `$nothing`. Instead, you can use `hide $env.FOO`. +* `$env.PROMPT_COMMAND` is a block instead of a string containing the source of the command to run. You can put this into your `config.nu`, for example: `let-env PROMPT_COMMAND = { echo "foo" }`. + +## Future Directions + +* We might add default conversion of PATH/Path environment variables between a list and a string. +* We can make Nushell recognize both PATH and Path (and throw an error if they are both set and have different values?). diff --git a/docs/How_To_Coloring_and_Theming.md b/docs/How_To_Coloring_and_Theming.md new file mode 100644 index 0000000000..45cbf98cf7 --- /dev/null +++ b/docs/How_To_Coloring_and_Theming.md @@ -0,0 +1,465 @@ +# Coloring and Theming in Nushell + +There are a few main parts that nushell allows you to change the color. All of these can be set in the `config.nu` configuration file. If you see the hash/hashtag/pound mark `#` in the config file it means the text after it is commented out. + +1. table borders +2. primitive values +3. flatshapes (this is the command line syntax) +4. prompt +5. LS_COLORS + +## `Table borders` +___ + +Table borders are controlled by the `table_mode` setting in the `config.nu`. Here is an example: +``` +let $config = { + table_mode: rounded +} +``` + +Here are the current options for `table_mode`: +1. `rounded` # of course, this is the best one :) +2. `basic` +3. `compact` +4. `compact_double` +5. `light` +6. `thin` +7. `with_love` +8. `rounded` +9. `reinforced` +10. `heavy` +11. `none` +12. `other` + +### `Color symbologies` +--- + +* `r` - normal color red's abbreviation +* `rb` - normal color red's abbreviation with bold attribute +* `red` - normal color red +* `red_bold` - normal color red with bold attribute +* `"#ff0000"` - "#hex" format foreground color red (quotes are required) +* `{ fg: "#ff0000" bg: "#0000ff" attr: b }` - "full #hex" format foreground red in "#hex" format with a background of blue in "#hex" format with an attribute of bold abbreviated. + +### `attributes` +--- + +|code|meaning| +|-|-| +|l|blink| +|b|bold| +|d|dimmed| +|h|hidden| +|i|italic| +|r|reverse| +|s|strikethrough| +|u|underline| +|n|nothing| +||defaults to nothing| + +### `normal colors` and `abbreviations` + +|code|name| +|-|-| +|g|green| +|gb|green_bold| +|gu|green_underline| +|gi|green_italic| +|gd|green_dimmed| +|gr|green_reverse| +|gbl|green_blink| +|gst|green_strike| +|lg|light_green| +|lgb|light_green_bold| +|lgu|light_green_underline| +|lgi|light_green_italic| +|lgd|light_green_dimmed| +|lgr|light_green_reverse| +|lgbl|light_green_blink| +|lgst|light_green_strike| +|r|red| +|rb|red_bold| +|ru|red_underline| +|ri|red_italic| +|rd|red_dimmed| +|rr|red_reverse| +|rbl|red_blink| +|rst|red_strike| +|lr|light_red| +|lrb|light_red_bold| +|lru|light_red_underline| +|lri|light_red_italic| +|lrd|light_red_dimmed| +|lrr|light_red_reverse| +|lrbl|light_red_blink| +|lrst|light_red_strike| +|u|blue| +|ub|blue_bold| +|uu|blue_underline| +|ui|blue_italic| +|ud|blue_dimmed| +|ur|blue_reverse| +|ubl|blue_blink| +|ust|blue_strike| +|lu|light_blue| +|lub|light_blue_bold| +|luu|light_blue_underline| +|lui|light_blue_italic| +|lud|light_blue_dimmed| +|lur|light_blue_reverse| +|lubl|light_blue_blink| +|lust|light_blue_strike| +|b|black| +|bb|black_bold| +|bu|black_underline| +|bi|black_italic| +|bd|black_dimmed| +|br|black_reverse| +|bbl|black_blink| +|bst|black_strike| +|ligr|light_gray| +|ligrb|light_gray_bold| +|ligru|light_gray_underline| +|ligri|light_gray_italic| +|ligrd|light_gray_dimmed| +|ligrr|light_gray_reverse| +|ligrbl|light_gray_blink| +|ligrst|light_gray_strike| +|y|yellow| +|yb|yellow_bold| +|yu|yellow_underline| +|yi|yellow_italic| +|yd|yellow_dimmed| +|yr|yellow_reverse| +|ybl|yellow_blink| +|yst|yellow_strike| +|ly|light_yellow| +|lyb|light_yellow_bold| +|lyu|light_yellow_underline| +|lyi|light_yellow_italic| +|lyd|light_yellow_dimmed| +|lyr|light_yellow_reverse| +|lybl|light_yellow_blink| +|lyst|light_yellow_strike| +|p|purple| +|pb|purple_bold| +|pu|purple_underline| +|pi|purple_italic| +|pd|purple_dimmed| +|pr|purple_reverse| +|pbl|purple_blink| +|pst|purple_strike| +|lp|light_purple| +|lpb|light_purple_bold| +|lpu|light_purple_underline| +|lpi|light_purple_italic| +|lpd|light_purple_dimmed| +|lpr|light_purple_reverse| +|lpbl|light_purple_blink| +|lpst|light_purple_strike| +|c|cyan| +|cb|cyan_bold| +|cu|cyan_underline| +|ci|cyan_italic| +|cd|cyan_dimmed| +|cr|cyan_reverse| +|cbl|cyan_blink| +|cst|cyan_strike| +|lc|light_cyan| +|lcb|light_cyan_bold| +|lcu|light_cyan_underline| +|lci|light_cyan_italic| +|lcd|light_cyan_dimmed| +|lcr|light_cyan_reverse| +|lcbl|light_cyan_blink| +|lcst|light_cyan_strike| +|w|white| +|wb|white_bold| +|wu|white_underline| +|wi|white_italic| +|wd|white_dimmed| +|wr|white_reverse| +|wbl|white_blink| +|wst|white_strike| +|dgr|dark_gray| +|dgrb|dark_gray_bold| +|dgru|dark_gray_underline| +|dgri|dark_gray_italic| +|dgrd|dark_gray_dimmed| +|dgrr|dark_gray_reverse| +|dgrbl|dark_gray_blink| +|dgrst|dark_gray_strike| + +### `"#hex"` format +--- + +The "#hex" format is one way you typically see colors represented. It's simply the `#` character followed by 6 characters. The first two are for `red`, the second two are for `green`, and the third two are for `blue`. It's important that this string be surrounded in quotes, otherwise nushell thinks it's a commented out string. + +Example: The primary `red` color is `"#ff0000"` or `"#FF0000"`. Upper and lower case in letters shouldn't make a difference. + +This `"#hex"` format allows us to specify 24-bit truecolor tones to different parts of nushell. + +## `full "#hex"` format +--- +The `full "#hex"` format is a take on the `"#hex"` format but allows one to specify the foreground, background, and attributes in one line. + +Example: `{ fg: "#ff0000" bg: "#0000ff" attr: b }` + +* foreground of red in "#hex" format +* background of blue in "#hex" format +* attribute of bold abbreviated + +## `Primitive values` +___ + +Primitive values are things like `int` and `string`. Primitive values and flatshapes can be set with a variety of color symbologies seen above. + +This is the current list of primitives. Not all of these are configurable. The configurable ones are marked with *. + +| primitive | default color | configurable | +| - | - | - | +| `any`|| | +| `binary`|Color::White.normal()| * | +| `block`|Color::White.normal()| * | +| `bool`|Color::White.normal()| * | +| `cellpath`|Color::White.normal()| * | +| `condition`|| | +| `custom`|| | +| `date`|Color::White.normal()| * | +| `duration`|Color::White.normal()| * | +| `expression`|| | +| `filesize`|Color::White.normal()| * | +| `float`|Color::White.normal()| * | +| `glob`|| | +| `import`|| | +| `int`|Color::White.normal()| * | +| `list`|Color::White.normal()| * | +| `nothing`|Color::White.normal()| * | +| `number`|| | +| `operator`|| | +| `path`|| | +| `range`|Color::White.normal()| * | +| `record`|Color::White.normal()| * | +| `signature`|| | +| `string`|Color::White.normal()| * | +| `table`|| | +| `var`|| | +| `vardecl`|| | +| `variable`|| | + +#### special "primitives" (not really primitives but they exist solely for coloring) + +| primitive | default color | configurable | +| - | - | - | +| `leading_trailing_space_bg`|Color::Rgb(128, 128, 128))| *| +| `header`|Color::Green.bold()| *| +| `empty`|Color::Blue.normal()| *| +| `row_index`|Color::Green.bold()| *| +| `hints`|Color::DarkGray.normal()| *| + +Here's a small example of changing some of these values. +``` +let config = { + color_config: { + separator: purple + leading_trailing_space_bg: "#ffffff" + header: gb + date: wd + filesize: c + row_index: cb + bool: red + int: green + duration: blue_bold + range: purple + float: red + string: white + nothing: red + binary: red + cellpath: cyan + hints: dark_gray + } +} +``` +Here's another small example using multiple color syntaxes with some comments. +``` +let config = { + color_config: { + separator: "#88b719" # this sets only the foreground color like PR #486 + leading_trailing_space_bg: white # this sets only the foreground color in the original style + header: { # this is like PR #489 + fg: "#B01455", # note, quotes are required on the values with hex colors + bg: "#ffb900",# note, commas are not required, it could also be all on one line + attr: bli # note, there are no quotes around this value. it works with or without quotes + } + date: "#75507B" + filesize: "#729fcf" + row_index: { # note, that this is another way to set only the foreground, no need to specify bg and attr + fg: "#e50914" + } +} +``` + +## `FlatShape` values + +As mentioned above, `flatshape` is a term used to indicate the sytax coloring. + +Here's the current list of flat shapes. + +| flatshape | default style | configurable | +| - | - | - | +| `flatshape_block`| fg(Color::Blue).bold()| * | +| `flatshape_bool`| fg(Color::LightCyan)| * | +| `flatshape_custom`| bold()| * | +| `flatshape_external`| fg(Color::Cyan)| * | +| `flatshape_externalarg`| fg(Color::Green).bold()| * | +| `flatshape_filepath`| fg(Color::Cyan)| * | +| `flatshape_flag`| fg(Color::Blue).bold()| * | +| `flatshape_float`|fg(Color::Purple).bold() | * | +| `flatshape_garbage`| fg(Color::White).on(Color::Red).bold()| * | +| `flatshape_globpattern`| fg(Color::Cyan).bold()| * | +| `flatshape_int`|fg(Color::Purple).bold() | * | +| `flatshape_internalcall`| fg(Color::Cyan).bold()| * | +| `flatshape_list`| fg(Color::Cyan).bold()| * | +| `flatshape_literal`| fg(Color::Blue)| * | +| `flatshape_nothing`| fg(Color::LightCyan)| * | +| `flatshape_operator`| fg(Color::Yellow)| * | +| `flatshape_range`| fg(Color::Yellow).bold()| * | +| `flatshape_record`| fg(Color::Cyan).bold()| * | +| `flatshape_signature`| fg(Color::Green).bold()| * | +| `flatshape_string`| fg(Color::Green)| * | +| `flatshape_string_interpolation`| fg(Color::Cyan).bold()| * | +| `flatshape_table`| fg(Color::Blue).bold()| * | +| `flatshape_variable`| fg(Color::Purple)| * | + +Here's a small example of how to apply color to these items. Anything not specified will receive the default color. + +``` +let $config = { + color_config: { + flatshape_garbage: { fg: "#FFFFFF" bg: "#FF0000" attr: b} + flatshape_bool: green + flatshape_int: { fg: "#0000ff" attr: b} + } +} +``` + +## `Prompt` configuration and coloring + +The nushell prompt is configurable through these environment variables settings. + +* `PROMPT_COMMAND`: Code to execute for setting up the prompt (block) +* `PROMPT_COMMAND_RIGHT`: Code to execute for setting up the *RIGHT* prompt (block) (see oh-my.nu in nu_scripts) +* `PROMPT_INDICATOR` = "〉": The indicator printed after the prompt (by default ">"-like Unicode symbol) +* `PROMPT_INDICATOR_VI_INSERT` = ": " +* `PROMPT_INDICATOR_VI_NORMAL` = "v " +* `PROMPT_MULTILINE_INDICATOR` = "::: " + +Example: For a simple prompt one could do this. Note that `PROMPT_COMMAND` requires a `block` whereas the others require a `string`. + +`> let-env PROMPT_COMMAND = { build-string (date now | date format '%m/%d/%Y %I:%M:%S%.3f') ': ' (pwd | path basename) }` + +If you don't like the default `PROMPT_INDICATOR` you could change it like this. + +`> let-env PROMPT_INDICATOR = "> "` + +Coloring of the prompt is controlled by the `block` in `PROMPT_COMMAND` where you can write your own custom prompt. We've written a slightly fancy one that has git statuses located in the [nu_scripts repo](https://github.com/nushell/nu_scripts/blob/main/engine-q/prompt/oh-my.nu). + +## `LS_COLORS` colors for the `ls` command + +Nushell will respect and use the `LS_COLORS` environment variable setting on Mac, Linux, and Windows. This setting allows you to define the color of file types when you do a `ls`. For instance, you can make directories one color, *.md markdown files another color, *.toml files yet another color, etc. There are a variety of ways to color your file types. + +There's an exhaustive list [here](https://github.com/trapd00r/LS_COLORS), which is overkill, but gives you an rudimentary understanding of how to create a ls_colors file that `dircolors` can turn into a `LS_COLORS` environment variable. + +[This](https://www.linuxhowto.net/how-to-set-colors-for-ls-command/) is a pretty good introduction to `LS_COLORS`. I'm sure you can fine many more tutorials on the web. + +I like the `vivid` application and currently have it configured in my `config.nu` like this. You can find `vivid` [here](https://github.com/sharkdp/vivid). + +`let-env LS_COLORS = (vivid generate molokai | decode utf-8 | str trim)` + +## Theming + +Theming combines all the coloring above. Here's a quick example of one we put together quickly to demonstrate the ability to theme. This is a spin on the `base16` themes that we see so widespread on the web. + +The key to making theming work is to make sure you specify all themes and colors you're going to use in the `config.nu` file *before* you declare the `let config = ` line. + +``` +# lets define some colors + +let base00 = "#181818" # Default Background +let base01 = "#282828" # Lighter Background (Used for status bars, line number and folding marks) +let base02 = "#383838" # Selection Background +let base03 = "#585858" # Comments, Invisibles, Line Highlighting +let base04 = "#b8b8b8" # Dark Foreground (Used for status bars) +let base05 = "#d8d8d8" # Default Foreground, Caret, Delimiters, Operators +let base06 = "#e8e8e8" # Light Foreground (Not often used) +let base07 = "#f8f8f8" # Light Background (Not often used) +let base08 = "#ab4642" # Variables, XML Tags, Markup Link Text, Markup Lists, Diff Deleted +let base09 = "#dc9656" # Integers, Boolean, Constants, XML Attributes, Markup Link Url +let base0a = "#f7ca88" # Classes, Markup Bold, Search Text Background +let base0b = "#a1b56c" # Strings, Inherited Class, Markup Code, Diff Inserted +let base0c = "#86c1b9" # Support, Regular Expressions, Escape Characters, Markup Quotes +let base0d = "#7cafc2" # Functions, Methods, Attribute IDs, Headings +let base0e = "#ba8baf" # Keywords, Storage, Selector, Markup Italic, Diff Changed +let base0f = "#a16946" # Deprecated, Opening/Closing Embedded Language Tags, e.g. + +# we're creating a theme here that uses the colors we defined above. + +let base16_theme = { + separator: $base03 + leading_trailing_space_bg: $base04 + header: $base0b + date: $base0e + filesize: $base0d + row_index: $base0c + bool: $base08 + int: $base0b + duration: $base08 + range: $base08 + float: $base08 + string: $base04 + nothing: $base08 + binary: $base08 + cellpath: $base08 + hints: dark_gray + + # flatshape_garbage: { fg: $base07 bg: $base08 attr: b} # base16 white on red + # but i like the regular white on red for parse errors + flatshape_garbage: { fg: "#FFFFFF" bg: "#FF0000" attr: b} + flatshape_bool: $base0d + flatshape_int: { fg: $base0e attr: b} + flatshape_float: { fg: $base0e attr: b} + flatshape_range: { fg: $base0a attr: b} + flatshape_internalcall: { fg: $base0c attr: b} + flatshape_external: $base0c + flatshape_externalarg: { fg: $base0b attr: b} + flatshape_literal: $base0d + flatshape_operator: $base0a + flatshape_signature: { fg: $base0b attr: b} + flatshape_string: $base0b + flatshape_filepath: $base0d + flatshape_globpattern: { fg: $base0d attr: b} + flatshape_variable: $base0e + flatshape_flag: { fg: $base0d attr: b} + flatshape_custom: {attr: b} +} + +# now let's apply our regular config settings but also apply the "color_config:" theme that we specified above. + +let config = { + filesize_metric: $true + table_mode: rounded # basic, compact, compact_double, light, thin, with_love, rounded, reinforced, heavy, none, other + use_ls_colors: $true + color_config: $base16_theme # <-- this is the theme + use_grid_icons: $true + footer_mode: always #always, never, number_of_rows, auto + animate_prompt: $false + float_precision: 2 + without_color: $false + filesize_format: "b" # b, kb, kib, mb, mib, gb, gib, tb, tib, pb, pib, eb, eib, zb, zib, auto + edit_mode: emacs # vi + max_history_size: 10000 + log_level: error +} +``` +if you want to go full-tilt on theming, you'll want to theme all the items I mentioned at the very beginning, including LS_COLORS, and the prompt. Good luck! diff --git a/docs/Modules_and_Overlays.md b/docs/Modules_and_Overlays.md new file mode 100644 index 0000000000..930d86fa9a --- /dev/null +++ b/docs/Modules_and_Overlays.md @@ -0,0 +1,356 @@ +# Modules and Overlays + +Similar to many other programming languages, Nushell also has modules that let you import custom commands into a current scope. +However, since Nushell is also a shell, modules allow you to import environment variables which can be used to conveniently activate/deactivate various environments. + +## Basics + +A simple module can be defined like this: +``` +> module greetings { + export def hello [name: string] { + $"hello ($name)!" + } + + export def hi [where: string] { + $"hi ($where)!" + } +} +``` +We defined `hello` and `hi` custom commands inside a `greetings` module. +The `export` keyword makes it possible to later import the commands from the module. +The collection of exported symbols from a module is called an **overlay**. +You can say that the module `greetings` exports an overlay which consists of two custom commands "hello" and "hi". + +By itself, the module does not do anything. +We can verify its existence by printing all available overlays: +``` +> $scope.overlays +╭───┬───────────╮ +│ 0 │ greetings │ +╰───┴───────────╯ +``` + +To actually use its custom commands, we can call `use`: +``` +> use greetings + +> greetings hello "world" +hello world! + +> greetings hi "there" +hi there! +``` +The `hello` and `hi` commands are now available with the `greetings` prefix. + +In general, anything after the `use` keyword forms an **import pattern** which controls how the symbols are imported. +The import pattern can be one of the following +* Module name (just `greetings`): + * Imports all symbols with the module name as a prefix +* Module name + command name (`greetings hello`): + * Import only the selected command into the current scope +* Module name + list of names (`greetings [ hello, hi ]`): + * Import only the listed commands into the current scope +* Module name + everything (`greetings *`): + * Imports all names directly into the current scope + +We saw the first one already. Let's try the other ones: +``` +> use greetings hello + +> hello "world" +hello world! + +> hi "there" # fails because we brought only 'hello' +``` +``` +> use greetings [ hello hi ] + +> hello "world" +hello world! + +> hi "there" +hi there: +``` +``` +> use greetings * + +> hello "world" +hello world! + +> hi "there" +hi there! +``` + +## File as a Module + +Typing the module definition to the command line can be tedious. +You could save the module code into a script and `source` it. +However, there is another way that lets Nushell implicitly treat a source file as a module. +Let's start by saving the body of the module definition into a file: +``` +# greetings.nu + +export def hello [name: string] { + $"hello ($name)!" +} + +export def hi [where: string] { + $"hi ($where)!" +} +``` + +Now, you can use `use` directly on the file: +``` +> use greetings.nu + +> greetings hello "world" +hello world! + +> greetings hi "there" +hi there! +``` + +Nushell automatically infers the module's name from the base name of the file ("greetings" without the ".nu" extension). +You can use any import patterns as described above with the file name instead of the module name. + +## Local Custom Commands + +Any custom commands defined in a module without the `export` keyword will work only in the module's scope: +``` +# greetings.nu + +export def hello [name: string] { + greetings-helper "hello" "world" +} + +export def hi [where: string] { + greetings-helper "hi" "there" +} + +def greetings-helper [greeting: string, subject: string] { + $"($greeting) ($subject)!" +} +``` +Then, in Nushell we import all definitions from the "greetings.nu": +``` +> use greetings.nu * + +> hello "world" +hello world! + +> hi "there" +hi there! + +> greetings-helper "foo" "bar" # fails because 'greetings-helper' is not exported +``` + +## Environment Variables + +So far we used modules just to import custom commands. +It is possible to export environment variables the same way. +The syntax is slightly different than what you might be used to from commands like `let-env` or `load-env`: +``` +# greetings.nu + +export env MYNAME { "Arthur, King of the Britons" } + +export def hello [name: string] { + $"hello ($name)" +} +``` +`use` works the same way as with custom commands: +``` +> use greetings.nu + +> $env."greetings MYNAME" +Arthur, King of the Britons + +> greetings hello $env."greetings MYNAME" +hello Arthur, King of the Britons! +``` + +You can notice we do not assign the value to `MYNAME` directly. +Instead, we give it a block of code (`{ ...}`) that gets evaluated every time we call `use`. +We can demonstrate this property for example with the `random` command: +``` +> module roll { export env ROLL { random dice | into string } } + +> use roll ROLL + +> $env.ROLL +4 + +> $env.ROLL +4 + +> use roll ROLL + +> $env.ROLL +6 + +> $env.ROLL +6 +``` + +## Hiding + +Any custom command or environment variable, imported from a module or not, can be "hidden", restoring the previous definition. +We do this with the `hide` command: +``` +> def foo [] { "foo" } + +> foo +foo + +> hide foo + +> foo # error! command not found! +``` + +The `hide` command also accepts import patterns, just like `use`. +The import pattern is interpreted slightly differently, though. +It can be one of the following: +* Module, custom command, or environment variable name (just `foo` or `greetings`): + * If the name is a custom command or an environment variable, hides it directly. Otherwise: + * If the name is a module name, hides all of its overlay prefixed with the module name +* Module name + name (`greetings hello`): + * Hides only the prefixed command / environment variable +* Module name + list of names (`greetings [ hello, hi ]`): + * Hides only the prefixed commands / environment variables +* Module name + everything (`greetings *`): + * Hides the whole module's overlay, without the prefix + +Let's show these with examples. +We saw direct hiding of a custom command already. +Let's try environment variables: +``` +> let-env FOO = "FOO" + +> $env.FOO +FOO + +> hide FOO + +> $env.FOO # error! environment variable not found! +``` +The first case also applies to commands / environment variables brought from a module (using the "greetings.nu" file defined above): +``` +> use greetings.nu * + +> $env.MYNAME +Arthur, King of the Britons + +> hello "world" +hello world! + +> hide MYNAME + +> $env.MYNAME # error! environment variable not found! + +> hide hello + +> hello "world" # error! command not found! +``` +And finally, when the name is the module name (assuming the previous `greetings` module): +``` +> use greetings.nu + +> $env."greetings MYNAME" +Arthur, King of the Britons + +> greetings hello "world" +hello world! + +> hide greetings + +> $env."greetings MYNAME" # error! environment variable not found! + +> greetings hello "world" # error! command not found! +``` + +To demonstrate the other cases (again, assuming the same `greetings` module): +``` +> use greetings.nu + +> hide greetings hello + +> $env."greetings MYNAME" +Arthur, King of the Britons + +> greetings hello "world" # error! command not found! +``` +``` +> use greetings.nu + +> hide greetings [ hello MYNAME ] + +> $env."greetings MYNAME" # error! environment variable not found! + +> greetings hello "world" # error! command not found! +``` +``` +> use greetings.nu + +> hide greetings * + +> $env."greetings MYNAME" # error! environment variable not found! + +> greetings hello "world" # error! command not found! +``` + +## Examples + +You can find an example config setup at https://github.com/nushell/nu_scripts/tree/main/engine-q/example-config. +It creates the `$config` variable using the module system. + +## Known Issues + +* It might be more appropriate to use `$scope.modules` instead of `$scope.overlays` + +## Future Design Ideas + +The future paragraphs describe some ideas + +### Exporting aliases + +We should allow exporting aliases as it is a common tool for creating shell environments alongside environment variables. +We need to decide a proper syntax. + +### Recursive modules + +We should allow using modules within modules. +That is, allowing to use `use` (and `hide`?) within the `module name { ... }` block or a module file. +This leads to a more generic question of having some standard project layout. + +### Renaming imports + +To avoid name clashing. +For example: `use dataframe as df`. + +### Dynamic names for environment variables + +The `load-env` command exists because we needed to define the environment variable name at runtime. +Currently, both `let-env` and `export env` require static environment variable names. +Could we allow them to accept an expression in place of the name? +For example `export env (whoami | str screaming-snake-case).0 { "foo" }` or `let-env (whoami | str screaming-snake-case).0 = "foo"` + +### To Source or Not To Source + +Currently, there are two ways to define a module in a file: +Write the literal `module name { ... }` into a file, use `source` run the file, then `use` to import from the module. +The second way is to use the `use name.nu` directly, which does not require the `module name { ... }` wrapper. +We can keep it as it is, or push into one of the following directions: + +1. Rename `source` to `run` and modify it so that it runs in its own scope. Any modifications would be lost, it would be more like running a custom command. This would make it impossible for a random script to modify your environment since the only way to do that would be with the module files and the `use` command. The disadvantage is that it makes it impossible to have "startup scripts" and places some roadblocks to the user experience. +2. Remove `use` and rely on `source` and `module name { ... }` only. This resembles, e.g., Julia's `include(file.jl)` style and makes it quite intuitive. It is not very "pure" or "secure" as dedicated module files with `use`. + +We might explore these as we start creating bigger programs and get a feel how a Nushell project structure could look like (and whether or not we should try to enforce one). + +## Unlikely Design Ideas + +### Exporting variables + +`export var name { ... }` which would export a variable the same way you export environment variable. +This would allow for defining global constants in a module (think `math PI`) but can lead to bugs overwriting existing variables. +Use custom commands instead: `export def PI [] { 3.14159 }`. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000..0223e99107 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,3 @@ +# Documentation + +Here is a collection of various pages documenting the changes made in engine-q which should probably end up in the book after we merge it to Nushell. diff --git a/.azure/azure-pipelines.yml b/old_nushell/.azure/azure-pipelines.yml similarity index 100% rename from .azure/azure-pipelines.yml rename to old_nushell/.azure/azure-pipelines.yml diff --git a/.cargo/config.toml b/old_nushell/.cargo/config.toml similarity index 100% rename from .cargo/config.toml rename to old_nushell/.cargo/config.toml diff --git a/.editorconfig b/old_nushell/.editorconfig similarity index 100% rename from .editorconfig rename to old_nushell/.editorconfig diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/old_nushell/.github/ISSUE_TEMPLATE/bug_report.yml similarity index 100% rename from .github/ISSUE_TEMPLATE/bug_report.yml rename to old_nushell/.github/ISSUE_TEMPLATE/bug_report.yml diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/old_nushell/.github/ISSUE_TEMPLATE/feature_request.yml similarity index 100% rename from .github/ISSUE_TEMPLATE/feature_request.yml rename to old_nushell/.github/ISSUE_TEMPLATE/feature_request.yml diff --git a/.github/workflows/release.yml b/old_nushell/.github/workflows/release.yml similarity index 100% rename from .github/workflows/release.yml rename to old_nushell/.github/workflows/release.yml diff --git a/.github/workflows/stale.yml b/old_nushell/.github/workflows/stale.yml similarity index 100% rename from .github/workflows/stale.yml rename to old_nushell/.github/workflows/stale.yml diff --git a/.github/workflows/winget-submission.yml b/old_nushell/.github/workflows/winget-submission.yml similarity index 100% rename from .github/workflows/winget-submission.yml rename to old_nushell/.github/workflows/winget-submission.yml diff --git a/old_nushell/.gitignore b/old_nushell/.gitignore new file mode 100644 index 0000000000..4c234e523b --- /dev/null +++ b/old_nushell/.gitignore @@ -0,0 +1,22 @@ +/target +/scratch +**/*.rs.bk +history.txt +tests/fixtures/nuplayground +crates/*/target + +# Debian/Ubuntu +debian/.debhelper/ +debian/debhelper-build-stamp +debian/files +debian/nu.substvars +debian/nu/ + +# macOS junk +.DS_Store + +# JetBrains' IDE items +.idea/* + +# VSCode's IDE items +.vscode/* diff --git a/CODE_OF_CONDUCT.md b/old_nushell/CODE_OF_CONDUCT.md similarity index 100% rename from CODE_OF_CONDUCT.md rename to old_nushell/CODE_OF_CONDUCT.md diff --git a/CONTRIBUTING.md b/old_nushell/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.md rename to old_nushell/CONTRIBUTING.md diff --git a/old_nushell/Cargo.lock b/old_nushell/Cargo.lock new file mode 100644 index 0000000000..595c1f5a7e --- /dev/null +++ b/old_nushell/Cargo.lock @@ -0,0 +1,5359 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +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", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ef4730490ad1c4eae5c4325b2a95f521d023e5c885853ff7aca0a6a1631db3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "697ed7edc0f1711de49ce108c541623a0af97c6c60b2f6e2b65229847ac843c2" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "ansi_colours" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60e2fb6138a49ad9f1cb3c6d8f8ccbdd5e62b4dab317c1b435a47ecd7da1d28f" +dependencies = [ + "cc", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "anyhow" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203" + +[[package]] +name = "arrayvec" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" +dependencies = [ + "nodrop", +] + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "arrow-format" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7da2d9660bfaebbdb0a44a33b3bd1dcb5a952fafa02c0dfc6a51ea471fef2a" +dependencies = [ + "flatbuffers", +] + +[[package]] +name = "arrow2" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d873e2775c3d87a4e8d77aa544cbd43f34a0779d5164c59e7c6a1dd0678eb395" +dependencies = [ + "ahash", + "arrow-format", + "base64", + "chrono", + "csv", + "futures 0.3.18", + "hash_hasher", + "indexmap", + "lexical-core", + "multiversion", + "num-traits", + "parquet2", + "serde", + "serde_json", + "simdutf8", + "streaming-iterator", + "strength_reduce", +] + +[[package]] +name = "async-stream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171374e7e3b2504e0e5236e3b59260560f9fe94bfe9ac39ba5e4e929c5590625" +dependencies = [ + "async-stream-impl", + "futures-core", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "648ed8c8d2ce5409ccd57453d9d1b214b342a0d69376a6feda1fd6cae3299308" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "backtrace" +version = "0.3.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321629d8ba6513061f26707241fa9bc89524ff1cd7a915a97ef0c62c666ce1b6" +dependencies = [ + "addr2line", + "cc", + "cfg-if 1.0.0", + "libc", + "miniz_oxide 0.4.4", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aaf33151a6429fe9211d1b276eafdf70cdff28b071e76c0b0e1503221ea3744" +dependencies = [ + "num-bigint 0.4.3", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "bitpacking" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c7d2ac73c167c06af4a5f37e6e59d84148d57ccbe4480b76f0273eefea82d7" +dependencies = [ + "crunchy", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71cb90ade945043d3d53597b2fc359bb063db8ade2bcffe7997351d0756e9d50" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80" +dependencies = [ + "alloc-no-stdlib", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" + +[[package]] +name = "byte-unit" +version = "4.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956ffc5b0ec7d7a6949e3f21fd63ba5af4cffdc2ba1e0b7bf62b481458c4ae7f" +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 = "bzip2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6afcd980b5f3a45017c57e57a2fcccbb351cc43a356ce117ef760ef8052b89b0" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "calamine" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b86ca78da4bdce5ac0f0bdbc0218ad14232f1e668376e044233f64c527cf5abb" +dependencies = [ + "byteorder", + "codepage", + "encoding_rs", + "log", + "quick-xml 0.19.0", + "serde", + "zip", +] + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "cc" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "serde", + "time 0.1.44", + "winapi 0.3.9", +] + +[[package]] +name = "chrono-humanize" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eddc119501d583fd930cb92144e605f44e0252c38dd89d9247fffa1993375cb" +dependencies = [ + "chrono", +] + +[[package]] +name = "chrono-tz" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2554a3155fec064362507487171dcc4edc3df60cb10f3a1fb10ed8094822b120" +dependencies = [ + "chrono", + "parse-zoneinfo", +] + +[[package]] +name = "clipboard-win" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db8340083d28acb43451166543b98c838299b7e0863621be53a338adceea0ed" +dependencies = [ + "error-code", + "str-buf", + "winapi 0.3.9", +] + +[[package]] +name = "clircle" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e68bbd985a63de680ab4d1ad77b6306611a8f961b282c8b5ab513e6de934e396" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "serde", + "winapi 0.3.9", +] + +[[package]] +name = "codepage" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b0e9222c0cdf2c6ac27d73f664f9520266fa911c3106329d359f8861cb8bde9" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3993e6445baa160675931ec041a5e03ca84b9c6e32a056150d3aa2bdda0a1f45" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "regex", + "terminal_size", + "unicode-width", + "winapi 0.3.9", +] + +[[package]] +name = "const-sha1" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb58b6451e8c2a812ad979ed1d83378caa5e927eef2622017a45f251457c2c9d" + +[[package]] +name = "content_inspector" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38" +dependencies = [ + "memchr", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "core-foundation" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "cpufeatures" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "738c290dfaea84fc1ca15ad9c168d083b05a714e1efddd8edaab678dc28d2836" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec02e091aa634e2c3ada4a392989e7c3116673ef0ac5b72232439094d73b7fd" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", + "lazy_static", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" +dependencies = [ + "cfg-if 1.0.0", + "lazy_static", +] + +[[package]] +name = "crossterm" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c36c10130df424b2f3552fcc2ddcd9b28a27b1e54b358b45874f88d1ca6888c" +dependencies = [ + "bitflags", + "crossterm_winapi", + "lazy_static", + "libc", + "mio", + "parking_lot", + "signal-hook", + "winapi 0.3.9", +] + +[[package]] +name = "crossterm_winapi" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0da8964ace4d3e4a044fd027919b2237000b24315a37c916f61809f1ff2140b9" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "cssparser" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "matches", + "phf", + "proc-macro2", + "quote", + "smallvec", + "syn", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfae75de57f2b2e85e8768c3ea840fd159c8f33e2b6522c7835b7abac81be16e" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "cstr_core" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "917ba9efe9e1e736671d5a03f006afc4e7e3f32503e2077e0bcaf519c0c8c1d3" +dependencies = [ + "cty", + "memchr", +] + +[[package]] +name = "csv" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" +dependencies = [ + "bstr", + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +dependencies = [ + "memchr", +] + +[[package]] +name = "ctrlc" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "377c9b002a72a0b2c1a18c62e2f3864bdfea4a015e3683a96e24aa45dd6c02d1" +dependencies = [ + "nix", + "winapi 0.3.9", +] + +[[package]] +name = "cty" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version 0.4.0", + "syn", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if 1.0.0", + "dirs-sys-next", +] + +[[package]] +name = "dirs" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if 1.0.0", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" +dependencies = [ + "libc", + "redox_users", + "winapi 0.3.9", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi 0.3.9", +] + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + +[[package]] +name = "dtoa-short" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03329ae10e79ede66c9ce4dc930aa8599043b0743008548680f25b91502d6" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtparse" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13276c5dbd7f365e00efe6631242772fe6615e1899df84d1f6ce3ae7b48209f6" +dependencies = [ + "chrono", + "chrono-tz", + "lazy_static", + "num-traits", + "rust_decimal", +] + +[[package]] +name = "dunce" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a68a4904193147e0a8dec3314640e6db742afd5f6e634f428a6af230d9b3591" + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "eml-parser" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031fe36712cec8b81c5b76b555666ce855a4dfc2dcc35bb907046bf2ef545578" +dependencies = [ + "regex", +] + +[[package]] +name = "encode_unicode" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746" +dependencies = [ + "cfg-if 1.0.0", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "version_check", +] + +[[package]] +name = "error-code" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5115567ac25674e0043e472be13d14e537f37ea8aa4bdc4aef0c89add1db1ff" +dependencies = [ + "libc", + "str-buf", +] + +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fd-lock" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc110fe50727d46a428eed832df40affe9bf74d077cac1bf3f2718e823f14c5" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "windows-sys", +] + +[[package]] +name = "filesize" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12d741e2415d4e2e5bd1c1d00409d1a8865a57892c2d689b504365655d237d43" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "flatbuffers" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4c5738bcd7fad10315029c50026f83c9da5e4a21f8ed66826f43e0e2bde5f6" +dependencies = [ + "bitflags", + "smallvec", + "thiserror", +] + +[[package]] +name = "flate2" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" +dependencies = [ + "cfg-if 1.0.0", + "crc32fast", + "libc", + "miniz_oxide 0.4.4", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c9c1ce3fa9336301af935ab852c437817d14cd33690446569392e65170aac3b" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.1.31" +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" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc8cd39e3dbf865f7340dce6a2d401d24fd37c6fe6c4f0ee0de8bfca2252d27" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445" + +[[package]] +name = "futures-executor" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e481354db6b5c353246ccf6a728b0c5511d752c08da7260546fc0933869daa11" + +[[package]] +name = "futures-macro" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89f17b21645bc4ed773c69af9c9a0effd4a3f1a3876eadd453469f8854e7fdd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "996c6442437b62d21a32cd9906f9c41e7dc1e19a9579843fad948696769305af" + +[[package]] +name = "futures-task" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12" + +[[package]] +name = "futures-util" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e" +dependencies = [ + "futures 0.1.31", + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", + "tokio-io", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getset" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45727250e75cc04ff2846a66397da8ef2b3db8e40e0cef4df67950a07621eb9" +dependencies = [ + "proc-macro-error", + "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 = "git-version" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b0decc02f4636b9ccad390dcbe77b722a77efedfa393caf8379a51d5c61899" +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 = "git2" +version = "0.13.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29229cc1b24c0e6062f6e742aa3e256492a5323365e5ed3413599f8a5eff7d6" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "url", +] + +[[package]] +name = "gjson" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4599d0e9dce476280e2da1f334811e2b26d63a6b000e13b7b50cc980bae49698" + +[[package]] +name = "glob" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd819562fcebdac5afc5c113c3ec36f902840b70fd4fc458799c8ce4607ae55" +dependencies = [ + "bytes 1.1.0", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hamcrest2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f837c62de05dc9cc71ff6486cd85de8856a330395ae338a04bfcefe5e91075" +dependencies = [ + "num 0.2.1", + "regex", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hash_hasher" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74721d007512d0cb3338cd20f0654ac913920061a4c4d0d8708edb3f2a698c0c" + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", + "rayon", +] + +[[package]] +name = "hashlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "heapless" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c1ad878e07405df82b695089e63d278244344f80e764074d0bdfe99b89460f3" +dependencies = [ + "hash32", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aafcf38a1a36118242d29b92e1b08ef84e67e4a5ed06e0a80be20e6a32bfed6b" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "htmlescape" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" + +[[package]] +name = "http" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b" +dependencies = [ + "bytes 1.1.0", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" +dependencies = [ + "bytes 1.1.0", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + +[[package]] +name = "hyper" +version = "0.14.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436ec0091e4f20e655156a30a0df3770fe2900aa301e548e08446ec794b6953c" +dependencies = [ + "bytes 1.1.0", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes 1.1.0", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "ical" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9f7215ad0d77e69644570dee000c7678a47ba7441062c1b5f918adde0d73cf" +dependencies = [ + "thiserror", +] + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "image" +version = "0.23.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "jpeg-decoder", + "num-iter", + "num-rational 0.3.2", + "num-traits", + "png", +] + +[[package]] +name = "indexmap" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +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", +] + +[[package]] +name = "integer-encoding" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90c11140ffea82edce8dcd74137ce9324ec24b3cf0175fc9d7e29164da9915b8" +dependencies = [ + "async-trait", + "futures-util", +] + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + +[[package]] +name = "ipnet" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" + +[[package]] +name = "is_debug" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06d198e9919d9822d5f7083ba8530e04de87841eaf21ead9af8f2304efd57c89" + +[[package]] +name = "is_executable" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9acdc6d67b75e626ad644734e8bc6df893d9cd2a834129065d3dd6158ea9c8" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "itertools" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "jobserver" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34e981f88d060a67815388470172638f1af16b3a12e581cb75142f190161bf9" +dependencies = [ + "lexical-core", +] + +[[package]] +name = "lexical-core" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3926d8f156019890be4abe5fd3785e0cff1001e06f59c597641fd513a5a284" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4d066d004fa762d9da995ed21aa8845bb9f6e4265f540d716fb4b315197bf0e" +dependencies = [ + "lexical-parse-integer", + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-parse-integer" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2c92badda8cc0fc4f3d3cc1c30aaefafb830510c8781ce4e8669881f3ed53ac" +dependencies = [ + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-util" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff669ccaae16ee33af90dc51125755efed17f1309626ba5c12052512b11e291" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "lexical-write-float" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b5186948c7b297abaaa51560f2581dae625e5ce7dfc2d8fdc56345adb6dc576" +dependencies = [ + "lexical-util", + "lexical-write-integer", + "static_assertions", +] + +[[package]] +name = "lexical-write-integer" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ece956492e0e40fd95ef8658a34d53a3b8c2015762fdcaaff2167b28de1f56ef" +dependencies = [ + "lexical-util", + "static_assertions", +] + +[[package]] +name = "libc" +version = "0.2.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" + +[[package]] +name = "libgit2-sys" +version = "0.12.26+1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e1c899248e606fbfe68dcb31d8b0176ebab833b103824af31bddf4b7457494" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + +[[package]] +name = "libm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" + +[[package]] +name = "libsqlite3-sys" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd5850c449b40bacb498b2bbdfaff648b1b055630073ba8db499caf2d0ea9f2" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66" +dependencies = [ + "cc", + "libc", + "pkg-config", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" +dependencies = [ + "serde", + "serde_test", +] + +[[package]] +name = "lock_api" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "lz4" +version = "1.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac20ed6991e01bf6a2e68cc73df2b389707403662a8ba89f68511fb340f724c" +dependencies = [ + "libc", + "lz4-sys", +] + +[[package]] +name = "lz4-sys" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dca79aa95d8b3226213ad454d328369853be3a1382d89532a854f4d69640acae" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "markup5ever" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "md-5" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" +dependencies = [ + "block-buffer", + "digest", + "opaque-debug", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "memmap2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4647a11b578fead29cdbb34d4adef8dd3dc35b876c9c6d5240d83f205abfe96e" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" +dependencies = [ + "autocfg", +] + +[[package]] +name = "meval" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79496a5651c8d57cd033c5add8ca7ee4e3d5f7587a4777484640d9cb60392d9" +dependencies = [ + "fnv", + "nom", +] + +[[package]] +name = "mime" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + +[[package]] +name = "mio" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "winapi 0.3.9", +] + +[[package]] +name = "miow" +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", +] + +[[package]] +name = "multiversion" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "025c962a3dd3cc5e0e520aa9c612201d127dcdf28616974961a649dca64f5373" +dependencies = [ + "multiversion-macros", +] + +[[package]] +name = "multiversion-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a3e2bde382ebf960c1f3e79689fa5941625fe9bf694a1cb64af3e85faff3af" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "native-tls" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3bb9a13fa32bc5aeb64150cd3f32d6cf4c748f8f8a417cce5d2eb976a8370ba" +dependencies = [ + "bitflags", + "cc", + "cfg-if 1.0.0", + "libc", + "memoffset", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "nom" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce" + +[[package]] +name = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "nu" +version = "0.43.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.43.0" +dependencies = [ + "doc-comment", + "overload", + "regex", + "serde", + "serde_json", + "winapi 0.3.9", +] + +[[package]] +name = "nu-cli" +version = "0.43.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.43.0" +dependencies = [ + "base64", + "bigdecimal", + "calamine", + "chrono", + "chrono-tz", + "crossterm", + "csv", + "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", + "nu-ansi-term", + "nu-data", + "nu-engine", + "nu-errors", + "nu-json", + "nu-parser", + "nu-path", + "nu-plugin", + "nu-pretty-hex", + "nu-protocol", + "nu-serde", + "nu-source", + "nu-stream", + "nu-table", + "nu-test-support", + "nu-value-ext", + "num-bigint 0.4.3", + "num-format", + "num-traits", + "parking_lot", + "polars", + "quick-xml 0.22.0", + "quickcheck", + "quickcheck_macros", + "rand 0.8.4", + "regex", + "reqwest", + "roxmltree", + "rust-embed", + "rustyline", + "serde", + "serde_ini", + "serde_json", + "serde_urlencoded", + "serde_yaml", + "sha2", + "shadow-rs", + "strip-ansi-escapes", + "sysinfo", + "term", + "term_size", + "thiserror", + "titlecase", + "tokio", + "toml", + "trash", + "umask", + "unicode-segmentation", + "url", + "users", + "uuid", + "which", + "zip", +] + +[[package]] +name = "nu-completion" +version = "0.43.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.43.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.43.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.43.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.43.0" +dependencies = [ + "lazy_static", + "linked-hash-map", + "nu-path", + "nu-test-support", + "num-traits", + "regex", + "serde", + "serde_json", +] + +[[package]] +name = "nu-parser" +version = "0.43.0" +dependencies = [ + "bigdecimal", + "derive-new", + "indexmap", + "itertools", + "log", + "nu-data", + "nu-errors", + "nu-path", + "nu-protocol", + "nu-source", + "nu-test-support", + "num-bigint 0.4.3", + "smart-default", +] + +[[package]] +name = "nu-path" +version = "0.43.0" +dependencies = [ + "dirs-next", + "dunce", +] + +[[package]] +name = "nu-plugin" +version = "0.43.0" +dependencies = [ + "indexmap", + "nu-errors", + "nu-protocol", + "nu-source", + "nu-test-support", + "nu-value-ext", + "serde", + "serde_json", +] + +[[package]] +name = "nu-pretty-hex" +version = "0.43.0" +dependencies = [ + "heapless", + "nu-ansi-term", + "rand 0.8.4", +] + +[[package]] +name = "nu-protocol" +version = "0.43.0" +dependencies = [ + "bigdecimal", + "byte-unit", + "chrono", + "chrono-humanize", + "derive-new", + "getset", + "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.43.0" +dependencies = [ + "bigdecimal", + "insta", + "nu-protocol", + "nu-source", + "serde", + "thiserror", +] + +[[package]] +name = "nu-source" +version = "0.43.0" +dependencies = [ + "derive-new", + "getset", + "pretty", + "serde", + "termcolor", +] + +[[package]] +name = "nu-stream" +version = "0.43.0" +dependencies = [ + "nu-errors", + "nu-protocol", + "nu-source", +] + +[[package]] +name = "nu-table" +version = "0.43.0" +dependencies = [ + "atty", + "nu-ansi-term", + "regex", + "strip-ansi-escapes", + "unicode-width", +] + +[[package]] +name = "nu-test-support" +version = "0.43.0" +dependencies = [ + "bigdecimal", + "chrono", + "getset", + "glob", + "hamcrest2", + "indexmap", + "nu-errors", + "nu-path", + "nu-protocol", + "nu-source", + "num-bigint 0.4.3", + "tempfile", +] + +[[package]] +name = "nu-value-ext" +version = "0.43.0" +dependencies = [ + "indexmap", + "itertools", + "nu-errors", + "nu-protocol", + "nu-source", + "num-traits", +] + +[[package]] +name = "nu_plugin_binaryview" +version = "0.43.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.43.0" +dependencies = [ + "crossterm", + "nu-data", + "nu-errors", + "nu-plugin", + "nu-protocol", + "nu-source", + "nu-value-ext", + "tui", +] + +[[package]] +name = "nu_plugin_from_bson" +version = "0.43.0" +dependencies = [ + "bigdecimal", + "bson", + "nu-errors", + "nu-plugin", + "nu-protocol", + "nu-source", +] + +[[package]] +name = "nu_plugin_from_mp4" +version = "0.43.0" +dependencies = [ + "mp4", + "nu-errors", + "nu-plugin", + "nu-protocol", + "nu-source", + "tempfile", +] + +[[package]] +name = "nu_plugin_from_sqlite" +version = "0.43.0" +dependencies = [ + "bigdecimal", + "nu-errors", + "nu-plugin", + "nu-protocol", + "nu-source", + "rusqlite", + "tempfile", +] + +[[package]] +name = "nu_plugin_inc" +version = "0.43.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.43.0" +dependencies = [ + "nu-errors", + "nu-plugin", + "nu-protocol", + "regex", +] + +[[package]] +name = "nu_plugin_query_json" +version = "0.43.0" +dependencies = [ + "gjson", + "nu-errors", + "nu-plugin", + "nu-protocol", + "nu-source", +] + +[[package]] +name = "nu_plugin_s3" +version = "0.43.0" +dependencies = [ + "futures 0.3.18", + "nu-errors", + "nu-plugin", + "nu-protocol", + "nu-source", + "s3handler", +] + +[[package]] +name = "nu_plugin_selector" +version = "0.43.0" +dependencies = [ + "indexmap", + "nu-errors", + "nu-plugin", + "nu-protocol", + "nu-source", + "scraper", +] + +[[package]] +name = "nu_plugin_start" +version = "0.43.0" +dependencies = [ + "glob", + "nu-errors", + "nu-plugin", + "nu-protocol", + "nu-source", + "open", + "url", + "webbrowser", +] + +[[package]] +name = "nu_plugin_textview" +version = "0.43.0" +dependencies = [ + "bat", + "nu-data", + "nu-errors", + "nu-plugin", + "nu-protocol", + "nu-source", + "term_size", + "url", +] + +[[package]] +name = "nu_plugin_to_bson" +version = "0.43.0" +dependencies = [ + "bson", + "nu-errors", + "nu-plugin", + "nu-protocol", + "nu-source", + "num-traits", +] + +[[package]] +name = "nu_plugin_to_sqlite" +version = "0.43.0" +dependencies = [ + "hex", + "nu-errors", + "nu-plugin", + "nu-protocol", + "nu-source", + "rusqlite", + "tempfile", +] + +[[package]] +name = "nu_plugin_tree" +version = "0.43.0" +dependencies = [ + "derive-new", + "nu-errors", + "nu-plugin", + "nu-protocol", + "ptree", +] + +[[package]] +name = "nu_plugin_xpath" +version = "0.43.0" +dependencies = [ + "bigdecimal", + "indexmap", + "nu-errors", + "nu-plugin", + "nu-protocol", + "nu-source", + "nu-test-support", + "sxd-document", + "sxd-xpath", +] + +[[package]] +name = "num" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" +dependencies = [ + "num-bigint 0.2.6", + "num-complex 0.2.4", + "num-integer", + "num-iter", + "num-rational 0.2.4", + "num-traits", +] + +[[package]] +name = "num" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +dependencies = [ + "num-bigint 0.4.3", + "num-complex 0.4.0", + "num-integer", + "num-iter", + "num-rational 0.4.0", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg", + "num-integer", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26873667bbbb7c5182d4a37c1add32cdf09f841af72da53318fdb81543c15085" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-format" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bafe4179722c2894288ee77a9f044f02811c86af699344c498b0840c698a2465" +dependencies = [ + "arrayvec 0.4.12", + "itoa", + "num-bigint 0.2.6", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +dependencies = [ + "autocfg", + "num-bigint 0.2.6", + "num-integer", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" +dependencies = [ + "autocfg", + "num-bigint 0.4.3", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "object" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ac1d3f9a1d3616fd9a60c8d74296f22406a238b6a72f5cc1e6f314df4ffbf9" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.8.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", +] + +[[package]] +name = "opaque-debug" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "foreign-types", + "libc", + "once_cell", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" + +[[package]] +name = "openssl-sys" +version = "0.9.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df13d165e607909b363a4757a6f133f8a818a74e9d3a98d09c6128e15fa4c73" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ordered-float" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3305af35278dd29f46fcdd139e0b1fbfae2153f0e5928b39b035542dd31e37b7" +dependencies = [ + "num-traits", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if 1.0.0", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi 0.3.9", +] + +[[package]] +name = "parquet-format-async-temp" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03abc2f9c83fe9ceec83f47c76cc071bfd56caba33794340330f35623ab1f544" +dependencies = [ + "async-trait", + "byteorder", + "futures 0.3.18", + "integer-encoding", + "ordered-float", +] + +[[package]] +name = "parquet2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db82df54cdd88931d29b850190915b9069bb93fba8e1aefc0d59d8ca81603d6d" +dependencies = [ + "async-stream", + "bitpacking", + "brotli", + "flate2", + "futures 0.3.18", + "lz4", + "parquet-format-async-temp", + "snap", + "streaming-decompression", + "zstd", +] + +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "peresil" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f658886ed52e196e850cfbbfddab9eaa7f6d90dd0929e264c31e5cec07e09e57" + +[[package]] +name = "pest" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +dependencies = [ + "ucd-trie", +] + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_macros", + "phf_shared", + "proc-macro-hack", +] + +[[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", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared", + "rand 0.7.3", +] + +[[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", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.22" +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", +] + +[[package]] +name = "polars" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c94a25d46e93b64eac7848c028a545dc08fa01e148e4942c5442b3843c3a598" +dependencies = [ + "polars-core", + "polars-io", + "polars-lazy", +] + +[[package]] +name = "polars-arrow" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cc4488d2f2d6b901bb6e5728e58966013a272cae48861070b676215a79b4a99" +dependencies = [ + "arrow2", + "num 0.4.0", + "thiserror", +] + +[[package]] +name = "polars-core" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6771524063d742a08163d96875ca5df71dff7113f27da58db5ec5fa164165bf6" +dependencies = [ + "ahash", + "anyhow", + "arrow2", + "chrono", + "comfy-table", + "hashbrown", + "itertools", + "lazy_static", + "num 0.4.0", + "num_cpus", + "polars-arrow", + "rand 0.7.3", + "rand_distr", + "rayon", + "regex", + "serde", + "serde_json", + "thiserror", + "unsafe_unwrap", +] + +[[package]] +name = "polars-io" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11a5f5f51525043ee7befd49e586e6919345237826a5f17b53956f8242100957" +dependencies = [ + "ahash", + "anyhow", + "arrow2", + "csv-core", + "dirs", + "lazy_static", + "lexical", + "memchr", + "memmap2", + "num 0.4.0", + "num_cpus", + "polars-arrow", + "polars-core", + "rayon", + "regex", + "simdutf8", +] + +[[package]] +name = "polars-lazy" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3ea647e2fa59d1bbbf90929c5d10ef6a9018aac256d1c6d0e8248211804b61" +dependencies = [ + "ahash", + "itertools", + "polars-arrow", + "polars-core", + "polars-io", + "rayon", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "pretty" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60c0d9f6fc88ecdd245d90c1920ff76a430ab34303fc778d33b1d0a4c3bf6d3" +dependencies = [ + "typed-arena", +] + +[[package]] +name = "pretty_env_logger" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" +dependencies = [ + "env_logger 0.7.1", + "log", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro2" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "ptree" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0de80796b316aec75344095a6d2ef68ec9b8f573b9e7adc821149ba3598e270" +dependencies = [ + "serde", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-xml" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d72d5477478f85bd00b6521780dfba1ec6cdaadcf90b8b181c36d7de561f9b" +dependencies = [ + "encoding_rs", + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b" +dependencies = [ + "memchr", +] + +[[package]] +name = "quickcheck" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" +dependencies = [ + "env_logger 0.8.4", + "log", + "rand 0.8.4", +] + +[[package]] +name = "quickcheck_macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b22a693222d716a9587786f37ac3f6b4faedb5b80c23914e7303ff5a1d8016e9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "rand_hc 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom 0.2.3", +] + +[[package]] +name = "rand_distr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9532ada3929fb8b2e9dbe28d1e06c9b2cc65813f074fcb6bd5fbefeff9d56" +dependencies = [ + "num-traits", + "rand 0.7.3", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core 0.6.3", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rawkey" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ad6efac35ef044d565b23f0d111d76aa21ab2e86934b1225f7071d42e58ebad" +dependencies = [ + "readkey", + "user32-sys", + "winapi 0.3.9", + "x11", +] + +[[package]] +name = "rayon" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" +dependencies = [ + "autocfg", + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "lazy_static", + "num_cpus", +] + +[[package]] +name = "readkey" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86d401b6d6a1725a59f1b4e813275d289dff3ad09c72b373a10a7a8217ba3146" + +[[package]] +name = "redox_syscall" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags", +] + +[[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", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "reqwest" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bea77bc708afa10e59905c3d4af7c8fd43c9214251673095ff8b14345fcbc5" +dependencies = [ + "base64", + "bytes 1.1.0", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "result" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194d8e591e405d1eecf28819740abed6d719d1a2db87fc0bcdedee9a26d55560" + +[[package]] +name = "roxmltree" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "921904a62e410e37e215c40381b7117f830d9d89ba60ab5236170541dd25646b" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "rstest" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "041bb0202c14f6a158bbbf086afb03d0c6e975c2dec7d4912f8061ed44f290af" +dependencies = [ + "cfg-if 1.0.0", + "proc-macro2", + "quote", + "rustc_version 0.3.3", + "syn", +] + +[[package]] +name = "rusqlite" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a82b0b91fad72160c56bf8da7a549b25d7c31109f52cc1437eac4c0ad2550a7" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "memchr", + "smallvec", +] + +[[package]] +name = "rust-embed" +version = "5.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fe1fe6aac5d6bb9e1ffd81002340363272a7648234ec7bdfac5ee202cb65523" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "5.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed91c41c42ef7bf687384439c312e75e0da9c149b0390889b94de3c7d9d9e66" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a512219132473ab0a77b52077059f1c47ce4af7fbdc94503e9862a34422876d" +dependencies = [ + "walkdir", +] + +[[package]] +name = "rust_decimal" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a93c95e3d5c1d997e6e4ba9bda898f4e1d73934cd05510c972f10087d0ef00c1" +dependencies = [ + "byteorder", + "lazy_static", + "num 0.2.1", + "serde", +] + +[[package]] +name = "rustc-demangle" +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", +] + +[[package]] +name = "rustversion" +version = "1.0.5" +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", +] + +[[package]] +name = "ryu" +version = "1.0.6" +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" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi 0.3.9", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "scraper" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e02aa790c80c2e494130dec6a522033b6a23603ffc06360e9fe6c611ea2c12" +dependencies = [ + "cssparser", + "ego-tree", + "getopts", + "html5ever", + "matches", + "selectors", + "smallvec", + "tendril", +] + +[[package]] +name = "security-framework" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" +dependencies = [ + "bitflags", + "cssparser", + "derive_more", + "fxhash", + "log", + "matches", + "phf", + "phf_codegen", + "precomputed-hash", + "servo_arc", + "smallvec", + "thin-slice", +] + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" + +[[package]] +name = "semver-parser" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" +dependencies = [ + "pest", +] + +[[package]] +name = "serde" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_ini" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb236687e2bb073a7521c021949be944641e671b8505a94069ca37b656c81139" +dependencies = [ + "result", + "serde", + "void", +] + +[[package]] +name = "serde_json" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_test" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82178225dbdeae2d5d190e8649287db6a3a32c6d24da22ae3146325aa353e4c" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8c608a35705a5d3cdc9fbe403147647ff34b921f8e833e49306df898f9b20af" +dependencies = [ + "dtoa", + "indexmap", + "serde", + "yaml-rust", +] + +[[package]] +name = "serial_test" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0bccbcf40c8938196944a3da0e133e031a33f4d6b72db3bda3cc556e361905d" +dependencies = [ + "lazy_static", + "parking_lot", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2acd6defeddb41eb60bb468f8825d0cfd0c2a76bc03bfd235b6a1dc4f6a1ad5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "servo_arc" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc30b1e1e8c40c121ca33b86c23308a090d19974ef001b4bf6e61fd1a0fb095c" + +[[package]] +name = "sha2" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa" +dependencies = [ + "block-buffer", + "cfg-if 1.0.0", + "cpufeatures", + "digest", + "opaque-debug", +] + +[[package]] +name = "shadow-rs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8935e920eb80ff8f5a5bced990325d12f6cc1015154a3852c6a23cf5bd71c447" +dependencies = [ + "chrono", + "git2", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e31d442c16f047a671b5a71e2161d6e68814012b7f5379d269ebd915fac2729" +dependencies = [ + "libc", + "mio", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "simdutf8" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970da16e7c682fa90a261cf0724dee241c9f7831635ecc4e988ae8f3b505559" + +[[package]] +name = "similar" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec" + +[[package]] +name = "siphasher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b" + +[[package]] +name = "slab" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" + +[[package]] +name = "smallvec" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" + +[[package]] +name = "smart-default" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133659a15339456eeeb07572eb02a91c91e9815e9cbc89566944d2c8d3efdbf6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "snap" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45456094d1983e2ee2a18fdfebce3189fa451699d0502cb8e3b49dba5ba41451" + +[[package]] +name = "socket2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "spin" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "511254be0c5bcf062b019a6c89c01a664aa359ded62f78aa72c6fc137c0590e5" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bc687acd5dc742c4a7094f2927a8614a68e4743ef682e7a2f9f0f711656cc92" +dependencies = [ + "fallible-streaming-iterator", +] + +[[package]] +name = "streaming-iterator" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "303235c177994a476226b80d076bd333b7b560fb05bd242a10609d11b07f81f5" + +[[package]] +name = "strength_reduce" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3ff2f71c82567c565ba4b3009a9350a96a7269eaa4001ebedae926230bc2254" + +[[package]] +name = "string_cache" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923f0f39b6267d37d23ce71ae7235602134b250ace715dd2c90421998ddac0c6" +dependencies = [ + "lazy_static", + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f24c8e5e19d22a726626f1a5e16fe15b132dcf21d10177fa5a45ce7962996b97" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strip-ansi-escapes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "011cbb39cf7c1f62871aea3cc46e5817b0937b49e9447370c93cacbe93a766d8" +dependencies = [ + "vte", +] + +[[package]] +name = "strum" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7318c509b5ba57f18533982607f24070a55d353e90d4cae30c467cdb2ad5ac5c" + +[[package]] +name = "strum_macros" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8bc6b87a5112aeeab1f4a9f7ab634fe6cbefc4850006df31267f4cfb9e3149" +dependencies = [ + "heck 0.3.3", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "sxd-document" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94d82f37be9faf1b10a82c4bd492b74f698e40082f0f40de38ab275f31d42078" +dependencies = [ + "peresil", + "typed-arena", +] + +[[package]] +name = "sxd-xpath" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36e39da5d30887b5690e29de4c5ebb8ddff64ebd9933f98a01daaa4fd11b36ea" +dependencies = [ + "peresil", + "quick-error", + "sxd-document", +] + +[[package]] +name = "syn" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91f89ebb59fa30d4f65fafc2d68e94f6975256fd87e812dd99cb6e020c8563df" +dependencies = [ + "cc", + "cstr_core", + "libc", + "web-sys", + "winapi 0.3.9", +] + +[[package]] +name = "sysinfo" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e757000a4bed2b1be9be65a3f418b9696adf30bb419214c73997422de73a591" +dependencies = [ + "cfg-if 1.0.0", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "winapi 0.3.9", +] + +[[package]] +name = "tempfile" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "rand 0.8.4", + "redox_syscall", + "remove_dir_all", + "winapi 0.3.9", +] + +[[package]] +name = "tendril" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9ef557cb397a4f0a5a3a628f06515f78563f2209e64d47055d9dc6052bf5e33" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +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", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "thin-slice" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" + +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +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", +] + +[[package]] +name = "tinyvec" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "titlecase" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f565e410cfc24c2f2a89960b023ca192689d7f77d3f8d4f4af50c2d8affe1117" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "tokio" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144" +dependencies = [ + "autocfg", + "bytes 1.1.0", + "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", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" +dependencies = [ + "bytes 1.1.0", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "tower-service" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + +[[package]] +name = "tracing" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" +dependencies = [ + "cfg-if 1.0.0", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "trash" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3ebb6cb2db7947ab9f65dec9f7c5dbe01042b708f564242dcfb6d5cb2957cbc" +dependencies = [ + "chrono", + "libc", + "log", + "objc", + "scopeguard", + "url", + "windows", +] + +[[package]] +name = "try-lock" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9b2228007eba4120145f785df0f6c92ea538f5a3635a612ecf4e334c8c1446d" + +[[package]] +name = "typenum" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" + +[[package]] +name = "ucd-trie" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" + +[[package]] +name = "umask" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "982efbf70ec4d28f7862062c03dd1a4def601a5079e0faf1edc55f2ad0f6fe46" + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "unsafe_unwrap" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1230ec65f13e0f9b28d789da20d2d419511893ea9dac2c1f4ef67b8b14e5da80" + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" +dependencies = [ + "libc", + "log", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-width" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cf7d77f457ef8dfa11e4cd5933c5ddb5dc52a94664071951219a97710f0a32b" + +[[package]] +name = "utf8parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.3", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "vte" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983" +dependencies = [ + "arrayvec 0.5.2", + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi 0.3.9", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasm-bindgen" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" + +[[package]] +name = "web-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea187a8ef279bc014ec368c27a920da2024d2a711109bfbe3440585d5cf27ad9" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "361f3533a83ee1a28c9be59683f40043db02dbedf6479ce8795657386195c97f" +dependencies = [ + "const-sha1", + "windows_gen", + "windows_macros", +] + +[[package]] +name = "windows-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82ca39602d5cbfa692c4b67e3bcbb2751477355141c1ed434c94da4186836ff6" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52695a41e536859d5308cc613b4a022261a274390b25bd29dfff4bf08505f3c2" + +[[package]] +name = "windows_gen" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54154dbc515d58723f6b6053c12f1065da7389f733660581b2391bd1af480452" +dependencies = [ + "syn", +] + +[[package]] +name = "windows_i686_gnu" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f54725ac23affef038fecb177de6c9bf065787c2f432f79e3c373da92f3e1d8a" + +[[package]] +name = "windows_i686_msvc" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d5158a43cc43623c0729d1ad6647e62fa384a3d135fd15108d37c683461f64" + +[[package]] +name = "windows_macros" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f7794c652845dc466cb8dc1b86c08345707c8144bc53e9086430047c7d33b76" +dependencies = [ + "syn", + "windows_gen", +] + +[[package]] +name = "windows_x86_64_gnu" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc31f409f565611535130cfe7ee8e6655d3fa99c1c61013981e491921b5ce954" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f2b8c7cbd3bfdddd9ab98769f9746a7fad1bca236554cd032b78d768bc0e89f" + +[[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi 0.3.9", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "114ba2b24d2167ef6d67d7d04c8cc86522b87f490025f39f0303b7db5bf5e3d8" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "zip" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815" +dependencies = [ + "byteorder", + "bzip2", + "crc32fast", + "flate2", + "thiserror", + "time 0.1.44", +] + +[[package]] +name = "zstd" +version = "0.9.0+zstd.1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07749a5dc2cb6b36661290245e350f15ec3bbb304e493db54a1d354480522ccd" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "4.1.1+zstd.1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91c90f2c593b003603e5e0493c837088df4469da25aafff8bce42ba48caf079" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "1.6.1+zstd.1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "615120c7a2431d16cf1cf979e7fc31ba7a5b5e5707b29c8a99e5dbf8a8392a33" +dependencies = [ + "cc", + "libc", +] diff --git a/old_nushell/Cargo.toml b/old_nushell/Cargo.toml new file mode 100644 index 0000000000..a831623195 --- /dev/null +++ b/old_nushell/Cargo.toml @@ -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.43.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.43.0", path="./crates/nu-cli", default-features=false } +nu-command = { version = "0.43.0", path="./crates/nu-command" } +nu-completion = { version = "0.43.0", path="./crates/nu-completion" } +nu-data = { version = "0.43.0", path="./crates/nu-data" } +nu-engine = { version = "0.43.0", path="./crates/nu-engine" } +nu-errors = { version = "0.43.0", path="./crates/nu-errors" } +nu-parser = { version = "0.43.0", path="./crates/nu-parser" } +nu-path = { version = "0.43.0", path="./crates/nu-path" } +nu-plugin = { version = "0.43.0", path="./crates/nu-plugin" } +nu-protocol = { version = "0.43.0", path="./crates/nu-protocol" } +nu-source = { version = "0.43.0", path="./crates/nu-source" } +nu-value-ext = { version = "0.43.0", path="./crates/nu-value-ext" } + +nu_plugin_binaryview = { version = "0.43.0", path="./crates/nu_plugin_binaryview", optional=true } +nu_plugin_chart = { version = "0.43.0", path="./crates/nu_plugin_chart", optional=true } +nu_plugin_from_bson = { version = "0.43.0", path="./crates/nu_plugin_from_bson", optional=true } +nu_plugin_from_sqlite = { version = "0.43.0", path="./crates/nu_plugin_from_sqlite", optional=true } +nu_plugin_inc = { version = "0.43.0", path="./crates/nu_plugin_inc", optional=true } +nu_plugin_match = { version = "0.43.0", path="./crates/nu_plugin_match", optional=true } +nu_plugin_query_json = { version = "0.43.0", path="./crates/nu_plugin_query_json", optional=true } +nu_plugin_s3 = { version = "0.43.0", path="./crates/nu_plugin_s3", optional=true } +nu_plugin_selector = { version = "0.43.0", path="./crates/nu_plugin_selector", optional=true } +nu_plugin_start = { version = "0.43.0", path="./crates/nu_plugin_start", optional=true } +nu_plugin_textview = { version = "0.43.0", path="./crates/nu_plugin_textview", optional=true } +nu_plugin_to_bson = { version = "0.43.0", path="./crates/nu_plugin_to_bson", optional=true } +nu_plugin_to_sqlite = { version = "0.43.0", path="./crates/nu_plugin_to_sqlite", optional=true } +nu_plugin_tree = { version = "0.43.0", path="./crates/nu_plugin_tree", optional=true } +nu_plugin_xpath = { version = "0.43.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.43.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/old_nushell/LICENSE b/old_nushell/LICENSE new file mode 100644 index 0000000000..70f227d78f --- /dev/null +++ b/old_nushell/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 - 2021 Nushell Project + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile.toml b/old_nushell/Makefile.toml similarity index 100% rename from Makefile.toml rename to old_nushell/Makefile.toml diff --git a/README.build.txt b/old_nushell/README.build.txt similarity index 100% rename from README.build.txt rename to old_nushell/README.build.txt diff --git a/old_nushell/README.md b/old_nushell/README.md new file mode 100644 index 0000000000..ea2da1fb1d --- /dev/null +++ b/old_nushell/README.md @@ -0,0 +1,283 @@ +# README + +[![Crates.io](https://img.shields.io/crates/v/nu.svg)](https://crates.io/crates/nu) +[![Build Status](https://dev.azure.com/nushell/nushell/_apis/build/status/nushell.nushell?branchName=main)](https://dev.azure.com/nushell/nushell/_build/latest?definitionId=2&branchName=main) +[![Discord](https://img.shields.io/discord/601130461678272522.svg?logo=discord)](https://discord.gg/NtAbbGn) +[![The Changelog #363](https://img.shields.io/badge/The%20Changelog-%23363-61c192.svg)](https://changelog.com/podcast/363) +[![@nu_shell](https://img.shields.io/badge/twitter-@nu_shell-1DA1F3?style=flat-square)](https://twitter.com/nu_shell) +![GitHub commit activity](https://img.shields.io/github/commit-activity/m/nushell/nushell) +![GitHub contributors](https://img.shields.io/github/contributors/nushell/nushell) + +## Nushell + +A new type of shell. + +![Example of nushell](images/nushell-autocomplete5.gif "Example of nushell") + +## Status + +This project has reached a minimum-viable product level of quality. +While contributors dogfood it as their daily driver, it may be unstable for some commands. +Future releases will work to fill out missing features and improve stability. +Its design is also subject to change as it matures. + +Nu comes with a set of built-in commands (listed below). +If a command is unknown, the command will shell-out and execute it (using cmd on Windows or bash on Linux and macOS), correctly passing through stdin, stdout, and stderr, so things like your daily git workflows and even `vim` will work just fine. + +## Learning more + +There are a few good resources to learn about Nu. +There is a [book](https://www.nushell.sh/book/) about Nu that is currently in progress. +The book focuses on using Nu and its core concepts. + +If you're a developer who would like to contribute to Nu, we're also working on a [book for developers](https://www.nushell.sh/contributor-book/) to help you get started. +There are also [good first issues](https://github.com/nushell/nushell/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) to help you dive in. + +We also have an active [Discord](https://discord.gg/NtAbbGn) and [Twitter](https://twitter.com/nu_shell) if you'd like to come and chat with us. + +You can also find information on more specific topics in our [cookbook](https://www.nushell.sh/cookbook/). + +## Installation + +### Local + +Up-to-date installation instructions can be found in the [installation chapter of the book](https://www.nushell.sh/book/installation.html). **Windows users**: please note that Nu works on Windows 10 and does not currently have Windows 7/8.1 support. + +To build Nu, you will need to use the **latest stable (1.51 or later)** version of the compiler. + +Required dependencies: + +- pkg-config and libssl (only needed on Linux) + - On Debian/Ubuntu: `apt install pkg-config libssl-dev` + +Optional dependencies: + +- To use Nu with all possible optional features enabled, you'll also need the following: + - On Linux (on Debian/Ubuntu): `apt install libxcb-composite0-dev libx11-dev` + +To install Nu via cargo (make sure you have installed [rustup](https://rustup.rs/) and the latest stable compiler via `rustup install stable`): + +```shell +cargo install nu +``` + +To install Nu via the [Windows Package Manager](https://aka.ms/winget-cli): + +```shell +winget install nushell +``` + +You can also build Nu yourself with all the bells and whistles (be sure to have installed the [dependencies](https://www.nushell.sh/book/installation.html#dependencies) for your platform), once you have checked out this repo with git: + +```shell +cargo build --workspace --features=extra +``` +### Packaging status + +[![Packaging status](https://repology.org/badge/vertical-allrepos/nushell.svg)](https://repology.org/project/nushell/versions) + +#### Fedora + +[COPR repo](https://copr.fedorainfracloud.org/coprs/atim/nushell/): `sudo dnf copr enable atim/nushell -y && sudo dnf install nushell -y` + +## Philosophy + +Nu draws inspiration from projects like PowerShell, functional programming languages, and modern CLI tools. +Rather than thinking of files and services as raw streams of text, Nu looks at each input as something with structure. +For example, when you list the contents of a directory, what you get back is a table of rows, where each row represents an item in that directory. +These values can be piped through a series of steps, in a series of commands called a 'pipeline'. + +### Pipelines + +In Unix, it's common to pipe between commands to split up a sophisticated command over multiple steps. +Nu takes this a step further and builds heavily on the idea of _pipelines_. +Just as the Unix philosophy, Nu allows commands to output to stdout and read from stdin. +Additionally, commands can output structured data (you can think of this as a third kind of stream). +Commands that work in the pipeline fit into one of three categories: + +- Commands that produce a stream (e.g., `ls`) +- Commands that filter a stream (eg, `where type == "Dir"`) +- Commands that consume the output of the pipeline (e.g., `autoview`) + +Commands are separated by the pipe symbol (`|`) to denote a pipeline flowing left to right. + +```shell +> ls | where type == "Dir" | autoview +───┬────────┬──────┬───────┬────────────── + # │ name │ type │ size │ modified +───┼────────┼──────┼───────┼────────────── + 0 │ assets │ Dir │ 128 B │ 5 months ago + 1 │ crates │ Dir │ 704 B │ 50 mins ago + 2 │ debian │ Dir │ 352 B │ 5 months ago + 3 │ docs │ Dir │ 192 B │ 50 mins ago + 4 │ images │ Dir │ 160 B │ 5 months ago + 5 │ src │ Dir │ 128 B │ 1 day ago + 6 │ target │ Dir │ 160 B │ 5 days ago + 7 │ tests │ Dir │ 192 B │ 3 months ago +───┴────────┴──────┴───────┴────────────── +``` + +Because most of the time you'll want to see the output of a pipeline, `autoview` is assumed. +We could have also written the above: + +```shell +> ls | where type == Dir +``` + +Being able to use the same commands and compose them differently is an important philosophy in Nu. +For example, we could use the built-in `ps` command to get a list of the running processes, using the same `where` as above. + +```shell +> ps | where cpu > 0 +───┬────────┬───────────────────┬──────────┬─────────┬──────────┬────────── + # │ pid │ name │ status │ cpu │ mem │ virtual +───┼────────┼───────────────────┼──────────┼─────────┼──────────┼────────── + 0 │ 435 │ irq/142-SYNA327 │ Sleeping │ 7.5699 │ 0 B │ 0 B + 1 │ 1609 │ pulseaudio │ Sleeping │ 6.5605 │ 10.6 MB │ 2.3 GB + 2 │ 1625 │ gnome-shell │ Sleeping │ 6.5684 │ 639.6 MB │ 7.3 GB + 3 │ 2202 │ Web Content │ Sleeping │ 6.8157 │ 320.8 MB │ 3.0 GB + 4 │ 328788 │ nu_plugin_core_ps │ Sleeping │ 92.5750 │ 5.9 MB │ 633.2 MB +───┴────────┴───────────────────┴──────────┴─────────┴──────────┴────────── +``` + +### Opening files + +Nu can load file and URL contents as raw text or structured data (if it recognizes the format). +For example, you can load a .toml file as structured data and explore it: + +```shell +> open Cargo.toml +────────────────────┬─────────────────────────── + bin │ [table 18 rows] + build-dependencies │ [row serde toml] + dependencies │ [row 29 columns] + dev-dependencies │ [row nu-test-support] + features │ [row 19 columns] + package │ [row 12 columns] + workspace │ [row members] +────────────────────┴─────────────────────────── +``` + +We can pipeline this into a command that gets the contents of one of the columns: + +```shell +> open Cargo.toml | get package +───────────────┬──────────────────────────────────── + authors │ [table 1 rows] + default-run │ nu + description │ A new type of shell + documentation │ https://www.nushell.sh/book/ + edition │ 2018 + exclude │ [table 1 rows] + homepage │ https://www.nushell.sh + license │ MIT + name │ nu + readme │ README.md + repository │ https://github.com/nushell/nushell + version │ 0.32.0 +───────────────┴──────────────────────────────────── +``` + +Finally, we can use commands outside of Nu once we have the data we want: + +```shell +> open Cargo.toml | get package.version +0.32.0 +``` + +### Configuration + +Nu has early support for configuring the shell. You can refer to the book for a list of [all supported variables](https://www.nushell.sh/book/configuration.html). + +To set one of these variables, you can use `config set`. For example: + +```shell +> config set line_editor.edit_mode "vi" +> config set path $nu.path +``` + +### Shells + +Nu will work inside of a single directory and allow you to navigate around your filesystem by default. +Nu also offers a way of adding additional working directories that you can jump between, allowing you to work in multiple directories simultaneously. + +To do so, use the `enter` command, which will allow you to create a new "shell" and enter it at the specified path. +You can toggle between this new shell and the original shell with the `p` (for previous) and `n` (for next), allowing you to navigate around a ring buffer of shells. +Once you're done with a shell, you can `exit` it and remove it from the ring buffer. + +Finally, to get a list of all the current shells, you can use the `shells` command. + +### Plugins + +Nu supports plugins that offer additional functionality to the shell and follow the same structured data model that built-in commands use. +This allows you to extend nu for your needs. + +There are a few examples in the `plugins` directory. + +Plugins are binaries that are available in your path and follow a `nu_plugin_*` naming convention. +These binaries interact with nu via a simple JSON-RPC protocol where the command identifies itself and passes along its configuration, making it available for use. +If the plugin is a filter, data streams to it one element at a time, and it can stream data back in return via stdin/stdout. +If the plugin is a sink, it is given the full vector of final data and is given free reign over stdin/stdout to use as it pleases. + +## Goals + +Nu adheres closely to a set of goals that make up its design philosophy. As features are added, they are checked against these goals. + +- First and foremost, Nu is cross-platform. Commands and techniques should carry between platforms and offer consistent first-class support for Windows, macOS, and Linux. + +- Nu ensures direct compatibility with existing platform-specific executables that make up people's workflows. + +- Nu's workflow and tools should have the usability in day-to-day experience of using a shell in 2019 (and beyond). + +- Nu views data as both structured and unstructured. It is a structured shell like PowerShell. + +- Finally, Nu views data functionally. Rather than using mutation, pipelines act as a means to load, change, and save data without mutable state. + +## Commands + +You can find a list of Nu commands, complete with documentation, in [quick command references](https://www.nushell.sh/book/command_reference.html). + +## Progress + +Nu is in heavy development and will naturally change as it matures and people use it. The chart below isn't meant to be exhaustive, but rather helps give an idea for some of the areas of development and their relative completion: + +| Features | Not started | Prototype | MVP | Preview | Mature | Notes | +| ------------- | :---------: | :-------: | :-: | :-----: | :----: | -------------------------------------------------------------------- | +| Aliases | | | X | | | Aliases allow for shortening large commands, while passing flags | +| Notebook | | X | | | | Initial jupyter support, but it loses state and lacks features | +| File ops | | | X | | | cp, mv, rm, mkdir have some support, but lacking others | +| Environment | | | X | | | Temporary environment and scoped environment variables | +| Shells | | X | | | | Basic value and file shells, but no opt-in/opt-out for commands | +| Protocol | | | X | | | Streaming protocol is serviceable | +| Plugins | | X | | | | Plugins work on one row at a time, lack batching and expression eval | +| Errors | | | X | | | Error reporting works, but could use usability polish | +| Documentation | | | X | | | Book updated to latest release, including usage examples | +| Paging | | X | | | | Textview has paging, but we'd like paging for tables | +| Functions | | | X | | | Functions and aliases are supported | +| Variables | | | X | | | Nu supports variables and environment variables | +| Completions | | | X | | | Completions for filepaths | +| Type-checking | | | X | | | Commands check basic types, but input/output isn't checked | + +## Officially Supported By + +Please submit an issue or PR to be added to this list. + +### Integrations +- [zoxide](https://github.com/ajeetdsouza/zoxide) +- [starship](https://github.com/starship/starship) +### Mentions +- [The Python Launcher for Unix](https://github.com/brettcannon/python-launcher#how-do-i-get-a-table-of-python-executables-in-nushell) + +## Contributing + +See [Contributing](CONTRIBUTING.md) for details. + +Thanks to all the people who already contributed! + + + + + +## License + +The project is made available under the MIT license. See the `LICENSE` file for more information. diff --git a/crates/README.md b/old_nushell/crates/README.md similarity index 100% rename from crates/README.md rename to old_nushell/crates/README.md diff --git a/crates/nu-ansi-term/.gitignore b/old_nushell/crates/nu-ansi-term/.gitignore similarity index 100% rename from crates/nu-ansi-term/.gitignore rename to old_nushell/crates/nu-ansi-term/.gitignore diff --git a/crates/nu-ansi-term/Cargo.toml b/old_nushell/crates/nu-ansi-term/Cargo.toml similarity index 100% rename from crates/nu-ansi-term/Cargo.toml rename to old_nushell/crates/nu-ansi-term/Cargo.toml diff --git a/crates/nu-ansi-term/LICENCE b/old_nushell/crates/nu-ansi-term/LICENCE similarity index 100% rename from crates/nu-ansi-term/LICENCE rename to old_nushell/crates/nu-ansi-term/LICENCE diff --git a/crates/nu-ansi-term/README.md b/old_nushell/crates/nu-ansi-term/README.md similarity index 100% rename from crates/nu-ansi-term/README.md rename to old_nushell/crates/nu-ansi-term/README.md diff --git a/crates/nu-ansi-term/examples/256_colors.rs b/old_nushell/crates/nu-ansi-term/examples/256_colors.rs similarity index 100% rename from crates/nu-ansi-term/examples/256_colors.rs rename to old_nushell/crates/nu-ansi-term/examples/256_colors.rs diff --git a/crates/nu-ansi-term/examples/basic_colors.rs b/old_nushell/crates/nu-ansi-term/examples/basic_colors.rs similarity index 100% rename from crates/nu-ansi-term/examples/basic_colors.rs rename to old_nushell/crates/nu-ansi-term/examples/basic_colors.rs diff --git a/crates/nu-ansi-term/examples/gradient_colors.rs b/old_nushell/crates/nu-ansi-term/examples/gradient_colors.rs similarity index 100% rename from crates/nu-ansi-term/examples/gradient_colors.rs rename to old_nushell/crates/nu-ansi-term/examples/gradient_colors.rs diff --git a/crates/nu-ansi-term/examples/rgb_colors.rs b/old_nushell/crates/nu-ansi-term/examples/rgb_colors.rs similarity index 100% rename from crates/nu-ansi-term/examples/rgb_colors.rs rename to old_nushell/crates/nu-ansi-term/examples/rgb_colors.rs diff --git a/crates/nu-ansi-term/src/ansi.rs b/old_nushell/crates/nu-ansi-term/src/ansi.rs similarity index 100% rename from crates/nu-ansi-term/src/ansi.rs rename to old_nushell/crates/nu-ansi-term/src/ansi.rs diff --git a/crates/nu-ansi-term/src/debug.rs b/old_nushell/crates/nu-ansi-term/src/debug.rs similarity index 100% rename from crates/nu-ansi-term/src/debug.rs rename to old_nushell/crates/nu-ansi-term/src/debug.rs diff --git a/crates/nu-ansi-term/src/difference.rs b/old_nushell/crates/nu-ansi-term/src/difference.rs similarity index 100% rename from crates/nu-ansi-term/src/difference.rs rename to old_nushell/crates/nu-ansi-term/src/difference.rs diff --git a/crates/nu-ansi-term/src/display.rs b/old_nushell/crates/nu-ansi-term/src/display.rs similarity index 100% rename from crates/nu-ansi-term/src/display.rs rename to old_nushell/crates/nu-ansi-term/src/display.rs diff --git a/crates/nu-ansi-term/src/gradient.rs b/old_nushell/crates/nu-ansi-term/src/gradient.rs similarity index 100% rename from crates/nu-ansi-term/src/gradient.rs rename to old_nushell/crates/nu-ansi-term/src/gradient.rs diff --git a/crates/nu-ansi-term/src/lib.rs b/old_nushell/crates/nu-ansi-term/src/lib.rs similarity index 100% rename from crates/nu-ansi-term/src/lib.rs rename to old_nushell/crates/nu-ansi-term/src/lib.rs diff --git a/crates/nu-ansi-term/src/rgb.rs b/old_nushell/crates/nu-ansi-term/src/rgb.rs similarity index 100% rename from crates/nu-ansi-term/src/rgb.rs rename to old_nushell/crates/nu-ansi-term/src/rgb.rs diff --git a/crates/nu-ansi-term/src/style.rs b/old_nushell/crates/nu-ansi-term/src/style.rs similarity index 100% rename from crates/nu-ansi-term/src/style.rs rename to old_nushell/crates/nu-ansi-term/src/style.rs diff --git a/crates/nu-ansi-term/src/util.rs b/old_nushell/crates/nu-ansi-term/src/util.rs similarity index 100% rename from crates/nu-ansi-term/src/util.rs rename to old_nushell/crates/nu-ansi-term/src/util.rs diff --git a/crates/nu-ansi-term/src/windows.rs b/old_nushell/crates/nu-ansi-term/src/windows.rs similarity index 100% rename from crates/nu-ansi-term/src/windows.rs rename to old_nushell/crates/nu-ansi-term/src/windows.rs diff --git a/crates/nu-ansi-term/src/write.rs b/old_nushell/crates/nu-ansi-term/src/write.rs similarity index 100% rename from crates/nu-ansi-term/src/write.rs rename to old_nushell/crates/nu-ansi-term/src/write.rs diff --git a/old_nushell/crates/nu-cli/Cargo.toml b/old_nushell/crates/nu-cli/Cargo.toml new file mode 100644 index 0000000000..305edeb457 --- /dev/null +++ b/old_nushell/crates/nu-cli/Cargo.toml @@ -0,0 +1,43 @@ +[package] +authors = ["The Nu Project Contributors"] +description = "CLI for nushell" +edition = "2018" +license = "MIT" +name = "nu-cli" +version = "0.43.0" +build = "build.rs" + +[lib] +doctest = false + +[dependencies] +nu-completion = { version = "0.43.0", path="../nu-completion" } +nu-command = { version = "0.43.0", path="../nu-command" } +nu-data = { version = "0.43.0", path="../nu-data" } +nu-engine = { version = "0.43.0", path="../nu-engine" } +nu-errors = { version = "0.43.0", path="../nu-errors" } +nu-parser = { version = "0.43.0", path="../nu-parser" } +nu-protocol = { version = "0.43.0", path="../nu-protocol" } +nu-source = { version = "0.43.0", path="../nu-source" } +nu-stream = { version = "0.43.0", path="../nu-stream" } +nu-ansi-term = { version = "0.43.0", path="../nu-ansi-term" } +nu-path = { version = "0.43.0", path="../nu-path" } + +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" + +[build-dependencies] +shadow-rs = "0.8.1" + +[features] +default = ["shadow-rs"] +rustyline-support = ["rustyline", "nu-engine/rustyline-support"] +stable = [] diff --git a/crates/nu-cli/README.md b/old_nushell/crates/nu-cli/README.md similarity index 100% rename from crates/nu-cli/README.md rename to old_nushell/crates/nu-cli/README.md diff --git a/crates/nu-cli/build.rs b/old_nushell/crates/nu-cli/build.rs similarity index 100% rename from crates/nu-cli/build.rs rename to old_nushell/crates/nu-cli/build.rs diff --git a/crates/nu-cli/src/app.rs b/old_nushell/crates/nu-cli/src/app.rs similarity index 100% rename from crates/nu-cli/src/app.rs rename to old_nushell/crates/nu-cli/src/app.rs diff --git a/crates/nu-cli/src/app/logger.rs b/old_nushell/crates/nu-cli/src/app/logger.rs similarity index 100% rename from crates/nu-cli/src/app/logger.rs rename to old_nushell/crates/nu-cli/src/app/logger.rs diff --git a/crates/nu-cli/src/app/options.rs b/old_nushell/crates/nu-cli/src/app/options.rs similarity index 100% rename from crates/nu-cli/src/app/options.rs rename to old_nushell/crates/nu-cli/src/app/options.rs diff --git a/crates/nu-cli/src/app/options_parser.rs b/old_nushell/crates/nu-cli/src/app/options_parser.rs similarity index 100% rename from crates/nu-cli/src/app/options_parser.rs rename to old_nushell/crates/nu-cli/src/app/options_parser.rs diff --git a/crates/nu-cli/src/app/stopwatch.rs b/old_nushell/crates/nu-cli/src/app/stopwatch.rs similarity index 100% rename from crates/nu-cli/src/app/stopwatch.rs rename to old_nushell/crates/nu-cli/src/app/stopwatch.rs diff --git a/crates/nu-cli/src/cli.rs b/old_nushell/crates/nu-cli/src/cli.rs similarity index 100% rename from crates/nu-cli/src/cli.rs rename to old_nushell/crates/nu-cli/src/cli.rs diff --git a/crates/nu-cli/src/keybinding.rs b/old_nushell/crates/nu-cli/src/keybinding.rs similarity index 100% rename from crates/nu-cli/src/keybinding.rs rename to old_nushell/crates/nu-cli/src/keybinding.rs diff --git a/old_nushell/crates/nu-cli/src/lib.rs b/old_nushell/crates/nu-cli/src/lib.rs new file mode 100644 index 0000000000..4b3e2fb932 --- /dev/null +++ b/old_nushell/crates/nu-cli/src/lib.rs @@ -0,0 +1,15 @@ +pub mod app; +mod cli; +#[cfg(feature = "rustyline-support")] +mod keybinding; +mod line_editor; +#[cfg(feature = "rustyline-support")] +mod shell; + +#[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; diff --git a/crates/nu-cli/src/line_editor.rs b/old_nushell/crates/nu-cli/src/line_editor.rs similarity index 100% rename from crates/nu-cli/src/line_editor.rs rename to old_nushell/crates/nu-cli/src/line_editor.rs diff --git a/crates/nu-cli/src/shell.rs b/old_nushell/crates/nu-cli/src/shell.rs similarity index 100% rename from crates/nu-cli/src/shell.rs rename to old_nushell/crates/nu-cli/src/shell.rs diff --git a/old_nushell/crates/nu-command/Cargo.toml b/old_nushell/crates/nu-command/Cargo.toml new file mode 100644 index 0000000000..943bc4480d --- /dev/null +++ b/old_nushell/crates/nu-command/Cargo.toml @@ -0,0 +1,122 @@ +[package] +authors = ["The Nu Project Contributors"] +build = "build.rs" +description = "Commands for Nushell" +edition = "2018" +license = "MIT" +name = "nu-command" +version = "0.43.0" + +[lib] +doctest = false + +[dependencies] +nu-data = { version = "0.43.0", path="../nu-data" } +nu-engine = { version = "0.43.0", path="../nu-engine" } +nu-errors = { version = "0.43.0", path="../nu-errors" } +nu-json = { version = "0.43.0", path="../nu-json" } +nu-path = { version = "0.43.0", path="../nu-path" } +nu-parser = { version = "0.43.0", path="../nu-parser" } +nu-plugin = { version = "0.43.0", path="../nu-plugin" } +nu-protocol = { version = "0.43.0", path="../nu-protocol" } +nu-serde = { version = "0.43.0", path="../nu-serde" } +nu-source = { version = "0.43.0", path="../nu-source" } +nu-stream = { version = "0.43.0", path="../nu-stream" } +nu-table = { version = "0.43.0", path="../nu-table" } +nu-test-support = { version = "0.43.0", path="../nu-test-support" } +nu-value-ext = { version = "0.43.0", path="../nu-value-ext" } +nu-ansi-term = { version = "0.43.0", path="../nu-ansi-term" } +nu-pretty-hex = { version = "0.43.0", path="../nu-pretty-hex" } + +url = "2.2.1" +mime = "0.3.16" +heck = "0.4.0" +base64 = "0.13.0" +bigdecimal = { version = "0.3.0", features = ["serde"] } +calamine = "0.18.0" +chrono = { version="0.4.19", features=["serde"] } +chrono-tz = "0.5.3" +crossterm = { version="0.19.0", optional=true } +csv = "1.1.3" +ctrlc = { version="3.1.7", optional=true } +derive-new = "0.5.8" +dirs-next = "2.0.0" +dtparse = "1.2.0" +eml-parser = "0.1.0" +encoding_rs = "0.8.28" +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"] } +itertools = "0.10.0" +lazy_static = "1.*" +log = "0.4.14" +md-5 = "0.9.1" +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" +quick-xml = "0.22" +rand = "0.8" +regex = "1.4.3" +reqwest = {version = "0.11", optional = true } +roxmltree = "0.14.0" +rust-embed = "5.9.0" +rustyline = { version="9.0.0", optional=true } +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" +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"] + +[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"] } + +[build-dependencies] +shadow-rs = "0.8.1" + +[dev-dependencies] +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/old_nushell/crates/nu-command/README.md similarity index 100% rename from crates/nu-command/README.md rename to old_nushell/crates/nu-command/README.md diff --git a/crates/nu-command/assets/228_themes.zip b/old_nushell/crates/nu-command/assets/228_themes.zip similarity index 100% rename from crates/nu-command/assets/228_themes.zip rename to old_nushell/crates/nu-command/assets/228_themes.zip diff --git a/crates/nu-command/build.rs b/old_nushell/crates/nu-command/build.rs similarity index 100% rename from crates/nu-command/build.rs rename to old_nushell/crates/nu-command/build.rs diff --git a/crates/nu-command/src/args.rs b/old_nushell/crates/nu-command/src/args.rs similarity index 100% rename from crates/nu-command/src/args.rs rename to old_nushell/crates/nu-command/src/args.rs diff --git a/crates/nu-command/src/classified/external.rs b/old_nushell/crates/nu-command/src/classified/external.rs similarity index 100% rename from crates/nu-command/src/classified/external.rs rename to old_nushell/crates/nu-command/src/classified/external.rs diff --git a/crates/nu-command/src/classified/mod.rs b/old_nushell/crates/nu-command/src/classified/mod.rs similarity index 100% rename from crates/nu-command/src/classified/mod.rs rename to old_nushell/crates/nu-command/src/classified/mod.rs diff --git a/crates/nu-command/src/commands/charting/chart.rs b/old_nushell/crates/nu-command/src/commands/charting/chart.rs similarity index 100% rename from crates/nu-command/src/commands/charting/chart.rs rename to old_nushell/crates/nu-command/src/commands/charting/chart.rs diff --git a/crates/nu-command/src/commands/charting/histogram.rs b/old_nushell/crates/nu-command/src/commands/charting/histogram.rs similarity index 100% rename from crates/nu-command/src/commands/charting/histogram.rs rename to old_nushell/crates/nu-command/src/commands/charting/histogram.rs diff --git a/crates/nu-command/src/commands/charting/mod.rs b/old_nushell/crates/nu-command/src/commands/charting/mod.rs similarity index 100% rename from crates/nu-command/src/commands/charting/mod.rs rename to old_nushell/crates/nu-command/src/commands/charting/mod.rs diff --git a/crates/nu-command/src/commands/config/clear.rs b/old_nushell/crates/nu-command/src/commands/config/clear.rs similarity index 100% rename from crates/nu-command/src/commands/config/clear.rs rename to old_nushell/crates/nu-command/src/commands/config/clear.rs diff --git a/crates/nu-command/src/commands/config/command.rs b/old_nushell/crates/nu-command/src/commands/config/command.rs similarity index 100% rename from crates/nu-command/src/commands/config/command.rs rename to old_nushell/crates/nu-command/src/commands/config/command.rs diff --git a/crates/nu-command/src/commands/config/get.rs b/old_nushell/crates/nu-command/src/commands/config/get.rs similarity index 100% rename from crates/nu-command/src/commands/config/get.rs rename to old_nushell/crates/nu-command/src/commands/config/get.rs diff --git a/crates/nu-command/src/commands/config/mod.rs b/old_nushell/crates/nu-command/src/commands/config/mod.rs similarity index 100% rename from crates/nu-command/src/commands/config/mod.rs rename to old_nushell/crates/nu-command/src/commands/config/mod.rs diff --git a/crates/nu-command/src/commands/config/path.rs b/old_nushell/crates/nu-command/src/commands/config/path.rs similarity index 100% rename from crates/nu-command/src/commands/config/path.rs rename to old_nushell/crates/nu-command/src/commands/config/path.rs diff --git a/crates/nu-command/src/commands/config/remove.rs b/old_nushell/crates/nu-command/src/commands/config/remove.rs similarity index 100% rename from crates/nu-command/src/commands/config/remove.rs rename to old_nushell/crates/nu-command/src/commands/config/remove.rs diff --git a/crates/nu-command/src/commands/config/set.rs b/old_nushell/crates/nu-command/src/commands/config/set.rs similarity index 100% rename from crates/nu-command/src/commands/config/set.rs rename to old_nushell/crates/nu-command/src/commands/config/set.rs diff --git a/crates/nu-command/src/commands/config/set_into.rs b/old_nushell/crates/nu-command/src/commands/config/set_into.rs similarity index 100% rename from crates/nu-command/src/commands/config/set_into.rs rename to old_nushell/crates/nu-command/src/commands/config/set_into.rs diff --git a/crates/nu-command/src/commands/conversions/into/binary.rs b/old_nushell/crates/nu-command/src/commands/conversions/into/binary.rs similarity index 100% rename from crates/nu-command/src/commands/conversions/into/binary.rs rename to old_nushell/crates/nu-command/src/commands/conversions/into/binary.rs diff --git a/crates/nu-command/src/commands/conversions/into/column_path.rs b/old_nushell/crates/nu-command/src/commands/conversions/into/column_path.rs similarity index 100% rename from crates/nu-command/src/commands/conversions/into/column_path.rs rename to old_nushell/crates/nu-command/src/commands/conversions/into/column_path.rs diff --git a/crates/nu-command/src/commands/conversions/into/command.rs b/old_nushell/crates/nu-command/src/commands/conversions/into/command.rs similarity index 100% rename from crates/nu-command/src/commands/conversions/into/command.rs rename to old_nushell/crates/nu-command/src/commands/conversions/into/command.rs diff --git a/crates/nu-command/src/commands/conversions/into/filepath.rs b/old_nushell/crates/nu-command/src/commands/conversions/into/filepath.rs similarity index 100% rename from crates/nu-command/src/commands/conversions/into/filepath.rs rename to old_nushell/crates/nu-command/src/commands/conversions/into/filepath.rs diff --git a/crates/nu-command/src/commands/conversions/into/filesize.rs b/old_nushell/crates/nu-command/src/commands/conversions/into/filesize.rs similarity index 100% rename from crates/nu-command/src/commands/conversions/into/filesize.rs rename to old_nushell/crates/nu-command/src/commands/conversions/into/filesize.rs diff --git a/crates/nu-command/src/commands/conversions/into/int.rs b/old_nushell/crates/nu-command/src/commands/conversions/into/int.rs similarity index 100% rename from crates/nu-command/src/commands/conversions/into/int.rs rename to old_nushell/crates/nu-command/src/commands/conversions/into/int.rs diff --git a/crates/nu-command/src/commands/conversions/into/mod.rs b/old_nushell/crates/nu-command/src/commands/conversions/into/mod.rs similarity index 100% rename from crates/nu-command/src/commands/conversions/into/mod.rs rename to old_nushell/crates/nu-command/src/commands/conversions/into/mod.rs diff --git a/crates/nu-command/src/commands/conversions/into/string.rs b/old_nushell/crates/nu-command/src/commands/conversions/into/string.rs similarity index 100% rename from crates/nu-command/src/commands/conversions/into/string.rs rename to old_nushell/crates/nu-command/src/commands/conversions/into/string.rs diff --git a/crates/nu-command/src/commands/conversions/mod.rs b/old_nushell/crates/nu-command/src/commands/conversions/mod.rs similarity index 100% rename from crates/nu-command/src/commands/conversions/mod.rs rename to old_nushell/crates/nu-command/src/commands/conversions/mod.rs diff --git a/crates/nu-command/src/commands/core_commands/alias.rs b/old_nushell/crates/nu-command/src/commands/core_commands/alias.rs similarity index 100% rename from crates/nu-command/src/commands/core_commands/alias.rs rename to old_nushell/crates/nu-command/src/commands/core_commands/alias.rs diff --git a/crates/nu-command/src/commands/core_commands/debug.rs b/old_nushell/crates/nu-command/src/commands/core_commands/debug.rs similarity index 100% rename from crates/nu-command/src/commands/core_commands/debug.rs rename to old_nushell/crates/nu-command/src/commands/core_commands/debug.rs diff --git a/crates/nu-command/src/commands/core_commands/def.rs b/old_nushell/crates/nu-command/src/commands/core_commands/def.rs similarity index 100% rename from crates/nu-command/src/commands/core_commands/def.rs rename to old_nushell/crates/nu-command/src/commands/core_commands/def.rs diff --git a/crates/nu-command/src/commands/core_commands/describe.rs b/old_nushell/crates/nu-command/src/commands/core_commands/describe.rs similarity index 100% rename from crates/nu-command/src/commands/core_commands/describe.rs rename to old_nushell/crates/nu-command/src/commands/core_commands/describe.rs diff --git a/crates/nu-command/src/commands/core_commands/do_.rs b/old_nushell/crates/nu-command/src/commands/core_commands/do_.rs similarity index 100% rename from crates/nu-command/src/commands/core_commands/do_.rs rename to old_nushell/crates/nu-command/src/commands/core_commands/do_.rs diff --git a/crates/nu-command/src/commands/core_commands/echo.rs b/old_nushell/crates/nu-command/src/commands/core_commands/echo.rs similarity index 100% rename from crates/nu-command/src/commands/core_commands/echo.rs rename to old_nushell/crates/nu-command/src/commands/core_commands/echo.rs diff --git a/crates/nu-command/src/commands/core_commands/error/make.rs b/old_nushell/crates/nu-command/src/commands/core_commands/error/make.rs similarity index 100% rename from crates/nu-command/src/commands/core_commands/error/make.rs rename to old_nushell/crates/nu-command/src/commands/core_commands/error/make.rs diff --git a/crates/nu-command/src/commands/core_commands/error/mod.rs b/old_nushell/crates/nu-command/src/commands/core_commands/error/mod.rs similarity index 100% rename from crates/nu-command/src/commands/core_commands/error/mod.rs rename to old_nushell/crates/nu-command/src/commands/core_commands/error/mod.rs diff --git a/crates/nu-command/src/commands/core_commands/find.rs b/old_nushell/crates/nu-command/src/commands/core_commands/find.rs similarity index 100% rename from crates/nu-command/src/commands/core_commands/find.rs rename to old_nushell/crates/nu-command/src/commands/core_commands/find.rs diff --git a/crates/nu-command/src/commands/core_commands/help.rs b/old_nushell/crates/nu-command/src/commands/core_commands/help.rs similarity index 100% rename from crates/nu-command/src/commands/core_commands/help.rs rename to old_nushell/crates/nu-command/src/commands/core_commands/help.rs diff --git a/crates/nu-command/src/commands/core_commands/history.rs b/old_nushell/crates/nu-command/src/commands/core_commands/history.rs similarity index 100% rename from crates/nu-command/src/commands/core_commands/history.rs rename to old_nushell/crates/nu-command/src/commands/core_commands/history.rs diff --git a/crates/nu-command/src/commands/core_commands/if_.rs b/old_nushell/crates/nu-command/src/commands/core_commands/if_.rs similarity index 100% rename from crates/nu-command/src/commands/core_commands/if_.rs rename to old_nushell/crates/nu-command/src/commands/core_commands/if_.rs diff --git a/crates/nu-command/src/commands/core_commands/ignore.rs b/old_nushell/crates/nu-command/src/commands/core_commands/ignore.rs similarity index 100% rename from crates/nu-command/src/commands/core_commands/ignore.rs rename to old_nushell/crates/nu-command/src/commands/core_commands/ignore.rs diff --git a/crates/nu-command/src/commands/core_commands/let_.rs b/old_nushell/crates/nu-command/src/commands/core_commands/let_.rs similarity index 100% rename from crates/nu-command/src/commands/core_commands/let_.rs rename to old_nushell/crates/nu-command/src/commands/core_commands/let_.rs diff --git a/crates/nu-command/src/commands/core_commands/mod.rs b/old_nushell/crates/nu-command/src/commands/core_commands/mod.rs similarity index 100% rename from crates/nu-command/src/commands/core_commands/mod.rs rename to old_nushell/crates/nu-command/src/commands/core_commands/mod.rs diff --git a/crates/nu-command/src/commands/core_commands/nu_plugin.rs b/old_nushell/crates/nu-command/src/commands/core_commands/nu_plugin.rs similarity index 100% rename from crates/nu-command/src/commands/core_commands/nu_plugin.rs rename to old_nushell/crates/nu-command/src/commands/core_commands/nu_plugin.rs diff --git a/crates/nu-command/src/commands/core_commands/nu_signature.rs b/old_nushell/crates/nu-command/src/commands/core_commands/nu_signature.rs similarity index 100% rename from crates/nu-command/src/commands/core_commands/nu_signature.rs rename to old_nushell/crates/nu-command/src/commands/core_commands/nu_signature.rs diff --git a/crates/nu-command/src/commands/core_commands/source.rs b/old_nushell/crates/nu-command/src/commands/core_commands/source.rs similarity index 100% rename from crates/nu-command/src/commands/core_commands/source.rs rename to old_nushell/crates/nu-command/src/commands/core_commands/source.rs diff --git a/crates/nu-command/src/commands/core_commands/tags.rs b/old_nushell/crates/nu-command/src/commands/core_commands/tags.rs similarity index 100% rename from crates/nu-command/src/commands/core_commands/tags.rs rename to old_nushell/crates/nu-command/src/commands/core_commands/tags.rs diff --git a/crates/nu-command/src/commands/core_commands/tutor.rs b/old_nushell/crates/nu-command/src/commands/core_commands/tutor.rs similarity index 100% rename from crates/nu-command/src/commands/core_commands/tutor.rs rename to old_nushell/crates/nu-command/src/commands/core_commands/tutor.rs diff --git a/crates/nu-command/src/commands/core_commands/unalias.rs b/old_nushell/crates/nu-command/src/commands/core_commands/unalias.rs similarity index 100% rename from crates/nu-command/src/commands/core_commands/unalias.rs rename to old_nushell/crates/nu-command/src/commands/core_commands/unalias.rs diff --git a/crates/nu-command/src/commands/core_commands/version.rs b/old_nushell/crates/nu-command/src/commands/core_commands/version.rs similarity index 100% rename from crates/nu-command/src/commands/core_commands/version.rs rename to old_nushell/crates/nu-command/src/commands/core_commands/version.rs diff --git a/crates/nu-command/src/commands/dataframe/aggregate.rs b/old_nushell/crates/nu-command/src/commands/dataframe/aggregate.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/aggregate.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/aggregate.rs diff --git a/crates/nu-command/src/commands/dataframe/append.rs b/old_nushell/crates/nu-command/src/commands/dataframe/append.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/append.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/append.rs diff --git a/crates/nu-command/src/commands/dataframe/column.rs b/old_nushell/crates/nu-command/src/commands/dataframe/column.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/column.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/column.rs diff --git a/crates/nu-command/src/commands/dataframe/command.rs b/old_nushell/crates/nu-command/src/commands/dataframe/command.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/command.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/command.rs diff --git a/crates/nu-command/src/commands/dataframe/describe.rs b/old_nushell/crates/nu-command/src/commands/dataframe/describe.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/describe.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/describe.rs diff --git a/crates/nu-command/src/commands/dataframe/drop.rs b/old_nushell/crates/nu-command/src/commands/dataframe/drop.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/drop.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/drop.rs diff --git a/crates/nu-command/src/commands/dataframe/drop_duplicates.rs b/old_nushell/crates/nu-command/src/commands/dataframe/drop_duplicates.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/drop_duplicates.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/drop_duplicates.rs diff --git a/crates/nu-command/src/commands/dataframe/drop_nulls.rs b/old_nushell/crates/nu-command/src/commands/dataframe/drop_nulls.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/drop_nulls.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/drop_nulls.rs diff --git a/crates/nu-command/src/commands/dataframe/dtypes.rs b/old_nushell/crates/nu-command/src/commands/dataframe/dtypes.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/dtypes.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/dtypes.rs diff --git a/crates/nu-command/src/commands/dataframe/dummies.rs b/old_nushell/crates/nu-command/src/commands/dataframe/dummies.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/dummies.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/dummies.rs diff --git a/crates/nu-command/src/commands/dataframe/filter.rs b/old_nushell/crates/nu-command/src/commands/dataframe/filter.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/filter.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/filter.rs diff --git a/crates/nu-command/src/commands/dataframe/first.rs b/old_nushell/crates/nu-command/src/commands/dataframe/first.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/first.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/first.rs diff --git a/crates/nu-command/src/commands/dataframe/get.rs b/old_nushell/crates/nu-command/src/commands/dataframe/get.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/get.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/get.rs diff --git a/crates/nu-command/src/commands/dataframe/groupby.rs b/old_nushell/crates/nu-command/src/commands/dataframe/groupby.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/groupby.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/groupby.rs diff --git a/crates/nu-command/src/commands/dataframe/join.rs b/old_nushell/crates/nu-command/src/commands/dataframe/join.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/join.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/join.rs diff --git a/crates/nu-command/src/commands/dataframe/last.rs b/old_nushell/crates/nu-command/src/commands/dataframe/last.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/last.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/last.rs diff --git a/crates/nu-command/src/commands/dataframe/list.rs b/old_nushell/crates/nu-command/src/commands/dataframe/list.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/list.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/list.rs diff --git a/crates/nu-command/src/commands/dataframe/melt.rs b/old_nushell/crates/nu-command/src/commands/dataframe/melt.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/melt.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/melt.rs diff --git a/crates/nu-command/src/commands/dataframe/mod.rs b/old_nushell/crates/nu-command/src/commands/dataframe/mod.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/mod.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/mod.rs diff --git a/crates/nu-command/src/commands/dataframe/open.rs b/old_nushell/crates/nu-command/src/commands/dataframe/open.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/open.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/open.rs diff --git a/crates/nu-command/src/commands/dataframe/pivot.rs b/old_nushell/crates/nu-command/src/commands/dataframe/pivot.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/pivot.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/pivot.rs diff --git a/crates/nu-command/src/commands/dataframe/rename.rs b/old_nushell/crates/nu-command/src/commands/dataframe/rename.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/rename.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/rename.rs diff --git a/crates/nu-command/src/commands/dataframe/sample.rs b/old_nushell/crates/nu-command/src/commands/dataframe/sample.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/sample.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/sample.rs diff --git a/crates/nu-command/src/commands/dataframe/select.rs b/old_nushell/crates/nu-command/src/commands/dataframe/select.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/select.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/select.rs diff --git a/crates/nu-command/src/commands/dataframe/series/all_false.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/all_false.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/all_false.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/all_false.rs diff --git a/crates/nu-command/src/commands/dataframe/series/all_true.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/all_true.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/all_true.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/all_true.rs diff --git a/crates/nu-command/src/commands/dataframe/series/arg_max.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/arg_max.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/arg_max.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/arg_max.rs diff --git a/crates/nu-command/src/commands/dataframe/series/arg_min.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/arg_min.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/arg_min.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/arg_min.rs diff --git a/crates/nu-command/src/commands/dataframe/series/arg_sort.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/arg_sort.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/arg_sort.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/arg_sort.rs diff --git a/crates/nu-command/src/commands/dataframe/series/arg_true.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/arg_true.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/arg_true.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/arg_true.rs diff --git a/crates/nu-command/src/commands/dataframe/series/arg_unique.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/arg_unique.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/arg_unique.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/arg_unique.rs diff --git a/crates/nu-command/src/commands/dataframe/series/concatenate.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/concatenate.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/concatenate.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/concatenate.rs diff --git a/crates/nu-command/src/commands/dataframe/series/contains.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/contains.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/contains.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/contains.rs diff --git a/crates/nu-command/src/commands/dataframe/series/cumulative.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/cumulative.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/cumulative.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/cumulative.rs diff --git a/crates/nu-command/src/commands/dataframe/series/get_day.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/get_day.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/get_day.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/get_day.rs diff --git a/crates/nu-command/src/commands/dataframe/series/get_hour.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/get_hour.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/get_hour.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/get_hour.rs diff --git a/crates/nu-command/src/commands/dataframe/series/get_minute.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/get_minute.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/get_minute.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/get_minute.rs diff --git a/crates/nu-command/src/commands/dataframe/series/get_month.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/get_month.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/get_month.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/get_month.rs diff --git a/crates/nu-command/src/commands/dataframe/series/get_nanosecond.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/get_nanosecond.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/get_nanosecond.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/get_nanosecond.rs diff --git a/crates/nu-command/src/commands/dataframe/series/get_ordinal.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/get_ordinal.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/get_ordinal.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/get_ordinal.rs diff --git a/crates/nu-command/src/commands/dataframe/series/get_second.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/get_second.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/get_second.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/get_second.rs diff --git a/crates/nu-command/src/commands/dataframe/series/get_week.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/get_week.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/get_week.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/get_week.rs diff --git a/crates/nu-command/src/commands/dataframe/series/get_weekday.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/get_weekday.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/get_weekday.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/get_weekday.rs diff --git a/crates/nu-command/src/commands/dataframe/series/get_year.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/get_year.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/get_year.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/get_year.rs diff --git a/crates/nu-command/src/commands/dataframe/series/is_duplicated.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/is_duplicated.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/is_duplicated.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/is_duplicated.rs diff --git a/crates/nu-command/src/commands/dataframe/series/is_in.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/is_in.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/is_in.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/is_in.rs diff --git a/crates/nu-command/src/commands/dataframe/series/is_not_null.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/is_not_null.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/is_not_null.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/is_not_null.rs diff --git a/crates/nu-command/src/commands/dataframe/series/is_null.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/is_null.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/is_null.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/is_null.rs diff --git a/crates/nu-command/src/commands/dataframe/series/is_unique.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/is_unique.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/is_unique.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/is_unique.rs diff --git a/crates/nu-command/src/commands/dataframe/series/mod.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/mod.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/mod.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/mod.rs diff --git a/crates/nu-command/src/commands/dataframe/series/n_null.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/n_null.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/n_null.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/n_null.rs diff --git a/crates/nu-command/src/commands/dataframe/series/n_unique.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/n_unique.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/n_unique.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/n_unique.rs diff --git a/crates/nu-command/src/commands/dataframe/series/not.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/not.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/not.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/not.rs diff --git a/crates/nu-command/src/commands/dataframe/series/rename.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/rename.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/rename.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/rename.rs diff --git a/crates/nu-command/src/commands/dataframe/series/replace.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/replace.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/replace.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/replace.rs diff --git a/crates/nu-command/src/commands/dataframe/series/replace_all.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/replace_all.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/replace_all.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/replace_all.rs diff --git a/crates/nu-command/src/commands/dataframe/series/rolling.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/rolling.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/rolling.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/rolling.rs diff --git a/crates/nu-command/src/commands/dataframe/series/set.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/set.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/set.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/set.rs diff --git a/crates/nu-command/src/commands/dataframe/series/set_with_idx.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/set_with_idx.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/set_with_idx.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/set_with_idx.rs diff --git a/crates/nu-command/src/commands/dataframe/series/shift.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/shift.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/shift.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/shift.rs diff --git a/crates/nu-command/src/commands/dataframe/series/str_lengths.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/str_lengths.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/str_lengths.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/str_lengths.rs diff --git a/crates/nu-command/src/commands/dataframe/series/str_slice.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/str_slice.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/str_slice.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/str_slice.rs diff --git a/crates/nu-command/src/commands/dataframe/series/strftime.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/strftime.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/strftime.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/strftime.rs diff --git a/crates/nu-command/src/commands/dataframe/series/to_lowercase.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/to_lowercase.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/to_lowercase.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/to_lowercase.rs diff --git a/crates/nu-command/src/commands/dataframe/series/to_uppercase.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/to_uppercase.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/to_uppercase.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/to_uppercase.rs diff --git a/crates/nu-command/src/commands/dataframe/series/unique.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/unique.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/unique.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/unique.rs diff --git a/crates/nu-command/src/commands/dataframe/series/value_counts.rs b/old_nushell/crates/nu-command/src/commands/dataframe/series/value_counts.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/series/value_counts.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/series/value_counts.rs diff --git a/crates/nu-command/src/commands/dataframe/shape.rs b/old_nushell/crates/nu-command/src/commands/dataframe/shape.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/shape.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/shape.rs diff --git a/crates/nu-command/src/commands/dataframe/show.rs b/old_nushell/crates/nu-command/src/commands/dataframe/show.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/show.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/show.rs diff --git a/crates/nu-command/src/commands/dataframe/slice.rs b/old_nushell/crates/nu-command/src/commands/dataframe/slice.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/slice.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/slice.rs diff --git a/crates/nu-command/src/commands/dataframe/sort.rs b/old_nushell/crates/nu-command/src/commands/dataframe/sort.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/sort.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/sort.rs diff --git a/crates/nu-command/src/commands/dataframe/take.rs b/old_nushell/crates/nu-command/src/commands/dataframe/take.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/take.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/take.rs diff --git a/crates/nu-command/src/commands/dataframe/to_csv.rs b/old_nushell/crates/nu-command/src/commands/dataframe/to_csv.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/to_csv.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/to_csv.rs diff --git a/crates/nu-command/src/commands/dataframe/to_df.rs b/old_nushell/crates/nu-command/src/commands/dataframe/to_df.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/to_df.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/to_df.rs diff --git a/crates/nu-command/src/commands/dataframe/to_parquet.rs b/old_nushell/crates/nu-command/src/commands/dataframe/to_parquet.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/to_parquet.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/to_parquet.rs diff --git a/crates/nu-command/src/commands/dataframe/utils.rs b/old_nushell/crates/nu-command/src/commands/dataframe/utils.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/utils.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/utils.rs diff --git a/crates/nu-command/src/commands/dataframe/where_.rs b/old_nushell/crates/nu-command/src/commands/dataframe/where_.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/where_.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/where_.rs diff --git a/crates/nu-command/src/commands/dataframe/with_column.rs b/old_nushell/crates/nu-command/src/commands/dataframe/with_column.rs similarity index 100% rename from crates/nu-command/src/commands/dataframe/with_column.rs rename to old_nushell/crates/nu-command/src/commands/dataframe/with_column.rs diff --git a/crates/nu-command/src/commands/env/autoenv.rs b/old_nushell/crates/nu-command/src/commands/env/autoenv.rs similarity index 100% rename from crates/nu-command/src/commands/env/autoenv.rs rename to old_nushell/crates/nu-command/src/commands/env/autoenv.rs diff --git a/crates/nu-command/src/commands/env/autoenv_trust.rs b/old_nushell/crates/nu-command/src/commands/env/autoenv_trust.rs similarity index 100% rename from crates/nu-command/src/commands/env/autoenv_trust.rs rename to old_nushell/crates/nu-command/src/commands/env/autoenv_trust.rs diff --git a/crates/nu-command/src/commands/env/autoenv_untrust.rs b/old_nushell/crates/nu-command/src/commands/env/autoenv_untrust.rs similarity index 100% rename from crates/nu-command/src/commands/env/autoenv_untrust.rs rename to old_nushell/crates/nu-command/src/commands/env/autoenv_untrust.rs diff --git a/crates/nu-command/src/commands/env/let_env.rs b/old_nushell/crates/nu-command/src/commands/env/let_env.rs similarity index 100% rename from crates/nu-command/src/commands/env/let_env.rs rename to old_nushell/crates/nu-command/src/commands/env/let_env.rs diff --git a/crates/nu-command/src/commands/env/load_env.rs b/old_nushell/crates/nu-command/src/commands/env/load_env.rs similarity index 100% rename from crates/nu-command/src/commands/env/load_env.rs rename to old_nushell/crates/nu-command/src/commands/env/load_env.rs diff --git a/crates/nu-command/src/commands/env/mod.rs b/old_nushell/crates/nu-command/src/commands/env/mod.rs similarity index 100% rename from crates/nu-command/src/commands/env/mod.rs rename to old_nushell/crates/nu-command/src/commands/env/mod.rs diff --git a/crates/nu-command/src/commands/env/unlet_env.rs b/old_nushell/crates/nu-command/src/commands/env/unlet_env.rs similarity index 100% rename from crates/nu-command/src/commands/env/unlet_env.rs rename to old_nushell/crates/nu-command/src/commands/env/unlet_env.rs diff --git a/crates/nu-command/src/commands/env/with_env.rs b/old_nushell/crates/nu-command/src/commands/env/with_env.rs similarity index 100% rename from crates/nu-command/src/commands/env/with_env.rs rename to old_nushell/crates/nu-command/src/commands/env/with_env.rs diff --git a/crates/nu-command/src/commands/filesystem/cd.rs b/old_nushell/crates/nu-command/src/commands/filesystem/cd.rs similarity index 100% rename from crates/nu-command/src/commands/filesystem/cd.rs rename to old_nushell/crates/nu-command/src/commands/filesystem/cd.rs diff --git a/crates/nu-command/src/commands/filesystem/cp.rs b/old_nushell/crates/nu-command/src/commands/filesystem/cp.rs similarity index 100% rename from crates/nu-command/src/commands/filesystem/cp.rs rename to old_nushell/crates/nu-command/src/commands/filesystem/cp.rs diff --git a/crates/nu-command/src/commands/filesystem/ls.rs b/old_nushell/crates/nu-command/src/commands/filesystem/ls.rs similarity index 100% rename from crates/nu-command/src/commands/filesystem/ls.rs rename to old_nushell/crates/nu-command/src/commands/filesystem/ls.rs diff --git a/crates/nu-command/src/commands/filesystem/mkdir.rs b/old_nushell/crates/nu-command/src/commands/filesystem/mkdir.rs similarity index 100% rename from crates/nu-command/src/commands/filesystem/mkdir.rs rename to old_nushell/crates/nu-command/src/commands/filesystem/mkdir.rs diff --git a/crates/nu-command/src/commands/filesystem/mod.rs b/old_nushell/crates/nu-command/src/commands/filesystem/mod.rs similarity index 100% rename from crates/nu-command/src/commands/filesystem/mod.rs rename to old_nushell/crates/nu-command/src/commands/filesystem/mod.rs diff --git a/crates/nu-command/src/commands/filesystem/mv.rs b/old_nushell/crates/nu-command/src/commands/filesystem/mv.rs similarity index 100% rename from crates/nu-command/src/commands/filesystem/mv.rs rename to old_nushell/crates/nu-command/src/commands/filesystem/mv.rs diff --git a/crates/nu-command/src/commands/filesystem/open.rs b/old_nushell/crates/nu-command/src/commands/filesystem/open.rs similarity index 100% rename from crates/nu-command/src/commands/filesystem/open.rs rename to old_nushell/crates/nu-command/src/commands/filesystem/open.rs diff --git a/crates/nu-command/src/commands/filesystem/rm.rs b/old_nushell/crates/nu-command/src/commands/filesystem/rm.rs similarity index 100% rename from crates/nu-command/src/commands/filesystem/rm.rs rename to old_nushell/crates/nu-command/src/commands/filesystem/rm.rs diff --git a/crates/nu-command/src/commands/filesystem/save.rs b/old_nushell/crates/nu-command/src/commands/filesystem/save.rs similarity index 100% rename from crates/nu-command/src/commands/filesystem/save.rs rename to old_nushell/crates/nu-command/src/commands/filesystem/save.rs diff --git a/crates/nu-command/src/commands/filesystem/touch.rs b/old_nushell/crates/nu-command/src/commands/filesystem/touch.rs similarity index 100% rename from crates/nu-command/src/commands/filesystem/touch.rs rename to old_nushell/crates/nu-command/src/commands/filesystem/touch.rs diff --git a/crates/nu-command/src/commands/filters/all.rs b/old_nushell/crates/nu-command/src/commands/filters/all.rs similarity index 100% rename from crates/nu-command/src/commands/filters/all.rs rename to old_nushell/crates/nu-command/src/commands/filters/all.rs diff --git a/crates/nu-command/src/commands/filters/any.rs b/old_nushell/crates/nu-command/src/commands/filters/any.rs similarity index 100% rename from crates/nu-command/src/commands/filters/any.rs rename to old_nushell/crates/nu-command/src/commands/filters/any.rs diff --git a/crates/nu-command/src/commands/filters/append.rs b/old_nushell/crates/nu-command/src/commands/filters/append.rs similarity index 100% rename from crates/nu-command/src/commands/filters/append.rs rename to old_nushell/crates/nu-command/src/commands/filters/append.rs diff --git a/crates/nu-command/src/commands/filters/collect.rs b/old_nushell/crates/nu-command/src/commands/filters/collect.rs similarity index 100% rename from crates/nu-command/src/commands/filters/collect.rs rename to old_nushell/crates/nu-command/src/commands/filters/collect.rs diff --git a/crates/nu-command/src/commands/filters/compact.rs b/old_nushell/crates/nu-command/src/commands/filters/compact.rs similarity index 100% rename from crates/nu-command/src/commands/filters/compact.rs rename to old_nushell/crates/nu-command/src/commands/filters/compact.rs diff --git a/crates/nu-command/src/commands/filters/default.rs b/old_nushell/crates/nu-command/src/commands/filters/default.rs similarity index 100% rename from crates/nu-command/src/commands/filters/default.rs rename to old_nushell/crates/nu-command/src/commands/filters/default.rs diff --git a/crates/nu-command/src/commands/filters/drop/column.rs b/old_nushell/crates/nu-command/src/commands/filters/drop/column.rs similarity index 100% rename from crates/nu-command/src/commands/filters/drop/column.rs rename to old_nushell/crates/nu-command/src/commands/filters/drop/column.rs diff --git a/crates/nu-command/src/commands/filters/drop/command.rs b/old_nushell/crates/nu-command/src/commands/filters/drop/command.rs similarity index 100% rename from crates/nu-command/src/commands/filters/drop/command.rs rename to old_nushell/crates/nu-command/src/commands/filters/drop/command.rs diff --git a/crates/nu-command/src/commands/filters/drop/mod.rs b/old_nushell/crates/nu-command/src/commands/filters/drop/mod.rs similarity index 100% rename from crates/nu-command/src/commands/filters/drop/mod.rs rename to old_nushell/crates/nu-command/src/commands/filters/drop/mod.rs diff --git a/crates/nu-command/src/commands/filters/drop/nth.rs b/old_nushell/crates/nu-command/src/commands/filters/drop/nth.rs similarity index 100% rename from crates/nu-command/src/commands/filters/drop/nth.rs rename to old_nushell/crates/nu-command/src/commands/filters/drop/nth.rs diff --git a/crates/nu-command/src/commands/filters/each/command.rs b/old_nushell/crates/nu-command/src/commands/filters/each/command.rs similarity index 100% rename from crates/nu-command/src/commands/filters/each/command.rs rename to old_nushell/crates/nu-command/src/commands/filters/each/command.rs diff --git a/crates/nu-command/src/commands/filters/each/group.rs b/old_nushell/crates/nu-command/src/commands/filters/each/group.rs similarity index 100% rename from crates/nu-command/src/commands/filters/each/group.rs rename to old_nushell/crates/nu-command/src/commands/filters/each/group.rs diff --git a/crates/nu-command/src/commands/filters/each/mod.rs b/old_nushell/crates/nu-command/src/commands/filters/each/mod.rs similarity index 100% rename from crates/nu-command/src/commands/filters/each/mod.rs rename to old_nushell/crates/nu-command/src/commands/filters/each/mod.rs diff --git a/crates/nu-command/src/commands/filters/each/window.rs b/old_nushell/crates/nu-command/src/commands/filters/each/window.rs similarity index 100% rename from crates/nu-command/src/commands/filters/each/window.rs rename to old_nushell/crates/nu-command/src/commands/filters/each/window.rs diff --git a/crates/nu-command/src/commands/filters/empty.rs b/old_nushell/crates/nu-command/src/commands/filters/empty.rs similarity index 100% rename from crates/nu-command/src/commands/filters/empty.rs rename to old_nushell/crates/nu-command/src/commands/filters/empty.rs diff --git a/crates/nu-command/src/commands/filters/every.rs b/old_nushell/crates/nu-command/src/commands/filters/every.rs similarity index 100% rename from crates/nu-command/src/commands/filters/every.rs rename to old_nushell/crates/nu-command/src/commands/filters/every.rs diff --git a/crates/nu-command/src/commands/filters/first.rs b/old_nushell/crates/nu-command/src/commands/filters/first.rs similarity index 100% rename from crates/nu-command/src/commands/filters/first.rs rename to old_nushell/crates/nu-command/src/commands/filters/first.rs diff --git a/crates/nu-command/src/commands/filters/flatten.rs b/old_nushell/crates/nu-command/src/commands/filters/flatten.rs similarity index 100% rename from crates/nu-command/src/commands/filters/flatten.rs rename to old_nushell/crates/nu-command/src/commands/filters/flatten.rs diff --git a/crates/nu-command/src/commands/filters/get.rs b/old_nushell/crates/nu-command/src/commands/filters/get.rs similarity index 100% rename from crates/nu-command/src/commands/filters/get.rs rename to old_nushell/crates/nu-command/src/commands/filters/get.rs diff --git a/crates/nu-command/src/commands/filters/group_by.rs b/old_nushell/crates/nu-command/src/commands/filters/group_by.rs similarity index 100% rename from crates/nu-command/src/commands/filters/group_by.rs rename to old_nushell/crates/nu-command/src/commands/filters/group_by.rs diff --git a/crates/nu-command/src/commands/filters/group_by_date.rs b/old_nushell/crates/nu-command/src/commands/filters/group_by_date.rs similarity index 100% rename from crates/nu-command/src/commands/filters/group_by_date.rs rename to old_nushell/crates/nu-command/src/commands/filters/group_by_date.rs diff --git a/crates/nu-command/src/commands/filters/headers.rs b/old_nushell/crates/nu-command/src/commands/filters/headers.rs similarity index 100% rename from crates/nu-command/src/commands/filters/headers.rs rename to old_nushell/crates/nu-command/src/commands/filters/headers.rs diff --git a/crates/nu-command/src/commands/filters/insert.rs b/old_nushell/crates/nu-command/src/commands/filters/insert.rs similarity index 100% rename from crates/nu-command/src/commands/filters/insert.rs rename to old_nushell/crates/nu-command/src/commands/filters/insert.rs diff --git a/crates/nu-command/src/commands/filters/keep/command.rs b/old_nushell/crates/nu-command/src/commands/filters/keep/command.rs similarity index 100% rename from crates/nu-command/src/commands/filters/keep/command.rs rename to old_nushell/crates/nu-command/src/commands/filters/keep/command.rs diff --git a/crates/nu-command/src/commands/filters/keep/mod.rs b/old_nushell/crates/nu-command/src/commands/filters/keep/mod.rs similarity index 100% rename from crates/nu-command/src/commands/filters/keep/mod.rs rename to old_nushell/crates/nu-command/src/commands/filters/keep/mod.rs diff --git a/crates/nu-command/src/commands/filters/keep/until.rs b/old_nushell/crates/nu-command/src/commands/filters/keep/until.rs similarity index 100% rename from crates/nu-command/src/commands/filters/keep/until.rs rename to old_nushell/crates/nu-command/src/commands/filters/keep/until.rs diff --git a/crates/nu-command/src/commands/filters/keep/while_.rs b/old_nushell/crates/nu-command/src/commands/filters/keep/while_.rs similarity index 100% rename from crates/nu-command/src/commands/filters/keep/while_.rs rename to old_nushell/crates/nu-command/src/commands/filters/keep/while_.rs diff --git a/crates/nu-command/src/commands/filters/last.rs b/old_nushell/crates/nu-command/src/commands/filters/last.rs similarity index 100% rename from crates/nu-command/src/commands/filters/last.rs rename to old_nushell/crates/nu-command/src/commands/filters/last.rs diff --git a/crates/nu-command/src/commands/filters/length.rs b/old_nushell/crates/nu-command/src/commands/filters/length.rs similarity index 100% rename from crates/nu-command/src/commands/filters/length.rs rename to old_nushell/crates/nu-command/src/commands/filters/length.rs diff --git a/crates/nu-command/src/commands/filters/merge.rs b/old_nushell/crates/nu-command/src/commands/filters/merge.rs similarity index 100% rename from crates/nu-command/src/commands/filters/merge.rs rename to old_nushell/crates/nu-command/src/commands/filters/merge.rs diff --git a/crates/nu-command/src/commands/filters/mod.rs b/old_nushell/crates/nu-command/src/commands/filters/mod.rs similarity index 100% rename from crates/nu-command/src/commands/filters/mod.rs rename to old_nushell/crates/nu-command/src/commands/filters/mod.rs diff --git a/crates/nu-command/src/commands/filters/move_.rs b/old_nushell/crates/nu-command/src/commands/filters/move_.rs similarity index 100% rename from crates/nu-command/src/commands/filters/move_.rs rename to old_nushell/crates/nu-command/src/commands/filters/move_.rs diff --git a/crates/nu-command/src/commands/filters/nth.rs b/old_nushell/crates/nu-command/src/commands/filters/nth.rs similarity index 100% rename from crates/nu-command/src/commands/filters/nth.rs rename to old_nushell/crates/nu-command/src/commands/filters/nth.rs diff --git a/crates/nu-command/src/commands/filters/pivot.rs b/old_nushell/crates/nu-command/src/commands/filters/pivot.rs similarity index 100% rename from crates/nu-command/src/commands/filters/pivot.rs rename to old_nushell/crates/nu-command/src/commands/filters/pivot.rs diff --git a/crates/nu-command/src/commands/filters/prepend.rs b/old_nushell/crates/nu-command/src/commands/filters/prepend.rs similarity index 100% rename from crates/nu-command/src/commands/filters/prepend.rs rename to old_nushell/crates/nu-command/src/commands/filters/prepend.rs diff --git a/crates/nu-command/src/commands/filters/range.rs b/old_nushell/crates/nu-command/src/commands/filters/range.rs similarity index 100% rename from crates/nu-command/src/commands/filters/range.rs rename to old_nushell/crates/nu-command/src/commands/filters/range.rs diff --git a/crates/nu-command/src/commands/filters/reduce.rs b/old_nushell/crates/nu-command/src/commands/filters/reduce.rs similarity index 100% rename from crates/nu-command/src/commands/filters/reduce.rs rename to old_nushell/crates/nu-command/src/commands/filters/reduce.rs diff --git a/crates/nu-command/src/commands/filters/reject.rs b/old_nushell/crates/nu-command/src/commands/filters/reject.rs similarity index 100% rename from crates/nu-command/src/commands/filters/reject.rs rename to old_nushell/crates/nu-command/src/commands/filters/reject.rs diff --git a/crates/nu-command/src/commands/filters/rename.rs b/old_nushell/crates/nu-command/src/commands/filters/rename.rs similarity index 100% rename from crates/nu-command/src/commands/filters/rename.rs rename to old_nushell/crates/nu-command/src/commands/filters/rename.rs diff --git a/crates/nu-command/src/commands/filters/reverse.rs b/old_nushell/crates/nu-command/src/commands/filters/reverse.rs similarity index 100% rename from crates/nu-command/src/commands/filters/reverse.rs rename to old_nushell/crates/nu-command/src/commands/filters/reverse.rs diff --git a/crates/nu-command/src/commands/filters/roll/column.rs b/old_nushell/crates/nu-command/src/commands/filters/roll/column.rs similarity index 100% rename from crates/nu-command/src/commands/filters/roll/column.rs rename to old_nushell/crates/nu-command/src/commands/filters/roll/column.rs diff --git a/crates/nu-command/src/commands/filters/roll/command.rs b/old_nushell/crates/nu-command/src/commands/filters/roll/command.rs similarity index 100% rename from crates/nu-command/src/commands/filters/roll/command.rs rename to old_nushell/crates/nu-command/src/commands/filters/roll/command.rs diff --git a/crates/nu-command/src/commands/filters/roll/mod.rs b/old_nushell/crates/nu-command/src/commands/filters/roll/mod.rs similarity index 100% rename from crates/nu-command/src/commands/filters/roll/mod.rs rename to old_nushell/crates/nu-command/src/commands/filters/roll/mod.rs diff --git a/crates/nu-command/src/commands/filters/roll/up.rs b/old_nushell/crates/nu-command/src/commands/filters/roll/up.rs similarity index 100% rename from crates/nu-command/src/commands/filters/roll/up.rs rename to old_nushell/crates/nu-command/src/commands/filters/roll/up.rs diff --git a/crates/nu-command/src/commands/filters/rotate/command.rs b/old_nushell/crates/nu-command/src/commands/filters/rotate/command.rs similarity index 100% rename from crates/nu-command/src/commands/filters/rotate/command.rs rename to old_nushell/crates/nu-command/src/commands/filters/rotate/command.rs diff --git a/crates/nu-command/src/commands/filters/rotate/counter_clockwise.rs b/old_nushell/crates/nu-command/src/commands/filters/rotate/counter_clockwise.rs similarity index 100% rename from crates/nu-command/src/commands/filters/rotate/counter_clockwise.rs rename to old_nushell/crates/nu-command/src/commands/filters/rotate/counter_clockwise.rs diff --git a/crates/nu-command/src/commands/filters/rotate/mod.rs b/old_nushell/crates/nu-command/src/commands/filters/rotate/mod.rs similarity index 100% rename from crates/nu-command/src/commands/filters/rotate/mod.rs rename to old_nushell/crates/nu-command/src/commands/filters/rotate/mod.rs diff --git a/crates/nu-command/src/commands/filters/select.rs b/old_nushell/crates/nu-command/src/commands/filters/select.rs similarity index 100% rename from crates/nu-command/src/commands/filters/select.rs rename to old_nushell/crates/nu-command/src/commands/filters/select.rs diff --git a/crates/nu-command/src/commands/filters/shuffle.rs b/old_nushell/crates/nu-command/src/commands/filters/shuffle.rs similarity index 100% rename from crates/nu-command/src/commands/filters/shuffle.rs rename to old_nushell/crates/nu-command/src/commands/filters/shuffle.rs diff --git a/crates/nu-command/src/commands/filters/skip/command.rs b/old_nushell/crates/nu-command/src/commands/filters/skip/command.rs similarity index 100% rename from crates/nu-command/src/commands/filters/skip/command.rs rename to old_nushell/crates/nu-command/src/commands/filters/skip/command.rs diff --git a/crates/nu-command/src/commands/filters/skip/mod.rs b/old_nushell/crates/nu-command/src/commands/filters/skip/mod.rs similarity index 100% rename from crates/nu-command/src/commands/filters/skip/mod.rs rename to old_nushell/crates/nu-command/src/commands/filters/skip/mod.rs diff --git a/crates/nu-command/src/commands/filters/skip/until.rs b/old_nushell/crates/nu-command/src/commands/filters/skip/until.rs similarity index 100% rename from crates/nu-command/src/commands/filters/skip/until.rs rename to old_nushell/crates/nu-command/src/commands/filters/skip/until.rs diff --git a/crates/nu-command/src/commands/filters/skip/while_.rs b/old_nushell/crates/nu-command/src/commands/filters/skip/while_.rs similarity index 100% rename from crates/nu-command/src/commands/filters/skip/while_.rs rename to old_nushell/crates/nu-command/src/commands/filters/skip/while_.rs diff --git a/crates/nu-command/src/commands/filters/sort_by.rs b/old_nushell/crates/nu-command/src/commands/filters/sort_by.rs similarity index 100% rename from crates/nu-command/src/commands/filters/sort_by.rs rename to old_nushell/crates/nu-command/src/commands/filters/sort_by.rs diff --git a/crates/nu-command/src/commands/filters/uniq.rs b/old_nushell/crates/nu-command/src/commands/filters/uniq.rs similarity index 100% rename from crates/nu-command/src/commands/filters/uniq.rs rename to old_nushell/crates/nu-command/src/commands/filters/uniq.rs diff --git a/crates/nu-command/src/commands/filters/update.rs b/old_nushell/crates/nu-command/src/commands/filters/update.rs similarity index 100% rename from crates/nu-command/src/commands/filters/update.rs rename to old_nushell/crates/nu-command/src/commands/filters/update.rs diff --git a/crates/nu-command/src/commands/filters/update_cells.rs b/old_nushell/crates/nu-command/src/commands/filters/update_cells.rs similarity index 100% rename from crates/nu-command/src/commands/filters/update_cells.rs rename to old_nushell/crates/nu-command/src/commands/filters/update_cells.rs diff --git a/crates/nu-command/src/commands/filters/where_.rs b/old_nushell/crates/nu-command/src/commands/filters/where_.rs similarity index 100% rename from crates/nu-command/src/commands/filters/where_.rs rename to old_nushell/crates/nu-command/src/commands/filters/where_.rs diff --git a/crates/nu-command/src/commands/filters/wrap.rs b/old_nushell/crates/nu-command/src/commands/filters/wrap.rs similarity index 100% rename from crates/nu-command/src/commands/filters/wrap.rs rename to old_nushell/crates/nu-command/src/commands/filters/wrap.rs diff --git a/crates/nu-command/src/commands/filters/zip_.rs b/old_nushell/crates/nu-command/src/commands/filters/zip_.rs similarity index 100% rename from crates/nu-command/src/commands/filters/zip_.rs rename to old_nushell/crates/nu-command/src/commands/filters/zip_.rs diff --git a/crates/nu-command/src/commands/formats/from/command.rs b/old_nushell/crates/nu-command/src/commands/formats/from/command.rs similarity index 100% rename from crates/nu-command/src/commands/formats/from/command.rs rename to old_nushell/crates/nu-command/src/commands/formats/from/command.rs diff --git a/crates/nu-command/src/commands/formats/from/csv.rs b/old_nushell/crates/nu-command/src/commands/formats/from/csv.rs similarity index 100% rename from crates/nu-command/src/commands/formats/from/csv.rs rename to old_nushell/crates/nu-command/src/commands/formats/from/csv.rs diff --git a/crates/nu-command/src/commands/formats/from/delimited.rs b/old_nushell/crates/nu-command/src/commands/formats/from/delimited.rs similarity index 100% rename from crates/nu-command/src/commands/formats/from/delimited.rs rename to old_nushell/crates/nu-command/src/commands/formats/from/delimited.rs diff --git a/crates/nu-command/src/commands/formats/from/eml.rs b/old_nushell/crates/nu-command/src/commands/formats/from/eml.rs similarity index 100% rename from crates/nu-command/src/commands/formats/from/eml.rs rename to old_nushell/crates/nu-command/src/commands/formats/from/eml.rs diff --git a/crates/nu-command/src/commands/formats/from/ics.rs b/old_nushell/crates/nu-command/src/commands/formats/from/ics.rs similarity index 100% rename from crates/nu-command/src/commands/formats/from/ics.rs rename to old_nushell/crates/nu-command/src/commands/formats/from/ics.rs diff --git a/crates/nu-command/src/commands/formats/from/ini.rs b/old_nushell/crates/nu-command/src/commands/formats/from/ini.rs similarity index 100% rename from crates/nu-command/src/commands/formats/from/ini.rs rename to old_nushell/crates/nu-command/src/commands/formats/from/ini.rs diff --git a/crates/nu-command/src/commands/formats/from/json.rs b/old_nushell/crates/nu-command/src/commands/formats/from/json.rs similarity index 100% rename from crates/nu-command/src/commands/formats/from/json.rs rename to old_nushell/crates/nu-command/src/commands/formats/from/json.rs diff --git a/crates/nu-command/src/commands/formats/from/mod.rs b/old_nushell/crates/nu-command/src/commands/formats/from/mod.rs similarity index 100% rename from crates/nu-command/src/commands/formats/from/mod.rs rename to old_nushell/crates/nu-command/src/commands/formats/from/mod.rs diff --git a/crates/nu-command/src/commands/formats/from/ods.rs b/old_nushell/crates/nu-command/src/commands/formats/from/ods.rs similarity index 100% rename from crates/nu-command/src/commands/formats/from/ods.rs rename to old_nushell/crates/nu-command/src/commands/formats/from/ods.rs diff --git a/crates/nu-command/src/commands/formats/from/ssv.rs b/old_nushell/crates/nu-command/src/commands/formats/from/ssv.rs similarity index 100% rename from crates/nu-command/src/commands/formats/from/ssv.rs rename to old_nushell/crates/nu-command/src/commands/formats/from/ssv.rs diff --git a/crates/nu-command/src/commands/formats/from/toml.rs b/old_nushell/crates/nu-command/src/commands/formats/from/toml.rs similarity index 100% rename from crates/nu-command/src/commands/formats/from/toml.rs rename to old_nushell/crates/nu-command/src/commands/formats/from/toml.rs diff --git a/crates/nu-command/src/commands/formats/from/tsv.rs b/old_nushell/crates/nu-command/src/commands/formats/from/tsv.rs similarity index 100% rename from crates/nu-command/src/commands/formats/from/tsv.rs rename to old_nushell/crates/nu-command/src/commands/formats/from/tsv.rs diff --git a/crates/nu-command/src/commands/formats/from/url.rs b/old_nushell/crates/nu-command/src/commands/formats/from/url.rs similarity index 100% rename from crates/nu-command/src/commands/formats/from/url.rs rename to old_nushell/crates/nu-command/src/commands/formats/from/url.rs diff --git a/crates/nu-command/src/commands/formats/from/vcf.rs b/old_nushell/crates/nu-command/src/commands/formats/from/vcf.rs similarity index 100% rename from crates/nu-command/src/commands/formats/from/vcf.rs rename to old_nushell/crates/nu-command/src/commands/formats/from/vcf.rs diff --git a/crates/nu-command/src/commands/formats/from/xlsx.rs b/old_nushell/crates/nu-command/src/commands/formats/from/xlsx.rs similarity index 100% rename from crates/nu-command/src/commands/formats/from/xlsx.rs rename to old_nushell/crates/nu-command/src/commands/formats/from/xlsx.rs diff --git a/crates/nu-command/src/commands/formats/from/xml.rs b/old_nushell/crates/nu-command/src/commands/formats/from/xml.rs similarity index 100% rename from crates/nu-command/src/commands/formats/from/xml.rs rename to old_nushell/crates/nu-command/src/commands/formats/from/xml.rs diff --git a/crates/nu-command/src/commands/formats/from/yaml.rs b/old_nushell/crates/nu-command/src/commands/formats/from/yaml.rs similarity index 100% rename from crates/nu-command/src/commands/formats/from/yaml.rs rename to old_nushell/crates/nu-command/src/commands/formats/from/yaml.rs diff --git a/old_nushell/crates/nu-command/src/commands/formats/mod.rs b/old_nushell/crates/nu-command/src/commands/formats/mod.rs new file mode 100644 index 0000000000..86f06f85fd --- /dev/null +++ b/old_nushell/crates/nu-command/src/commands/formats/mod.rs @@ -0,0 +1,5 @@ +mod from; +mod to; + +pub use from::*; +pub use to::*; diff --git a/crates/nu-command/src/commands/formats/to/command.rs b/old_nushell/crates/nu-command/src/commands/formats/to/command.rs similarity index 100% rename from crates/nu-command/src/commands/formats/to/command.rs rename to old_nushell/crates/nu-command/src/commands/formats/to/command.rs diff --git a/crates/nu-command/src/commands/formats/to/csv.rs b/old_nushell/crates/nu-command/src/commands/formats/to/csv.rs similarity index 100% rename from crates/nu-command/src/commands/formats/to/csv.rs rename to old_nushell/crates/nu-command/src/commands/formats/to/csv.rs diff --git a/crates/nu-command/src/commands/formats/to/delimited.rs b/old_nushell/crates/nu-command/src/commands/formats/to/delimited.rs similarity index 100% rename from crates/nu-command/src/commands/formats/to/delimited.rs rename to old_nushell/crates/nu-command/src/commands/formats/to/delimited.rs diff --git a/crates/nu-command/src/commands/formats/to/html.rs b/old_nushell/crates/nu-command/src/commands/formats/to/html.rs similarity index 100% rename from crates/nu-command/src/commands/formats/to/html.rs rename to old_nushell/crates/nu-command/src/commands/formats/to/html.rs diff --git a/crates/nu-command/src/commands/formats/to/json.rs b/old_nushell/crates/nu-command/src/commands/formats/to/json.rs similarity index 100% rename from crates/nu-command/src/commands/formats/to/json.rs rename to old_nushell/crates/nu-command/src/commands/formats/to/json.rs diff --git a/crates/nu-command/src/commands/formats/to/md.rs b/old_nushell/crates/nu-command/src/commands/formats/to/md.rs similarity index 100% rename from crates/nu-command/src/commands/formats/to/md.rs rename to old_nushell/crates/nu-command/src/commands/formats/to/md.rs diff --git a/crates/nu-command/src/commands/formats/to/mod.rs b/old_nushell/crates/nu-command/src/commands/formats/to/mod.rs similarity index 100% rename from crates/nu-command/src/commands/formats/to/mod.rs rename to old_nushell/crates/nu-command/src/commands/formats/to/mod.rs diff --git a/crates/nu-command/src/commands/formats/to/toml.rs b/old_nushell/crates/nu-command/src/commands/formats/to/toml.rs similarity index 100% rename from crates/nu-command/src/commands/formats/to/toml.rs rename to old_nushell/crates/nu-command/src/commands/formats/to/toml.rs diff --git a/crates/nu-command/src/commands/formats/to/tsv.rs b/old_nushell/crates/nu-command/src/commands/formats/to/tsv.rs similarity index 100% rename from crates/nu-command/src/commands/formats/to/tsv.rs rename to old_nushell/crates/nu-command/src/commands/formats/to/tsv.rs diff --git a/crates/nu-command/src/commands/formats/to/url.rs b/old_nushell/crates/nu-command/src/commands/formats/to/url.rs similarity index 100% rename from crates/nu-command/src/commands/formats/to/url.rs rename to old_nushell/crates/nu-command/src/commands/formats/to/url.rs diff --git a/crates/nu-command/src/commands/formats/to/xml.rs b/old_nushell/crates/nu-command/src/commands/formats/to/xml.rs similarity index 100% rename from crates/nu-command/src/commands/formats/to/xml.rs rename to old_nushell/crates/nu-command/src/commands/formats/to/xml.rs diff --git a/crates/nu-command/src/commands/formats/to/yaml.rs b/old_nushell/crates/nu-command/src/commands/formats/to/yaml.rs similarity index 100% rename from crates/nu-command/src/commands/formats/to/yaml.rs rename to old_nushell/crates/nu-command/src/commands/formats/to/yaml.rs diff --git a/crates/nu-command/src/commands/generators/cal.rs b/old_nushell/crates/nu-command/src/commands/generators/cal.rs similarity index 100% rename from crates/nu-command/src/commands/generators/cal.rs rename to old_nushell/crates/nu-command/src/commands/generators/cal.rs diff --git a/crates/nu-command/src/commands/generators/date/command.rs b/old_nushell/crates/nu-command/src/commands/generators/date/command.rs similarity index 100% rename from crates/nu-command/src/commands/generators/date/command.rs rename to old_nushell/crates/nu-command/src/commands/generators/date/command.rs diff --git a/crates/nu-command/src/commands/generators/date/format.rs b/old_nushell/crates/nu-command/src/commands/generators/date/format.rs similarity index 100% rename from crates/nu-command/src/commands/generators/date/format.rs rename to old_nushell/crates/nu-command/src/commands/generators/date/format.rs diff --git a/crates/nu-command/src/commands/generators/date/humanize.rs b/old_nushell/crates/nu-command/src/commands/generators/date/humanize.rs similarity index 100% rename from crates/nu-command/src/commands/generators/date/humanize.rs rename to old_nushell/crates/nu-command/src/commands/generators/date/humanize.rs diff --git a/crates/nu-command/src/commands/generators/date/list_timezone.rs b/old_nushell/crates/nu-command/src/commands/generators/date/list_timezone.rs similarity index 100% rename from crates/nu-command/src/commands/generators/date/list_timezone.rs rename to old_nushell/crates/nu-command/src/commands/generators/date/list_timezone.rs diff --git a/crates/nu-command/src/commands/generators/date/mod.rs b/old_nushell/crates/nu-command/src/commands/generators/date/mod.rs similarity index 100% rename from crates/nu-command/src/commands/generators/date/mod.rs rename to old_nushell/crates/nu-command/src/commands/generators/date/mod.rs diff --git a/crates/nu-command/src/commands/generators/date/now.rs b/old_nushell/crates/nu-command/src/commands/generators/date/now.rs similarity index 100% rename from crates/nu-command/src/commands/generators/date/now.rs rename to old_nushell/crates/nu-command/src/commands/generators/date/now.rs diff --git a/old_nushell/crates/nu-command/src/commands/generators/date/parser.rs b/old_nushell/crates/nu-command/src/commands/generators/date/parser.rs new file mode 100644 index 0000000000..d2d1f3a851 --- /dev/null +++ b/old_nushell/crates/nu-command/src/commands/generators/date/parser.rs @@ -0,0 +1,107 @@ +// Modified from chrono::format::scan + +use chrono::{DateTime, FixedOffset, Local, Offset, TimeZone}; +use chrono_tz::Tz; +use titlecase::titlecase; + +#[derive(Debug, Clone, PartialEq, Eq, Copy)] +pub enum ParseErrorKind { + /// Given field is out of permitted range. + OutOfRange, + + /// The input string has some invalid character sequence for given formatting items. + Invalid, + + /// The input string has been prematurely ended. + TooShort, +} + +pub fn datetime_in_timezone( + dt: &DateTime, + s: &str, +) -> Result, ParseErrorKind> { + match timezone_offset_internal(s, true, true) { + Ok(offset) => match FixedOffset::east_opt(offset) { + Some(offset) => Ok(dt.with_timezone(&offset)), + None => Err(ParseErrorKind::OutOfRange), + }, + Err(ParseErrorKind::Invalid) => { + if s.to_lowercase() == "local" { + Ok(dt.with_timezone(Local::now().offset())) + } else { + let tz: Tz = parse_timezone_internal(s)?; + let offset = tz.offset_from_utc_datetime(&dt.naive_utc()).fix(); + Ok(dt.with_timezone(&offset)) + } + } + Err(e) => Err(e), + } +} + +fn parse_timezone_internal(s: &str) -> Result { + if let Ok(tz) = s.parse() { + Ok(tz) + } else if let Ok(tz) = titlecase(s).parse() { + Ok(tz) + } else if let Ok(tz) = s.to_uppercase().parse() { + Ok(tz) + } else { + Err(ParseErrorKind::Invalid) + } +} + +fn timezone_offset_internal( + mut s: &str, + consume_colon: bool, + allow_missing_minutes: bool, +) -> Result { + fn digits(s: &str) -> Result<(u8, u8), ParseErrorKind> { + let b = s.as_bytes(); + if b.len() < 2 { + Err(ParseErrorKind::TooShort) + } else { + Ok((b[0], b[1])) + } + } + let negative = match s.as_bytes().first() { + Some(&b'+') => false, + Some(&b'-') => true, + Some(_) => return Err(ParseErrorKind::Invalid), + None => return Err(ParseErrorKind::TooShort), + }; + s = &s[1..]; + + // hours (00--99) + let hours = match digits(s)? { + (h1 @ b'0'..=b'9', h2 @ b'0'..=b'9') => i32::from((h1 - b'0') * 10 + (h2 - b'0')), + _ => return Err(ParseErrorKind::Invalid), + }; + s = &s[2..]; + + // colons (and possibly other separators) + if consume_colon { + s = s.trim_start_matches(|c: char| c == ':' || c.is_whitespace()); + } + + // minutes (00--59) + // if the next two items are digits then we have to add minutes + let minutes = if let Ok(ds) = digits(s) { + match ds { + (m1 @ b'0'..=b'5', m2 @ b'0'..=b'9') => i32::from((m1 - b'0') * 10 + (m2 - b'0')), + (b'6'..=b'9', b'0'..=b'9') => return Err(ParseErrorKind::OutOfRange), + _ => return Err(ParseErrorKind::Invalid), + } + } else if allow_missing_minutes { + 0 + } else { + return Err(ParseErrorKind::TooShort); + }; + match s.len() { + len if len >= 2 => &s[2..], + len if len == 0 => s, + _ => return Err(ParseErrorKind::TooShort), + }; + + let seconds = hours * 3600 + minutes * 60; + Ok(if negative { -seconds } else { seconds }) +} diff --git a/crates/nu-command/src/commands/generators/date/to_table.rs b/old_nushell/crates/nu-command/src/commands/generators/date/to_table.rs similarity index 100% rename from crates/nu-command/src/commands/generators/date/to_table.rs rename to old_nushell/crates/nu-command/src/commands/generators/date/to_table.rs diff --git a/crates/nu-command/src/commands/generators/date/to_timezone.rs b/old_nushell/crates/nu-command/src/commands/generators/date/to_timezone.rs similarity index 100% rename from crates/nu-command/src/commands/generators/date/to_timezone.rs rename to old_nushell/crates/nu-command/src/commands/generators/date/to_timezone.rs diff --git a/crates/nu-command/src/commands/generators/for_in.rs b/old_nushell/crates/nu-command/src/commands/generators/for_in.rs similarity index 100% rename from crates/nu-command/src/commands/generators/for_in.rs rename to old_nushell/crates/nu-command/src/commands/generators/for_in.rs diff --git a/crates/nu-command/src/commands/generators/hash_/base64_.rs b/old_nushell/crates/nu-command/src/commands/generators/hash_/base64_.rs similarity index 100% rename from crates/nu-command/src/commands/generators/hash_/base64_.rs rename to old_nushell/crates/nu-command/src/commands/generators/hash_/base64_.rs diff --git a/crates/nu-command/src/commands/generators/hash_/command.rs b/old_nushell/crates/nu-command/src/commands/generators/hash_/command.rs similarity index 100% rename from crates/nu-command/src/commands/generators/hash_/command.rs rename to old_nushell/crates/nu-command/src/commands/generators/hash_/command.rs diff --git a/crates/nu-command/src/commands/generators/hash_/generic_digest.rs b/old_nushell/crates/nu-command/src/commands/generators/hash_/generic_digest.rs similarity index 100% rename from crates/nu-command/src/commands/generators/hash_/generic_digest.rs rename to old_nushell/crates/nu-command/src/commands/generators/hash_/generic_digest.rs diff --git a/crates/nu-command/src/commands/generators/hash_/md5_.rs b/old_nushell/crates/nu-command/src/commands/generators/hash_/md5_.rs similarity index 100% rename from crates/nu-command/src/commands/generators/hash_/md5_.rs rename to old_nushell/crates/nu-command/src/commands/generators/hash_/md5_.rs diff --git a/crates/nu-command/src/commands/generators/hash_/mod.rs b/old_nushell/crates/nu-command/src/commands/generators/hash_/mod.rs similarity index 100% rename from crates/nu-command/src/commands/generators/hash_/mod.rs rename to old_nushell/crates/nu-command/src/commands/generators/hash_/mod.rs diff --git a/crates/nu-command/src/commands/generators/hash_/sha256_.rs b/old_nushell/crates/nu-command/src/commands/generators/hash_/sha256_.rs similarity index 100% rename from crates/nu-command/src/commands/generators/hash_/sha256_.rs rename to old_nushell/crates/nu-command/src/commands/generators/hash_/sha256_.rs diff --git a/crates/nu-command/src/commands/generators/mod.rs b/old_nushell/crates/nu-command/src/commands/generators/mod.rs similarity index 100% rename from crates/nu-command/src/commands/generators/mod.rs rename to old_nushell/crates/nu-command/src/commands/generators/mod.rs diff --git a/crates/nu-command/src/commands/generators/seq.rs b/old_nushell/crates/nu-command/src/commands/generators/seq.rs similarity index 100% rename from crates/nu-command/src/commands/generators/seq.rs rename to old_nushell/crates/nu-command/src/commands/generators/seq.rs diff --git a/crates/nu-command/src/commands/generators/seq_dates.rs b/old_nushell/crates/nu-command/src/commands/generators/seq_dates.rs similarity index 100% rename from crates/nu-command/src/commands/generators/seq_dates.rs rename to old_nushell/crates/nu-command/src/commands/generators/seq_dates.rs diff --git a/crates/nu-command/src/commands/math/abs.rs b/old_nushell/crates/nu-command/src/commands/math/abs.rs similarity index 100% rename from crates/nu-command/src/commands/math/abs.rs rename to old_nushell/crates/nu-command/src/commands/math/abs.rs diff --git a/crates/nu-command/src/commands/math/avg.rs b/old_nushell/crates/nu-command/src/commands/math/avg.rs similarity index 100% rename from crates/nu-command/src/commands/math/avg.rs rename to old_nushell/crates/nu-command/src/commands/math/avg.rs diff --git a/crates/nu-command/src/commands/math/ceil.rs b/old_nushell/crates/nu-command/src/commands/math/ceil.rs similarity index 100% rename from crates/nu-command/src/commands/math/ceil.rs rename to old_nushell/crates/nu-command/src/commands/math/ceil.rs diff --git a/crates/nu-command/src/commands/math/command.rs b/old_nushell/crates/nu-command/src/commands/math/command.rs similarity index 100% rename from crates/nu-command/src/commands/math/command.rs rename to old_nushell/crates/nu-command/src/commands/math/command.rs diff --git a/crates/nu-command/src/commands/math/eval.rs b/old_nushell/crates/nu-command/src/commands/math/eval.rs similarity index 100% rename from crates/nu-command/src/commands/math/eval.rs rename to old_nushell/crates/nu-command/src/commands/math/eval.rs diff --git a/crates/nu-command/src/commands/math/floor.rs b/old_nushell/crates/nu-command/src/commands/math/floor.rs similarity index 100% rename from crates/nu-command/src/commands/math/floor.rs rename to old_nushell/crates/nu-command/src/commands/math/floor.rs diff --git a/crates/nu-command/src/commands/math/max.rs b/old_nushell/crates/nu-command/src/commands/math/max.rs similarity index 100% rename from crates/nu-command/src/commands/math/max.rs rename to old_nushell/crates/nu-command/src/commands/math/max.rs diff --git a/crates/nu-command/src/commands/math/median.rs b/old_nushell/crates/nu-command/src/commands/math/median.rs similarity index 100% rename from crates/nu-command/src/commands/math/median.rs rename to old_nushell/crates/nu-command/src/commands/math/median.rs diff --git a/crates/nu-command/src/commands/math/min.rs b/old_nushell/crates/nu-command/src/commands/math/min.rs similarity index 100% rename from crates/nu-command/src/commands/math/min.rs rename to old_nushell/crates/nu-command/src/commands/math/min.rs diff --git a/crates/nu-command/src/commands/math/mod.rs b/old_nushell/crates/nu-command/src/commands/math/mod.rs similarity index 100% rename from crates/nu-command/src/commands/math/mod.rs rename to old_nushell/crates/nu-command/src/commands/math/mod.rs diff --git a/crates/nu-command/src/commands/math/mode.rs b/old_nushell/crates/nu-command/src/commands/math/mode.rs similarity index 100% rename from crates/nu-command/src/commands/math/mode.rs rename to old_nushell/crates/nu-command/src/commands/math/mode.rs diff --git a/crates/nu-command/src/commands/math/product.rs b/old_nushell/crates/nu-command/src/commands/math/product.rs similarity index 100% rename from crates/nu-command/src/commands/math/product.rs rename to old_nushell/crates/nu-command/src/commands/math/product.rs diff --git a/crates/nu-command/src/commands/math/reducers.rs b/old_nushell/crates/nu-command/src/commands/math/reducers.rs similarity index 100% rename from crates/nu-command/src/commands/math/reducers.rs rename to old_nushell/crates/nu-command/src/commands/math/reducers.rs diff --git a/crates/nu-command/src/commands/math/round.rs b/old_nushell/crates/nu-command/src/commands/math/round.rs similarity index 100% rename from crates/nu-command/src/commands/math/round.rs rename to old_nushell/crates/nu-command/src/commands/math/round.rs diff --git a/crates/nu-command/src/commands/math/sqrt.rs b/old_nushell/crates/nu-command/src/commands/math/sqrt.rs similarity index 100% rename from crates/nu-command/src/commands/math/sqrt.rs rename to old_nushell/crates/nu-command/src/commands/math/sqrt.rs diff --git a/crates/nu-command/src/commands/math/stddev.rs b/old_nushell/crates/nu-command/src/commands/math/stddev.rs similarity index 100% rename from crates/nu-command/src/commands/math/stddev.rs rename to old_nushell/crates/nu-command/src/commands/math/stddev.rs diff --git a/crates/nu-command/src/commands/math/sum.rs b/old_nushell/crates/nu-command/src/commands/math/sum.rs similarity index 100% rename from crates/nu-command/src/commands/math/sum.rs rename to old_nushell/crates/nu-command/src/commands/math/sum.rs diff --git a/crates/nu-command/src/commands/math/utils.rs b/old_nushell/crates/nu-command/src/commands/math/utils.rs similarity index 100% rename from crates/nu-command/src/commands/math/utils.rs rename to old_nushell/crates/nu-command/src/commands/math/utils.rs diff --git a/crates/nu-command/src/commands/math/variance.rs b/old_nushell/crates/nu-command/src/commands/math/variance.rs similarity index 100% rename from crates/nu-command/src/commands/math/variance.rs rename to old_nushell/crates/nu-command/src/commands/math/variance.rs diff --git a/crates/nu-command/src/commands/mod.rs b/old_nushell/crates/nu-command/src/commands/mod.rs similarity index 100% rename from crates/nu-command/src/commands/mod.rs rename to old_nushell/crates/nu-command/src/commands/mod.rs diff --git a/crates/nu-command/src/commands/network/fetch.rs b/old_nushell/crates/nu-command/src/commands/network/fetch.rs similarity index 100% rename from crates/nu-command/src/commands/network/fetch.rs rename to old_nushell/crates/nu-command/src/commands/network/fetch.rs diff --git a/crates/nu-command/src/commands/network/mod.rs b/old_nushell/crates/nu-command/src/commands/network/mod.rs similarity index 100% rename from crates/nu-command/src/commands/network/mod.rs rename to old_nushell/crates/nu-command/src/commands/network/mod.rs diff --git a/crates/nu-command/src/commands/network/post.rs b/old_nushell/crates/nu-command/src/commands/network/post.rs similarity index 100% rename from crates/nu-command/src/commands/network/post.rs rename to old_nushell/crates/nu-command/src/commands/network/post.rs diff --git a/crates/nu-command/src/commands/network/url_/command.rs b/old_nushell/crates/nu-command/src/commands/network/url_/command.rs similarity index 100% rename from crates/nu-command/src/commands/network/url_/command.rs rename to old_nushell/crates/nu-command/src/commands/network/url_/command.rs diff --git a/crates/nu-command/src/commands/network/url_/host.rs b/old_nushell/crates/nu-command/src/commands/network/url_/host.rs similarity index 100% rename from crates/nu-command/src/commands/network/url_/host.rs rename to old_nushell/crates/nu-command/src/commands/network/url_/host.rs diff --git a/crates/nu-command/src/commands/network/url_/mod.rs b/old_nushell/crates/nu-command/src/commands/network/url_/mod.rs similarity index 100% rename from crates/nu-command/src/commands/network/url_/mod.rs rename to old_nushell/crates/nu-command/src/commands/network/url_/mod.rs diff --git a/crates/nu-command/src/commands/network/url_/path.rs b/old_nushell/crates/nu-command/src/commands/network/url_/path.rs similarity index 100% rename from crates/nu-command/src/commands/network/url_/path.rs rename to old_nushell/crates/nu-command/src/commands/network/url_/path.rs diff --git a/crates/nu-command/src/commands/network/url_/query.rs b/old_nushell/crates/nu-command/src/commands/network/url_/query.rs similarity index 100% rename from crates/nu-command/src/commands/network/url_/query.rs rename to old_nushell/crates/nu-command/src/commands/network/url_/query.rs diff --git a/crates/nu-command/src/commands/network/url_/scheme.rs b/old_nushell/crates/nu-command/src/commands/network/url_/scheme.rs similarity index 100% rename from crates/nu-command/src/commands/network/url_/scheme.rs rename to old_nushell/crates/nu-command/src/commands/network/url_/scheme.rs diff --git a/crates/nu-command/src/commands/path/basename.rs b/old_nushell/crates/nu-command/src/commands/path/basename.rs similarity index 100% rename from crates/nu-command/src/commands/path/basename.rs rename to old_nushell/crates/nu-command/src/commands/path/basename.rs diff --git a/crates/nu-command/src/commands/path/command.rs b/old_nushell/crates/nu-command/src/commands/path/command.rs similarity index 100% rename from crates/nu-command/src/commands/path/command.rs rename to old_nushell/crates/nu-command/src/commands/path/command.rs diff --git a/crates/nu-command/src/commands/path/dirname.rs b/old_nushell/crates/nu-command/src/commands/path/dirname.rs similarity index 100% rename from crates/nu-command/src/commands/path/dirname.rs rename to old_nushell/crates/nu-command/src/commands/path/dirname.rs diff --git a/crates/nu-command/src/commands/path/exists.rs b/old_nushell/crates/nu-command/src/commands/path/exists.rs similarity index 100% rename from crates/nu-command/src/commands/path/exists.rs rename to old_nushell/crates/nu-command/src/commands/path/exists.rs diff --git a/crates/nu-command/src/commands/path/expand.rs b/old_nushell/crates/nu-command/src/commands/path/expand.rs similarity index 100% rename from crates/nu-command/src/commands/path/expand.rs rename to old_nushell/crates/nu-command/src/commands/path/expand.rs diff --git a/crates/nu-command/src/commands/path/join.rs b/old_nushell/crates/nu-command/src/commands/path/join.rs similarity index 100% rename from crates/nu-command/src/commands/path/join.rs rename to old_nushell/crates/nu-command/src/commands/path/join.rs diff --git a/crates/nu-command/src/commands/path/mod.rs b/old_nushell/crates/nu-command/src/commands/path/mod.rs similarity index 100% rename from crates/nu-command/src/commands/path/mod.rs rename to old_nushell/crates/nu-command/src/commands/path/mod.rs diff --git a/crates/nu-command/src/commands/path/parse.rs b/old_nushell/crates/nu-command/src/commands/path/parse.rs similarity index 100% rename from crates/nu-command/src/commands/path/parse.rs rename to old_nushell/crates/nu-command/src/commands/path/parse.rs diff --git a/crates/nu-command/src/commands/path/relative_to.rs b/old_nushell/crates/nu-command/src/commands/path/relative_to.rs similarity index 100% rename from crates/nu-command/src/commands/path/relative_to.rs rename to old_nushell/crates/nu-command/src/commands/path/relative_to.rs diff --git a/crates/nu-command/src/commands/path/split.rs b/old_nushell/crates/nu-command/src/commands/path/split.rs similarity index 100% rename from crates/nu-command/src/commands/path/split.rs rename to old_nushell/crates/nu-command/src/commands/path/split.rs diff --git a/crates/nu-command/src/commands/path/type.rs b/old_nushell/crates/nu-command/src/commands/path/type.rs similarity index 100% rename from crates/nu-command/src/commands/path/type.rs rename to old_nushell/crates/nu-command/src/commands/path/type.rs diff --git a/crates/nu-command/src/commands/pathvar/add.rs b/old_nushell/crates/nu-command/src/commands/pathvar/add.rs similarity index 100% rename from crates/nu-command/src/commands/pathvar/add.rs rename to old_nushell/crates/nu-command/src/commands/pathvar/add.rs diff --git a/crates/nu-command/src/commands/pathvar/append.rs b/old_nushell/crates/nu-command/src/commands/pathvar/append.rs similarity index 100% rename from crates/nu-command/src/commands/pathvar/append.rs rename to old_nushell/crates/nu-command/src/commands/pathvar/append.rs diff --git a/crates/nu-command/src/commands/pathvar/command.rs b/old_nushell/crates/nu-command/src/commands/pathvar/command.rs similarity index 100% rename from crates/nu-command/src/commands/pathvar/command.rs rename to old_nushell/crates/nu-command/src/commands/pathvar/command.rs diff --git a/crates/nu-command/src/commands/pathvar/mod.rs b/old_nushell/crates/nu-command/src/commands/pathvar/mod.rs similarity index 100% rename from crates/nu-command/src/commands/pathvar/mod.rs rename to old_nushell/crates/nu-command/src/commands/pathvar/mod.rs diff --git a/crates/nu-command/src/commands/pathvar/remove.rs b/old_nushell/crates/nu-command/src/commands/pathvar/remove.rs similarity index 100% rename from crates/nu-command/src/commands/pathvar/remove.rs rename to old_nushell/crates/nu-command/src/commands/pathvar/remove.rs diff --git a/crates/nu-command/src/commands/pathvar/reset.rs b/old_nushell/crates/nu-command/src/commands/pathvar/reset.rs similarity index 100% rename from crates/nu-command/src/commands/pathvar/reset.rs rename to old_nushell/crates/nu-command/src/commands/pathvar/reset.rs diff --git a/crates/nu-command/src/commands/pathvar/save.rs b/old_nushell/crates/nu-command/src/commands/pathvar/save.rs similarity index 100% rename from crates/nu-command/src/commands/pathvar/save.rs rename to old_nushell/crates/nu-command/src/commands/pathvar/save.rs diff --git a/crates/nu-command/src/commands/platform/ansi/command.rs b/old_nushell/crates/nu-command/src/commands/platform/ansi/command.rs similarity index 100% rename from crates/nu-command/src/commands/platform/ansi/command.rs rename to old_nushell/crates/nu-command/src/commands/platform/ansi/command.rs diff --git a/crates/nu-command/src/commands/platform/ansi/gradient.rs b/old_nushell/crates/nu-command/src/commands/platform/ansi/gradient.rs similarity index 100% rename from crates/nu-command/src/commands/platform/ansi/gradient.rs rename to old_nushell/crates/nu-command/src/commands/platform/ansi/gradient.rs diff --git a/crates/nu-command/src/commands/platform/ansi/mod.rs b/old_nushell/crates/nu-command/src/commands/platform/ansi/mod.rs similarity index 100% rename from crates/nu-command/src/commands/platform/ansi/mod.rs rename to old_nushell/crates/nu-command/src/commands/platform/ansi/mod.rs diff --git a/crates/nu-command/src/commands/platform/ansi/strip.rs b/old_nushell/crates/nu-command/src/commands/platform/ansi/strip.rs similarity index 100% rename from crates/nu-command/src/commands/platform/ansi/strip.rs rename to old_nushell/crates/nu-command/src/commands/platform/ansi/strip.rs diff --git a/crates/nu-command/src/commands/platform/benchmark.rs b/old_nushell/crates/nu-command/src/commands/platform/benchmark.rs similarity index 100% rename from crates/nu-command/src/commands/platform/benchmark.rs rename to old_nushell/crates/nu-command/src/commands/platform/benchmark.rs diff --git a/crates/nu-command/src/commands/platform/clear.rs b/old_nushell/crates/nu-command/src/commands/platform/clear.rs similarity index 100% rename from crates/nu-command/src/commands/platform/clear.rs rename to old_nushell/crates/nu-command/src/commands/platform/clear.rs diff --git a/crates/nu-command/src/commands/platform/du.rs b/old_nushell/crates/nu-command/src/commands/platform/du.rs similarity index 100% rename from crates/nu-command/src/commands/platform/du.rs rename to old_nushell/crates/nu-command/src/commands/platform/du.rs diff --git a/crates/nu-command/src/commands/platform/exec.rs b/old_nushell/crates/nu-command/src/commands/platform/exec.rs similarity index 100% rename from crates/nu-command/src/commands/platform/exec.rs rename to old_nushell/crates/nu-command/src/commands/platform/exec.rs diff --git a/crates/nu-command/src/commands/platform/kill.rs b/old_nushell/crates/nu-command/src/commands/platform/kill.rs similarity index 100% rename from crates/nu-command/src/commands/platform/kill.rs rename to old_nushell/crates/nu-command/src/commands/platform/kill.rs diff --git a/crates/nu-command/src/commands/platform/mod.rs b/old_nushell/crates/nu-command/src/commands/platform/mod.rs similarity index 100% rename from crates/nu-command/src/commands/platform/mod.rs rename to old_nushell/crates/nu-command/src/commands/platform/mod.rs diff --git a/crates/nu-command/src/commands/platform/pwd.rs b/old_nushell/crates/nu-command/src/commands/platform/pwd.rs similarity index 100% rename from crates/nu-command/src/commands/platform/pwd.rs rename to old_nushell/crates/nu-command/src/commands/platform/pwd.rs diff --git a/crates/nu-command/src/commands/platform/run_external.rs b/old_nushell/crates/nu-command/src/commands/platform/run_external.rs similarity index 100% rename from crates/nu-command/src/commands/platform/run_external.rs rename to old_nushell/crates/nu-command/src/commands/platform/run_external.rs diff --git a/crates/nu-command/src/commands/platform/sleep.rs b/old_nushell/crates/nu-command/src/commands/platform/sleep.rs similarity index 100% rename from crates/nu-command/src/commands/platform/sleep.rs rename to old_nushell/crates/nu-command/src/commands/platform/sleep.rs diff --git a/crates/nu-command/src/commands/platform/termsize.rs b/old_nushell/crates/nu-command/src/commands/platform/termsize.rs similarity index 100% rename from crates/nu-command/src/commands/platform/termsize.rs rename to old_nushell/crates/nu-command/src/commands/platform/termsize.rs diff --git a/crates/nu-command/src/commands/platform/which_.rs b/old_nushell/crates/nu-command/src/commands/platform/which_.rs similarity index 100% rename from crates/nu-command/src/commands/platform/which_.rs rename to old_nushell/crates/nu-command/src/commands/platform/which_.rs diff --git a/crates/nu-command/src/commands/random/bool.rs b/old_nushell/crates/nu-command/src/commands/random/bool.rs similarity index 100% rename from crates/nu-command/src/commands/random/bool.rs rename to old_nushell/crates/nu-command/src/commands/random/bool.rs diff --git a/crates/nu-command/src/commands/random/chars.rs b/old_nushell/crates/nu-command/src/commands/random/chars.rs similarity index 100% rename from crates/nu-command/src/commands/random/chars.rs rename to old_nushell/crates/nu-command/src/commands/random/chars.rs diff --git a/crates/nu-command/src/commands/random/command.rs b/old_nushell/crates/nu-command/src/commands/random/command.rs similarity index 100% rename from crates/nu-command/src/commands/random/command.rs rename to old_nushell/crates/nu-command/src/commands/random/command.rs diff --git a/crates/nu-command/src/commands/random/decimal.rs b/old_nushell/crates/nu-command/src/commands/random/decimal.rs similarity index 100% rename from crates/nu-command/src/commands/random/decimal.rs rename to old_nushell/crates/nu-command/src/commands/random/decimal.rs diff --git a/crates/nu-command/src/commands/random/dice.rs b/old_nushell/crates/nu-command/src/commands/random/dice.rs similarity index 100% rename from crates/nu-command/src/commands/random/dice.rs rename to old_nushell/crates/nu-command/src/commands/random/dice.rs diff --git a/crates/nu-command/src/commands/random/integer.rs b/old_nushell/crates/nu-command/src/commands/random/integer.rs similarity index 100% rename from crates/nu-command/src/commands/random/integer.rs rename to old_nushell/crates/nu-command/src/commands/random/integer.rs diff --git a/crates/nu-command/src/commands/random/mod.rs b/old_nushell/crates/nu-command/src/commands/random/mod.rs similarity index 100% rename from crates/nu-command/src/commands/random/mod.rs rename to old_nushell/crates/nu-command/src/commands/random/mod.rs diff --git a/crates/nu-command/src/commands/random/uuid.rs b/old_nushell/crates/nu-command/src/commands/random/uuid.rs similarity index 100% rename from crates/nu-command/src/commands/random/uuid.rs rename to old_nushell/crates/nu-command/src/commands/random/uuid.rs diff --git a/crates/nu-command/src/commands/shells/command.rs b/old_nushell/crates/nu-command/src/commands/shells/command.rs similarity index 100% rename from crates/nu-command/src/commands/shells/command.rs rename to old_nushell/crates/nu-command/src/commands/shells/command.rs diff --git a/crates/nu-command/src/commands/shells/enter.rs b/old_nushell/crates/nu-command/src/commands/shells/enter.rs similarity index 100% rename from crates/nu-command/src/commands/shells/enter.rs rename to old_nushell/crates/nu-command/src/commands/shells/enter.rs diff --git a/crates/nu-command/src/commands/shells/exit.rs b/old_nushell/crates/nu-command/src/commands/shells/exit.rs similarity index 100% rename from crates/nu-command/src/commands/shells/exit.rs rename to old_nushell/crates/nu-command/src/commands/shells/exit.rs diff --git a/crates/nu-command/src/commands/shells/goto.rs b/old_nushell/crates/nu-command/src/commands/shells/goto.rs similarity index 100% rename from crates/nu-command/src/commands/shells/goto.rs rename to old_nushell/crates/nu-command/src/commands/shells/goto.rs diff --git a/crates/nu-command/src/commands/shells/mod.rs b/old_nushell/crates/nu-command/src/commands/shells/mod.rs similarity index 100% rename from crates/nu-command/src/commands/shells/mod.rs rename to old_nushell/crates/nu-command/src/commands/shells/mod.rs diff --git a/crates/nu-command/src/commands/shells/next.rs b/old_nushell/crates/nu-command/src/commands/shells/next.rs similarity index 100% rename from crates/nu-command/src/commands/shells/next.rs rename to old_nushell/crates/nu-command/src/commands/shells/next.rs diff --git a/crates/nu-command/src/commands/shells/prev.rs b/old_nushell/crates/nu-command/src/commands/shells/prev.rs similarity index 100% rename from crates/nu-command/src/commands/shells/prev.rs rename to old_nushell/crates/nu-command/src/commands/shells/prev.rs diff --git a/crates/nu-command/src/commands/strings/build_string.rs b/old_nushell/crates/nu-command/src/commands/strings/build_string.rs similarity index 100% rename from crates/nu-command/src/commands/strings/build_string.rs rename to old_nushell/crates/nu-command/src/commands/strings/build_string.rs diff --git a/crates/nu-command/src/commands/strings/char_.rs b/old_nushell/crates/nu-command/src/commands/strings/char_.rs similarity index 100% rename from crates/nu-command/src/commands/strings/char_.rs rename to old_nushell/crates/nu-command/src/commands/strings/char_.rs diff --git a/crates/nu-command/src/commands/strings/detect/columns.rs b/old_nushell/crates/nu-command/src/commands/strings/detect/columns.rs similarity index 100% rename from crates/nu-command/src/commands/strings/detect/columns.rs rename to old_nushell/crates/nu-command/src/commands/strings/detect/columns.rs diff --git a/crates/nu-command/src/commands/strings/detect/mod.rs b/old_nushell/crates/nu-command/src/commands/strings/detect/mod.rs similarity index 100% rename from crates/nu-command/src/commands/strings/detect/mod.rs rename to old_nushell/crates/nu-command/src/commands/strings/detect/mod.rs diff --git a/crates/nu-command/src/commands/strings/format/command.rs b/old_nushell/crates/nu-command/src/commands/strings/format/command.rs similarity index 100% rename from crates/nu-command/src/commands/strings/format/command.rs rename to old_nushell/crates/nu-command/src/commands/strings/format/command.rs diff --git a/crates/nu-command/src/commands/strings/format/format_filesize.rs b/old_nushell/crates/nu-command/src/commands/strings/format/format_filesize.rs similarity index 100% rename from crates/nu-command/src/commands/strings/format/format_filesize.rs rename to old_nushell/crates/nu-command/src/commands/strings/format/format_filesize.rs diff --git a/crates/nu-command/src/commands/strings/format/mod.rs b/old_nushell/crates/nu-command/src/commands/strings/format/mod.rs similarity index 100% rename from crates/nu-command/src/commands/strings/format/mod.rs rename to old_nushell/crates/nu-command/src/commands/strings/format/mod.rs diff --git a/crates/nu-command/src/commands/strings/lines.rs b/old_nushell/crates/nu-command/src/commands/strings/lines.rs similarity index 100% rename from crates/nu-command/src/commands/strings/lines.rs rename to old_nushell/crates/nu-command/src/commands/strings/lines.rs diff --git a/crates/nu-command/src/commands/strings/mod.rs b/old_nushell/crates/nu-command/src/commands/strings/mod.rs similarity index 100% rename from crates/nu-command/src/commands/strings/mod.rs rename to old_nushell/crates/nu-command/src/commands/strings/mod.rs diff --git a/crates/nu-command/src/commands/strings/parse/command.rs b/old_nushell/crates/nu-command/src/commands/strings/parse/command.rs similarity index 100% rename from crates/nu-command/src/commands/strings/parse/command.rs rename to old_nushell/crates/nu-command/src/commands/strings/parse/command.rs diff --git a/crates/nu-command/src/commands/strings/parse/mod.rs b/old_nushell/crates/nu-command/src/commands/strings/parse/mod.rs similarity index 100% rename from crates/nu-command/src/commands/strings/parse/mod.rs rename to old_nushell/crates/nu-command/src/commands/strings/parse/mod.rs diff --git a/crates/nu-command/src/commands/strings/size.rs b/old_nushell/crates/nu-command/src/commands/strings/size.rs similarity index 100% rename from crates/nu-command/src/commands/strings/size.rs rename to old_nushell/crates/nu-command/src/commands/strings/size.rs diff --git a/crates/nu-command/src/commands/strings/split/chars.rs b/old_nushell/crates/nu-command/src/commands/strings/split/chars.rs similarity index 100% rename from crates/nu-command/src/commands/strings/split/chars.rs rename to old_nushell/crates/nu-command/src/commands/strings/split/chars.rs diff --git a/crates/nu-command/src/commands/strings/split/column.rs b/old_nushell/crates/nu-command/src/commands/strings/split/column.rs similarity index 100% rename from crates/nu-command/src/commands/strings/split/column.rs rename to old_nushell/crates/nu-command/src/commands/strings/split/column.rs diff --git a/crates/nu-command/src/commands/strings/split/command.rs b/old_nushell/crates/nu-command/src/commands/strings/split/command.rs similarity index 100% rename from crates/nu-command/src/commands/strings/split/command.rs rename to old_nushell/crates/nu-command/src/commands/strings/split/command.rs diff --git a/crates/nu-command/src/commands/strings/split/mod.rs b/old_nushell/crates/nu-command/src/commands/strings/split/mod.rs similarity index 100% rename from crates/nu-command/src/commands/strings/split/mod.rs rename to old_nushell/crates/nu-command/src/commands/strings/split/mod.rs diff --git a/crates/nu-command/src/commands/strings/split/row.rs b/old_nushell/crates/nu-command/src/commands/strings/split/row.rs similarity index 100% rename from crates/nu-command/src/commands/strings/split/row.rs rename to old_nushell/crates/nu-command/src/commands/strings/split/row.rs diff --git a/crates/nu-command/src/commands/strings/split_by.rs b/old_nushell/crates/nu-command/src/commands/strings/split_by.rs similarity index 100% rename from crates/nu-command/src/commands/strings/split_by.rs rename to old_nushell/crates/nu-command/src/commands/strings/split_by.rs diff --git a/crates/nu-command/src/commands/strings/str_/capitalize.rs b/old_nushell/crates/nu-command/src/commands/strings/str_/capitalize.rs similarity index 100% rename from crates/nu-command/src/commands/strings/str_/capitalize.rs rename to old_nushell/crates/nu-command/src/commands/strings/str_/capitalize.rs diff --git a/crates/nu-command/src/commands/strings/str_/case/camel_case.rs b/old_nushell/crates/nu-command/src/commands/strings/str_/case/camel_case.rs similarity index 100% rename from crates/nu-command/src/commands/strings/str_/case/camel_case.rs rename to old_nushell/crates/nu-command/src/commands/strings/str_/case/camel_case.rs diff --git a/crates/nu-command/src/commands/strings/str_/case/kebab_case.rs b/old_nushell/crates/nu-command/src/commands/strings/str_/case/kebab_case.rs similarity index 100% rename from crates/nu-command/src/commands/strings/str_/case/kebab_case.rs rename to old_nushell/crates/nu-command/src/commands/strings/str_/case/kebab_case.rs diff --git a/crates/nu-command/src/commands/strings/str_/case/mod.rs b/old_nushell/crates/nu-command/src/commands/strings/str_/case/mod.rs similarity index 100% rename from crates/nu-command/src/commands/strings/str_/case/mod.rs rename to old_nushell/crates/nu-command/src/commands/strings/str_/case/mod.rs diff --git a/crates/nu-command/src/commands/strings/str_/case/pascal_case.rs b/old_nushell/crates/nu-command/src/commands/strings/str_/case/pascal_case.rs similarity index 100% rename from crates/nu-command/src/commands/strings/str_/case/pascal_case.rs rename to old_nushell/crates/nu-command/src/commands/strings/str_/case/pascal_case.rs diff --git a/crates/nu-command/src/commands/strings/str_/case/screaming_snake_case.rs b/old_nushell/crates/nu-command/src/commands/strings/str_/case/screaming_snake_case.rs similarity index 100% rename from crates/nu-command/src/commands/strings/str_/case/screaming_snake_case.rs rename to old_nushell/crates/nu-command/src/commands/strings/str_/case/screaming_snake_case.rs diff --git a/crates/nu-command/src/commands/strings/str_/case/snake_case.rs b/old_nushell/crates/nu-command/src/commands/strings/str_/case/snake_case.rs similarity index 100% rename from crates/nu-command/src/commands/strings/str_/case/snake_case.rs rename to old_nushell/crates/nu-command/src/commands/strings/str_/case/snake_case.rs diff --git a/crates/nu-command/src/commands/strings/str_/collect.rs b/old_nushell/crates/nu-command/src/commands/strings/str_/collect.rs similarity index 100% rename from crates/nu-command/src/commands/strings/str_/collect.rs rename to old_nushell/crates/nu-command/src/commands/strings/str_/collect.rs diff --git a/crates/nu-command/src/commands/strings/str_/command.rs b/old_nushell/crates/nu-command/src/commands/strings/str_/command.rs similarity index 100% rename from crates/nu-command/src/commands/strings/str_/command.rs rename to old_nushell/crates/nu-command/src/commands/strings/str_/command.rs diff --git a/crates/nu-command/src/commands/strings/str_/contains.rs b/old_nushell/crates/nu-command/src/commands/strings/str_/contains.rs similarity index 100% rename from crates/nu-command/src/commands/strings/str_/contains.rs rename to old_nushell/crates/nu-command/src/commands/strings/str_/contains.rs diff --git a/crates/nu-command/src/commands/strings/str_/downcase.rs b/old_nushell/crates/nu-command/src/commands/strings/str_/downcase.rs similarity index 100% rename from crates/nu-command/src/commands/strings/str_/downcase.rs rename to old_nushell/crates/nu-command/src/commands/strings/str_/downcase.rs diff --git a/crates/nu-command/src/commands/strings/str_/ends_with.rs b/old_nushell/crates/nu-command/src/commands/strings/str_/ends_with.rs similarity index 100% rename from crates/nu-command/src/commands/strings/str_/ends_with.rs rename to old_nushell/crates/nu-command/src/commands/strings/str_/ends_with.rs diff --git a/crates/nu-command/src/commands/strings/str_/find_replace.rs b/old_nushell/crates/nu-command/src/commands/strings/str_/find_replace.rs similarity index 100% rename from crates/nu-command/src/commands/strings/str_/find_replace.rs rename to old_nushell/crates/nu-command/src/commands/strings/str_/find_replace.rs diff --git a/crates/nu-command/src/commands/strings/str_/index_of.rs b/old_nushell/crates/nu-command/src/commands/strings/str_/index_of.rs similarity index 100% rename from crates/nu-command/src/commands/strings/str_/index_of.rs rename to old_nushell/crates/nu-command/src/commands/strings/str_/index_of.rs diff --git a/crates/nu-command/src/commands/strings/str_/length.rs b/old_nushell/crates/nu-command/src/commands/strings/str_/length.rs similarity index 100% rename from crates/nu-command/src/commands/strings/str_/length.rs rename to old_nushell/crates/nu-command/src/commands/strings/str_/length.rs diff --git a/crates/nu-command/src/commands/strings/str_/lpad.rs b/old_nushell/crates/nu-command/src/commands/strings/str_/lpad.rs similarity index 100% rename from crates/nu-command/src/commands/strings/str_/lpad.rs rename to old_nushell/crates/nu-command/src/commands/strings/str_/lpad.rs diff --git a/crates/nu-command/src/commands/strings/str_/mod.rs b/old_nushell/crates/nu-command/src/commands/strings/str_/mod.rs similarity index 100% rename from crates/nu-command/src/commands/strings/str_/mod.rs rename to old_nushell/crates/nu-command/src/commands/strings/str_/mod.rs diff --git a/crates/nu-command/src/commands/strings/str_/reverse.rs b/old_nushell/crates/nu-command/src/commands/strings/str_/reverse.rs similarity index 100% rename from crates/nu-command/src/commands/strings/str_/reverse.rs rename to old_nushell/crates/nu-command/src/commands/strings/str_/reverse.rs diff --git a/crates/nu-command/src/commands/strings/str_/rpad.rs b/old_nushell/crates/nu-command/src/commands/strings/str_/rpad.rs similarity index 100% rename from crates/nu-command/src/commands/strings/str_/rpad.rs rename to old_nushell/crates/nu-command/src/commands/strings/str_/rpad.rs diff --git a/crates/nu-command/src/commands/strings/str_/starts_with.rs b/old_nushell/crates/nu-command/src/commands/strings/str_/starts_with.rs similarity index 100% rename from crates/nu-command/src/commands/strings/str_/starts_with.rs rename to old_nushell/crates/nu-command/src/commands/strings/str_/starts_with.rs diff --git a/crates/nu-command/src/commands/strings/str_/substring.rs b/old_nushell/crates/nu-command/src/commands/strings/str_/substring.rs similarity index 100% rename from crates/nu-command/src/commands/strings/str_/substring.rs rename to old_nushell/crates/nu-command/src/commands/strings/str_/substring.rs diff --git a/crates/nu-command/src/commands/strings/str_/to_datetime.rs b/old_nushell/crates/nu-command/src/commands/strings/str_/to_datetime.rs similarity index 100% rename from crates/nu-command/src/commands/strings/str_/to_datetime.rs rename to old_nushell/crates/nu-command/src/commands/strings/str_/to_datetime.rs diff --git a/crates/nu-command/src/commands/strings/str_/to_decimal.rs b/old_nushell/crates/nu-command/src/commands/strings/str_/to_decimal.rs similarity index 100% rename from crates/nu-command/src/commands/strings/str_/to_decimal.rs rename to old_nushell/crates/nu-command/src/commands/strings/str_/to_decimal.rs diff --git a/crates/nu-command/src/commands/strings/str_/to_integer.rs b/old_nushell/crates/nu-command/src/commands/strings/str_/to_integer.rs similarity index 100% rename from crates/nu-command/src/commands/strings/str_/to_integer.rs rename to old_nushell/crates/nu-command/src/commands/strings/str_/to_integer.rs diff --git a/crates/nu-command/src/commands/strings/str_/trim/command.rs b/old_nushell/crates/nu-command/src/commands/strings/str_/trim/command.rs similarity index 100% rename from crates/nu-command/src/commands/strings/str_/trim/command.rs rename to old_nushell/crates/nu-command/src/commands/strings/str_/trim/command.rs diff --git a/crates/nu-command/src/commands/strings/str_/trim/mod.rs b/old_nushell/crates/nu-command/src/commands/strings/str_/trim/mod.rs similarity index 100% rename from crates/nu-command/src/commands/strings/str_/trim/mod.rs rename to old_nushell/crates/nu-command/src/commands/strings/str_/trim/mod.rs diff --git a/crates/nu-command/src/commands/strings/str_/upcase.rs b/old_nushell/crates/nu-command/src/commands/strings/str_/upcase.rs similarity index 100% rename from crates/nu-command/src/commands/strings/str_/upcase.rs rename to old_nushell/crates/nu-command/src/commands/strings/str_/upcase.rs diff --git a/crates/nu-command/src/commands/system/mod.rs b/old_nushell/crates/nu-command/src/commands/system/mod.rs similarity index 100% rename from crates/nu-command/src/commands/system/mod.rs rename to old_nushell/crates/nu-command/src/commands/system/mod.rs diff --git a/crates/nu-command/src/commands/system/ps.rs b/old_nushell/crates/nu-command/src/commands/system/ps.rs similarity index 100% rename from crates/nu-command/src/commands/system/ps.rs rename to old_nushell/crates/nu-command/src/commands/system/ps.rs diff --git a/crates/nu-command/src/commands/system/sys.rs b/old_nushell/crates/nu-command/src/commands/system/sys.rs similarity index 100% rename from crates/nu-command/src/commands/system/sys.rs rename to old_nushell/crates/nu-command/src/commands/system/sys.rs diff --git a/crates/nu-command/src/commands/viewers/autoview/command.rs b/old_nushell/crates/nu-command/src/commands/viewers/autoview/command.rs similarity index 100% rename from crates/nu-command/src/commands/viewers/autoview/command.rs rename to old_nushell/crates/nu-command/src/commands/viewers/autoview/command.rs diff --git a/crates/nu-command/src/commands/viewers/autoview/mod.rs b/old_nushell/crates/nu-command/src/commands/viewers/autoview/mod.rs similarity index 100% rename from crates/nu-command/src/commands/viewers/autoview/mod.rs rename to old_nushell/crates/nu-command/src/commands/viewers/autoview/mod.rs diff --git a/crates/nu-command/src/commands/viewers/autoview/options.rs b/old_nushell/crates/nu-command/src/commands/viewers/autoview/options.rs similarity index 100% rename from crates/nu-command/src/commands/viewers/autoview/options.rs rename to old_nushell/crates/nu-command/src/commands/viewers/autoview/options.rs diff --git a/crates/nu-command/src/commands/viewers/bat_constants.rs b/old_nushell/crates/nu-command/src/commands/viewers/bat_constants.rs similarity index 100% rename from crates/nu-command/src/commands/viewers/bat_constants.rs rename to old_nushell/crates/nu-command/src/commands/viewers/bat_constants.rs diff --git a/crates/nu-command/src/commands/viewers/mod.rs b/old_nushell/crates/nu-command/src/commands/viewers/mod.rs similarity index 100% rename from crates/nu-command/src/commands/viewers/mod.rs rename to old_nushell/crates/nu-command/src/commands/viewers/mod.rs diff --git a/crates/nu-command/src/commands/viewers/table/command.rs b/old_nushell/crates/nu-command/src/commands/viewers/table/command.rs similarity index 100% rename from crates/nu-command/src/commands/viewers/table/command.rs rename to old_nushell/crates/nu-command/src/commands/viewers/table/command.rs diff --git a/crates/nu-command/src/commands/viewers/table/mod.rs b/old_nushell/crates/nu-command/src/commands/viewers/table/mod.rs similarity index 100% rename from crates/nu-command/src/commands/viewers/table/mod.rs rename to old_nushell/crates/nu-command/src/commands/viewers/table/mod.rs diff --git a/crates/nu-command/src/commands/viewers/table/options.rs b/old_nushell/crates/nu-command/src/commands/viewers/table/options.rs similarity index 100% rename from crates/nu-command/src/commands/viewers/table/options.rs rename to old_nushell/crates/nu-command/src/commands/viewers/table/options.rs diff --git a/old_nushell/crates/nu-command/src/default_context.rs b/old_nushell/crates/nu-command/src/default_context.rs new file mode 100644 index 0000000000..9d091957ee --- /dev/null +++ b/old_nushell/crates/nu-command/src/default_context.rs @@ -0,0 +1,372 @@ +use crate::prelude::*; +use nu_engine::whole_stream_command; +use std::error::Error; + +pub fn create_default_context(interactive: bool) -> Result> { + let context = EvaluationContext::basic(); + + { + use crate::commands::*; + + 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), + ]); + + //Dataframe commands + #[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), + ]); + } + + Ok(context) +} diff --git a/crates/nu-command/src/examples.rs b/old_nushell/crates/nu-command/src/examples.rs similarity index 100% rename from crates/nu-command/src/examples.rs rename to old_nushell/crates/nu-command/src/examples.rs diff --git a/crates/nu-command/src/examples/double_echo.rs b/old_nushell/crates/nu-command/src/examples/double_echo.rs similarity index 100% rename from crates/nu-command/src/examples/double_echo.rs rename to old_nushell/crates/nu-command/src/examples/double_echo.rs diff --git a/crates/nu-command/src/examples/double_ls.rs b/old_nushell/crates/nu-command/src/examples/double_ls.rs similarity index 100% rename from crates/nu-command/src/examples/double_ls.rs rename to old_nushell/crates/nu-command/src/examples/double_ls.rs diff --git a/crates/nu-command/src/examples/sample.rs b/old_nushell/crates/nu-command/src/examples/sample.rs similarity index 100% rename from crates/nu-command/src/examples/sample.rs rename to old_nushell/crates/nu-command/src/examples/sample.rs diff --git a/crates/nu-command/src/examples/stub_generate.rs b/old_nushell/crates/nu-command/src/examples/stub_generate.rs similarity index 100% rename from crates/nu-command/src/examples/stub_generate.rs rename to old_nushell/crates/nu-command/src/examples/stub_generate.rs diff --git a/old_nushell/crates/nu-command/src/lib.rs b/old_nushell/crates/nu-command/src/lib.rs new file mode 100644 index 0000000000..ecb91f5c2c --- /dev/null +++ b/old_nushell/crates/nu-command/src/lib.rs @@ -0,0 +1,27 @@ +#![recursion_limit = "2048"] + +#[cfg(test)] +#[macro_use] +extern crate indexmap; + +#[macro_use] +mod prelude; +mod classified; +pub mod commands; +mod default_context; +pub mod utils; + +#[cfg(test)] +mod examples; + +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; + +// TODO: Temporary redirect +pub use nu_protocol::{did_you_mean, TaggedDictBuilder}; diff --git a/crates/nu-command/src/prelude.rs b/old_nushell/crates/nu-command/src/prelude.rs similarity index 100% rename from crates/nu-command/src/prelude.rs rename to old_nushell/crates/nu-command/src/prelude.rs diff --git a/crates/nu-command/src/utils.rs b/old_nushell/crates/nu-command/src/utils.rs similarity index 100% rename from crates/nu-command/src/utils.rs rename to old_nushell/crates/nu-command/src/utils.rs diff --git a/crates/nu-command/src/utils/suggestions.rs b/old_nushell/crates/nu-command/src/utils/suggestions.rs similarity index 100% rename from crates/nu-command/src/utils/suggestions.rs rename to old_nushell/crates/nu-command/src/utils/suggestions.rs diff --git a/crates/nu-command/src/utils/test_bins.rs b/old_nushell/crates/nu-command/src/utils/test_bins.rs similarity index 100% rename from crates/nu-command/src/utils/test_bins.rs rename to old_nushell/crates/nu-command/src/utils/test_bins.rs diff --git a/old_nushell/crates/nu-command/tests/commands/all.rs b/old_nushell/crates/nu-command/tests/commands/all.rs new file mode 100644 index 0000000000..2012ff0de6 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/all.rs @@ -0,0 +1,69 @@ +use nu_test_support::pipeline as input; +use nu_test_support::playground::{says, Playground}; + +use hamcrest2::assert_that; +use hamcrest2::prelude::*; + +#[test] +fn checks_all_rows_are_true() { + Playground::setup("all_test_1", |_, nu| { + assert_that!( + nu.pipeline(&input( + r#" + echo [ "Andrés", "Andrés", "Andrés" ] + | all? $it == "Andrés" + "# + )), + says().stdout("true") + ); + }) +} + +#[test] +fn checks_all_rows_are_false_with_param() { + Playground::setup("all_test_1", |_, nu| { + assert_that!( + nu.pipeline(&input( + r#" + [1, 2, 3, 4] | all? { |a| $a >= 5 } + "# + )), + says().stdout("false") + ); + }) +} + +#[test] +fn checks_all_rows_are_true_with_param() { + Playground::setup("all_test_1", |_, nu| { + assert_that!( + nu.pipeline(&input( + r#" + [1, 2, 3, 4] | all? { |a| $a < 5 } + "# + )), + says().stdout("true") + ); + }) +} + +#[test] +fn checks_all_columns_of_a_table_is_true() { + Playground::setup("any_test_1", |_, nu| { + assert_that!( + nu.pipeline(&input( + r#" + echo [ + [ first_name, last_name, rusty_at, likes ]; + [ Andrés, Robalino, 10/11/2013, 1 ] + [ Jonathan, Turner, 10/12/2013, 1 ] + [ Darren, Schroeder, 10/11/2013, 1 ] + [ Yehuda, Katz, 10/11/2013, 1 ] + ] + | all? likes > 0 + "# + )), + says().stdout("true") + ); + }) +} diff --git a/old_nushell/crates/nu-command/tests/commands/any.rs b/old_nushell/crates/nu-command/tests/commands/any.rs new file mode 100644 index 0000000000..255673f800 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/any.rs @@ -0,0 +1,41 @@ +use nu_test_support::pipeline as input; +use nu_test_support::playground::{says, Playground}; + +use hamcrest2::assert_that; +use hamcrest2::prelude::*; + +#[test] +fn checks_any_row_is_true() { + Playground::setup("any_test_1", |_, nu| { + assert_that!( + nu.pipeline(&input( + r#" + echo [ "Ecuador", "USA", "New Zealand" ] + | any? $it == "New Zealand" + "# + )), + says().stdout("true") + ); + }) +} + +#[test] +fn checks_any_column_of_a_table_is_true() { + Playground::setup("any_test_1", |_, nu| { + assert_that!( + nu.pipeline(&input( + r#" + echo [ + [ first_name, last_name, rusty_at, likes ]; + [ Andrés, Robalino, 10/11/2013, 1 ] + [ Jonathan, Turner, 10/12/2013, 1 ] + [ Darren, Schroeder, 10/11/2013, 1 ] + [ Yehuda, Katz, 10/11/2013, 1 ] + ] + | any? rusty_at == 10/12/2013 + "# + )), + says().stdout("true") + ); + }) +} diff --git a/old_nushell/crates/nu-command/tests/commands/append.rs b/old_nushell/crates/nu-command/tests/commands/append.rs new file mode 100644 index 0000000000..36318264a3 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/append.rs @@ -0,0 +1,21 @@ +use nu_test_support::pipeline as input; +use nu_test_support::playground::{says, Playground}; + +use hamcrest2::assert_that; +use hamcrest2::prelude::*; + +#[test] +fn adds_a_row_to_the_end() { + Playground::setup("append_test_1", |_, nu| { + assert_that!( + nu.pipeline(&input( + r#" + echo [ "Andrés N. Robalino", "Jonathan Turner", "Yehuda Katz" ] + | append "pollo loco" + | nth 3 + "# + )), + says().stdout("pollo loco") + ); + }) +} diff --git a/old_nushell/crates/nu-command/tests/commands/cal.rs b/old_nushell/crates/nu-command/tests/commands/cal.rs new file mode 100644 index 0000000000..eafceb9005 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/cal.rs @@ -0,0 +1,79 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn cal_full_year() { + let actual = nu!( + cwd: ".", pipeline( + r#" + cal -y --full-year 2010 | first | to json + "# + )); + + let first_week_2010_json = r#"{"year":2010,"sunday":null,"monday":null,"tuesday":null,"wednesday":null,"thursday":null,"friday":1,"saturday":2}"#; + + assert_eq!(actual.out, first_week_2010_json); +} + +#[test] +fn cal_february_2020_leap_year() { + let actual = nu!( + cwd: ".", pipeline( + r#" + cal -ym --full-year 2020 --month-names | where month == "february" | to json + "# + )); + + let cal_february_json = r#"[{"year":2020,"month":"february","sunday":null,"monday":null,"tuesday":null,"wednesday":null,"thursday":null,"friday":null,"saturday":1},{"year":2020,"month":"february","sunday":2,"monday":3,"tuesday":4,"wednesday":5,"thursday":6,"friday":7,"saturday":8},{"year":2020,"month":"february","sunday":9,"monday":10,"tuesday":11,"wednesday":12,"thursday":13,"friday":14,"saturday":15},{"year":2020,"month":"february","sunday":16,"monday":17,"tuesday":18,"wednesday":19,"thursday":20,"friday":21,"saturday":22},{"year":2020,"month":"february","sunday":23,"monday":24,"tuesday":25,"wednesday":26,"thursday":27,"friday":28,"saturday":29}]"#; + + assert_eq!(actual.out, cal_february_json); +} + +#[test] +fn cal_friday_the_thirteenths_in_2015() { + let actual = nu!( + cwd: ".", pipeline( + r#" + cal --full-year 2015 | default friday 0 | where friday == 13 | length + "# + )); + + assert!(actual.out.contains('3')); +} + +#[test] +fn cal_rows_in_2020() { + let actual = nu!( + cwd: ".", pipeline( + r#" + cal --full-year 2020 | length + "# + )); + + assert!(actual.out.contains("62")); +} + +#[test] +fn cal_week_day_start_monday() { + let actual = nu!( + cwd: ".", pipeline( + r#" + cal --full-year 2020 -m --month-names --week-start monday | where month == january | to json + "# + )); + + let cal_january_json = r#"[{"month":"january","monday":null,"tuesday":null,"wednesday":1,"thursday":2,"friday":3,"saturday":4,"sunday":5},{"month":"january","monday":6,"tuesday":7,"wednesday":8,"thursday":9,"friday":10,"saturday":11,"sunday":12},{"month":"january","monday":13,"tuesday":14,"wednesday":15,"thursday":16,"friday":17,"saturday":18,"sunday":19},{"month":"january","monday":20,"tuesday":21,"wednesday":22,"thursday":23,"friday":24,"saturday":25,"sunday":26},{"month":"january","monday":27,"tuesday":28,"wednesday":29,"thursday":30,"friday":31,"saturday":null,"sunday":null}]"#; + + assert_eq!(actual.out, cal_january_json); +} + +#[test] +fn cal_sees_pipeline_year() { + let actual = nu!( + cwd: ".", pipeline( + r#" + cal --full-year 1020 | get monday | first 3 | to json + "# + )); + + assert_eq!(actual.out, "[3,10,17]"); +} diff --git a/old_nushell/crates/nu-command/tests/commands/cd.rs b/old_nushell/crates/nu-command/tests/commands/cd.rs new file mode 100644 index 0000000000..229aa23d53 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/cd.rs @@ -0,0 +1,458 @@ +use nu_test_support::fs::{Stub::EmptyFile, Stub::FileWithContent}; +use nu_test_support::nu; +use nu_test_support::playground::Playground; +use std::path::PathBuf; + +#[test] +fn filesystem_change_from_current_directory_using_relative_path() { + Playground::setup("cd_test_1", |dirs, _| { + let actual = nu!( + cwd: dirs.root(), + r#" + cd cd_test_1 + echo (pwd) + "# + ); + + assert_eq!(PathBuf::from(actual.out), *dirs.test()); + }) +} + +#[test] +fn filesystem_change_from_current_directory_using_absolute_path() { + Playground::setup("cd_test_2", |dirs, _| { + let actual = nu!( + cwd: dirs.test(), + r#" + cd "{}" + echo (pwd) + "#, + dirs.formats() + ); + + assert_eq!(PathBuf::from(actual.out), dirs.formats()); + }) +} + +#[test] +fn filesystem_switch_back_to_previous_working_directory() { + Playground::setup("cd_test_3", |dirs, sandbox| { + sandbox.mkdir("odin"); + + let actual = nu!( + cwd: dirs.test().join("odin"), + r#" + cd {} + cd - + echo (pwd) + "#, + dirs.test() + ); + + assert_eq!(PathBuf::from(actual.out), dirs.test().join("odin")); + }) +} + +#[test] +fn filesytem_change_from_current_directory_using_relative_path_and_dash() { + Playground::setup("cd_test_4", |dirs, sandbox| { + sandbox.within("odin").mkdir("-"); + + let actual = nu!( + cwd: dirs.test(), + r#" + cd odin/- + echo (pwd) + "# + ); + + assert_eq!( + PathBuf::from(actual.out), + dirs.test().join("odin").join("-") + ); + }) +} + +#[test] +fn filesystem_change_current_directory_to_parent_directory() { + Playground::setup("cd_test_5", |dirs, _| { + let actual = nu!( + cwd: dirs.test(), + r#" + cd .. + echo (pwd) + "# + ); + + assert_eq!(PathBuf::from(actual.out), *dirs.root()); + }) +} + +#[test] +fn filesystem_change_current_directory_to_two_parents_up_using_multiple_dots() { + Playground::setup("cd_test_6", |dirs, sandbox| { + sandbox.within("foo").mkdir("bar"); + + let actual = nu!( + cwd: dirs.test().join("foo/bar"), + r#" + cd ... + echo (pwd) + "# + ); + + assert_eq!(PathBuf::from(actual.out), *dirs.test()); + }) +} + +#[test] +fn filesystem_change_current_directory_to_parent_directory_after_delete_cwd() { + Playground::setup("cd_test_7", |dirs, sandbox| { + sandbox.within("foo").mkdir("bar"); + + let actual = nu!( + cwd: dirs.test().join("foo/bar"), + r#" + rm {}/foo/bar + echo "," + cd .. + echo (pwd) + "#, + dirs.test() + ); + + let actual = actual.out.split(',').nth(1).unwrap(); + + assert_eq!(PathBuf::from(actual), *dirs.test().join("foo")); + }) +} + +#[test] +fn filesystem_change_to_home_directory() { + Playground::setup("cd_test_8", |dirs, _| { + let actual = nu!( + cwd: dirs.test(), + r#" + cd ~ + echo (pwd) + "# + ); + + assert_eq!(Some(PathBuf::from(actual.out)), dirs_next::home_dir()); + }) +} + +#[test] +fn filesystem_change_to_a_directory_containing_spaces() { + Playground::setup("cd_test_9", |dirs, sandbox| { + sandbox.mkdir("robalino turner katz"); + + let actual = nu!( + cwd: dirs.test(), + r#" + cd "robalino turner katz" + echo (pwd) + "# + ); + + assert_eq!( + PathBuf::from(actual.out), + dirs.test().join("robalino turner katz") + ); + }) +} + +#[test] +fn filesystem_not_a_directory() { + Playground::setup("cd_test_10", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("ferris_did_it.txt")]); + + let actual = nu!( + cwd: dirs.test(), + "cd ferris_did_it.txt" + ); + + assert!( + actual.err.contains("ferris_did_it.txt"), + "actual={:?}", + actual.err + ); + assert!( + actual.err.contains("is not a directory"), + "actual={:?}", + actual.err + ); + }) +} + +#[test] +fn filesystem_directory_not_found() { + Playground::setup("cd_test_11", |dirs, _| { + let actual = nu!( + cwd: dirs.test(), + "cd dir_that_does_not_exist" + + ); + + assert!( + actual.err.contains("dir_that_does_not_exist"), + "actual={:?}", + actual.err + ); + assert!( + actual.err.contains("directory not found"), + "actual={:?}", + actual.err + ); + }) +} + +#[test] +fn filesystem_change_directory_to_symlink_relative() { + Playground::setup("cd_test_12", |dirs, sandbox| { + sandbox.mkdir("foo"); + sandbox.mkdir("boo"); + sandbox.symlink("foo", "foo_link"); + + let actual = nu!( + cwd: dirs.test().join("boo"), + r#" + cd ../foo_link + echo (pwd) + "# + ); + + assert_eq!(PathBuf::from(actual.out), dirs.test().join("foo")); + }) +} + +#[test] +fn valuesystem_change_from_current_path_using_relative_path() { + Playground::setup("cd_test_13", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [[bin]] + path = "src/plugins/turner.rs" + + [[bin]] + path = "src/plugins/robalino.rs" + + [[bin]] + path = "src/plugins/katz.rs" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), + r#" + enter sample.toml + cd bin + pwd + exit + "# + ); + + assert_eq!(PathBuf::from(actual.out), PathBuf::from("/bin")); + }) +} + +#[test] +fn valuesystem_change_from_current_path_using_absolute_path() { + Playground::setup("cd_test_14", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [dependencies] + turner-ts = "0.1.1" + robalino-tkd = "0.0.1" + katz-ember = "0.2.3" + + [[bin]] + path = "src/plugins/arepa.rs" + + [[bin]] + path = "src/plugins/bbq.rs" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), + r#" + enter sample.toml + cd bin + cd /dependencies + pwd + exit + "# + ); + + assert_eq!(PathBuf::from(actual.out), PathBuf::from("/dependencies")); + }) +} + +#[test] +fn valuesystem_switch_back_to_previous_working_path() { + Playground::setup("cd_test_15", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [dependencies] + turner-ts = "0.1.1" + robalino-tkd = "0.0.1" + katz-ember = "0.2.3" + odin-gf = "0.2.1" + + [[bin]] + path = "src/plugins/arepa.rs" + + [[bin]] + path = "src/plugins/bbq.rs" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), + r#" + enter sample.toml + cd dependencies + cd /bin + cd - + pwd + exit + "# + ); + + assert_eq!(PathBuf::from(actual.out), PathBuf::from("/dependencies")); + }) +} + +#[test] +fn valuesystem_change_from_current_path_using_relative_path_and_dash() { + Playground::setup("cd_test_16", |dirs, sandbox| { + sandbox + .with_files(vec![FileWithContent( + "sample.toml", + r#" + [package] + - = ["Yehuda Katz ", "Jonathan Turner ", "Andrés N. Robalino "] + + [[bin]] + path = "src/plugins/arepa.rs" + + [[bin]] + path = "src/plugins/bbq.rs" + "# + )]); + + let actual = nu!( + cwd: dirs.test(), + r#" + enter sample.toml + cd package/- + cd /bin + cd - + pwd + exit + "# + ); + + assert_eq!(PathBuf::from(actual.out), PathBuf::from("/package/-")); + }) +} + +#[test] +fn valuesystem_change_current_path_to_parent_path() { + Playground::setup("cd_test_17", |dirs, sandbox| { + sandbox + .with_files(vec![FileWithContent( + "sample.toml", + r#" + [package] + emberenios = ["Yehuda Katz ", "Jonathan Turner ", "Andrés N. Robalino "] + "# + )]); + + let actual = nu!( + cwd: dirs.test(), + r#" + enter sample.toml + cd package/emberenios + cd .. + pwd + exit + "# + ); + + assert_eq!(PathBuf::from(actual.out), PathBuf::from("/package")); + }) +} + +#[test] +fn valuesystem_change_to_a_path_containing_spaces() { + Playground::setup("cd_test_18", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + ["pa que te"] + el = "pollo loco" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), + r#" + enter sample.toml + cd "pa que te" + pwd + exit + "# + ); + + assert_eq!( + PathBuf::from(actual.out), + PathBuf::from("/").join("pa que te") + ); + }) +} + +#[test] +fn valuesystem_path_not_found() { + Playground::setup("cd_test_19", |dirs, _| { + let actual = nu!( + cwd: dirs.formats(), + r#" + enter cargo_sample.toml + cd im_a_path_that_does_not_exist + exit + "# + ); + + assert!(actual.err.contains("Can not change to path inside")); + assert!(actual.err.contains("No such path exists")); + }) +} + +#[cfg(target_os = "windows")] +#[test] +fn test_change_windows_drive() { + Playground::setup("cd_test_20", |dirs, sandbox| { + sandbox.mkdir("test_folder"); + + let _actual = nu!( + cwd: dirs.test(), + r#" + subst Z: test_folder + Z: + echo "some text" | save test_file.txt + cd ~ + subst Z: /d + "# + ); + assert!(dirs + .test() + .join("test_folder") + .join("test_file.txt") + .exists()); + }) +} diff --git a/crates/nu-command/tests/commands/compact.rs b/old_nushell/crates/nu-command/tests/commands/compact.rs similarity index 100% rename from crates/nu-command/tests/commands/compact.rs rename to old_nushell/crates/nu-command/tests/commands/compact.rs diff --git a/crates/nu-command/tests/commands/config.rs b/old_nushell/crates/nu-command/tests/commands/config.rs similarity index 100% rename from crates/nu-command/tests/commands/config.rs rename to old_nushell/crates/nu-command/tests/commands/config.rs diff --git a/old_nushell/crates/nu-command/tests/commands/cp.rs b/old_nushell/crates/nu-command/tests/commands/cp.rs new file mode 100644 index 0000000000..9fe1272b77 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/cp.rs @@ -0,0 +1,237 @@ +use nu_test_support::fs::{files_exist_at, AbsoluteFile, Stub::EmptyFile}; +use nu_test_support::nu; +use nu_test_support::playground::Playground; +use std::path::Path; + +#[test] +fn copies_a_file() { + Playground::setup("cp_test_1", |dirs, _| { + nu!( + cwd: dirs.root(), + "cp \"{}\" cp_test_1/sample.ini", + dirs.formats().join("sample.ini") + ); + + assert!(dirs.test().join("sample.ini").exists()); + }); +} + +#[test] +fn copies_the_file_inside_directory_if_path_to_copy_is_directory() { + Playground::setup("cp_test_2", |dirs, _| { + let expected_file = AbsoluteFile::new(dirs.test().join("sample.ini")); + + nu!( + cwd: dirs.formats(), + "cp ../formats/sample.ini {}", + expected_file.dir() + ); + + assert!(dirs.test().join("sample.ini").exists()); + }) +} + +#[test] +fn error_if_attempting_to_copy_a_directory_to_another_directory() { + Playground::setup("cp_test_3", |dirs, _| { + let actual = nu!( + cwd: dirs.formats(), + "cp ../formats {}", dirs.test() + ); + + assert!(actual.err.contains("../formats")); + assert!(actual.err.contains("resolves to a directory (not copied)")); + }); +} + +#[test] +fn copies_the_directory_inside_directory_if_path_to_copy_is_directory_and_with_recursive_flag() { + Playground::setup("cp_test_4", |dirs, sandbox| { + sandbox + .within("originals") + .with_files(vec![ + EmptyFile("yehuda.txt"), + EmptyFile("jonathan.txt"), + EmptyFile("andres.txt"), + ]) + .mkdir("expected"); + + let expected_dir = dirs.test().join("expected").join("originals"); + + nu!( + cwd: dirs.test(), + "cp originals expected -r" + ); + + assert!(expected_dir.exists()); + assert!(files_exist_at( + vec![ + Path::new("yehuda.txt"), + Path::new("jonathan.txt"), + Path::new("andres.txt") + ], + expected_dir + )); + }) +} + +#[test] +fn deep_copies_with_recursive_flag() { + Playground::setup("cp_test_5", |dirs, sandbox| { + sandbox + .within("originals") + .with_files(vec![EmptyFile("manifest.txt")]) + .within("originals/contributors") + .with_files(vec![ + EmptyFile("yehuda.txt"), + EmptyFile("jonathan.txt"), + EmptyFile("andres.txt"), + ]) + .within("originals/contributors/jonathan") + .with_files(vec![EmptyFile("errors.txt"), EmptyFile("multishells.txt")]) + .within("originals/contributors/andres") + .with_files(vec![EmptyFile("coverage.txt"), EmptyFile("commands.txt")]) + .within("originals/contributors/yehuda") + .with_files(vec![EmptyFile("defer-evaluation.txt")]) + .mkdir("expected"); + + let expected_dir = dirs.test().join("expected").join("originals"); + + let jonathans_expected_copied_dir = expected_dir.join("contributors").join("jonathan"); + let andres_expected_copied_dir = expected_dir.join("contributors").join("andres"); + let yehudas_expected_copied_dir = expected_dir.join("contributors").join("yehuda"); + + nu!( + cwd: dirs.test(), + "cp originals expected --recursive" + ); + + assert!(expected_dir.exists()); + assert!(files_exist_at( + vec![Path::new("errors.txt"), Path::new("multishells.txt")], + jonathans_expected_copied_dir + )); + assert!(files_exist_at( + vec![Path::new("coverage.txt"), Path::new("commands.txt")], + andres_expected_copied_dir + )); + assert!(files_exist_at( + vec![Path::new("defer-evaluation.txt")], + yehudas_expected_copied_dir + )); + }) +} + +#[test] +fn copies_using_path_with_wildcard() { + Playground::setup("cp_test_6", |dirs, _| { + nu!( + cwd: dirs.formats(), + "cp ../formats/* {}", dirs.test() + ); + + assert!(files_exist_at( + vec![ + Path::new("caco3_plastics.csv"), + Path::new("cargo_sample.toml"), + Path::new("jonathan.xml"), + Path::new("sample.ini"), + Path::new("sgml_description.json"), + Path::new("utf16.ini"), + ], + dirs.test() + )); + }) +} + +#[test] +fn copies_using_a_glob() { + Playground::setup("cp_test_7", |dirs, _| { + nu!( + cwd: dirs.formats(), + "cp * {}", dirs.test() + ); + + assert!(files_exist_at( + vec![ + Path::new("caco3_plastics.csv"), + Path::new("cargo_sample.toml"), + Path::new("jonathan.xml"), + Path::new("sample.ini"), + Path::new("sgml_description.json"), + Path::new("utf16.ini"), + ], + dirs.test() + )); + }); +} + +#[test] +fn copies_same_file_twice() { + Playground::setup("cp_test_8", |dirs, _| { + nu!( + cwd: dirs.root(), + "cp \"{}\" cp_test_8/sample.ini", + dirs.formats().join("sample.ini") + ); + + nu!( + cwd: dirs.root(), + "cp \"{}\" cp_test_8/sample.ini", + dirs.formats().join("sample.ini") + ); + + assert!(dirs.test().join("sample.ini").exists()); + }); +} + +#[test] +fn copy_files_using_glob_two_parents_up_using_multiple_dots() { + Playground::setup("cp_test_9", |dirs, sandbox| { + sandbox.within("foo").within("bar").with_files(vec![ + EmptyFile("jonathan.json"), + EmptyFile("andres.xml"), + EmptyFile("yehuda.yaml"), + EmptyFile("kevin.txt"), + EmptyFile("many_more.ppl"), + ]); + + nu!( + cwd: dirs.test().join("foo/bar"), + r#" + cp * ... + "# + ); + + assert!(files_exist_at( + vec![ + "yehuda.yaml", + "jonathan.json", + "andres.xml", + "kevin.txt", + "many_more.ppl", + ], + dirs.test() + )); + }) +} + +#[test] +fn copy_file_and_dir_from_two_parents_up_using_multiple_dots_to_current_dir_recursive() { + Playground::setup("cp_test_10", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("hello_there")]); + sandbox.mkdir("hello_again"); + sandbox.within("foo").mkdir("bar"); + + nu!( + cwd: dirs.test().join("foo/bar"), + r#" + cp -r .../hello* . + "# + ); + + let expected = dirs.test().join("foo/bar"); + + assert!(files_exist_at(vec!["hello_there", "hello_again"], expected)); + }) +} diff --git a/old_nushell/crates/nu-command/tests/commands/def.rs b/old_nushell/crates/nu-command/tests/commands/def.rs new file mode 100644 index 0000000000..e0f218b1eb --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/def.rs @@ -0,0 +1,19 @@ +use nu_test_support::nu; +use nu_test_support::playground::Playground; +use std::fs; +#[test] +fn def_with_comment() { + Playground::setup("def_with_comment", |dirs, _| { + let data = r#" +#My echo +def e [arg] {echo $arg} + "#; + fs::write(dirs.root().join("def_test"), data).expect("Unable to write file"); + let actual = nu!( + cwd: dirs.root(), + "source def_test; help e | to json" + ); + + assert!(actual.out.contains("My echo\\n\\n")); + }); +} diff --git a/crates/nu-command/tests/commands/default.rs b/old_nushell/crates/nu-command/tests/commands/default.rs similarity index 100% rename from crates/nu-command/tests/commands/default.rs rename to old_nushell/crates/nu-command/tests/commands/default.rs diff --git a/old_nushell/crates/nu-command/tests/commands/drop.rs b/old_nushell/crates/nu-command/tests/commands/drop.rs new file mode 100644 index 0000000000..e723ba7690 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/drop.rs @@ -0,0 +1,90 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn columns() { + let actual = nu!( + cwd: ".", pipeline(r#" + echo [ + [arepas, color]; + + [3, white] + [8, yellow] + [4, white] + ] + | drop column + | get + | length + "#) + ); + + assert_eq!(actual.out, "1"); +} + +#[test] +fn more_columns_than_table_has() { + let actual = nu!( + cwd: ".", pipeline(r#" + echo [ + [arepas, color]; + + [3, white] + [8, yellow] + [4, white] + ] + | drop column 3 + | get + | empty? + "#) + ); + + assert_eq!(actual.out, "true"); +} + +#[test] +fn rows() { + let actual = nu!( + cwd: ".", pipeline(r#" + echo [ + [arepas]; + + [3] + [8] + [4] + ] + | drop 2 + | get arepas + | math sum + "#) + ); + + assert_eq!(actual.out, "3"); +} + +#[test] +fn more_rows_than_table_has() { + let actual = nu!(cwd: ".", "date | drop 50 | length"); + + assert_eq!(actual.out, "0"); +} + +#[test] +fn nth_range_inclusive() { + let actual = nu!(cwd: ".", "echo 10..15 | drop nth (2..3) | to json"); + + assert_eq!(actual.out, "[10,11,14,15]"); +} + +#[test] +fn nth_range_exclusive() { + let actual = nu!(cwd: ".", "echo 10..15 | drop nth (1..<3) | to json"); + + assert_eq!(actual.out, "[10,13,14,15]"); +} + +#[test] +fn nth_missing_first_argument() { + let actual = nu!(cwd: ".", "echo 10..15 | drop nth \"\""); + + assert!(actual.err.contains("Expected int or range")); + assert!(actual.err.contains("found string")); +} diff --git a/old_nushell/crates/nu-command/tests/commands/each.rs b/old_nushell/crates/nu-command/tests/commands/each.rs new file mode 100644 index 0000000000..c4d7edb022 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/each.rs @@ -0,0 +1,73 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn each_works_separately() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo [1 2 3] | each { echo $it 10 | math sum } | to json + "# + )); + + assert_eq!(actual.out, "[11,12,13]"); +} + +#[test] +fn each_group_works() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo [1 2 3 4 5 6] | each group 3 { $it } | to json + "# + )); + + assert_eq!(actual.out, "[[1,2,3],[4,5,6]]"); +} + +#[test] +fn each_window() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo [1 2 3 4] | each window 3 { $it } | to json + "# + )); + + assert_eq!(actual.out, "[[1,2,3],[2,3,4]]"); +} + +#[test] +fn each_window_stride() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo [1 2 3 4 5 6] | each window 3 -s 2 { echo $it } | to json + "# + )); + + assert_eq!(actual.out, "[[1,2,3],[3,4,5]]"); +} + +#[test] +fn each_no_args_in_block() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo [[foo bar]; [a b] [c d] [e f]] | each { to json } | nth 1 | str collect + "# + )); + + assert_eq!(actual.out, r#"{"foo":"c","bar":"d"}"#); +} + +#[test] +fn each_implicit_it_in_block() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo [[foo bar]; [a b] [c d] [e f]] | each { nu --testbin cococo $it.foo } + "# + )); + + assert_eq!(actual.out, "ace"); +} diff --git a/old_nushell/crates/nu-command/tests/commands/echo.rs b/old_nushell/crates/nu-command/tests/commands/echo.rs new file mode 100644 index 0000000000..e23609f9a6 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/echo.rs @@ -0,0 +1,61 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn echo_range_is_lazy() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo 1..10000000000 | first 3 | to json + "# + )); + + assert_eq!(actual.out, "[1,2,3]"); +} + +#[test] +fn echo_range_handles_inclusive() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo 1..3 | to json + "# + )); + + assert_eq!(actual.out, "[1,2,3]"); +} + +#[test] +fn echo_range_handles_exclusive() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo 1..<3 | to json + "# + )); + + assert_eq!(actual.out, "[1,2]"); +} + +#[test] +fn echo_range_handles_inclusive_down() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo 3..1 | to json + "# + )); + + assert_eq!(actual.out, "[3,2,1]"); +} + +#[test] +fn echo_range_handles_exclusive_down() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo 3..<1 | to json + "# + )); + + assert_eq!(actual.out, "[3,2]"); +} diff --git a/old_nushell/crates/nu-command/tests/commands/empty.rs b/old_nushell/crates/nu-command/tests/commands/empty.rs new file mode 100644 index 0000000000..f5ec14cf8e --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/empty.rs @@ -0,0 +1,86 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn reports_emptiness() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo [[are_empty]; + [([[check]; [[]] ])] + [([[check]; [""] ])] + [([[check]; [(wrap)] ])] + ] + | get are_empty + | empty? check + | where check + | length + "# + )); + + assert_eq!(actual.out, "3"); +} + +#[test] +fn sets_block_run_value_for_an_empty_column() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo [ + [ first_name, last_name, rusty_at, likes ]; + [ Andrés, Robalino, 10/11/2013, 1 ] + [ Jonathan, Turner, 10/12/2013, 1 ] + [ Jason, Gedge, 10/11/2013, 1 ] + [ Yehuda, Katz, 10/11/2013, '' ] + ] + | empty? likes -b { 1 } + | get likes + | math sum + "# + )); + + assert_eq!(actual.out, "4"); +} + +#[test] +fn sets_block_run_value_for_many_empty_columns() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo [ + [ boost check ]; + [ 1, [] ] + [ 1, "" ] + [ 1, (wrap) ] + ] + | empty? boost check -b { 1 } + | get boost check + | math sum + "# + )); + + assert_eq!(actual.out, "6"); +} + +#[test] +fn passing_a_block_will_set_contents_on_empty_cells_and_leave_non_empty_ones_untouched() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo [ + [ NAME, LVL, HP ]; + [ Andrés, 30, 3000 ] + [ Alistair, 29, 2900 ] + [ Arepas, "", "" ] + [ Jorge, 30, 3000 ] + ] + | empty? LVL -b { 9 } + | empty? HP -b { + $it.LVL * 1000 + } + | math sum + | get HP + "# + )); + + assert_eq!(actual.out, "17900"); +} diff --git a/old_nushell/crates/nu-command/tests/commands/enter.rs b/old_nushell/crates/nu-command/tests/commands/enter.rs new file mode 100644 index 0000000000..d7377eade6 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/enter.rs @@ -0,0 +1,85 @@ +use nu_test_support::fs::{files_exist_at, Stub::EmptyFile}; +use nu_test_support::nu; +use nu_test_support::playground::Playground; +use std::path::Path; + +#[test] +fn knows_the_filesystems_entered() { + Playground::setup("enter_test_1", |dirs, sandbox| { + sandbox + .within("red_pill") + .with_files(vec![ + EmptyFile("andres.nu"), + EmptyFile("jonathan.nu"), + EmptyFile("yehuda.nu"), + ]) + .within("blue_pill") + .with_files(vec![ + EmptyFile("bash.nxt"), + EmptyFile("korn.nxt"), + EmptyFile("powedsh.nxt"), + ]) + .mkdir("expected"); + + let red_pill_dir = dirs.test().join("red_pill"); + let blue_pill_dir = dirs.test().join("blue_pill"); + let expected = dirs.test().join("expected"); + let expected_recycled = expected.join("recycled"); + + nu!( + cwd: dirs.test(), + r#" + enter expected + mkdir recycled + enter ../red_pill + mv jonathan.nu ../expected + enter ../blue_pill + cp *.nxt ../expected/recycled + p + p + mv ../red_pill/yehuda.nu . + n + mv andres.nu ../expected/andres.nu + exit + cd .. + rm red_pill --recursive + exit + n + rm blue_pill --recursive + exit + "# + ); + + assert!(!red_pill_dir.exists()); + assert!(files_exist_at( + vec![ + Path::new("andres.nu"), + Path::new("jonathan.nu"), + Path::new("yehuda.nu"), + ], + expected + )); + + assert!(!blue_pill_dir.exists()); + assert!(files_exist_at( + vec![ + Path::new("bash.nxt"), + Path::new("korn.nxt"), + Path::new("powedsh.nxt"), + ], + expected_recycled + )); + }) +} + +#[test] +fn errors_if_file_not_found() { + Playground::setup("enter_test_2", |dirs, _| { + let actual = nu!( + cwd: dirs.test(), + "enter i_dont_exist.csv" + ); + + assert!(actual.err.contains("Cannot find file")); + }) +} diff --git a/old_nushell/crates/nu-command/tests/commands/every.rs b/old_nushell/crates/nu-command/tests/commands/every.rs new file mode 100644 index 0000000000..f48b2bf2ab --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/every.rs @@ -0,0 +1,209 @@ +use nu_test_support::fs::Stub::EmptyFile; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn gets_all_rows_by_every_zero() { + Playground::setup("every_test_1", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("amigos.txt"), + EmptyFile("arepas.clu"), + EmptyFile("los.txt"), + EmptyFile("tres.txt"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | get name + | every 0 + | to json + "# + )); + + assert_eq!( + actual.out, + r#"["amigos.txt","arepas.clu","los.txt","tres.txt"]"# + ); + }) +} + +#[test] +fn gets_no_rows_by_every_skip_zero() { + Playground::setup("every_test_2", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("amigos.txt"), + EmptyFile("arepas.clu"), + EmptyFile("los.txt"), + EmptyFile("tres.txt"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | get name + | every 0 --skip + | to json + "# + )); + + assert_eq!(actual.out, ""); + }) +} + +#[test] +fn gets_all_rows_by_every_one() { + Playground::setup("every_test_3", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("amigos.txt"), + EmptyFile("arepas.clu"), + EmptyFile("los.txt"), + EmptyFile("tres.txt"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | get name + | every 1 + | to json + "# + )); + + assert_eq!( + actual.out, + r#"["amigos.txt","arepas.clu","los.txt","tres.txt"]"# + ); + }) +} + +#[test] +fn gets_no_rows_by_every_skip_one() { + Playground::setup("every_test_4", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("amigos.txt"), + EmptyFile("arepas.clu"), + EmptyFile("los.txt"), + EmptyFile("tres.txt"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | get name + | every 1 --skip + | to json + "# + )); + + assert_eq!(actual.out, ""); + }) +} + +#[test] +fn gets_first_row_by_every_too_much() { + Playground::setup("every_test_5", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("amigos.txt"), + EmptyFile("arepas.clu"), + EmptyFile("los.txt"), + EmptyFile("tres.txt"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | get name + | every 999 + "# + )); + + let expected = nu!( + cwd: dirs.test(), pipeline( + r#" + echo [ amigos.txt ] + "# + )); + + assert_eq!(actual.out, expected.out); + }) +} + +#[test] +fn gets_all_rows_except_first_by_every_skip_too_much() { + Playground::setup("every_test_6", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("amigos.txt"), + EmptyFile("arepas.clu"), + EmptyFile("los.txt"), + EmptyFile("tres.txt"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | get name + | every 999 --skip + | to json + "# + )); + + assert_eq!(actual.out, r#"["arepas.clu","los.txt","tres.txt"]"#); + }) +} + +#[test] +fn gets_every_third_row() { + Playground::setup("every_test_7", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("amigos.txt"), + EmptyFile("arepas.clu"), + EmptyFile("los.txt"), + EmptyFile("quatro.txt"), + EmptyFile("tres.txt"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | get name + | every 3 + | to json + "# + )); + + assert_eq!(actual.out, r#"["amigos.txt","quatro.txt"]"#); + }) +} + +#[test] +fn skips_every_third_row() { + Playground::setup("every_test_8", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("amigos.txt"), + EmptyFile("arepas.clu"), + EmptyFile("los.txt"), + EmptyFile("quatro.txt"), + EmptyFile("tres.txt"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | get name + | every 3 --skip + | to json + "# + )); + + assert_eq!(actual.out, r#"["arepas.clu","los.txt","tres.txt"]"#); + }) +} diff --git a/old_nushell/crates/nu-command/tests/commands/find.rs b/old_nushell/crates/nu-command/tests/commands/find.rs new file mode 100644 index 0000000000..0c8b04fe40 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/find.rs @@ -0,0 +1,111 @@ +use nu_test_support::fs::Stub::EmptyFile; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn find_with_list_search_with_string() { + let actual = nu!( + cwd: ".", pipeline( + r#" + [moe larry curly] | find moe + "# + )); + + assert_eq!(actual.out, "moe"); +} + +#[test] +fn find_with_list_search_with_char() { + let actual = nu!( + cwd: ".", pipeline( + r#" + [moe larry curly] | find l | to json + "# + )); + + assert_eq!(actual.out, r#"["larry","curly"]"#); +} + +#[test] +fn find_with_list_search_with_number() { + let actual = nu!( + cwd: ".", pipeline( + r#" + [1 2 3 4 5] | find 3 + "# + )); + + assert_eq!(actual.out, "3"); +} + +#[test] +fn find_with_string_search_with_string() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo Cargo.toml | find toml + "# + )); + + assert_eq!(actual.out, "Cargo.toml"); +} + +#[test] +fn find_with_string_search_with_string_not_found() { + let actual = nu!( + cwd: ".", pipeline( + r#" + [moe larry curly] | find shemp + "# + )); + + assert_eq!(actual.out, ""); +} + +#[test] +fn find_with_filepath_search_with_string() { + Playground::setup("filepath_test_1", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("amigos.txt"), + EmptyFile("arepas.clu"), + EmptyFile("los.txt"), + EmptyFile("tres.txt"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | get name + | find arep + | to json + "# + )); + + assert_eq!(actual.out, r#""arepas.clu""#); + }) +} + +#[test] +fn find_with_filepath_search_with_multiple_patterns() { + Playground::setup("filepath_test_2", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("amigos.txt"), + EmptyFile("arepas.clu"), + EmptyFile("los.txt"), + EmptyFile("tres.txt"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | get name + | find arep ami + | to json + "# + )); + + assert_eq!(actual.out, r#"["amigos.txt","arepas.clu"]"#); + }) +} diff --git a/crates/nu-command/tests/commands/first.rs b/old_nushell/crates/nu-command/tests/commands/first.rs similarity index 100% rename from crates/nu-command/tests/commands/first.rs rename to old_nushell/crates/nu-command/tests/commands/first.rs diff --git a/old_nushell/crates/nu-command/tests/commands/flatten.rs b/old_nushell/crates/nu-command/tests/commands/flatten.rs new file mode 100644 index 0000000000..2e36a4e705 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/flatten.rs @@ -0,0 +1,184 @@ +use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn flatten_nested_tables_with_columns() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo [[origin, people]; [Ecuador, ('Andres' | wrap name)]] + [[origin, people]; [Nu, ('nuno' | wrap name)]] + | flatten + | get name + | str collect ',' + "# + )); + + assert_eq!(actual.out, "Andres,nuno"); +} + +#[test] +fn flatten_nested_tables_that_have_many_columns() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo [[origin, people]; [Ecuador, (echo [[name, meal]; ['Andres', 'arepa']])]] + [[origin, people]; [USA, (echo [[name, meal]; ['Katz', 'nurepa']])]] + | flatten + | get meal + | str collect ',' + "# + )); + + assert_eq!(actual.out, "arepa,nurepa"); +} + +#[test] +fn flatten_nested_tables() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo [[Andrés, Nicolás, Robalino]] | flatten | nth 1 + "# + )); + + assert_eq!(actual.out, "Nicolás"); +} + +#[test] +fn flatten_row_column_explicitly() { + Playground::setup("flatten_test_1", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "katz.json", + r#" + [ + { + "people": { + "name": "Andres", + "meal": "arepa" + } + }, + { + "people": { + "name": "Katz", + "meal": "nurepa" + } + } + ] + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), + "open katz.json | flatten people | where name == Andres | length" + ); + + assert_eq!(actual.out, "1"); + }) +} + +#[test] +fn flatten_row_columns_having_same_column_names_flats_separately() { + Playground::setup("flatten_test_2", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "katz.json", + r#" + [ + { + "people": { + "name": "Andres", + "meal": "arepa" + }, + "city": [{"name": "Guayaquil"}, {"name": "Samborondón"}] + }, + { + "people": { + "name": "Katz", + "meal": "nurepa" + }, + "city": [{"name": "Oregon"}, {"name": "Brooklin"}] + } + ] + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), + "open katz.json | flatten | flatten people city | get city_name | length" + ); + + assert_eq!(actual.out, "4"); + }) +} + +#[test] +fn flatten_table_columns_explicitly() { + Playground::setup("flatten_test_3", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "katz.json", + r#" + [ + { + "people": { + "name": "Andres", + "meal": "arepa" + }, + "city": ["Guayaquil", "Samborondón"] + }, + { + "people": { + "name": "Katz", + "meal": "nurepa" + }, + "city": ["Oregon", "Brooklin"] + } + ] + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), + "open katz.json | flatten city | where people.name == Katz | length" + ); + + assert_eq!(actual.out, "2"); + }) +} + +#[test] +fn flatten_more_than_one_column_that_are_subtables_not_supported() { + Playground::setup("flatten_test_4", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "katz.json", + r#" + [ + { + "people": { + "name": "Andres", + "meal": "arepa" + } + "tags": ["carbohydrate", "corn", "maiz"], + "city": ["Guayaquil", "Samborondón"] + }, + { + "people": { + "name": "Katz", + "meal": "nurepa" + }, + "tags": ["carbohydrate", "shell food", "amigos flavor"], + "city": ["Oregon", "Brooklin"] + } + ] + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), + "open katz.json | flatten tags city" + ); + + assert!(actual.err.contains("tried flattening")); + assert!(actual.err.contains("but is flattened already")); + }) +} diff --git a/old_nushell/crates/nu-command/tests/commands/format.rs b/old_nushell/crates/nu-command/tests/commands/format.rs new file mode 100644 index 0000000000..d3b585ecf8 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/format.rs @@ -0,0 +1,93 @@ +use nu_test_support::fs::Stub::{EmptyFile, FileWithContentToBeTrimmed}; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn creates_the_resulting_string_from_the_given_fields() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open cargo_sample.toml + | get package + | format "{name} has license {license}" + "# + )); + + assert_eq!(actual.out, "nu has license ISC"); +} + +#[test] +fn given_fields_can_be_column_paths() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open cargo_sample.toml + | format "{package.name} is {package.description}" + "# + )); + + assert_eq!(actual.out, "nu is a new type of shell"); +} + +#[test] +fn can_use_variables() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open cargo_sample.toml + | format "{$it.package.name} is {$it.package.description}" + "# + )); + + assert_eq!(actual.out, "nu is a new type of shell"); +} + +#[test] +fn format_filesize_works() { + Playground::setup("format_filesize_test_1", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("yehuda.txt"), + EmptyFile("jonathan.txt"), + EmptyFile("andres.txt"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | format filesize size KB + | get size + | first + "# + )); + + assert_eq!(actual.out, "0.0 KB"); + }) +} + +#[test] +fn format_filesize_works_with_nonempty_files() { + Playground::setup( + "format_filesize_works_with_nonempty_files", + |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "sample.toml", + r#" + [dependency] + name = "nu" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), + "ls sample.toml | format filesize size B | get size | first" + ); + + #[cfg(not(windows))] + assert_eq!(actual.out, "25"); + + #[cfg(windows)] + assert_eq!(actual.out, "27"); + }, + ) +} diff --git a/old_nushell/crates/nu-command/tests/commands/get.rs b/old_nushell/crates/nu-command/tests/commands/get.rs new file mode 100644 index 0000000000..667acf94dd --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/get.rs @@ -0,0 +1,222 @@ +use nu_test_support::fs::Stub::FileWithContent; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn fetches_a_row() { + Playground::setup("get_test_1", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + nu_party_venue = "zion" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.toml + | get nu_party_venue + "# + )); + + assert_eq!(actual.out, "zion"); + }) +} + +#[test] +fn fetches_by_index() { + Playground::setup("get_test_2", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [package] + name = "nu" + version = "0.4.1" + authors = ["Yehuda Katz ", "Jonathan Turner ", "Andrés N. Robalino "] + description = "When arepas shells are tasty and fun." + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.toml + | get package.authors.2 + "# + )); + + assert_eq!(actual.out, "Andrés N. Robalino "); + }) +} +#[test] +fn fetches_by_column_path() { + Playground::setup("get_test_3", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [package] + name = "nu" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.toml + | get package.name + "# + )); + + assert_eq!(actual.out, "nu"); + }) +} + +#[test] +fn column_paths_are_either_double_quoted_or_regular_unquoted_words_separated_by_dot() { + Playground::setup("get_test_4", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [package] + 9999 = ["Yehuda Katz ", "Jonathan Turner ", "Andrés N. Robalino "] + description = "When arepas shells are tasty and fun." + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.toml + | get package."9999" + | length + "# + )); + + assert_eq!(actual.out, "3"); + }) +} + +#[test] +fn fetches_more_than_one_column_path() { + Playground::setup("get_test_5", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [[fortune_tellers]] + name = "Andrés N. Robalino" + arepas = 1 + + [[fortune_tellers]] + name = "Jonathan Turner" + arepas = 1 + + [[fortune_tellers]] + name = "Yehuda Katz" + arepas = 1 + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.toml + | get fortune_tellers.2.name fortune_tellers.0.name fortune_tellers.1.name + | nth 2 + "# + )); + + assert_eq!(actual.out, "Jonathan Turner"); + }) +} + +#[test] +fn errors_fetching_by_column_not_present() { + Playground::setup("get_test_6", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [taconushell] + sentence_words = ["Yo", "quiero", "taconushell"] + [pizzanushell] + sentence-words = ["I", "want", "pizza"] + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.toml + | get taco + "# + )); + + assert!(actual.err.contains("Unknown column"),); + assert!(actual.err.contains("There isn't a column named 'taco'"),); + assert!(actual.err.contains("Perhaps you meant 'taconushell'?"),); + assert!(actual + .err + .contains("Columns available: pizzanushell, taconushell"),); + }) +} + +#[test] +fn errors_fetching_by_column_using_a_number() { + Playground::setup("get_test_7", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [spanish_lesson] + 0 = "can only be fetched with 0 double quoted." + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.toml + | get spanish_lesson.0 + "# + )); + + assert!(actual.err.contains("No rows available"),); + assert!(actual.err.contains("A row at '0' can't be indexed."),); + assert!(actual + .err + .contains("Appears to contain columns. Columns available: 0"),) + }) +} +#[test] +fn errors_fetching_by_index_out_of_bounds() { + Playground::setup("get_test_8", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [spanish_lesson] + sentence_words = ["Yo", "quiero", "taconushell"] + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.toml + | get spanish_lesson.sentence_words.3 + "# + )); + + assert!(actual.err.contains("Row not found"),); + assert!(actual.err.contains("There isn't a row indexed at 3"),); + assert!(actual.err.contains("The table only has 3 rows (0 to 2)"),) + }) +} + +#[test] +fn quoted_column_access() { + let actual = nu!( + cwd: "tests/fixtures/formats", + r#"echo '[{"foo bar": {"baz": 4}}]' | from json | get "foo bar".baz "# + ); + + assert_eq!(actual.out, "4"); +} diff --git a/old_nushell/crates/nu-command/tests/commands/group_by.rs b/old_nushell/crates/nu-command/tests/commands/group_by.rs new file mode 100644 index 0000000000..690a5ddd24 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/group_by.rs @@ -0,0 +1,97 @@ +use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn groups() { + Playground::setup("group_by_test_1", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "los_tres_caballeros.csv", + r#" + first_name,last_name,rusty_at,type + Andrés,Robalino,10/11/2013,A + Jonathan,Turner,10/12/2013,B + Yehuda,Katz,10/11/2013,A + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open los_tres_caballeros.csv + | group-by rusty_at + | get "10/11/2013" + | length + "# + )); + + assert_eq!(actual.out, "2"); + }) +} + +#[test] +fn errors_if_given_unknown_column_name() { + Playground::setup("group_by_test_2", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "los_tres_caballeros.json", + r#" + { + "nu": { + "committers": [ + {"name": "Andrés N. Robalino"}, + {"name": "Jonathan Turner"}, + {"name": "Yehuda Katz"} + ], + "releases": [ + {"version": "0.2"} + {"version": "0.8"}, + {"version": "0.9999999"} + ], + "0xATYKARNU": [ + ["Th", "e", " "], + ["BIG", " ", "UnO"], + ["punto", "cero"] + ] + } + } + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open los_tres_caballeros.json + | group-by { get nu.releases.version } + "# + )); + + assert!(actual + .err + .contains("requires a table with one value for grouping")); + }) +} + +#[test] +fn errors_if_block_given_evaluates_more_than_one_row() { + Playground::setup("group_by_test_3", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "los_tres_caballeros.csv", + r#" + first_name,last_name,rusty_at,type + Andrés,Robalino,10/11/2013,A + Jonathan,Turner,10/12/2013,B + Yehuda,Katz,10/11/2013,A + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open los_tres_caballeros.csv + | group-by ttype + "# + )); + + assert!(actual.err.contains("Unknown column")); + }) +} diff --git a/old_nushell/crates/nu-command/tests/commands/hash_/mod.rs b/old_nushell/crates/nu-command/tests/commands/hash_/mod.rs new file mode 100644 index 0000000000..61ad8f155b --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/hash_/mod.rs @@ -0,0 +1,114 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn base64_defaults_to_encoding_with_standard_character_type() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo 'username:password' | hash base64 + "# + ) + ); + + assert_eq!(actual.out, "dXNlcm5hbWU6cGFzc3dvcmQ="); +} + +#[test] +fn base64_encode_characterset_binhex() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo 'username:password' | hash base64 --character_set binhex --encode + "# + ) + ); + + assert_eq!(actual.out, "F@0NEPjJD97kE\'&bEhFZEP3"); +} + +#[test] +fn error_when_invalid_character_set_given() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo 'username:password' | hash base64 --character_set 'this is invalid' --encode + "# + ) + ); + + assert!(actual + .err + .contains("this is invalid is not a valid character-set")); +} + +#[test] +fn base64_decode_characterset_binhex() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo "F@0NEPjJD97kE'&bEhFZEP3" | hash base64 --character_set binhex --decode + "# + ) + ); + + assert_eq!(actual.out, "username:password"); +} + +#[test] +fn error_invalid_decode_value() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo "this should not be a valid encoded value" | hash base64 --character_set url-safe --decode + "# + ) + ); + + assert!(actual + .err + .contains("invalid base64 input for character set url-safe")); +} + +#[test] +fn error_use_both_flags() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo 'username:password' | hash base64 --encode --decode + "# + ) + ); + + assert!(actual + .err + .contains("only one of --decode and --encode flags can be used")); +} + +#[test] +fn md5_works_with_file() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sample.db | hash md5 + "# + ) + ); + + assert_eq!(actual.out, "4de97601d232c427977ef11db396c951"); +} + +#[test] +fn sha256_works_with_file() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sample.db | hash sha256 + "# + ) + ); + + assert_eq!( + actual.out, + "2f5050e7eea415c1f3d80b5d93355efd15043ec9157a2bb167a9e73f2ae651f2" + ); +} diff --git a/old_nushell/crates/nu-command/tests/commands/headers.rs b/old_nushell/crates/nu-command/tests/commands/headers.rs new file mode 100644 index 0000000000..1eadf754a0 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/headers.rs @@ -0,0 +1,31 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn headers_uses_first_row_as_header() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sample_headers.xlsx + | get Sheet1 + | headers + | get header0 + | from json"# + )); + + assert_eq!(actual.out, "r1c0r2c0") +} + +#[test] +fn headers_adds_missing_column_name() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sample_headers.xlsx + | get Sheet1 + | headers + | get Column1 + | from json"# + )); + + assert_eq!(actual.out, "r1c1r2c1") +} diff --git a/old_nushell/crates/nu-command/tests/commands/help.rs b/old_nushell/crates/nu-command/tests/commands/help.rs new file mode 100644 index 0000000000..bab3102f05 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/help.rs @@ -0,0 +1,31 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn help_commands_length() { + let actual = nu!( + cwd: ".", pipeline( + r#" + help commands | length + "# + )); + + let output = actual.out; + let output_int: i32 = output.parse().unwrap(); + let is_positive = output_int.is_positive(); + assert!(is_positive); +} + +#[test] +fn help_generate_docs_length() { + let actual = nu!( + cwd: ".", pipeline( + r#" + help generate_docs | flatten | length + "# + )); + + let output = actual.out; + let output_int: i32 = output.parse().unwrap(); + let is_positive = output_int.is_positive(); + assert!(is_positive); +} diff --git a/old_nushell/crates/nu-command/tests/commands/histogram.rs b/old_nushell/crates/nu-command/tests/commands/histogram.rs new file mode 100644 index 0000000000..1ff4fef314 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/histogram.rs @@ -0,0 +1,109 @@ +use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn summarizes_by_column_given() { + Playground::setup("histogram_test_1", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "los_tres_caballeros.csv", + r#" + first_name,last_name,rusty_at + Andrés,Robalino,Ecuador + Jonathan,Turner,Estados Unidos + Yehuda,Katz,Estados Unidos + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open los_tres_caballeros.csv + | histogram rusty_at countries + | where rusty_at == "Ecuador" + | get countries + "# + )); + + assert_eq!( + actual.out, + "**************************************************" + ); + // 50% + }) +} + +#[test] +fn summarizes_by_values() { + Playground::setup("histogram_test_2", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "los_tres_caballeros.csv", + r#" + first_name,last_name,rusty_at + Andrés,Robalino,Ecuador + Jonathan,Turner,Estados Unidos + Yehuda,Katz,Estados Unidos + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open los_tres_caballeros.csv + | get rusty_at + | histogram + | where value == "Estados Unidos" + | get count + "# + )); + + assert_eq!(actual.out, "2"); + }) +} + +#[test] +fn help() { + Playground::setup("histogram_test_3", |dirs, _sandbox| { + let help_command = nu!( + cwd: dirs.test(), pipeline( + r#" + help histogram + "# + )); + + let help_short = nu!( + cwd: dirs.test(), pipeline( + r#" + histogram -h + "# + )); + + let help_long = nu!( + cwd: dirs.test(), pipeline( + r#" + histogram --help + "# + )); + + assert_eq!(help_short.out, help_command.out); + assert_eq!(help_long.out, help_command.out); + }) +} + +#[test] +fn count() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo [[bit]; [1] [0] [0] [0] [0] [0] [0] [1]] + | histogram bit + | sort-by count + | reject frequency + | to json + "# + )); + + let bit_json = r#"[{"bit":"1","count":2,"percentage":"33.33%"},{"bit":"0","count":6,"percentage":"100.00%"}]"#; + + assert_eq!(actual.out, bit_json); +} diff --git a/crates/nu-command/tests/commands/insert.rs b/old_nushell/crates/nu-command/tests/commands/insert.rs similarity index 100% rename from crates/nu-command/tests/commands/insert.rs rename to old_nushell/crates/nu-command/tests/commands/insert.rs diff --git a/old_nushell/crates/nu-command/tests/commands/into_filesize.rs b/old_nushell/crates/nu-command/tests/commands/into_filesize.rs new file mode 100644 index 0000000000..b6919e4bdb --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/into_filesize.rs @@ -0,0 +1,76 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn into_filesize_int() { + let actual = nu!( + cwd: ".", pipeline( + r#" + 1 | into filesize + "# + )); + + assert!(actual.out.contains("1 B")); +} + +#[test] +fn into_filesize_decimal() { + let actual = nu!( + cwd: ".", pipeline( + r#" + 1.2 | into filesize + "# + )); + + assert!(actual.out.contains("1 B")); +} + +#[test] +fn into_filesize_str() { + let actual = nu!( + cwd: ".", pipeline( + r#" + '2000' | into filesize + "# + )); + + assert!(actual.out.contains("2.0 KB")); +} + +#[test] +fn into_filesize_str_newline() { + let actual = nu!( + cwd: ".", pipeline( + r#" + '2000 +' | into filesize + "# + )); + + assert!(actual.out.contains("2.0 KB")); +} + +#[test] +fn into_filesize_str_many_newlines() { + let actual = nu!( + cwd: ".", pipeline( + r#" + '2000 + +' | into filesize + "# + )); + + assert!(actual.out.contains("2.0 KB")); +} + +#[test] +fn into_filesize_filesize() { + let actual = nu!( + cwd: ".", pipeline( + r#" + 3kb | into filesize + "# + )); + + assert!(actual.out.contains("3.0 KB")); +} diff --git a/crates/nu-command/tests/commands/into_int.rs b/old_nushell/crates/nu-command/tests/commands/into_int.rs similarity index 100% rename from crates/nu-command/tests/commands/into_int.rs rename to old_nushell/crates/nu-command/tests/commands/into_int.rs diff --git a/crates/nu-command/tests/commands/keep/mod.rs b/old_nushell/crates/nu-command/tests/commands/keep/mod.rs similarity index 100% rename from crates/nu-command/tests/commands/keep/mod.rs rename to old_nushell/crates/nu-command/tests/commands/keep/mod.rs diff --git a/crates/nu-command/tests/commands/keep/rows.rs b/old_nushell/crates/nu-command/tests/commands/keep/rows.rs similarity index 100% rename from crates/nu-command/tests/commands/keep/rows.rs rename to old_nushell/crates/nu-command/tests/commands/keep/rows.rs diff --git a/old_nushell/crates/nu-command/tests/commands/keep/until.rs b/old_nushell/crates/nu-command/tests/commands/keep/until.rs new file mode 100644 index 0000000000..36e2783ee2 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/keep/until.rs @@ -0,0 +1,51 @@ +use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn condition_is_met() { + Playground::setup("keep_until_test_1", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "caballeros.txt", + r#" + CHICKEN SUMMARY report date: April 29th, 2020 + -------------------------------------------------------------------- + Chicken Collection,29/04/2020,30/04/2020,31/04/2020, + Yellow Chickens,,, + Andrés,1,1,1 + Jonathan,1,1,1 + Jason,1,1,1 + Yehuda,1,1,1 + Blue Chickens,,, + Andrés,1,1,2 + Jonathan,1,1,2 + Jason,1,1,2 + Yehuda,1,1,2 + Red Chickens,,, + Andrés,1,1,3 + Jonathan,1,1,3 + Jason,1,1,3 + Yehuda,1,1,3 + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open --raw caballeros.txt + | lines + | skip 2 + | split column ',' + | headers + | skip while "Chicken Collection" != "Blue Chickens" + | keep until "Chicken Collection" == "Red Chickens" + | skip 1 + | str to-int "31/04/2020" + | get "31/04/2020" + | math sum + "# + )); + + assert_eq!(actual.out, "8"); + }) +} diff --git a/old_nushell/crates/nu-command/tests/commands/keep/while_.rs b/old_nushell/crates/nu-command/tests/commands/keep/while_.rs new file mode 100644 index 0000000000..713fc41c85 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/keep/while_.rs @@ -0,0 +1,50 @@ +use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn condition_is_met() { + Playground::setup("keep_while_test_1", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "caballeros.txt", + r#" + CHICKEN SUMMARY report date: April 29th, 2020 + -------------------------------------------------------------------- + Chicken Collection,29/04/2020,30/04/2020,31/04/2020, + Yellow Chickens,,, + Andrés,1,1,1 + Jonathan,1,1,1 + Jason,1,1,1 + Yehuda,1,1,1 + Blue Chickens,,, + Andrés,1,1,2 + Jonathan,1,1,2 + Jason,1,1,2 + Yehuda,1,1,2 + Red Chickens,,, + Andrés,1,1,3 + Jonathan,1,1,3 + Jason,1,1,3 + Yehuda,1,1,3 + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open --raw caballeros.txt + | lines + | skip 2 + | split column ',' + | headers + | skip 1 + | keep while "Chicken Collection" != "Blue Chickens" + | str to-int "31/04/2020" + | get "31/04/2020" + | math sum + "# + )); + + assert_eq!(actual.out, "4"); + }) +} diff --git a/crates/nu-command/tests/commands/last.rs b/old_nushell/crates/nu-command/tests/commands/last.rs similarity index 100% rename from crates/nu-command/tests/commands/last.rs rename to old_nushell/crates/nu-command/tests/commands/last.rs diff --git a/crates/nu-command/tests/commands/length.rs b/old_nushell/crates/nu-command/tests/commands/length.rs similarity index 100% rename from crates/nu-command/tests/commands/length.rs rename to old_nushell/crates/nu-command/tests/commands/length.rs diff --git a/old_nushell/crates/nu-command/tests/commands/lines.rs b/old_nushell/crates/nu-command/tests/commands/lines.rs new file mode 100644 index 0000000000..e03b5ad4b0 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/lines.rs @@ -0,0 +1,50 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn lines() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open cargo_sample.toml -r + | lines + | skip while $it != "[dependencies]" + | skip 1 + | first 1 + | split column "=" + | get Column1 + | str trim + "# + )); + + assert_eq!(actual.out, "rustyline"); +} + +#[test] +fn lines_proper_buffering() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open lines_test.txt -r + | lines + | str length + | to json + "# + )); + + assert_eq!(actual.out, "[8193,3]"); +} + +#[test] +fn lines_multi_value_split() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sample-simple.json + | get first second + | lines + | length + "# + )); + + assert_eq!(actual.out, "5"); +} diff --git a/old_nushell/crates/nu-command/tests/commands/ls.rs b/old_nushell/crates/nu-command/tests/commands/ls.rs new file mode 100644 index 0000000000..a6f52a0374 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/ls.rs @@ -0,0 +1,346 @@ +use nu_test_support::fs::Stub::EmptyFile; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn lists_regular_files() { + Playground::setup("ls_test_1", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("yehuda.txt"), + EmptyFile("jonathan.txt"), + EmptyFile("andres.txt"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | length + "# + )); + + assert_eq!(actual.out, "3"); + }) +} + +#[test] +fn lists_regular_files_using_asterisk_wildcard() { + Playground::setup("ls_test_2", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("los.txt"), + EmptyFile("tres.txt"), + EmptyFile("amigos.txt"), + EmptyFile("arepas.clu"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls *.txt + | length + "# + )); + + assert_eq!(actual.out, "3"); + }) +} + +#[test] +fn lists_regular_files_using_question_mark_wildcard() { + Playground::setup("ls_test_3", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("yehuda.10.txt"), + EmptyFile("jonathan.10.txt"), + EmptyFile("andres.10.txt"), + EmptyFile("chicken_not_to_be_picked_up.100.txt"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls *.??.txt + | length + "# + )); + + assert_eq!(actual.out, "3"); + }) +} + +#[test] +fn lists_all_files_in_directories_from_stream() { + Playground::setup("ls_test_4", |dirs, sandbox| { + sandbox + .with_files(vec![EmptyFile("root1.txt"), EmptyFile("root2.txt")]) + .within("dir_a") + .with_files(vec![ + EmptyFile("yehuda.10.txt"), + EmptyFile("jonathan.10.txt"), + ]) + .within("dir_b") + .with_files(vec![ + EmptyFile("andres.10.txt"), + EmptyFile("chicken_not_to_be_picked_up.100.txt"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + echo dir_a dir_b + | each { ls $it } + | length + "# + )); + + assert_eq!(actual.out, "4"); + }) +} + +#[test] +fn does_not_fail_if_glob_matches_empty_directory() { + Playground::setup("ls_test_5", |dirs, sandbox| { + sandbox.within("dir_a"); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls dir_a + | length + "# + )); + + assert_eq!(actual.out, "0"); + }) +} + +#[test] +fn fails_when_glob_doesnt_match() { + Playground::setup("ls_test_5", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("root1.txt"), EmptyFile("root2.txt")]); + + let actual = nu!( + cwd: dirs.test(), + "ls root3*" + ); + + assert!(actual.err.contains("no matches found")); + }) +} + +#[test] +fn list_files_from_two_parents_up_using_multiple_dots() { + Playground::setup("ls_test_6", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("yahuda.yaml"), + EmptyFile("jonathan.json"), + EmptyFile("andres.xml"), + EmptyFile("kevin.txt"), + ]); + + sandbox.within("foo").mkdir("bar"); + + let actual = nu!( + cwd: dirs.test().join("foo/bar"), + r#" + ls ... | length + "# + ); + + assert_eq!(actual.out, "5"); + }) +} + +#[test] +fn lists_hidden_file_when_explicitly_specified() { + Playground::setup("ls_test_7", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("los.txt"), + EmptyFile("tres.txt"), + EmptyFile("amigos.txt"), + EmptyFile("arepas.clu"), + EmptyFile(".testdotfile"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls .testdotfile + | length + "# + )); + + assert_eq!(actual.out, "1"); + }) +} + +#[test] +fn lists_all_hidden_files_when_glob_contains_dot() { + Playground::setup("ls_test_8", |dirs, sandbox| { + sandbox + .with_files(vec![ + EmptyFile("root1.txt"), + EmptyFile("root2.txt"), + EmptyFile(".dotfile1"), + ]) + .within("dir_a") + .with_files(vec![ + EmptyFile("yehuda.10.txt"), + EmptyFile("jonathan.10.txt"), + EmptyFile(".dotfile2"), + ]) + .within("dir_b") + .with_files(vec![ + EmptyFile("andres.10.txt"), + EmptyFile("chicken_not_to_be_picked_up.100.txt"), + EmptyFile(".dotfile3"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls **/.* + | length + "# + )); + + assert_eq!(actual.out, "3"); + }) +} + +#[test] +// TODO Remove this cfg value when we have an OS-agnostic way +// of creating hidden files using the playground. +#[cfg(unix)] +fn lists_all_hidden_files_when_glob_does_not_contain_dot() { + Playground::setup("ls_test_8", |dirs, sandbox| { + sandbox + .with_files(vec![ + EmptyFile("root1.txt"), + EmptyFile("root2.txt"), + EmptyFile(".dotfile1"), + ]) + .within("dir_a") + .with_files(vec![ + EmptyFile("yehuda.10.txt"), + EmptyFile("jonathan.10.txt"), + EmptyFile(".dotfile2"), + ]) + .within(".dir_b") + .with_files(vec![ + EmptyFile("andres.10.txt"), + EmptyFile("chicken_not_to_be_picked_up.100.txt"), + EmptyFile(".dotfile3"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls **/* + | length + "# + )); + + assert_eq!(actual.out, "5"); + }) +} + +#[test] +#[cfg(unix)] +fn fails_with_ls_to_dir_without_permission() { + Playground::setup("ls_test_1", |dirs, sandbox| { + sandbox.within("dir_a").with_files(vec![ + EmptyFile("yehuda.11.txt"), + EmptyFile("jonathan.10.txt"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + chmod 000 dir_a; ls dir_a + "# + )); + assert!(actual + .err + .contains("The permissions of 0 do not allow access for this user")); + }) +} + +#[test] +fn lists_files_including_starting_with_dot() { + Playground::setup("ls_test_9", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("yehuda.txt"), + EmptyFile("jonathan.txt"), + EmptyFile("andres.txt"), + EmptyFile(".hidden1.txt"), + EmptyFile(".hidden2.txt"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls -a + | length + "# + )); + + assert_eq!(actual.out, "5"); + }) +} + +#[test] +fn list_all_columns() { + Playground::setup("ls_test_all_columns", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("Leonardo.yaml"), + EmptyFile("Raphael.json"), + EmptyFile("Donatello.xml"), + EmptyFile("Michelangelo.txt"), + ]); + // Normal Operation + let actual = nu!( + cwd: dirs.test(), + "ls | get | to md" + ); + let expected = ["name", "type", "size", "modified"].join(""); + assert_eq!(actual.out, expected, "column names are incorrect for ls"); + // Long + let actual = nu!( + cwd: dirs.test(), + "ls -l | get | to md" + ); + let expected = { + #[cfg(unix)] + { + [ + "name", + "type", + "target", + "num_links", + "inode", + "readonly", + "mode", + "uid", + "group", + "size", + "created", + "accessed", + "modified", + ] + .join("") + } + + #[cfg(windows)] + { + [ + "name", "type", "target", "readonly", "size", "created", "accessed", "modified", + ] + .join("") + } + }; + assert_eq!( + actual.out, expected, + "column names are incorrect for ls long" + ); + }); +} diff --git a/old_nushell/crates/nu-command/tests/commands/math/avg.rs b/old_nushell/crates/nu-command/tests/commands/math/avg.rs new file mode 100644 index 0000000000..e749192c44 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/math/avg.rs @@ -0,0 +1,25 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn can_average_numbers() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sgml_description.json + | get glossary.GlossDiv.GlossList.GlossEntry.Sections + | math avg + "# + )); + + assert_eq!(actual.out, "101.5") +} + +#[test] +fn can_average_bytes() { + let actual = nu!( + cwd: "tests/fixtures/formats", + "ls | sort-by name | skip 1 | first 2 | get size | math avg | format \"{$it}\" " + ); + + assert_eq!(actual.out, "1.6 KB"); +} diff --git a/old_nushell/crates/nu-command/tests/commands/math/eval.rs b/old_nushell/crates/nu-command/tests/commands/math/eval.rs new file mode 100644 index 0000000000..736d1f6419 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/math/eval.rs @@ -0,0 +1,85 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn evaluates_two_plus_two() { + let actual = nu!( + cwd: ".", pipeline( + r#" + math eval "2 + 2" + "# + )); + + assert!(actual.out.contains("4.0")); +} + +#[test] +fn evaluates_two_to_the_power_four() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo "2 ^ 4" | math eval + "# + )); + + assert!(actual.out.contains("16.0")); +} + +#[test] +fn evaluates_three_multiplied_by_five() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo "3 * 5" | math eval + "# + )); + + assert!(actual.out.contains("15.0")); +} + +#[test] +fn evaluates_twenty_four_divided_by_two() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo "24 / 2" | math eval + "# + )); + + assert!(actual.out.contains("12.0")); +} + +#[test] +fn evaluates_twenty_eight_minus_seven() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo "28 - 7" | math eval + "# + )); + + assert!(actual.out.contains("21")); +} + +#[test] +fn evaluates_pi() { + let actual = nu!( + cwd: ".", pipeline( + r#" + math eval pi + "# + )); + + assert!(actual.out.contains("3.14")); +} + +#[test] +fn evaluates_tau() { + let actual = nu!( + cwd: ".", pipeline( + r#" + math eval tau + "# + )); + + assert!(actual.out.contains("6.28")); +} diff --git a/crates/nu-command/tests/commands/math/median.rs b/old_nushell/crates/nu-command/tests/commands/math/median.rs similarity index 100% rename from crates/nu-command/tests/commands/math/median.rs rename to old_nushell/crates/nu-command/tests/commands/math/median.rs diff --git a/old_nushell/crates/nu-command/tests/commands/math/mod.rs b/old_nushell/crates/nu-command/tests/commands/math/mod.rs new file mode 100644 index 0000000000..21c4cab65a --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/math/mod.rs @@ -0,0 +1,296 @@ +mod avg; +mod eval; +mod median; +mod round; +mod sqrt; +mod sum; + +use nu_test_support::{nu, pipeline}; + +#[test] +fn one_arg() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + 1 + "# + )); + + assert_eq!(actual.out, "1"); +} + +#[test] +fn add() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + 1 + 1 + "# + )); + + assert_eq!(actual.out, "2"); +} + +#[test] +fn add_compound() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + 1 + 2 + 2 + "# + )); + + assert_eq!(actual.out, "5"); +} + +#[test] +fn precedence_of_operators() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + 1 + 2 * 2 + "# + )); + + assert_eq!(actual.out, "5"); +} + +#[test] +fn precedence_of_operators2() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + 1 + 2 * 2 + 1 + "# + )); + + assert_eq!(actual.out, "6"); +} + +#[test] +fn division_of_ints() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + 4 / 2 + "# + )); + + assert_eq!(actual.out, "2"); +} + +#[test] +fn division_of_ints2() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + 1 / 4 + "# + )); + + assert_eq!(actual.out, "0.25"); +} + +#[test] +fn error_zero_division_int_int() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + 1 / 0 + "# + )); + + assert!(actual.err.contains("division by zero")); +} + +#[test] +fn error_zero_division_decimal_int() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + 1.0 / 0 + "# + )); + + assert!(actual.err.contains("division by zero")); +} + +#[test] +fn error_zero_division_int_decimal() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + 1 / 0.0 + "# + )); + + assert!(actual.err.contains("division by zero")); +} + +#[test] +fn error_zero_division_decimal_decimal() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + 1.0 / 0.0 + "# + )); + + assert!(actual.err.contains("division by zero")); +} + +#[test] +fn proper_precedence_history() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + 2 / 2 / 2 + 1 + "# + )); + + assert_eq!(actual.out, "1.5"); +} + +#[test] +fn parens_precedence() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + 4 * (6 - 3) + "# + )); + + assert_eq!(actual.out, "12"); +} + +#[test] +fn modulo() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + 9 mod 2 + "# + )); + + assert_eq!(actual.out, "1"); +} + +#[test] +fn duration_math() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + 1wk + 1day + "# + )); + + assert_eq!(actual.out, "8day"); +} + +#[test] +fn duration_decimal_math() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + 5.5day + 0.5day + "# + )); + + assert_eq!(actual.out, "6day"); +} + +#[test] +fn duration_math_with_nanoseconds() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + 1wk + 10ns + "# + )); + + assert_eq!(actual.out, "7day 10ns"); +} + +#[test] +fn duration_decimal_math_with_nanoseconds() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + 1.5wk + 10ns + "# + )); + + assert_eq!(actual.out, "10day 10ns"); +} + +#[test] +fn duration_math_with_negative() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + 1day - 1wk + "# + )); + + assert_eq!(actual.out, "-6day"); +} + +#[test] +fn duration_math_shell_error_on_big_numbers() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + (date now) + 100000000000000day + "# + )); + + assert!(actual.err.contains("Duration overflow")); +} + +#[test] +fn compound_comparison() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + 4 > 3 && 2 > 1 + "# + )); + + assert_eq!(actual.out, "true"); +} + +#[test] +fn compound_comparison2() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + 4 < 3 || 2 > 1 + "# + )); + + assert_eq!(actual.out, "true"); +} + +#[test] +fn compound_where() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo '[{"a": 1, "b": 1}, {"a": 2, "b": 1}, {"a": 2, "b": 2}]' | from json | where a == 2 && b == 1 | to json + "# + )); + + assert_eq!(actual.out, r#"{"a":2,"b":1}"#); +} + +#[test] +fn compound_where_paren() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo '[{"a": 1, "b": 1}, {"a": 2, "b": 1}, {"a": 2, "b": 2}]' | from json | where ($it.a == 2 && $it.b == 1) || $it.b == 2 | to json + "# + )); + + assert_eq!(actual.out, r#"[{"a":2,"b":1},{"a":2,"b":2}]"#); +} diff --git a/crates/nu-command/tests/commands/math/round.rs b/old_nushell/crates/nu-command/tests/commands/math/round.rs similarity index 100% rename from crates/nu-command/tests/commands/math/round.rs rename to old_nushell/crates/nu-command/tests/commands/math/round.rs diff --git a/old_nushell/crates/nu-command/tests/commands/math/sqrt.rs b/old_nushell/crates/nu-command/tests/commands/math/sqrt.rs new file mode 100644 index 0000000000..c8e7eb24a9 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/math/sqrt.rs @@ -0,0 +1,31 @@ +use nu_test_support::nu; + +#[test] +fn can_sqrt_numbers() { + let actual = nu!( + cwd: ".", + "echo [0.25 2 4] | math sqrt | math sum" + ); + + assert_eq!(actual.out, "3.914213562373095048801688724209698078569671875376948073176679737990732478462107038850387534327641573"); +} + +#[test] +fn can_sqrt_irrational() { + let actual = nu!( + cwd: ".", + "echo 2 | math sqrt" + ); + + assert_eq!(actual.out, "1.414213562373095048801688724209698078569671875376948073176679737990732478462107038850387534327641573"); +} + +#[test] +fn can_sqrt_perfect_square() { + let actual = nu!( + cwd: ".", + "echo 4 | math sqrt" + ); + + assert_eq!(actual.out, "2"); +} diff --git a/old_nushell/crates/nu-command/tests/commands/math/sum.rs b/old_nushell/crates/nu-command/tests/commands/math/sum.rs new file mode 100644 index 0000000000..4e3623d818 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/math/sum.rs @@ -0,0 +1,87 @@ +use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; +use std::str::FromStr; + +#[test] +fn all() { + Playground::setup("sum_test_1", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "meals.json", + r#" + { + meals: [ + {description: "1 large egg", calories: 90}, + {description: "1 cup white rice", calories: 250}, + {description: "1 tablespoon fish oil", calories: 108} + ] + } + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open meals.json + | get meals + | get calories + | math sum + "# + )); + + assert_eq!(actual.out, "448"); + }) +} + +#[test] +#[allow(clippy::unreadable_literal)] +#[allow(clippy::float_cmp)] +fn compute_sum_of_individual_row() -> Result<(), String> { + let answers_for_columns = [ + ("cpu", 88.257434), + ("mem", 3032375296.), + ("virtual", 102579965952.), + ]; + for (column_name, expected_value) in answers_for_columns { + let actual = nu!( + cwd: "tests/fixtures/formats/", + format!("open sample-ps-output.json | select {} | math sum | get {}", column_name, column_name) + ); + let result = + f64::from_str(&actual.out).map_err(|_| String::from("Failed to parse float."))?; + assert_eq!(result, expected_value); + } + Ok(()) +} + +#[test] +#[allow(clippy::unreadable_literal)] +#[allow(clippy::float_cmp)] +fn compute_sum_of_table() -> Result<(), String> { + let answers_for_columns = [ + ("cpu", 88.257434), + ("mem", 3032375296.), + ("virtual", 102579965952.), + ]; + for (column_name, expected_value) in answers_for_columns { + let actual = nu!( + cwd: "tests/fixtures/formats/", + format!("open sample-ps-output.json | select cpu mem virtual | math sum | get {}", column_name) + ); + let result = + f64::from_str(&actual.out).map_err(|_| String::from("Failed to parse float."))?; + assert_eq!(result, expected_value); + } + Ok(()) +} + +#[test] +fn sum_of_a_row_containing_a_table_is_an_error() { + let actual = nu!( + cwd: "tests/fixtures/formats/", + "open sample-sys-output.json | math sum" + ); + assert!(actual + .err + .contains("Attempted to compute values that can't be operated on")); +} diff --git a/old_nushell/crates/nu-command/tests/commands/merge.rs b/old_nushell/crates/nu-command/tests/commands/merge.rs new file mode 100644 index 0000000000..92ec3fe5da --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/merge.rs @@ -0,0 +1,42 @@ +use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn row() { + Playground::setup("merge_test_1", |dirs, sandbox| { + sandbox.with_files(vec![ + FileWithContentToBeTrimmed( + "caballeros.csv", + r#" + name,country,luck + Andrés,Ecuador,0 + Jonathan,USA,0 + Jason,Canada,0 + Yehuda,USA,0 + "#, + ), + FileWithContentToBeTrimmed( + "new_caballeros.csv", + r#" + name,country,luck + Andrés Robalino,Guayaquil Ecuador,1 + Jonathan Turner,New Zealand,1 + "#, + ), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open caballeros.csv + | merge { open new_caballeros.csv } + | where country in ["Guayaquil Ecuador" "New Zealand"] + | get luck + | math sum + "# + )); + + assert_eq!(actual.out, "2"); + }) +} diff --git a/crates/nu-command/tests/commands/mkdir.rs b/old_nushell/crates/nu-command/tests/commands/mkdir.rs similarity index 100% rename from crates/nu-command/tests/commands/mkdir.rs rename to old_nushell/crates/nu-command/tests/commands/mkdir.rs diff --git a/old_nushell/crates/nu-command/tests/commands/mod.rs b/old_nushell/crates/nu-command/tests/commands/mod.rs new file mode 100644 index 0000000000..eaf8f6bbef --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/mod.rs @@ -0,0 +1,69 @@ +mod all; +mod any; +mod append; +mod cal; +mod cd; +mod compact; +mod config; +mod cp; +mod def; +mod default; +mod drop; +mod each; +mod echo; +mod empty; +mod enter; +mod every; +mod find; +mod first; +mod flatten; +mod format; +mod get; +mod group_by; +mod hash_; +mod headers; +mod help; +mod histogram; +mod insert; +mod into_filesize; +mod into_int; +mod keep; +mod last; +mod length; +mod lines; +mod ls; +mod math; +mod merge; +mod mkdir; +mod move_; +mod open; +mod parse; +mod path; +mod pathvar; +mod prepend; +mod random; +mod range; +mod reduce; +mod rename; +mod reverse; +mod rm; +mod roll; +mod rotate; +mod save; +mod select; +mod semicolon; +mod skip; +mod sort_by; +mod source; +mod split_by; +mod split_column; +mod split_row; +mod str_; +mod touch; +mod uniq; +mod update; +mod where_; +mod which; +mod with_env; +mod wrap; +mod zip; diff --git a/old_nushell/crates/nu-command/tests/commands/move_/column.rs b/old_nushell/crates/nu-command/tests/commands/move_/column.rs new file mode 100644 index 0000000000..caf1b2fd4f --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/move_/column.rs @@ -0,0 +1,137 @@ +use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn moves_a_column_before() { + Playground::setup("move_column_test_1", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "sample.csv", + r#" + column1,column2,column3,...,column98,column99,column100 + -------,-------,-------,---,--------, A ,--------- + -------,-------,-------,---,--------, N ,--------- + -------,-------,-------,---,--------, D ,--------- + -------,-------,-------,---,--------, R ,--------- + -------,-------,-------,---,--------, E ,--------- + -------,-------,-------,---,--------, S ,--------- + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.csv + | move column99 --before column1 + | rename chars + | get chars + | str trim + | str collect + "# + )); + + assert!(actual.out.contains("ANDRES")); + }) +} + +#[test] +fn moves_columns_before() { + Playground::setup("move_column_test_2", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "sample.csv", + r#" + column1,column2,column3,...,column98,column99,column100 + -------,-------, A ,---,--------, N ,--------- + -------,-------, D ,---,--------, R ,--------- + -------,-------, E ,---,--------, S ,--------- + -------,-------, : ,---,--------, : ,--------- + -------,-------, J ,---,--------, O ,--------- + -------,-------, N ,---,--------, A ,--------- + -------,-------, T ,---,--------, H ,--------- + -------,-------, A ,---,--------, N ,--------- + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.csv + | move column99 column3 --before column2 + | rename _ chars_1 chars_2 + | get chars_2 chars_1 + | str trim + | str collect + "# + )); + + assert!(actual.out.contains("ANDRES::JONATHAN")); + }) +} + +#[test] +fn moves_a_column_after() { + Playground::setup("move_column_test_3", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "sample.csv", + r#" + column1,column2,letters,...,column98,and_more,column100 + -------,-------, A ,---,--------, N ,--------- + -------,-------, D ,---,--------, R ,--------- + -------,-------, E ,---,--------, S ,--------- + -------,-------, : ,---,--------, : ,--------- + -------,-------, J ,---,--------, O ,--------- + -------,-------, N ,---,--------, A ,--------- + -------,-------, T ,---,--------, H ,--------- + -------,-------, A ,---,--------, N ,--------- + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.csv + | move letters --after and_more + | move letters and_more --before column2 + | rename _ chars_1 chars_2 + | get chars_1 chars_2 + | str trim + | str collect + "# + )); + + assert!(actual.out.contains("ANDRES::JONATHAN")); + }) +} + +#[test] +fn moves_columns_after() { + Playground::setup("move_column_test_4", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "sample.csv", + r#" + column1,column2,letters,...,column98,and_more,column100 + -------,-------, A ,---,--------, N ,--------- + -------,-------, D ,---,--------, R ,--------- + -------,-------, E ,---,--------, S ,--------- + -------,-------, : ,---,--------, : ,--------- + -------,-------, J ,---,--------, O ,--------- + -------,-------, N ,---,--------, A ,--------- + -------,-------, T ,---,--------, H ,--------- + -------,-------, A ,---,--------, N ,--------- + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.csv + | move letters and_more --after column1 + | get + | nth 1 2 + | str collect + "# + )); + + assert!(actual.out.contains("lettersand_more")); + }) +} diff --git a/crates/nu-command/tests/commands/move_/mod.rs b/old_nushell/crates/nu-command/tests/commands/move_/mod.rs similarity index 100% rename from crates/nu-command/tests/commands/move_/mod.rs rename to old_nushell/crates/nu-command/tests/commands/move_/mod.rs diff --git a/old_nushell/crates/nu-command/tests/commands/move_/mv.rs b/old_nushell/crates/nu-command/tests/commands/move_/mv.rs new file mode 100644 index 0000000000..5debb64b38 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/move_/mv.rs @@ -0,0 +1,361 @@ +use nu_test_support::fs::{files_exist_at, Stub::EmptyFile}; +use nu_test_support::nu; +use nu_test_support::playground::Playground; + +#[test] +fn moves_a_file() { + Playground::setup("mv_test_1", |dirs, sandbox| { + sandbox + .with_files(vec![EmptyFile("andres.txt")]) + .mkdir("expected"); + + let original = dirs.test().join("andres.txt"); + let expected = dirs.test().join("expected/yehuda.txt"); + + nu!( + cwd: dirs.test(), + "mv andres.txt expected/yehuda.txt" + ); + + assert!(!original.exists()); + assert!(expected.exists()); + }) +} + +#[test] +fn overwrites_if_moving_to_existing_file() { + Playground::setup("mv_test_2", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("andres.txt"), EmptyFile("jonathan.txt")]); + + let original = dirs.test().join("andres.txt"); + let expected = dirs.test().join("jonathan.txt"); + + nu!( + cwd: dirs.test(), + "mv andres.txt jonathan.txt" + ); + + assert!(!original.exists()); + assert!(expected.exists()); + }) +} + +#[test] +fn moves_a_directory() { + Playground::setup("mv_test_3", |dirs, sandbox| { + sandbox.mkdir("empty_dir"); + + let original_dir = dirs.test().join("empty_dir"); + let expected = dirs.test().join("renamed_dir"); + + nu!( + cwd: dirs.test(), + "mv empty_dir renamed_dir" + ); + + assert!(!original_dir.exists()); + assert!(expected.exists()); + }) +} + +#[test] +fn moves_the_file_inside_directory_if_path_to_move_is_existing_directory() { + Playground::setup("mv_test_4", |dirs, sandbox| { + sandbox + .with_files(vec![EmptyFile("jonathan.txt")]) + .mkdir("expected"); + + let original_dir = dirs.test().join("jonathan.txt"); + let expected = dirs.test().join("expected/jonathan.txt"); + + nu!( + cwd: dirs.test(), + "mv jonathan.txt expected" + ); + + assert!(!original_dir.exists()); + assert!(expected.exists()); + }) +} + +#[test] +fn moves_the_directory_inside_directory_if_path_to_move_is_existing_directory() { + Playground::setup("mv_test_5", |dirs, sandbox| { + sandbox + .within("contributors") + .with_files(vec![EmptyFile("jonathan.txt")]) + .mkdir("expected"); + + let original_dir = dirs.test().join("contributors"); + let expected = dirs.test().join("expected/contributors"); + + nu!( + cwd: dirs.test(), + "mv contributors expected" + ); + + assert!(!original_dir.exists()); + assert!(expected.exists()); + assert!(files_exist_at(vec!["jonathan.txt"], expected)) + }) +} + +#[test] +fn moves_using_path_with_wildcard() { + Playground::setup("mv_test_7", |dirs, sandbox| { + sandbox + .within("originals") + .with_files(vec![ + EmptyFile("andres.ini"), + EmptyFile("caco3_plastics.csv"), + EmptyFile("cargo_sample.toml"), + EmptyFile("jonathan.ini"), + EmptyFile("jonathan.xml"), + EmptyFile("sgml_description.json"), + EmptyFile("sample.ini"), + EmptyFile("utf16.ini"), + EmptyFile("yehuda.ini"), + ]) + .mkdir("work_dir") + .mkdir("expected"); + + let work_dir = dirs.test().join("work_dir"); + let expected = dirs.test().join("expected"); + + nu!(cwd: work_dir, "mv ../originals/*.ini ../expected"); + + assert!(files_exist_at( + vec!["yehuda.ini", "jonathan.ini", "sample.ini", "andres.ini",], + expected + )); + }) +} + +#[test] +fn moves_using_a_glob() { + Playground::setup("mv_test_8", |dirs, sandbox| { + sandbox + .within("meals") + .with_files(vec![ + EmptyFile("arepa.txt"), + EmptyFile("empanada.txt"), + EmptyFile("taquiza.txt"), + ]) + .mkdir("work_dir") + .mkdir("expected"); + + let meal_dir = dirs.test().join("meals"); + let work_dir = dirs.test().join("work_dir"); + let expected = dirs.test().join("expected"); + + nu!(cwd: work_dir, "mv ../meals/* ../expected"); + + assert!(meal_dir.exists()); + assert!(files_exist_at( + vec!["arepa.txt", "empanada.txt", "taquiza.txt",], + expected + )); + }) +} + +#[test] +fn moves_a_directory_with_files() { + Playground::setup("mv_test_9", |dirs, sandbox| { + sandbox + .mkdir("vehicles/car") + .mkdir("vehicles/bicycle") + .with_files(vec![ + EmptyFile("vehicles/car/car1.txt"), + EmptyFile("vehicles/car/car2.txt"), + ]) + .with_files(vec![ + EmptyFile("vehicles/bicycle/bicycle1.txt"), + EmptyFile("vehicles/bicycle/bicycle2.txt"), + ]); + + let original_dir = dirs.test().join("vehicles"); + let expected_dir = dirs.test().join("expected"); + + nu!( + cwd: dirs.test(), + "mv vehicles expected" + ); + + assert!(!original_dir.exists()); + assert!(expected_dir.exists()); + assert!(files_exist_at( + vec![ + "car/car1.txt", + "car/car2.txt", + "bicycle/bicycle1.txt", + "bicycle/bicycle2.txt" + ], + expected_dir + )); + }) +} + +#[test] +fn errors_if_source_doesnt_exist() { + Playground::setup("mv_test_10", |dirs, sandbox| { + sandbox.mkdir("test_folder"); + let actual = nu!( + cwd: dirs.test(), + "mv non-existing-file test_folder/" + ); + assert!(actual.err.contains("Invalid file or pattern")); + }) +} + +#[test] +fn errors_if_destination_doesnt_exist() { + Playground::setup("mv_test_10_1", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("empty.txt")]); + + let actual = nu!( + cwd: dirs.test(), + "mv empty.txt does/not/exist" + ); + + assert!(actual.err.contains("Destination directory does not exist")); + }) +} + +#[test] +fn errors_if_multiple_sources_but_destination_not_a_directory() { + Playground::setup("mv_test_10_2", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("file1.txt"), + EmptyFile("file2.txt"), + EmptyFile("file3.txt"), + ]); + + let actual = nu!( + cwd: dirs.test(), + "mv file?.txt not_a_dir" + ); + + assert!(actual + .err + .contains("Can only move multiple sources if destination is a directory")); + }) +} + +#[test] +fn errors_if_renaming_directory_to_an_existing_file() { + Playground::setup("mv_test_10_3", |dirs, sandbox| { + sandbox + .mkdir("mydir") + .with_files(vec![EmptyFile("empty.txt")]); + + let actual = nu!( + cwd: dirs.test(), + "mv mydir empty.txt" + ); + + assert!(actual.err.contains("Cannot rename a directory to a file")); + }) +} + +#[test] +fn errors_if_moving_to_itself() { + Playground::setup("mv_test_10_4", |dirs, sandbox| { + sandbox.mkdir("mydir").mkdir("mydir/mydir_2"); + + let actual = nu!( + cwd: dirs.test(), + "mv mydir mydir/mydir_2/" + ); + + assert!(actual.err.contains("cannot move to itself")); + }) +} + +#[test] +fn does_not_error_on_relative_parent_path() { + Playground::setup("mv_test_11", |dirs, sandbox| { + sandbox + .mkdir("first") + .with_files(vec![EmptyFile("first/william_hartnell.txt")]); + + let original = dirs.test().join("first/william_hartnell.txt"); + let expected = dirs.test().join("william_hartnell.txt"); + + nu!( + cwd: dirs.test().join("first"), + "mv william_hartnell.txt ./.." + ); + + assert!(!original.exists()); + assert!(expected.exists()); + }) +} + +#[test] +fn move_files_using_glob_two_parents_up_using_multiple_dots() { + Playground::setup("mv_test_12", |dirs, sandbox| { + sandbox.within("foo").within("bar").with_files(vec![ + EmptyFile("jonathan.json"), + EmptyFile("andres.xml"), + EmptyFile("yehuda.yaml"), + EmptyFile("kevin.txt"), + EmptyFile("many_more.ppl"), + ]); + + nu!( + cwd: dirs.test().join("foo/bar"), + r#" + mv * ... + "# + ); + + let files = vec![ + "yehuda.yaml", + "jonathan.json", + "andres.xml", + "kevin.txt", + "many_more.ppl", + ]; + + let original_dir = dirs.test().join("foo/bar"); + let destination_dir = dirs.test(); + + assert!(files_exist_at(files.clone(), destination_dir)); + assert!(!files_exist_at(files, original_dir)) + }) +} + +#[test] +fn move_file_from_two_parents_up_using_multiple_dots_to_current_dir() { + Playground::setup("cp_test_10", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("hello_there")]); + sandbox.within("foo").mkdir("bar"); + + nu!( + cwd: dirs.test().join("foo/bar"), + r#" + mv .../hello_there . + "# + ); + + let expected = dirs.test().join("foo/bar/hello_there"); + let original = dirs.test().join("hello_there"); + + assert!(expected.exists()); + assert!(!original.exists()); + }) +} + +#[test] +fn does_not_error_when_some_file_is_moving_into_itself() { + Playground::setup("mv_test_13", |dirs, sandbox| { + sandbox.mkdir("11").mkdir("12"); + + let original_dir = dirs.test().join("11"); + let expected = dirs.test().join("12/11"); + nu!(cwd: dirs.test(), "mv 1* 12"); + + assert!(!original_dir.exists()); + assert!(expected.exists()); + }) +} diff --git a/old_nushell/crates/nu-command/tests/commands/nth.rs b/old_nushell/crates/nu-command/tests/commands/nth.rs new file mode 100644 index 0000000000..a65dcd7ed0 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/nth.rs @@ -0,0 +1,37 @@ +#[test] +fn selects_a_row() { + Playground::setup("nth_test_1", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("notes.txt"), EmptyFile("arepas.txt")]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | sort-by name + | nth 0 + | get name + "# + )); + + assert_eq!(actual.out, "arepas.txt"); + }); +} + +#[test] +fn selects_many_rows() { + Playground::setup("nth_test_2", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("notes.txt"), EmptyFile("arepas.txt")]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | get name + | nth 1 0 + | length + "# + )); + + assert_eq!(actual.out, "2"); + }); +} diff --git a/old_nushell/crates/nu-command/tests/commands/open.rs b/old_nushell/crates/nu-command/tests/commands/open.rs new file mode 100644 index 0000000000..994d2699dc --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/open.rs @@ -0,0 +1,254 @@ +use nu_test_support::fs::Stub::EmptyFile; +use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn parses_csv() { + Playground::setup("open_test_1", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "nu.zion.csv", + r#" + author,lang,source + Jonathan Turner,Rust,New Zealand + Andres N. Robalino,Rust,Ecuador + Yehuda Katz,Rust,Estados Unidos + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open nu.zion.csv + | where author == "Andres N. Robalino" + | get source + "# + )); + + assert_eq!(actual.out, "Ecuador"); + }) +} + +// sample.bson has the following format: +// ━━━━━━━━━━┯━━━━━━━━━━━ +// _id │ root +// ──────────┼─────────── +// [object] │ [9 items] +// ━━━━━━━━━━┷━━━━━━━━━━━ +// +// the root value is: +// ━━━┯━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━┯━━━━━━━━━━ +// # │ _id │ a │ b │ c +// ───┼───────────────────┼─────────────────────────┼──────────┼────────── +// 0 │ [object] │ 1.000000000000000 │ hello │ [2 items] +// 1 │ [object] │ 42.00000000000000 │ whel │ hello +// 2 │ [object] │ [object] │ │ +// 3 │ [object] │ │ [object] │ +// 4 │ [object] │ │ │ [object] +// 5 │ [object] │ │ │ [object] +// 6 │ [object] │ [object] │ [object] │ +// 7 │ [object] │ │ [object] │ +// 8 │ 1.000000 │ │ [object] │ +// +// The decimal value is supposed to be π, but is currently wrong due to +// what appears to be an issue in the bson library that is under investigation. +// + +#[cfg(feature = "bson")] +#[test] +fn parses_bson() { + let actual = nu!( + cwd: "tests/fixtures/formats", + "open sample.bson | get root | nth 0 | get b" + ); + + assert_eq!(actual.out, "hello"); +} + +#[cfg(feature = "bson")] +#[test] +fn parses_more_bson_complexity() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sample.bson + | get root + | nth 6 + | get b + | get '$binary_subtype' + "# + )); + + assert_eq!(actual.out, "function"); +} + +// sample.db has the following format: +// +// ━━━┯━━━━━━━━━━━━┯━━━━━━━━━━━━━━ +// # │ table_name │ table_values +// ───┼────────────┼────────────── +// 0 │ strings │ [6 items] +// 1 │ ints │ [5 items] +// 2 │ floats │ [4 items] +// ━━━┷━━━━━━━━━━━━┷━━━━━━━━━━━━━━ +// +// In this case, this represents a sqlite database +// with three tables named `strings`, `ints`, and `floats`. +// The table_values represent the values for the tables: +// +// ━━━━┯━━━━━━━┯━━━━━━━━━━┯━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// # │ x │ y │ z │ f +// ────┼───────┼──────────┼──────┼────────────────────────────────────────────────────────────────────── +// 0 │ hello │ │ │ +// 1 │ hello │ │ │ +// 2 │ hello │ │ │ +// 3 │ hello │ │ │ +// 4 │ world │ │ │ +// 5 │ world │ │ │ +// 6 │ │ │ 1 │ +// 7 │ │ │ 42 │ +// 8 │ │ │ 425 │ +// 9 │ │ │ 4253 │ +// 10 │ │ │ │ +// 11 │ │ │ │ 3.400000000000000 +// 12 │ │ │ │ 3.141592650000000 +// 13 │ │ │ │ 23.00000000000000 +// 14 │ │ │ │ this string that doesn't really belong here but sqlite is what it is +// ━━━━┷━━━━━━━┷━━━━━━━━━━┷━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// +// We can see here that each table has different columns. `strings` has `x` and `y`, while +// `ints` has just `z`, and `floats` has only the column `f`. This means, in general, when working +// with sqlite, one will want to select a single table, e.g.: +// +// open sample.db | nth 1 | get table_values +// ━━━┯━━━━━━ +// # │ z +// ───┼────── +// 0 │ 1 +// 1 │ 42 +// 2 │ 425 +// 3 │ 4253 +// 4 │ +// ━━━┷━━━━━━ + +#[cfg(feature = "sqlite")] +#[test] +fn parses_sqlite() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sample.db + | get table_values + | nth 2 + | get x + "# + )); + + assert_eq!(actual.out, "hello"); +} + +#[test] +fn parses_toml() { + let actual = nu!( + cwd: "tests/fixtures/formats", + "open cargo_sample.toml | get package.edition" + ); + + assert_eq!(actual.out, "2018"); +} + +#[test] +fn parses_tsv() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open caco3_plastics.tsv + | first 1 + | get origin + "# + )); + + assert_eq!(actual.out, "SPAIN") +} + +#[test] +fn parses_json() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sgml_description.json + | get glossary.GlossDiv.GlossList.GlossEntry.GlossSee + "# + )); + + assert_eq!(actual.out, "markup") +} + +#[test] +fn parses_xml() { + let actual = nu!( + cwd: "tests/fixtures/formats", + "open jonathan.xml | get rss.children.channel.children | get item.children | get link.children.0" + ); + + assert_eq!( + actual.out, + "http://www.jonathanturner.org/2015/10/off-to-new-adventures.html" + ) +} + +#[test] +fn parses_ini() { + let actual = nu!( + cwd: "tests/fixtures/formats", + "open sample.ini | get SectionOne.integer" + ); + + assert_eq!(actual.out, "1234") +} + +#[test] +fn parses_utf16_ini() { + let actual = nu!( + cwd: "tests/fixtures/formats", + "open utf16.ini | rename info | get info | get IconIndex" + ); + + assert_eq!(actual.out, "-236") +} + +#[test] +fn errors_if_file_not_found() { + let actual = nu!( + cwd: "tests/fixtures/formats", + "open i_dont_exist.txt" + ); + let expected = "Cannot find file"; + assert!( + actual.err.contains(expected), + "Error:\n{}\ndoes not contain{}", + actual.err, + expected + ); +} + +#[test] +fn open_dir_is_ls() { + Playground::setup("open_dir", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("yehuda.txt"), + EmptyFile("jonathan.txt"), + EmptyFile("andres.txt"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open . + | length + "# + )); + + assert_eq!(actual.out, "3"); + }) +} diff --git a/old_nushell/crates/nu-command/tests/commands/parse.rs b/old_nushell/crates/nu-command/tests/commands/parse.rs new file mode 100644 index 0000000000..3b9c9c5272 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/parse.rs @@ -0,0 +1,187 @@ +use nu_test_support::fs::Stub; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +mod simple { + use super::*; + + #[test] + fn extracts_fields_from_the_given_the_pattern() { + Playground::setup("parse_test_1", |dirs, sandbox| { + sandbox.with_files(vec![Stub::FileWithContentToBeTrimmed( + "key_value_separated_arepa_ingredients.txt", + r#" + VAR1=Cheese + VAR2=JonathanParsed + VAR3=NushellSecretIngredient + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open key_value_separated_arepa_ingredients.txt + | lines + | each { echo $it | parse "{Name}={Value}" } + | nth 1 + | get Value + "# + )); + + assert_eq!(actual.out, "JonathanParsed"); + }) + } + + #[test] + fn double_open_curly_evalutes_to_a_single_curly() { + Playground::setup("parse_test_regex_2", |dirs, _sandbox| { + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + echo "{abc}123" + | parse "{{abc}{name}" + | get name + "# + )); + + assert_eq!(actual.out, "123"); + }) + } + + #[test] + fn properly_escapes_text() { + Playground::setup("parse_test_regex_3", |dirs, _sandbox| { + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + echo "(abc)123" + | parse "(abc){name}" + | get name + "# + )); + + assert_eq!(actual.out, "123"); + }) + } + + #[test] + fn properly_captures_empty_column() { + Playground::setup("parse_test_regex_4", |dirs, _sandbox| { + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + echo ["1:INFO:component:all is well" "2:ERROR::something bad happened"] + | parse "{timestamp}:{level}:{tag}:{entry}" + | get entry + | nth 1 + "# + )); + + assert_eq!(actual.out, "something bad happened"); + }) + } + + #[test] + fn errors_when_missing_closing_brace() { + Playground::setup("parse_test_regex_5", |dirs, _sandbox| { + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + echo "(abc)123" + | parse "(abc){name" + | get name + "# + )); + + assert!(actual.err.contains("invalid parse pattern")); + }) + } +} + +mod regex { + use super::*; + + fn nushell_git_log_oneline<'a>() -> Vec> { + vec![Stub::FileWithContentToBeTrimmed( + "nushell_git_log_oneline.txt", + r#" + ae87582c Fix missing invocation errors (#1846) + b89976da let format access variables also (#1842) + "#, + )] + } + + #[test] + fn extracts_fields_with_all_named_groups() { + Playground::setup("parse_test_regex_1", |dirs, sandbox| { + sandbox.with_files(nushell_git_log_oneline()); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open nushell_git_log_oneline.txt + | parse --regex "(?P\w+) (?P.+) \(#(?P\d+)\)" + | nth 1 + | get PR + "# + )); + + assert_eq!(actual.out, "1842"); + }) + } + + #[test] + fn extracts_fields_with_all_unnamed_groups() { + Playground::setup("parse_test_regex_2", |dirs, sandbox| { + sandbox.with_files(nushell_git_log_oneline()); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open nushell_git_log_oneline.txt + | parse --regex "(\w+) (.+) \(#(\d+)\)" + | nth 1 + | get Capture1 + "# + )); + + assert_eq!(actual.out, "b89976da"); + }) + } + + #[test] + fn extracts_fields_with_named_and_unnamed_groups() { + Playground::setup("parse_test_regex_3", |dirs, sandbox| { + sandbox.with_files(nushell_git_log_oneline()); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open nushell_git_log_oneline.txt + | parse --regex "(?P\w+) (.+) \(#(?P\d+)\)" + | nth 1 + | get Capture2 + "# + )); + + assert_eq!(actual.out, "let format access variables also"); + }) + } + + #[test] + fn errors_with_invalid_regex() { + Playground::setup("parse_test_regex_1", |dirs, sandbox| { + sandbox.with_files(nushell_git_log_oneline()); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open nushell_git_log_oneline.txt + | parse --regex "(?P\w+ unfinished capture group" + "# + )); + + assert!(actual.err.contains("unclosed group")); + }) + } +} diff --git a/crates/nu-command/tests/commands/path/basename.rs b/old_nushell/crates/nu-command/tests/commands/path/basename.rs similarity index 100% rename from crates/nu-command/tests/commands/path/basename.rs rename to old_nushell/crates/nu-command/tests/commands/path/basename.rs diff --git a/crates/nu-command/tests/commands/path/dirname.rs b/old_nushell/crates/nu-command/tests/commands/path/dirname.rs similarity index 100% rename from crates/nu-command/tests/commands/path/dirname.rs rename to old_nushell/crates/nu-command/tests/commands/path/dirname.rs diff --git a/old_nushell/crates/nu-command/tests/commands/path/exists.rs b/old_nushell/crates/nu-command/tests/commands/path/exists.rs new file mode 100644 index 0000000000..9a1b496e04 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/path/exists.rs @@ -0,0 +1,53 @@ +use nu_test_support::fs::Stub::EmptyFile; +use nu_test_support::nu; +use nu_test_support::playground::Playground; + +#[test] +fn checks_if_existing_file_exists() { + Playground::setup("path_exists_1", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("spam.txt")]); + + let actual = nu!( + cwd: dirs.test(), + "echo spam.txt | path exists" + ); + + assert_eq!(actual.out, "true"); + }) +} + +#[test] +fn checks_if_missing_file_exists() { + Playground::setup("path_exists_2", |dirs, _| { + let actual = nu!( + cwd: dirs.test(), + "echo spam.txt | path exists" + ); + + assert_eq!(actual.out, "false"); + }) +} + +#[test] +fn checks_if_dot_exists() { + Playground::setup("path_exists_3", |dirs, _| { + let actual = nu!( + cwd: dirs.test(), + "echo '.' | path exists" + ); + + assert_eq!(actual.out, "true"); + }) +} + +#[test] +fn checks_if_double_dot_exists() { + Playground::setup("path_exists_4", |dirs, _| { + let actual = nu!( + cwd: dirs.test(), + "echo '..' | path exists" + ); + + assert_eq!(actual.out, "true"); + }) +} diff --git a/crates/nu-command/tests/commands/path/expand.rs b/old_nushell/crates/nu-command/tests/commands/path/expand.rs similarity index 100% rename from crates/nu-command/tests/commands/path/expand.rs rename to old_nushell/crates/nu-command/tests/commands/path/expand.rs diff --git a/crates/nu-command/tests/commands/path/join.rs b/old_nushell/crates/nu-command/tests/commands/path/join.rs similarity index 100% rename from crates/nu-command/tests/commands/path/join.rs rename to old_nushell/crates/nu-command/tests/commands/path/join.rs diff --git a/crates/nu-command/tests/commands/path/mod.rs b/old_nushell/crates/nu-command/tests/commands/path/mod.rs similarity index 100% rename from crates/nu-command/tests/commands/path/mod.rs rename to old_nushell/crates/nu-command/tests/commands/path/mod.rs diff --git a/old_nushell/crates/nu-command/tests/commands/path/parse.rs b/old_nushell/crates/nu-command/tests/commands/path/parse.rs new file mode 100644 index 0000000000..b4ffb3b65e --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/path/parse.rs @@ -0,0 +1,136 @@ +use nu_test_support::{nu, pipeline}; + +#[cfg(windows)] +#[test] +fn parses_single_path_prefix() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo 'C:\users\viking\spam.txt' + | path parse + | get prefix + "# + )); + + assert_eq!(actual.out, "C:"); +} + +#[test] +fn parses_single_path_parent() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo 'home/viking/spam.txt' + | path parse + | get parent + "# + )); + + assert_eq!(actual.out, "home/viking"); +} + +#[test] +fn parses_single_path_stem() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo 'home/viking/spam.txt' + | path parse + | get stem + "# + )); + + assert_eq!(actual.out, "spam"); +} + +#[test] +fn parses_custom_extension_gets_extension() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo 'home/viking/spam.tar.gz' + | path parse -e tar.gz + | get extension + "# + )); + + assert_eq!(actual.out, "tar.gz"); +} + +#[test] +fn parses_custom_extension_gets_stem() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo 'home/viking/spam.tar.gz' + | path parse -e tar.gz + | get stem + "# + )); + + assert_eq!(actual.out, "spam"); +} + +#[test] +fn parses_ignoring_extension_gets_extension() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo 'home/viking/spam.tar.gz' + | path parse -e '' + | get extension + "# + )); + + assert_eq!(actual.out, ""); +} + +#[test] +fn parses_ignoring_extension_gets_stem() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo 'home/viking/spam.tar.gz' + | path parse -e "" + | get stem + "# + )); + + assert_eq!(actual.out, "spam.tar.gz"); +} + +#[test] +fn parses_column_path_extension() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo [[home, barn]; ['home/viking/spam.txt', 'barn/cow/moo.png']] + | path parse -c [ home barn ] + | get barn + | get extension + "# + )); + + assert_eq!(actual.out, "png"); +} + +#[test] +fn parses_into_correct_number_of_columns() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo 'home/viking/spam.txt' + | path parse + | pivot + | get Column0 + | length + "# + )); + + #[cfg(windows)] + let expected = "4"; + #[cfg(not(windows))] + let expected = "3"; + + assert_eq!(actual.out, expected); +} diff --git a/old_nushell/crates/nu-command/tests/commands/path/split.rs b/old_nushell/crates/nu-command/tests/commands/path/split.rs new file mode 100644 index 0000000000..97f1cbd37f --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/path/split.rs @@ -0,0 +1,47 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn splits_empty_path() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo '' | path split + "# + )); + + assert_eq!(actual.out, ""); +} + +#[test] +fn splits_correctly_single_path() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo ['home/viking/spam.txt'] + | path split + | last + "# + )); + + assert_eq!(actual.out, "spam.txt"); +} + +#[test] +fn splits_correctly_with_column_path() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo [ + [home, barn]; + + ['home/viking/spam.txt', 'barn/cow/moo.png'] + ['home/viking/eggs.txt', 'barn/goat/cheese.png'] + ] + | path split -c [ home barn ] + | get barn + | length + "# + )); + + assert_eq!(actual.out, "6"); +} diff --git a/old_nushell/crates/nu-command/tests/commands/path/type_.rs b/old_nushell/crates/nu-command/tests/commands/path/type_.rs new file mode 100644 index 0000000000..0699767f89 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/path/type_.rs @@ -0,0 +1,54 @@ +use nu_test_support::fs::Stub::EmptyFile; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn returns_type_of_missing_file() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo "spam.txt" + | path type + "# + )); + + assert_eq!(actual.out, ""); +} + +#[test] +fn returns_type_of_existing_file() { + Playground::setup("path_expand_1", |dirs, sandbox| { + sandbox + .within("menu") + .with_files(vec![EmptyFile("spam.txt")]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + echo "menu" + | path type + "# + )); + + assert_eq!(actual.out, "Dir"); + }) +} + +#[test] +fn returns_type_of_existing_directory() { + Playground::setup("path_expand_1", |dirs, sandbox| { + sandbox + .within("menu") + .with_files(vec![EmptyFile("spam.txt")]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + echo "menu/spam.txt" + | path type + "# + )); + + assert_eq!(actual.out, "File"); + }) +} diff --git a/crates/nu-command/tests/commands/pathvar/mod.rs b/old_nushell/crates/nu-command/tests/commands/pathvar/mod.rs similarity index 100% rename from crates/nu-command/tests/commands/pathvar/mod.rs rename to old_nushell/crates/nu-command/tests/commands/pathvar/mod.rs diff --git a/crates/nu-command/tests/commands/prepend.rs b/old_nushell/crates/nu-command/tests/commands/prepend.rs similarity index 100% rename from crates/nu-command/tests/commands/prepend.rs rename to old_nushell/crates/nu-command/tests/commands/prepend.rs diff --git a/crates/nu-command/tests/commands/random/bool.rs b/old_nushell/crates/nu-command/tests/commands/random/bool.rs similarity index 100% rename from crates/nu-command/tests/commands/random/bool.rs rename to old_nushell/crates/nu-command/tests/commands/random/bool.rs diff --git a/crates/nu-command/tests/commands/random/chars.rs b/old_nushell/crates/nu-command/tests/commands/random/chars.rs similarity index 100% rename from crates/nu-command/tests/commands/random/chars.rs rename to old_nushell/crates/nu-command/tests/commands/random/chars.rs diff --git a/old_nushell/crates/nu-command/tests/commands/random/decimal.rs b/old_nushell/crates/nu-command/tests/commands/random/decimal.rs new file mode 100644 index 0000000000..0e78509a78 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/random/decimal.rs @@ -0,0 +1,37 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn generates_an_decimal() { + let actual = nu!( + cwd: ".", pipeline( + r#" + random decimal 42..43 + "# + )); + + assert!(actual.out.contains("42") || actual.out.contains("43")); +} + +#[test] +fn generates_55() { + let actual = nu!( + cwd: ".", pipeline( + r#" + random decimal 55..55 + "# + )); + + assert!(actual.out.contains("55")); +} + +#[test] +fn generates_0() { + let actual = nu!( + cwd: ".", pipeline( + r#" + random decimal ..<1 + "# + )); + + assert!(actual.out.contains('0')); +} diff --git a/crates/nu-command/tests/commands/random/dice.rs b/old_nushell/crates/nu-command/tests/commands/random/dice.rs similarity index 100% rename from crates/nu-command/tests/commands/random/dice.rs rename to old_nushell/crates/nu-command/tests/commands/random/dice.rs diff --git a/old_nushell/crates/nu-command/tests/commands/random/integer.rs b/old_nushell/crates/nu-command/tests/commands/random/integer.rs new file mode 100644 index 0000000000..80cc8f5a23 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/random/integer.rs @@ -0,0 +1,37 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn generates_an_integer() { + let actual = nu!( + cwd: ".", pipeline( + r#" + random integer 42..43 + "# + )); + + assert!(actual.out.contains("42") || actual.out.contains("43")); +} + +#[test] +fn generates_55() { + let actual = nu!( + cwd: ".", pipeline( + r#" + random integer 55..55 + "# + )); + + assert!(actual.out.contains("55")); +} + +#[test] +fn generates_0() { + let actual = nu!( + cwd: ".", pipeline( + r#" + random integer ..<1 + "# + )); + + assert!(actual.out.contains('0')); +} diff --git a/crates/nu-command/tests/commands/random/mod.rs b/old_nushell/crates/nu-command/tests/commands/random/mod.rs similarity index 100% rename from crates/nu-command/tests/commands/random/mod.rs rename to old_nushell/crates/nu-command/tests/commands/random/mod.rs diff --git a/crates/nu-command/tests/commands/random/uuid.rs b/old_nushell/crates/nu-command/tests/commands/random/uuid.rs similarity index 100% rename from crates/nu-command/tests/commands/random/uuid.rs rename to old_nushell/crates/nu-command/tests/commands/random/uuid.rs diff --git a/crates/nu-command/tests/commands/range.rs b/old_nushell/crates/nu-command/tests/commands/range.rs similarity index 100% rename from crates/nu-command/tests/commands/range.rs rename to old_nushell/crates/nu-command/tests/commands/range.rs diff --git a/old_nushell/crates/nu-command/tests/commands/reduce.rs b/old_nushell/crates/nu-command/tests/commands/reduce.rs new file mode 100644 index 0000000000..dee4cd2244 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/reduce.rs @@ -0,0 +1,125 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn reduce_table_column() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo "[{month:2,total:30}, {month:3,total:10}, {month:4,total:3}, {month:5,total:60}]" + | from json + | get total + | reduce -f 20 { $it + (math eval $"($acc)^1.05")} + | into string -d 1 + "# + ) + ); + + assert_eq!(actual.out, "180.6"); +} + +#[test] +fn reduce_table_column_with_path() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo "[{month:2,total:30}, {month:3,total:10}, {month:4,total:3}, {month:5,total:60}]" + | from json + | reduce -f 20 { $it.total + (math eval $"($acc)^1.05")} + | into string -d 1 + "# + ) + ); + + assert_eq!(actual.out, "180.6"); +} + +#[test] +fn reduce_rows_example() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo a,b 1,2 3,4 + | split column , + | headers + | reduce -f 1.6 { $acc * ($it.a | str to-int) + ($it.b | str to-int) } + "# + ) + ); + + assert_eq!(actual.out, "14.8"); +} + +#[test] +fn reduce_numbered_example() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo one longest three bar + | reduce -n { if ($it.item | str length) > ($acc.item | str length) {echo $it} {echo $acc}} + | get index + "# + ) + ); + + assert_eq!(actual.out, "1"); +} + +#[test] +fn reduce_numbered_integer_addition_example() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo [1 2 3 4] + | reduce -n { $acc.item + $it.item } + | get item + "# + ) + ); + + assert_eq!(actual.out, "10"); +} + +#[test] +fn folding_with_tables() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo [10 20 30 40] + | reduce -f [] { + with-env [value $it] { + echo $acc | append (10 * ($nu.env.value | str to-int)) + } + } + | math sum + "# + ) + ); + + assert_eq!(actual.out, "1000"); +} + +#[test] +fn error_reduce_fold_type_mismatch() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo a b c | reduce -f 0 { $acc + $it } + "# + ) + ); + + assert!(actual.err.contains("Coercion")); +} + +#[test] +fn error_reduce_empty() { + let actual = nu!( + cwd: ".", pipeline( + r#" + reduce { $acc + $it } + "# + ) + ); + + assert!(actual.err.contains("needs input")); +} diff --git a/old_nushell/crates/nu-command/tests/commands/rename.rs b/old_nushell/crates/nu-command/tests/commands/rename.rs new file mode 100644 index 0000000000..f4ea04b66a --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/rename.rs @@ -0,0 +1,89 @@ +use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn changes_the_column_name() { + Playground::setup("rename_test_1", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "los_cuatro_mosqueteros.txt", + r#" + Andrés N. Robalino + Jonathan Turner + Yehuda Katz + Jason Gedge + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open los_cuatro_mosqueteros.txt + | lines + | wrap name + | rename mosqueteros + | get mosqueteros + | length + "# + )); + + assert_eq!(actual.out, "4"); + }) +} + +#[test] +fn keeps_remaining_original_names_given_less_new_names_than_total_original_names() { + Playground::setup("rename_test_2", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "los_cuatro_mosqueteros.txt", + r#" + Andrés N. Robalino + Jonathan Turner + Yehuda Katz + Jason Gedge + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open los_cuatro_mosqueteros.txt + | lines + | wrap name + | default hit "arepa!" + | rename mosqueteros + | get hit + | length + "# + )); + + assert_eq!(actual.out, "4"); + }) +} + +#[test] +fn errors_if_no_columns_present() { + Playground::setup("rename_test_3", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "los_cuatro_mosqueteros.txt", + r#" + Andrés N. Robalino + Jonathan Turner + Yehuda Katz + Jason Gedge + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open los_cuatro_mosqueteros.txt + | lines + | rename mosqueteros + "# + )); + + assert!(actual.err.contains("no column names available")); + assert!(actual.err.contains("can't rename")); + }) +} diff --git a/crates/nu-command/tests/commands/reverse.rs b/old_nushell/crates/nu-command/tests/commands/reverse.rs similarity index 100% rename from crates/nu-command/tests/commands/reverse.rs rename to old_nushell/crates/nu-command/tests/commands/reverse.rs diff --git a/old_nushell/crates/nu-command/tests/commands/rm.rs b/old_nushell/crates/nu-command/tests/commands/rm.rs new file mode 100644 index 0000000000..ae7e35828d --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/rm.rs @@ -0,0 +1,326 @@ +use nu_test_support::fs::{files_exist_at, Stub::EmptyFile}; +use nu_test_support::nu; +use nu_test_support::playground::Playground; + +#[test] +fn removes_a_file() { + Playground::setup("rm_test_1", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("i_will_be_deleted.txt")]); + + nu!( + cwd: dirs.root(), + "rm rm_test_1/i_will_be_deleted.txt" + ); + + let path = dirs.test().join("i_will_be_deleted.txt"); + + assert!(!path.exists()); + }) +} + +#[test] +fn removes_files_with_wildcard() { + Playground::setup("rm_test_2", |dirs, sandbox| { + sandbox + .within("src") + .with_files(vec![ + EmptyFile("cli.rs"), + EmptyFile("lib.rs"), + EmptyFile("prelude.rs"), + ]) + .within("src/parser") + .with_files(vec![EmptyFile("parse.rs"), EmptyFile("parser.rs")]) + .within("src/parser/parse") + .with_files(vec![EmptyFile("token_tree.rs")]) + .within("src/parser/hir") + .with_files(vec![ + EmptyFile("baseline_parse.rs"), + EmptyFile("baseline_parse_tokens.rs"), + ]); + + nu!( + cwd: dirs.test(), + r#"rm "src/*/*/*.rs""# + ); + + assert!(!files_exist_at( + vec![ + "src/parser/parse/token_tree.rs", + "src/parser/hir/baseline_parse.rs", + "src/parser/hir/baseline_parse_tokens.rs" + ], + dirs.test() + )); + + assert_eq!( + Playground::glob_vec(&format!("{}/src/*/*/*.rs", dirs.test().display())), + Vec::::new() + ); + }) +} + +#[test] +fn removes_deeply_nested_directories_with_wildcard_and_recursive_flag() { + Playground::setup("rm_test_3", |dirs, sandbox| { + sandbox + .within("src") + .with_files(vec![ + EmptyFile("cli.rs"), + EmptyFile("lib.rs"), + EmptyFile("prelude.rs"), + ]) + .within("src/parser") + .with_files(vec![EmptyFile("parse.rs"), EmptyFile("parser.rs")]) + .within("src/parser/parse") + .with_files(vec![EmptyFile("token_tree.rs")]) + .within("src/parser/hir") + .with_files(vec![ + EmptyFile("baseline_parse.rs"), + EmptyFile("baseline_parse_tokens.rs"), + ]); + + nu!( + cwd: dirs.test(), + "rm -r src/*" + ); + + assert!(!files_exist_at( + vec!["src/parser/parse", "src/parser/hir"], + dirs.test() + )); + }) +} + +#[test] +fn removes_directory_contents_without_recursive_flag_if_empty() { + Playground::setup("rm_test_4", |dirs, _| { + nu!( + cwd: dirs.root(), + "rm rm_test_4" + ); + + assert!(!dirs.test().exists()); + }) +} + +#[test] +fn removes_directory_contents_with_recursive_flag() { + Playground::setup("rm_test_5", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("yehuda.txt"), + EmptyFile("jonathan.txt"), + EmptyFile("andres.txt"), + ]); + + nu!( + cwd: dirs.root(), + "rm rm_test_5 --recursive" + ); + + assert!(!dirs.test().exists()); + }) +} + +#[test] +fn errors_if_attempting_to_delete_a_directory_with_content_without_recursive_flag() { + Playground::setup("rm_test_6", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("some_empty_file.txt")]); + let actual = nu!( + cwd: dirs.root(), + "rm rm_test_6" + ); + + assert!(dirs.test().exists()); + assert!(actual.err.contains("cannot remove non-empty directory")); + }) +} + +#[test] +fn errors_if_attempting_to_delete_single_dot_as_argument() { + Playground::setup("rm_test_7", |dirs, _| { + let actual = nu!( + cwd: dirs.root(), + "rm ." + ); + + assert!(actual.err.contains("cannot remove any parent directory")); + }) +} + +#[test] +fn errors_if_attempting_to_delete_two_dot_as_argument() { + Playground::setup("rm_test_8", |dirs, _| { + let actual = nu!( + cwd: dirs.root(), + "rm .." + ); + + assert!(actual.err.contains("cannot remove any parent directory")); + }) +} + +#[test] +fn removes_multiple_directories() { + Playground::setup("rm_test_9", |dirs, sandbox| { + sandbox + .within("src") + .with_files(vec![EmptyFile("a.rs"), EmptyFile("b.rs")]) + .within("src/cli") + .with_files(vec![EmptyFile("c.rs"), EmptyFile("d.rs")]) + .within("test") + .with_files(vec![EmptyFile("a_test.rs"), EmptyFile("b_test.rs")]); + + nu!( + cwd: dirs.test(), + "rm src test --recursive" + ); + + assert_eq!( + Playground::glob_vec(&format!("{}/*", dirs.test().display())), + Vec::::new() + ); + }) +} + +#[test] +fn removes_multiple_files() { + Playground::setup("rm_test_10", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("yehuda.txt"), + EmptyFile("jonathan.txt"), + EmptyFile("andres.txt"), + ]); + + nu!( + cwd: dirs.test(), + "rm yehuda.txt jonathan.txt andres.txt" + ); + + assert_eq!( + Playground::glob_vec(&format!("{}/*", dirs.test().display())), + Vec::::new() + ); + }) +} + +#[test] +fn removes_multiple_files_with_asterisks() { + Playground::setup("rm_test_11", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("yehuda.txt"), + EmptyFile("jonathan.txt"), + EmptyFile("andres.toml"), + ]); + + nu!( + cwd: dirs.test(), + "rm *.txt *.toml" + ); + + assert_eq!( + Playground::glob_vec(&format!("{}/*", dirs.test().display())), + Vec::::new() + ); + }) +} + +#[test] +fn allows_doubly_specified_file() { + Playground::setup("rm_test_12", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("yehuda.txt"), EmptyFile("jonathan.toml")]); + + let actual = nu!( + cwd: dirs.test(), + "rm *.txt yehuda* *.toml" + ); + + assert_eq!( + Playground::glob_vec(&format!("{}/*", dirs.test().display())), + Vec::::new() + ); + assert!(!actual.out.contains("error")) + }) +} + +#[test] +fn remove_files_from_two_parents_up_using_multiple_dots_and_glob() { + Playground::setup("rm_test_13", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("yehuda.txt"), + EmptyFile("jonathan.txt"), + EmptyFile("kevin.txt"), + ]); + + sandbox.within("foo").mkdir("bar"); + + nu!( + cwd: dirs.test().join("foo/bar"), + "rm .../*.txt" + ); + + assert!(!files_exist_at( + vec!["yehuda.txt", "jonathan.txt", "kevin.txt"], + dirs.test() + )); + }) +} + +#[test] +fn no_errors_if_attempting_to_delete_non_existent_file_with_f_flag() { + Playground::setup("rm_test_14", |dirs, _| { + let actual = nu!( + cwd: dirs.root(), + "rm -f non_existent_file.txt" + ); + + assert!(!actual.err.contains("no valid path")); + }) +} + +#[test] +fn rm_wildcard_keeps_dotfiles() { + Playground::setup("rm_test_15", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("foo"), EmptyFile(".bar")]); + + nu!( + cwd: dirs.test(), + r#"rm *"# + ); + + assert!(!files_exist_at(vec!["foo"], dirs.test())); + assert!(files_exist_at(vec![".bar"], dirs.test())); + }) +} + +#[test] +fn rm_wildcard_leading_dot_deletes_dotfiles() { + Playground::setup("rm_test_16", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("foo"), EmptyFile(".bar")]); + + nu!( + cwd: dirs.test(), + r#"rm .*"# + ); + + assert!(files_exist_at(vec!["foo"], dirs.test())); + assert!(!files_exist_at(vec![".bar"], dirs.test())); + }) +} + +#[test] +fn removes_files_with_case_sensitive_glob_matches_by_default() { + Playground::setup("glob_test", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("A0"), EmptyFile("a1")]); + + nu!( + cwd: dirs.root(), + "rm glob_test/A*" + ); + + let deleted_path = dirs.test().join("A0"); + let skipped_path = dirs.test().join("a1"); + + assert!(!deleted_path.exists()); + assert!(skipped_path.exists()); + }) +} diff --git a/old_nushell/crates/nu-command/tests/commands/roll.rs b/old_nushell/crates/nu-command/tests/commands/roll.rs new file mode 100644 index 0000000000..d99a0c5ddc --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/roll.rs @@ -0,0 +1,166 @@ +use nu_test_support::{nu, pipeline}; + +mod rows { + use super::*; + + fn table() -> String { + pipeline( + r#" + echo [ + [service, status]; + + [ruby, DOWN] + [db, DOWN] + [nud, DOWN] + [expected, HERE] + ]"#, + ) + } + + #[test] + fn roll_down_by_default() { + let actual = nu!( + cwd: ".", + format!("{} | {}", table(), pipeline(r#" + roll + | first + | get status + "#))); + + assert_eq!(actual.out, "HERE"); + } + + #[test] + fn can_roll_up() { + let actual = nu!( + cwd: ".", + format!("{} | {}", table(), pipeline(r#" + roll up 3 + | first + | get status + "#))); + + assert_eq!(actual.out, "HERE"); + } +} + +mod columns { + use super::*; + + fn table() -> String { + pipeline( + r#" + echo [ + [commit_author, origin, stars]; + + [ "Andres", EC, amarillito] + [ "Darren", US, black] + [ "Jonathan", US, black] + [ "Yehuda", US, black] + [ "Jason", CA, gold] + ]"#, + ) + } + + #[test] + fn roll_left_by_default() { + let actual = nu!( + cwd: ".", + format!("{} | {}", table(), pipeline(r#" + roll column + | get + | str collect "-" + "#))); + + assert_eq!(actual.out, "origin-stars-commit_author"); + } + + #[test] + fn can_roll_in_the_opposite_direction() { + let actual = nu!( + cwd: ".", + format!("{} | {}", table(), pipeline(r#" + roll column 2 --opposite + | get + | str collect "-" + "#))); + + assert_eq!(actual.out, "origin-stars-commit_author"); + } + + struct ThirtieTwo<'a>(usize, &'a str); + + #[test] + fn can_roll_the_cells_only_keeping_the_header_names() { + let four_bitstring = bitstring_to_nu_row_pipeline("00000100"); + let expected_value = ThirtieTwo(32, "bit1-bit2-bit3-bit4-bit5-bit6-bit7-bit8"); + + let actual = nu!( + cwd: ".", + format!("{} | roll column 3 --opposite --cells-only | get | str collect '-' ", four_bitstring) + ); + + assert_eq!(actual.out, expected_value.1); + } + + #[test] + fn four_in_bitstring_left_shifted_with_three_bits_should_be_32_in_decimal() { + let four_bitstring = "00000100"; + let expected_value = ThirtieTwo(32, "00100000"); + + assert_eq!( + shift_three_bits_to_the_left_to_bitstring(four_bitstring), + expected_value.0.to_string() + ); + } + + fn shift_three_bits_to_the_left_to_bitstring(bits: &str) -> String { + // this pipeline takes the bitstring and outputs a nu row literal + // for example the number 4 in bitstring: + // + // input: 00000100 + // + // output: + // [ + // [Column1, Column2, Column3, Column4, Column5, Column6, Column7, Column8]; + // [ 0, 0, 0, 0, 0, 1, 0, 0] + // ] + // + let bitstring_as_nu_row_pipeline = bitstring_to_nu_row_pipeline(bits); + + // this pipeline takes the nu bitstring row literal, computes it's + // decimal value. + let nu_row_literal_bitstring_to_decimal_value_pipeline = pipeline( + r#" + pivot bit --ignore-titles + | get bit + | reverse + | each --numbered { + $it.item * (2 ** $it.index) + } + | math sum + "#, + ); + + nu!( + cwd: ".", + format!("{} | roll column 3 | {}", bitstring_as_nu_row_pipeline, nu_row_literal_bitstring_to_decimal_value_pipeline) + ).out + } + + fn bitstring_to_nu_row_pipeline(bits: &str) -> String { + format!( + "echo '{}' | {}", + bits, + pipeline( + r#" + split chars + | each { str to-int } + | rotate counter-clockwise _ + | reject _ + | rename bit1 bit2 bit3 bit4 bit5 bit6 bit7 bit8 + "# + ) + ) + } +} diff --git a/old_nushell/crates/nu-command/tests/commands/rotate.rs b/old_nushell/crates/nu-command/tests/commands/rotate.rs new file mode 100644 index 0000000000..0432d89d59 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/rotate.rs @@ -0,0 +1,83 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn counter_clockwise() { + let table = pipeline( + r#" + echo [ + [col1, col2, EXPECTED]; + + [---, "|||", XX1] + [---, "|||", XX2] + [---, "|||", XX3] + ] + "#, + ); + + let expected = nu!(cwd: ".", pipeline( + r#" + echo [ + [ Column0, Column1, Column2, Column3]; + + [ EXPECTED, XX1, XX2, XX3] + [ col2, "|||", "|||", "|||"] + [ col1, ---, ---, ---] + ] + | where Column0 == EXPECTED + | get Column1 Column2 Column3 + | str collect "-" + "#, + )); + + let actual = nu!( + cwd: ".", + format!("{} | {}", table, pipeline(r#" + rotate counter-clockwise + | where Column0 == EXPECTED + | get Column1 Column2 Column3 + | str collect "-" + "#))); + + assert_eq!(actual.out, expected.out); +} + +#[test] +fn clockwise() { + let table = pipeline( + r#" + echo [ + [col1, col2, EXPECTED]; + + [ ---, "|||", XX1] + [ ---, "|||", XX2] + [ ---, "|||", XX3] + ] + "#, + ); + + let expected = nu!(cwd: ".", pipeline( + r#" + echo [ + [ Column0, Column1, Column2, Column3]; + + [ ---, ---, ---, col1] + [ "|||", "|||", "|||", col2] + [ XX3, XX2, XX1, EXPECTED] + ] + | where Column3 == EXPECTED + | get Column0 Column1 Column2 + | str collect "-" + "#, + )); + + let actual = nu!( + cwd: ".", + format!("{} | {}", table, pipeline(r#" + rotate + | where Column3 == EXPECTED + | get Column0 Column1 Column2 + | str collect "-" + "#))); + + assert_eq!(actual.out, expected.out); +} diff --git a/old_nushell/crates/nu-command/tests/commands/save.rs b/old_nushell/crates/nu-command/tests/commands/save.rs new file mode 100644 index 0000000000..ff953bcfb6 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/save.rs @@ -0,0 +1,67 @@ +use nu_test_support::fs::{file_contents, Stub::FileWithContent}; +use nu_test_support::nu; +use nu_test_support::playground::Playground; + +#[test] +fn figures_out_intelligently_where_to_write_out_with_metadata() { + Playground::setup("save_test_1", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "cargo_sample.toml", + r#" + [package] + name = "nu" + version = "0.1.1" + authors = ["Yehuda Katz "] + description = "A shell for the GitHub era" + license = "ISC" + edition = "2018" + "#, + )]); + + let subject_file = dirs.test().join("cargo_sample.toml"); + + nu!( + cwd: dirs.root(), + "open save_test_1/cargo_sample.toml | save" + ); + + let actual = file_contents(&subject_file); + assert!(actual.contains("0.1.1")); + }) +} + +#[test] +fn writes_out_csv() { + Playground::setup("save_test_2", |dirs, sandbox| { + sandbox.with_files(vec![]); + + let expected_file = dirs.test().join("cargo_sample.csv"); + + nu!( + cwd: dirs.root(), + r#"echo [[name, version, description, license, edition]; [nu, "0.14", "A new type of shell", "MIT", "2018"]] | save save_test_2/cargo_sample.csv"#, + ); + + let actual = file_contents(expected_file); + println!("{}", actual); + assert!(actual.contains("nu,0.14,A new type of shell,MIT,2018")); + }) +} + +#[test] +fn save_append_will_create_file_if_not_exists() { + Playground::setup("save_test_3", |dirs, sandbox| { + sandbox.with_files(vec![]); + + let expected_file = dirs.test().join("new-file.txt"); + + nu!( + cwd: dirs.root(), + r#"echo hello | save --raw --append save_test_3/new-file.txt"#, + ); + + let actual = file_contents(expected_file); + println!("{}", actual); + assert!(actual == "hello"); + }) +} diff --git a/old_nushell/crates/nu-command/tests/commands/select.rs b/old_nushell/crates/nu-command/tests/commands/select.rs new file mode 100644 index 0000000000..cb79f8142f --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/select.rs @@ -0,0 +1,122 @@ +use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn regular_columns() { + let actual = nu!(cwd: ".", pipeline( + r#" + echo [ + [first_name, last_name, rusty_at, type]; + + [Andrés Robalino 10/11/2013 A] + [Jonathan Turner 10/12/2013 B] + [Yehuda Katz 10/11/2013 A] + ] + | select rusty_at last_name + | nth 0 + | get last_name + "# + )); + + assert_eq!(actual.out, "Robalino"); +} + +#[test] +fn complex_nested_columns() { + Playground::setup("select_test_2", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "los_tres_caballeros.json", + r#" + { + "nu": { + "committers": [ + {"name": "Andrés N. Robalino"}, + {"name": "Jonathan Turner"}, + {"name": "Yehuda Katz"} + ], + "releases": [ + {"version": "0.2"} + {"version": "0.8"}, + {"version": "0.9999999"} + ], + "0xATYKARNU": [ + ["Th", "e", " "], + ["BIG", " ", "UnO"], + ["punto", "cero"] + ] + } + } + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open los_tres_caballeros.json + | select nu."0xATYKARNU" nu.committers.name nu.releases.version + | where nu_releases_version > "0.8" + | get nu_releases_version + "# + )); + + assert_eq!(actual.out, "0.9999999"); + }) +} + +#[test] +fn allows_if_given_unknown_column_name_is_missing() { + let actual = nu!(cwd: ".", pipeline( + r#" + echo [ + [first_name, last_name, rusty_at, type]; + + [Andrés Robalino 10/11/2013 A] + [Jonathan Turner 10/12/2013 B] + [Yehuda Katz 10/11/2013 A] + ] + | select rrusty_at first_name + | length + "# + )); + + assert_eq!(actual.out, "3"); +} + +#[test] +fn column_names_with_spaces() { + let actual = nu!(cwd: ".", pipeline( + r#" + echo [ + ["first name", "last name"]; + + [Andrés Robalino] + [Andrés Jnth] + ] + | select "last name" + | get "last name" + | str collect " " + "# + )); + + assert_eq!(actual.out, "Robalino Jnth"); +} + +#[test] +fn ignores_duplicate_columns_selected() { + let actual = nu!(cwd: ".", pipeline( + r#" + echo [ + ["first name", "last name"]; + + [Andrés Robalino] + [Andrés Jnth] + ] + | select "first name" "last name" "first name" + | get + | str collect " " + "# + )); + + assert_eq!(actual.out, "first name last name"); +} diff --git a/crates/nu-command/tests/commands/semicolon.rs b/old_nushell/crates/nu-command/tests/commands/semicolon.rs similarity index 100% rename from crates/nu-command/tests/commands/semicolon.rs rename to old_nushell/crates/nu-command/tests/commands/semicolon.rs diff --git a/crates/nu-command/tests/commands/skip/mod.rs b/old_nushell/crates/nu-command/tests/commands/skip/mod.rs similarity index 100% rename from crates/nu-command/tests/commands/skip/mod.rs rename to old_nushell/crates/nu-command/tests/commands/skip/mod.rs diff --git a/old_nushell/crates/nu-command/tests/commands/skip/until.rs b/old_nushell/crates/nu-command/tests/commands/skip/until.rs new file mode 100644 index 0000000000..cdc89e5f73 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/skip/until.rs @@ -0,0 +1,50 @@ +use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn condition_is_met() { + Playground::setup("skip_until_test_1", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "caballeros.txt", + r#" + CHICKEN SUMMARY report date: April 29th, 2020 + -------------------------------------------------------------------- + Chicken Collection,29/04/2020,30/04/2020,31/04/2020, + Yellow Chickens,,, + Andrés,0,0,1 + Jonathan,0,0,1 + Jason,0,0,1 + Yehuda,0,0,1 + Blue Chickens,,, + Andrés,0,0,1 + Jonathan,0,0,1 + Jason,0,0,1 + Yehuda,0,0,2 + Red Chickens,,, + Andrés,0,0,1 + Jonathan,0,0,1 + Jason,0,0,1 + Yehuda,0,0,3 + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open --raw caballeros.txt + | lines + | skip 2 + | split column ',' + | headers + | skip until "Chicken Collection" == "Red Chickens" + | skip 1 + | str to-int "31/04/2020" + | get "31/04/2020" + | math sum + "# + )); + + assert_eq!(actual.out, "6"); + }) +} diff --git a/old_nushell/crates/nu-command/tests/commands/skip/while_.rs b/old_nushell/crates/nu-command/tests/commands/skip/while_.rs new file mode 100644 index 0000000000..c0a55a7d6b --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/skip/while_.rs @@ -0,0 +1,50 @@ +use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn condition_is_met() { + Playground::setup("skip_while_test_1", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "caballeros.txt", + r#" + CHICKEN SUMMARY report date: April 29th, 2020 + -------------------------------------------------------------------- + Chicken Collection,29/04/2020,30/04/2020,31/04/2020, + Yellow Chickens,,, + Andrés,0,0,1 + Jonathan,0,0,1 + Jason,0,0,1 + Yehuda,0,0,1 + Blue Chickens,,, + Andrés,0,0,1 + Jonathan,0,0,1 + Jason,0,0,1 + Yehuda,0,0,2 + Red Chickens,,, + Andrés,0,0,1 + Jonathan,0,0,1 + Jason,0,0,1 + Yehuda,0,0,3 + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open --raw caballeros.txt + | lines + | skip 2 + | split column ',' + | headers + | skip while "Chicken Collection" != "Red Chickens" + | skip 1 + | str to-int "31/04/2020" + | get "31/04/2020" + | math sum + "# + )); + + assert_eq!(actual.out, "6"); + }) +} diff --git a/old_nushell/crates/nu-command/tests/commands/sort_by.rs b/old_nushell/crates/nu-command/tests/commands/sort_by.rs new file mode 100644 index 0000000000..4eb3c98fee --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/sort_by.rs @@ -0,0 +1,146 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn by_column() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open cargo_sample.toml --raw + | lines + | skip 1 + | first 4 + | split column "=" + | sort-by Column1 + | skip 1 + | first 1 + | get Column1 + | str trim + "# + )); + + assert_eq!(actual.out, "description"); +} + +#[test] +fn by_invalid_column() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open cargo_sample.toml --raw + | lines + | skip 1 + | first 4 + | split column "=" + | sort-by ColumnThatDoesNotExist + | skip 1 + | first 1 + | get Column1 + | str trim + "# + )); + + assert!(actual.err.contains("Can not find column to sort by")); + assert!(actual.err.contains("invalid column")); +} + +#[test] +fn by_invalid_types() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open cargo_sample.toml --raw + | echo [1 "foo"] + | sort-by + "# + )); + + assert!(actual.err.contains("Not all values can be compared")); + assert!(actual + .err + .contains("Unable to sort values, as \"integer\" cannot compare against \"string\"")); +} + +#[test] +fn sort_primitive_values() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open cargo_sample.toml --raw + | lines + | skip 1 + | first 6 + | sort-by + | first 1 + "# + )); + + assert_eq!(actual.out, "authors = [\"The Nu Project Contributors\"]"); +} + +#[test] +fn ls_sort_by_name_sensitive() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sample-ls-output.json + | sort-by name + | select name + | to json + "# + )); + + let json_output = r#"[{"name":"B.txt"},{"name":"C"},{"name":"a.txt"}]"#; + + assert_eq!(actual.out, json_output); +} + +#[test] +fn ls_sort_by_name_insensitive() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sample-ls-output.json + | sort-by -i name + | select name + | to json + "# + )); + + let json_output = r#"[{"name":"a.txt"},{"name":"B.txt"},{"name":"C"}]"#; + + assert_eq!(actual.out, json_output); +} + +#[test] +fn ls_sort_by_type_name_sensitive() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sample-ls-output.json + | sort-by type name + | select name type + | to json + "# + )); + + let json_output = r#"[{"name":"C","type":"Dir"},{"name":"B.txt","type":"File"},{"name":"a.txt","type":"File"}]"#; + + assert_eq!(actual.out, json_output); +} + +#[test] +fn ls_sort_by_type_name_insensitive() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sample-ls-output.json + | sort-by -i type name + | select name type + | to json + "# + )); + + let json_output = r#"[{"name":"C","type":"Dir"},{"name":"a.txt","type":"File"},{"name":"B.txt","type":"File"}]"#; + + assert_eq!(actual.out, json_output); +} diff --git a/old_nushell/crates/nu-command/tests/commands/source.rs b/old_nushell/crates/nu-command/tests/commands/source.rs new file mode 100644 index 0000000000..473c3e6a3b --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/source.rs @@ -0,0 +1,146 @@ +use nu_test_support::fs::{AbsolutePath, DisplayPath, Stub::FileWithContent}; +use nu_test_support::nu; +use nu_test_support::pipeline as input; +use nu_test_support::playground::{says, Playground}; + +use hamcrest2::assert_that; +use hamcrest2::prelude::*; + +#[should_panic] +#[test] +fn sources_also_files_under_custom_lib_dirs_path() { + Playground::setup("source_test_1", |dirs, nu| { + let file = AbsolutePath::new(dirs.test().join("config.toml")); + let library_path = AbsolutePath::new(dirs.test().join("lib")); + + nu.with_config(&file); + nu.with_files(vec![FileWithContent( + "config.toml", + &format!( + r#" + lib_dirs = ["{}"] + skip_welcome_message = true + "#, + library_path.display_path() + ), + )]); + + nu.within("lib").with_files(vec![FileWithContent( + "my_library.nu", + r#" + source my_library/main.nu + "#, + )]); + nu.within("lib/my_library").with_files(vec![FileWithContent( + "main.nu", + r#" + def hello [] { + echo "hello nu" + } + "#, + )]); + + assert_that!( + nu.pipeline(&input( + r#" + source my_library.nu ; + + hello + "#, + )), + says().stdout("hello nu") + ); + }) +} + +fn try_source_foo_with_double_quotes_in(testdir: &str, playdir: &str) { + Playground::setup(playdir, |dirs, sandbox| { + let testdir = String::from(testdir); + let mut foo_file = testdir.clone(); + foo_file.push_str("/foo.nu"); + + sandbox.mkdir(&testdir); + sandbox.with_files(vec![FileWithContent(&foo_file, "echo foo")]); + + let cmd = String::from("source ") + r#"""# + &foo_file + r#"""#; + + let actual = nu!(cwd: dirs.test(), &cmd); + + assert_eq!(actual.out, "foo"); + }); +} + +fn try_source_foo_with_single_quotes_in(testdir: &str, playdir: &str) { + Playground::setup(playdir, |dirs, sandbox| { + let testdir = String::from(testdir); + let mut foo_file = testdir.clone(); + foo_file.push_str("/foo.nu"); + + sandbox.mkdir(&testdir); + sandbox.with_files(vec![FileWithContent(&foo_file, "echo foo")]); + + let cmd = String::from("source ") + r#"'"# + &foo_file + r#"'"#; + + let actual = nu!(cwd: dirs.test(), &cmd); + + assert_eq!(actual.out, "foo"); + }); +} + +fn try_source_foo_without_quotes_in(testdir: &str, playdir: &str) { + Playground::setup(playdir, |dirs, sandbox| { + let testdir = String::from(testdir); + let mut foo_file = testdir.clone(); + foo_file.push_str("/foo.nu"); + + sandbox.mkdir(&testdir); + sandbox.with_files(vec![FileWithContent(&foo_file, "echo foo")]); + + let cmd = String::from("source ") + &foo_file; + + let actual = nu!(cwd: dirs.test(), &cmd); + + assert_eq!(actual.out, "foo"); + }); +} + +#[test] +fn sources_unicode_file_in_normal_dir() { + try_source_foo_with_single_quotes_in("foo", "source_test_1"); + try_source_foo_with_double_quotes_in("foo", "source_test_2"); + try_source_foo_without_quotes_in("foo", "source_test_3"); +} + +#[test] +fn sources_unicode_file_in_unicode_dir_without_spaces_1() { + try_source_foo_with_single_quotes_in("🚒", "source_test_4"); + try_source_foo_with_double_quotes_in("🚒", "source_test_5"); + try_source_foo_without_quotes_in("🚒", "source_test_6"); +} + +#[cfg(not(windows))] // ':' is not allowed in Windows paths +#[test] +fn sources_unicode_file_in_unicode_dir_without_spaces_2() { + try_source_foo_with_single_quotes_in(":fire_engine:", "source_test_7"); + try_source_foo_with_double_quotes_in(":fire_engine:", "source_test_8"); + try_source_foo_without_quotes_in(":fire_engine:", "source_test_9"); +} + +#[test] +fn sources_unicode_file_in_unicode_dir_with_spaces_1() { + try_source_foo_with_single_quotes_in("e-$ èрт🚒♞中片-j", "source_test_8"); + try_source_foo_with_double_quotes_in("e-$ èрт🚒♞中片-j", "source_test_9"); +} + +#[cfg(not(windows))] // ':' is not allowed in Windows paths +#[test] +fn sources_unicode_file_in_unicode_dir_with_spaces_2() { + try_source_foo_with_single_quotes_in("e-$ èрт:fire_engine:♞中片-j", "source_test_10"); + try_source_foo_with_double_quotes_in("e-$ èрт:fire_engine:♞中片-j", "source_test_11"); +} + +#[ignore] +#[test] +fn sources_unicode_file_in_non_utf8_dir() { + // How do I create non-UTF-8 path??? +} diff --git a/old_nushell/crates/nu-command/tests/commands/split_by.rs b/old_nushell/crates/nu-command/tests/commands/split_by.rs new file mode 100644 index 0000000000..bea39199e7 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/split_by.rs @@ -0,0 +1,54 @@ +use nu_test_support::fs::Stub::{EmptyFile, FileWithContentToBeTrimmed}; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn splits() { + Playground::setup("split_by_test_1", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "los_tres_caballeros.csv", + r#" + first_name,last_name,rusty_at,type + Andrés,Robalino,10/11/2013,A + Jonathan,Turner,10/12/2013,B + Yehuda,Katz,10/11/2013,A + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open los_tres_caballeros.csv + | group-by rusty_at + | split-by type + | get A."10/11/2013" + | length + "# + )); + + assert_eq!(actual.out, "2"); + }) +} + +#[test] +fn errors_if_no_table_given_as_input() { + Playground::setup("split_by_test_2", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("los.txt"), + EmptyFile("tres.txt"), + EmptyFile("amigos.txt"), + EmptyFile("arepas.clu"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | get name + | split-by type + "# + )); + + assert!(actual.err.contains("Expected table from pipeline")); + }) +} diff --git a/crates/nu-command/tests/commands/split_column.rs b/old_nushell/crates/nu-command/tests/commands/split_column.rs similarity index 100% rename from crates/nu-command/tests/commands/split_column.rs rename to old_nushell/crates/nu-command/tests/commands/split_column.rs diff --git a/crates/nu-command/tests/commands/split_row.rs b/old_nushell/crates/nu-command/tests/commands/split_row.rs similarity index 100% rename from crates/nu-command/tests/commands/split_row.rs rename to old_nushell/crates/nu-command/tests/commands/split_row.rs diff --git a/old_nushell/crates/nu-command/tests/commands/str_/collect.rs b/old_nushell/crates/nu-command/tests/commands/str_/collect.rs new file mode 100644 index 0000000000..4ac56cc140 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/str_/collect.rs @@ -0,0 +1,53 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn test_1() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo 1..5 | into string | str collect + "# + ) + ); + + assert_eq!(actual.out, "12345"); +} + +#[test] +fn test_2() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo [a b c d] | str collect "" + "# + ) + ); + + assert_eq!(actual.out, "abcd"); +} + +#[test] +fn construct_a_path() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo [sample txt] | str collect "." + "# + ) + ); + + assert_eq!(actual.out, "sample.txt"); +} + +#[test] +fn sum_one_to_four() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo 1..4 | into string | str collect "+" | math eval + "# + ) + ); + + assert!(actual.out.contains("10.0")); +} diff --git a/crates/nu-command/tests/commands/str_/into_string.rs b/old_nushell/crates/nu-command/tests/commands/str_/into_string.rs similarity index 100% rename from crates/nu-command/tests/commands/str_/into_string.rs rename to old_nushell/crates/nu-command/tests/commands/str_/into_string.rs diff --git a/old_nushell/crates/nu-command/tests/commands/str_/mod.rs b/old_nushell/crates/nu-command/tests/commands/str_/mod.rs new file mode 100644 index 0000000000..6e159f943e --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/str_/mod.rs @@ -0,0 +1,356 @@ +mod collect; + +use nu_test_support::fs::Stub::FileWithContent; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn trims() { + Playground::setup("str_test_1", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [dependency] + name = "nu " + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), + "open sample.toml | str trim dependency.name | get dependency.name" + ); + + assert_eq!(actual.out, "nu"); + }) +} + +#[test] +fn error_trim_multiple_chars() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo 'does it work now?!' | str trim -c '?!' + "# + ) + ); + + assert!(actual.err.contains("char")); +} + +#[test] +fn capitalizes() { + Playground::setup("str_test_2", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [dependency] + name = "nu" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), + "open sample.toml | str capitalize dependency.name | get dependency.name" + ); + + assert_eq!(actual.out, "Nu"); + }) +} + +#[test] +fn downcases() { + Playground::setup("str_test_3", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [dependency] + name = "LIGHT" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), + "open sample.toml | str downcase dependency.name | get dependency.name" + ); + + assert_eq!(actual.out, "light"); + }) +} + +#[test] +fn upcases() { + Playground::setup("str_test_4", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [package] + name = "nushell" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), + "open sample.toml | str upcase package.name | get package.name" + ); + + assert_eq!(actual.out, "NUSHELL"); + }) +} + +#[test] +fn camelcases() { + Playground::setup("str_test_3", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [dependency] + name = "THIS_IS_A_TEST" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), + "open sample.toml | str camel-case dependency.name | get dependency.name" + ); + + assert_eq!(actual.out, "thisIsATest"); + }) +} + +#[test] +fn converts_to_int() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo '{number_as_string: "1"}' + | from json + | str to-int number_as_string + | rename number + | where number == 1 + | get number + + "# + )); + + assert_eq!(actual.out, "1"); +} + +#[test] +fn converts_to_decimal() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo "3.1, 0.0415" + | split row "," + | str to-decimal + | math sum + "# + )); + + assert_eq!(actual.out, "3.1415"); +} + +#[test] +fn find_and_replaces() { + Playground::setup("str_test_6", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [fortune.teller] + phone = "1-800-KATZ" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.toml + | str find-replace KATZ "5289" fortune.teller.phone + | get fortune.teller.phone + "# + )); + + assert_eq!(actual.out, "1-800-5289"); + }) +} + +#[test] +fn find_and_replaces_without_passing_field() { + Playground::setup("str_test_7", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [fortune.teller] + phone = "1-800-KATZ" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.toml + | get fortune.teller.phone + | str find-replace KATZ "5289" + "# + )); + + assert_eq!(actual.out, "1-800-5289"); + }) +} + +#[test] +fn substrings_the_input() { + Playground::setup("str_test_8", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [fortune.teller] + phone = "1-800-ROBALINO" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.toml + | str substring 6,14 fortune.teller.phone + | get fortune.teller.phone + "# + )); + + assert_eq!(actual.out, "ROBALINO"); + }) +} + +#[test] +fn substring_errors_if_start_index_is_greater_than_end_index() { + Playground::setup("str_test_9", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [fortune.teller] + phone = "1-800-ROBALINO" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.toml + | str substring 6,5 fortune.teller.phone + "# + )); + + assert!(actual + .err + .contains("End must be greater than or equal to Start")) + }) +} + +#[test] +fn substrings_the_input_and_returns_the_string_if_end_index_exceeds_length() { + Playground::setup("str_test_10", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [package] + name = "nu-arepas" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.toml + | str substring 0,999 package.name + | get package.name + "# + )); + + assert_eq!(actual.out, "nu-arepas"); + }) +} + +#[test] +fn substrings_the_input_and_returns_blank_if_start_index_exceeds_length() { + Playground::setup("str_test_11", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [package] + name = "nu-arepas" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.toml + | str substring 50,999 package.name + | get package.name + "# + )); + + assert_eq!(actual.out, ""); + }) +} + +#[test] +fn substrings_the_input_and_treats_start_index_as_zero_if_blank_start_index_given() { + Playground::setup("str_test_12", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [package] + name = "nu-arepas" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.toml + | str substring ,2 package.name + | get package.name + "# + )); + + assert_eq!(actual.out, "nu"); + }) +} + +#[test] +fn substrings_the_input_and_treats_end_index_as_length_if_blank_end_index_given() { + Playground::setup("str_test_13", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [package] + name = "nu-arepas" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.toml + | str substring 3, package.name + | get package.name + "# + )); + + assert_eq!(actual.out, "arepas"); + }) +} + +#[test] +fn str_reverse() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo "nushell" | str reverse + "# + )); + + assert!(actual.out.contains("llehsun")); +} diff --git a/crates/nu-command/tests/commands/touch.rs b/old_nushell/crates/nu-command/tests/commands/touch.rs similarity index 100% rename from crates/nu-command/tests/commands/touch.rs rename to old_nushell/crates/nu-command/tests/commands/touch.rs diff --git a/old_nushell/crates/nu-command/tests/commands/uniq.rs b/old_nushell/crates/nu-command/tests/commands/uniq.rs new file mode 100644 index 0000000000..4f74a5af87 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/uniq.rs @@ -0,0 +1,231 @@ +use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn removes_duplicate_rows() { + Playground::setup("uniq_test_1", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "los_tres_caballeros.csv", + r#" + first_name,last_name,rusty_at,type + Andrés,Robalino,10/11/2013,A + Jonathan,Turner,10/12/2013,B + Yehuda,Katz,10/11/2013,A + Jonathan,Turner,10/12/2013,B + Yehuda,Katz,10/11/2013,A + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open los_tres_caballeros.csv + | uniq + | length + + "# + )); + + assert_eq!(actual.out, "3"); + }) +} + +#[test] +fn uniq_values() { + Playground::setup("uniq_test_2", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "los_tres_caballeros.csv", + r#" + first_name,last_name,rusty_at,type + Andrés,Robalino,10/11/2013,A + Jonathan,Turner,10/12/2013,B + Yehuda,Katz,10/11/2013,A + Jonathan,Turner,10/12/2013,B + Yehuda,Katz,10/11/2013,A + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open los_tres_caballeros.csv + | select type + | uniq + | length + + "# + )); + + assert_eq!(actual.out, "2"); + }) +} + +#[test] +fn nested_json_structures() { + Playground::setup("uniq_test_3", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "nested_json_structures.json", + r#" + [ + { + "name": "this is duplicated", + "nesting": [ { "a": "a", "b": "b" }, + { "c": "c", "d": "d" } + ], + "can_be_ordered_differently": { + "array": [1, 2, 3, 4, 5], + "something": { "else": "works" } + } + }, + { + "can_be_ordered_differently": { + "something": { "else": "works" }, + "array": [1, 2, 3, 4, 5] + }, + "nesting": [ { "b": "b", "a": "a" }, + { "d": "d", "c": "c" } + ], + "name": "this is duplicated" + }, + { + "name": "this is unique", + "nesting": [ { "a": "b", "b": "a" }, + { "c": "d", "d": "c" } + ], + "can_be_ordered_differently": { + "array": [], + "something": { "else": "does not work" } + } + }, + { + "name": "this is unique", + "nesting": [ { "a": "a", "b": "b", "c": "c" }, + { "d": "d", "e": "e", "f": "f" } + ], + "can_be_ordered_differently": { + "array": [], + "something": { "else": "works" } + } + } + ] + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open nested_json_structures.json + | uniq + | length + + "# + )); + assert_eq!(actual.out, "3"); + }) +} + +#[test] +fn uniq_when_keys_out_of_order() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo '[{"a": "a", "b": [1,2,3]},{"b": [1,2,3], "a": "a"}]' + | from json + | uniq + | length + + "# + )); + + assert_eq!(actual.out, "1"); +} + +#[test] +fn uniq_counting() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo '["A", "B", "A"]' + | from json + | wrap item + | uniq --count + | where item == A + | get count + "# + )); + assert_eq!(actual.out, "2"); + + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo '["A", "B", "A"]' + | from json + | wrap item + | uniq --count + | where item == B + | get count + "# + )); + assert_eq!(actual.out, "1"); +} + +#[test] +fn uniq_unique() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo [1 2 3 4 1 5] + | uniq --unique + "# + )); + let expected = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo [2 3 4 5] + "# + )); + print!("{}", actual.out); + print!("{}", expected.out); + assert_eq!(actual.out, expected.out); +} + +#[test] +fn uniq_simple_vals_ints() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo [1 2 3 4 1 5] + | uniq + "# + )); + let expected = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo [1 2 3 4 5] + "# + )); + print!("{}", actual.out); + print!("{}", expected.out); + assert_eq!(actual.out, expected.out); +} + +#[test] +fn uniq_simple_vals_strs() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo [A B C A] + | uniq + "# + )); + let expected = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo [A B C] + "# + )); + print!("{}", actual.out); + print!("{}", expected.out); + assert_eq!(actual.out, expected.out); +} diff --git a/crates/nu-command/tests/commands/update.rs b/old_nushell/crates/nu-command/tests/commands/update.rs similarity index 100% rename from crates/nu-command/tests/commands/update.rs rename to old_nushell/crates/nu-command/tests/commands/update.rs diff --git a/crates/nu-command/tests/commands/where_.rs b/old_nushell/crates/nu-command/tests/commands/where_.rs similarity index 100% rename from crates/nu-command/tests/commands/where_.rs rename to old_nushell/crates/nu-command/tests/commands/where_.rs diff --git a/crates/nu-command/tests/commands/which.rs b/old_nushell/crates/nu-command/tests/commands/which.rs similarity index 100% rename from crates/nu-command/tests/commands/which.rs rename to old_nushell/crates/nu-command/tests/commands/which.rs diff --git a/old_nushell/crates/nu-command/tests/commands/with_env.rs b/old_nushell/crates/nu-command/tests/commands/with_env.rs new file mode 100644 index 0000000000..46f135235e --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/with_env.rs @@ -0,0 +1,100 @@ +use nu_test_support::nu; + +#[test] +fn with_env_extends_environment() { + let actual = nu!( + cwd: "tests/fixtures/formats", + "with-env [FOO BARRRR] {echo $nu.env} | get FOO" + ); + + assert_eq!(actual.out, "BARRRR"); +} + +#[test] +fn with_env_shorthand() { + let actual = nu!( + cwd: "tests/fixtures/formats", + "FOO=BARRRR echo $nu.env | get FOO" + ); + + assert_eq!(actual.out, "BARRRR"); +} + +#[test] +fn shorthand_doesnt_reorder_arguments() { + let actual = nu!( + cwd: "tests/fixtures/formats", + "FOO=BARRRR nu --testbin cococo first second" + ); + + assert_eq!(actual.out, "first second"); +} + +#[test] +fn with_env_shorthand_trims_quotes() { + let actual = nu!( + cwd: "tests/fixtures/formats", + "FOO='BARRRR' echo $nu.env | get FOO" + ); + + assert_eq!(actual.out, "BARRRR"); +} + +#[test] +fn with_env_and_shorthand_same_result() { + let actual_shorthand = nu!( + cwd: "tests/fixtures/formats", + "FOO='BARRRR' echo $nu.env | get FOO" + ); + + let actual_normal = nu!( + cwd: "tests/fixtures/formats", + "with-env [FOO BARRRR] {echo $nu.env} | get FOO" + ); + + assert_eq!(actual_shorthand.out, actual_normal.out); +} + +#[test] +fn with_env_shorthand_nested_quotes() { + let actual = nu!( + cwd: "tests/fixtures/formats", + "FOO='-arg \"hello world\"' echo $nu.env | get FOO" + ); + + assert_eq!(actual.out, "-arg \"hello world\""); +} + +#[test] +fn with_env_hides_variables_in_parent_scope() { + let actual = nu!( + cwd: "tests/fixtures/formats", + r#" + let-env FOO = "1" + echo $nu.env.FOO + with-env [FOO $nothing] { + echo $nu.env.FOO + } + echo $nu.env.FOO + "# + ); + + assert_eq!(actual.out, "11"); + assert!(actual.err.contains("error")); + assert!(actual.err.contains("Unknown column")); +} + +#[test] +fn with_env_shorthand_can_not_hide_variables() { + let actual = nu!( + cwd: "tests/fixtures/formats", + r#" + let-env FOO = "1" + echo $nu.env.FOO + FOO=$nothing echo $nu.env.FOO + echo $nu.env.FOO + "# + ); + + assert_eq!(actual.out, "1$nothing1"); +} diff --git a/crates/nu-command/tests/commands/wrap.rs b/old_nushell/crates/nu-command/tests/commands/wrap.rs similarity index 100% rename from crates/nu-command/tests/commands/wrap.rs rename to old_nushell/crates/nu-command/tests/commands/wrap.rs diff --git a/old_nushell/crates/nu-command/tests/commands/zip.rs b/old_nushell/crates/nu-command/tests/commands/zip.rs new file mode 100644 index 0000000000..20aa6bf641 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/commands/zip.rs @@ -0,0 +1,77 @@ +use nu_test_support::fs::Stub::FileWithContent; +use nu_test_support::pipeline as input; +use nu_test_support::playground::{says, Playground}; + +use hamcrest2::assert_that; +use hamcrest2::prelude::*; + +const ZIP_POWERED_TEST_ASSERTION_SCRIPT: &str = r#" +def expect [ + left, + right, + --to-eq +] { + $left | zip { $right } | all? { + $it.name.0 == $it.name.1 && $it.commits.0 == $it.commits.1 + } +} + +def add-commits [n] { + each { + let contributor = $it; + let name = $it.name; + let commits = $it.commits; + + $contributor | merge { + [[commits]; [($commits + $n)]] + } + } +} +"#; + +#[test] +fn zips_two_tables() { + Playground::setup("zip_test_1", |dirs, nu| { + nu.with_files(vec![FileWithContent( + "zip_test.nu", + &format!("{}\n", ZIP_POWERED_TEST_ASSERTION_SCRIPT), + )]); + + assert_that!( + nu.pipeline(&input(&format!( + r#" + source {} ; + + let contributors = ([ + [name, commits]; + [andres, 10] + [ jt, 20] + ]); + + let actual = ($contributors | add-commits 10); + + expect $actual --to-eq [[name, commits]; [andres, 20] [jt, 30]] + "#, + dirs.test().join("zip_test.nu").display() + ))), + says().stdout("true") + ); + }) +} + +#[test] +fn zips_two_lists() { + Playground::setup("zip_test_2", |_, nu| { + assert_that!( + nu.pipeline(&input( + r#" + echo [0 2 4 6 8] | zip { [1 3 5 7 9] } + | flatten + | into string + | str collect '-' + "# + )), + says().stdout("0-1-2-3-4-5-6-7-8-9") + ); + }) +} diff --git a/crates/nu-command/tests/format_conversions/bson.rs b/old_nushell/crates/nu-command/tests/format_conversions/bson.rs similarity index 100% rename from crates/nu-command/tests/format_conversions/bson.rs rename to old_nushell/crates/nu-command/tests/format_conversions/bson.rs diff --git a/old_nushell/crates/nu-command/tests/format_conversions/csv.rs b/old_nushell/crates/nu-command/tests/format_conversions/csv.rs new file mode 100644 index 0000000000..0cddd3eaec --- /dev/null +++ b/old_nushell/crates/nu-command/tests/format_conversions/csv.rs @@ -0,0 +1,209 @@ +use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn table_to_csv_text_and_from_csv_text_back_into_table() { + let actual = nu!( + cwd: "tests/fixtures/formats", + "open caco3_plastics.csv | to csv | from csv | first 1 | get origin " + ); + + assert_eq!(actual.out, "SPAIN"); +} + +#[test] +fn table_to_csv_text() { + Playground::setup("filter_to_csv_test_1", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "csv_text_sample.txt", + r#" + importer,shipper,tariff_item,name,origin + Plasticos Rival,Reverte,2509000000,Calcium carbonate,Spain + Tigre Ecuador,OMYA Andina,3824909999,Calcium carbonate,Colombia + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open csv_text_sample.txt + | lines + | str trim + | split column "," a b c d origin + | last 1 + | to csv + | lines + | nth 1 + "# + )); + + assert!(actual + .out + .contains("Tigre Ecuador,OMYA Andina,3824909999,Calcium carbonate,Colombia")); + }) +} + +#[test] +fn table_to_csv_text_skipping_headers_after_conversion() { + Playground::setup("filter_to_csv_test_2", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "csv_text_sample.txt", + r#" + importer,shipper,tariff_item,name,origin + Plasticos Rival,Reverte,2509000000,Calcium carbonate,Spain + Tigre Ecuador,OMYA Andina,3824909999,Calcium carbonate,Colombia + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open csv_text_sample.txt + | lines + | str trim + | split column "," a b c d origin + | last 1 + | to csv --noheaders + "# + )); + + assert!(actual + .out + .contains("Tigre Ecuador,OMYA Andina,3824909999,Calcium carbonate,Colombia")); + }) +} + +#[test] +fn infers_types() { + Playground::setup("filter_from_csv_test_1", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "los_cuatro_mosqueteros.csv", + r#" + first_name,last_name,rusty_luck,d + Andrés,Robalino,1,d + Jonathan,Turner,1,d + Yehuda,Katz,1,d + Jason,Gedge,1,d + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open los_cuatro_mosqueteros.csv + | where rusty_luck > 0 + | length + "# + )); + + assert_eq!(actual.out, "4"); + }) +} + +#[test] +fn from_csv_text_to_table() { + Playground::setup("filter_from_csv_test_2", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "los_tres_caballeros.txt", + r#" + first_name,last_name,rusty_luck + Andrés,Robalino,1 + Jonathan,Turner,1 + Yehuda,Katz,1 + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open los_tres_caballeros.txt + | from csv + | get rusty_luck + | length + "# + )); + + assert_eq!(actual.out, "3"); + }) +} + +#[test] +fn from_csv_text_with_separator_to_table() { + Playground::setup("filter_from_csv_test_3", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "los_tres_caballeros.txt", + r#" + first_name;last_name;rusty_luck + Andrés;Robalino;1 + Jonathan;Turner;1 + Yehuda;Katz;1 + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open los_tres_caballeros.txt + | from csv --separator ';' + | get rusty_luck + | length + "# + )); + + assert_eq!(actual.out, "3"); + }) +} + +#[test] +fn from_csv_text_with_tab_separator_to_table() { + Playground::setup("filter_from_csv_test_4", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "los_tres_caballeros.txt", + r#" + first_name last_name rusty_luck + Andrés Robalino 1 + Jonathan Turner 1 + Yehuda Katz 1 + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open los_tres_caballeros.txt + | from csv --separator '\t' + | get rusty_luck + | length + "# + )); + + assert_eq!(actual.out, "3"); + }) +} + +#[test] +fn from_csv_text_skipping_headers_to_table() { + Playground::setup("filter_from_csv_test_5", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "los_tres_amigos.txt", + r#" + Andrés,Robalino,1 + Jonathan,Turner,1 + Yehuda,Katz,1 + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open los_tres_amigos.txt + | from csv --noheaders + | get Column3 + | length + "# + )); + + assert_eq!(actual.out, "3"); + }) +} diff --git a/crates/nu-command/tests/format_conversions/eml.rs b/old_nushell/crates/nu-command/tests/format_conversions/eml.rs similarity index 100% rename from crates/nu-command/tests/format_conversions/eml.rs rename to old_nushell/crates/nu-command/tests/format_conversions/eml.rs diff --git a/old_nushell/crates/nu-command/tests/format_conversions/html.rs b/old_nushell/crates/nu-command/tests/format_conversions/html.rs new file mode 100644 index 0000000000..36ebfff4a3 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/format_conversions/html.rs @@ -0,0 +1,91 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn out_html_simple() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo 3 | to html + "# + )); + + assert_eq!( + actual.out, + r"3" + ); +} + +#[test] +fn out_html_partial() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo 3 | to html -p + "# + )); + + assert_eq!( + actual.out, + "
3
" + ); +} + +#[test] +fn out_html_table() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo '{"name": "darren"}' | from json | to html + "# + )); + + assert_eq!( + actual.out, + r"
name
darren
" + ); +} + +#[test] +fn test_cd_html_color_flag_dark_false() { + let actual = nu!( + cwd: ".", pipeline( + r#" + cd --help | to html --html_color + "# + ) + ); + 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
-

" + ); +} + +#[test] +fn test_no_color_flag() { + let actual = nu!( + cwd: ".", pipeline( + r#" + cd --help | to html --no_color + "# + ) + ); + 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 -

" + ); +} + +#[test] +fn test_html_color_where_flag_dark_false() { + let actual = nu!( + cwd: ".", pipeline( + r#" + where --help | to html --html_color + "# + ) + ); + 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

" + ); +} diff --git a/old_nushell/crates/nu-command/tests/format_conversions/ics.rs b/old_nushell/crates/nu-command/tests/format_conversions/ics.rs new file mode 100644 index 0000000000..7e727cb723 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/format_conversions/ics.rs @@ -0,0 +1,99 @@ +use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn infers_types() { + Playground::setup("filter_from_ics_test_1", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "calendar.ics", + r#" + BEGIN:VCALENDAR + PRODID:-//Google Inc//Google Calendar 70.9054//EN + VERSION:2.0 + BEGIN:VEVENT + DTSTART:20171007T200000Z + DTEND:20171007T233000Z + DTSTAMP:20200319T182138Z + UID:4l80f6dcovnriq38g57g07btid@google.com + CREATED:20170719T202915Z + DESCRIPTION: + LAST-MODIFIED:20170930T190808Z + LOCATION: + SEQUENCE:1 + STATUS:CONFIRMED + SUMMARY:Maryland Game + TRANSP:TRANSPARENT + END:VEVENT + BEGIN:VEVENT + DTSTART:20171002T010000Z + DTEND:20171002T020000Z + DTSTAMP:20200319T182138Z + UID:2v61g7mij4s7ieoubm3sjpun5d@google.com + CREATED:20171001T180103Z + DESCRIPTION: + LAST-MODIFIED:20171001T180103Z + LOCATION: + SEQUENCE:0 + STATUS:CONFIRMED + SUMMARY:Halloween Wars + TRANSP:OPAQUE + END:VEVENT + END:VCALENDAR + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open calendar.ics + | get events + | length + "# + )); + + assert_eq!(actual.out, "2"); + }) +} + +#[test] +fn from_ics_text_to_table() { + Playground::setup("filter_from_ics_test_2", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "calendar.txt", + r#" + BEGIN:VCALENDAR + BEGIN:VEVENT + DTSTART:20171007T200000Z + DTEND:20171007T233000Z + DTSTAMP:20200319T182138Z + UID:4l80f6dcovnriq38g57g07btid@google.com + CREATED:20170719T202915Z + DESCRIPTION: + LAST-MODIFIED:20170930T190808Z + LOCATION: + SEQUENCE:1 + STATUS:CONFIRMED + SUMMARY:Maryland Game + TRANSP:TRANSPARENT + END:VEVENT + END:VCALENDAR + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open calendar.txt + | from ics + | get events + | get properties + | where name == "SUMMARY" + | first + | get value + "# + )); + + assert_eq!(actual.out, "Maryland Game"); + }) +} diff --git a/crates/nu-command/tests/format_conversions/json.rs b/old_nushell/crates/nu-command/tests/format_conversions/json.rs similarity index 100% rename from crates/nu-command/tests/format_conversions/json.rs rename to old_nushell/crates/nu-command/tests/format_conversions/json.rs diff --git a/crates/nu-command/tests/format_conversions/markdown.rs b/old_nushell/crates/nu-command/tests/format_conversions/markdown.rs similarity index 100% rename from crates/nu-command/tests/format_conversions/markdown.rs rename to old_nushell/crates/nu-command/tests/format_conversions/markdown.rs diff --git a/crates/nu-command/tests/format_conversions/mod.rs b/old_nushell/crates/nu-command/tests/format_conversions/mod.rs similarity index 100% rename from crates/nu-command/tests/format_conversions/mod.rs rename to old_nushell/crates/nu-command/tests/format_conversions/mod.rs diff --git a/old_nushell/crates/nu-command/tests/format_conversions/ods.rs b/old_nushell/crates/nu-command/tests/format_conversions/ods.rs new file mode 100644 index 0000000000..5d5ce7ca7a --- /dev/null +++ b/old_nushell/crates/nu-command/tests/format_conversions/ods.rs @@ -0,0 +1,30 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn from_ods_file_to_table() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sample_data.ods + | get SalesOrders + | nth 4 + | get Column2 + "# + )); + + assert_eq!(actual.out, "Gill"); +} + +#[test] +fn from_ods_file_to_table_select_sheet() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sample_data.ods --raw + | from ods -s ["SalesOrders"] + | get + "# + )); + + assert_eq!(actual.out, "SalesOrders"); +} diff --git a/crates/nu-command/tests/format_conversions/sqlite.rs b/old_nushell/crates/nu-command/tests/format_conversions/sqlite.rs similarity index 100% rename from crates/nu-command/tests/format_conversions/sqlite.rs rename to old_nushell/crates/nu-command/tests/format_conversions/sqlite.rs diff --git a/crates/nu-command/tests/format_conversions/ssv.rs b/old_nushell/crates/nu-command/tests/format_conversions/ssv.rs similarity index 100% rename from crates/nu-command/tests/format_conversions/ssv.rs rename to old_nushell/crates/nu-command/tests/format_conversions/ssv.rs diff --git a/crates/nu-command/tests/format_conversions/toml.rs b/old_nushell/crates/nu-command/tests/format_conversions/toml.rs similarity index 100% rename from crates/nu-command/tests/format_conversions/toml.rs rename to old_nushell/crates/nu-command/tests/format_conversions/toml.rs diff --git a/crates/nu-command/tests/format_conversions/tsv.rs b/old_nushell/crates/nu-command/tests/format_conversions/tsv.rs similarity index 100% rename from crates/nu-command/tests/format_conversions/tsv.rs rename to old_nushell/crates/nu-command/tests/format_conversions/tsv.rs diff --git a/crates/nu-command/tests/format_conversions/url.rs b/old_nushell/crates/nu-command/tests/format_conversions/url.rs similarity index 100% rename from crates/nu-command/tests/format_conversions/url.rs rename to old_nushell/crates/nu-command/tests/format_conversions/url.rs diff --git a/old_nushell/crates/nu-command/tests/format_conversions/vcf.rs b/old_nushell/crates/nu-command/tests/format_conversions/vcf.rs new file mode 100644 index 0000000000..9d03dd6254 --- /dev/null +++ b/old_nushell/crates/nu-command/tests/format_conversions/vcf.rs @@ -0,0 +1,82 @@ +use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn infers_types() { + Playground::setup("filter_from_vcf_test_1", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "contacts.vcf", + r#" + BEGIN:VCARD + VERSION:3.0 + FN:John Doe + N:Doe;John;;; + EMAIL;TYPE=INTERNET:john.doe99@gmail.com + item1.ORG:'Alpine Ski Resort' + item1.X-ABLabel:Other + item2.TITLE:'Ski Instructor' + item2.X-ABLabel:Other + BDAY:19001106 + NOTE:Facebook: john.doe.3\nWebsite: \nHometown: Cleveland\, Ohio + CATEGORIES:myContacts + END:VCARD + BEGIN:VCARD + VERSION:3.0 + FN:Alex Smith + N:Smith;Alex;;; + TEL;TYPE=CELL:(890) 123-4567 + CATEGORIES:Band,myContacts + END:VCARD + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open contacts.vcf + | length + "# + )); + + assert_eq!(actual.out, "2"); + }) +} + +#[test] +fn from_vcf_text_to_table() { + Playground::setup("filter_from_vcf_test_2", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "contacts.txt", + r#" + BEGIN:VCARD + VERSION:3.0 + FN:John Doe + N:Doe;John;;; + EMAIL;TYPE=INTERNET:john.doe99@gmail.com + item1.ORG:'Alpine Ski Resort' + item1.X-ABLabel:Other + item2.TITLE:'Ski Instructor' + item2.X-ABLabel:Other + BDAY:19001106 + NOTE:Facebook: john.doe.3\nWebsite: \nHometown: Cleveland\, Ohio + CATEGORIES:myContacts + END:VCARD + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open contacts.txt + | from vcf + | get properties + | where name == "EMAIL" + | first + | get value + "# + )); + + assert_eq!(actual.out, "john.doe99@gmail.com"); + }) +} diff --git a/old_nushell/crates/nu-command/tests/format_conversions/xlsx.rs b/old_nushell/crates/nu-command/tests/format_conversions/xlsx.rs new file mode 100644 index 0000000000..36b3aca17a --- /dev/null +++ b/old_nushell/crates/nu-command/tests/format_conversions/xlsx.rs @@ -0,0 +1,30 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn from_excel_file_to_table() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sample_data.xlsx + | get SalesOrders + | nth 4 + | get Column2 + "# + )); + + assert_eq!(actual.out, "Gill"); +} + +#[test] +fn from_excel_file_to_table_select_sheet() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open sample_data.xlsx --raw + | from xlsx -s ["SalesOrders"] + | get + "# + )); + + assert_eq!(actual.out, "SalesOrders"); +} diff --git a/old_nushell/crates/nu-command/tests/format_conversions/xml.rs b/old_nushell/crates/nu-command/tests/format_conversions/xml.rs new file mode 100644 index 0000000000..068296195a --- /dev/null +++ b/old_nushell/crates/nu-command/tests/format_conversions/xml.rs @@ -0,0 +1,16 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn table_to_xml_text_and_from_xml_text_back_into_table() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open jonathan.xml + | to xml + | from xml + | get rss.children.channel.children.0.item.children.0.guid.attributes.isPermaLink + "# + )); + + assert_eq!(actual.out, "true"); +} diff --git a/crates/nu-command/tests/format_conversions/yaml.rs b/old_nushell/crates/nu-command/tests/format_conversions/yaml.rs similarity index 100% rename from crates/nu-command/tests/format_conversions/yaml.rs rename to old_nushell/crates/nu-command/tests/format_conversions/yaml.rs diff --git a/old_nushell/crates/nu-command/tests/main.rs b/old_nushell/crates/nu-command/tests/main.rs new file mode 100644 index 0000000000..bd02ef653b --- /dev/null +++ b/old_nushell/crates/nu-command/tests/main.rs @@ -0,0 +1,18 @@ +use quickcheck_macros::quickcheck; + +mod commands; +mod format_conversions; + +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); + + if err.is_none() && err2.is_none() { + let context = EvaluationContext::basic(); + let _ = nu_parser::classify_block(&lite_block, &context.scope); + } + true +} diff --git a/crates/nu-completion/Cargo.toml b/old_nushell/crates/nu-completion/Cargo.toml similarity index 100% rename from crates/nu-completion/Cargo.toml rename to old_nushell/crates/nu-completion/Cargo.toml diff --git a/crates/nu-completion/src/command.rs b/old_nushell/crates/nu-completion/src/command.rs similarity index 100% rename from crates/nu-completion/src/command.rs rename to old_nushell/crates/nu-completion/src/command.rs diff --git a/crates/nu-completion/src/completer.rs b/old_nushell/crates/nu-completion/src/completer.rs similarity index 100% rename from crates/nu-completion/src/completer.rs rename to old_nushell/crates/nu-completion/src/completer.rs diff --git a/crates/nu-completion/src/engine.rs b/old_nushell/crates/nu-completion/src/engine.rs similarity index 100% rename from crates/nu-completion/src/engine.rs rename to old_nushell/crates/nu-completion/src/engine.rs diff --git a/crates/nu-completion/src/flag.rs b/old_nushell/crates/nu-completion/src/flag.rs similarity index 100% rename from crates/nu-completion/src/flag.rs rename to old_nushell/crates/nu-completion/src/flag.rs diff --git a/crates/nu-completion/src/lib.rs b/old_nushell/crates/nu-completion/src/lib.rs similarity index 100% rename from crates/nu-completion/src/lib.rs rename to old_nushell/crates/nu-completion/src/lib.rs diff --git a/crates/nu-completion/src/matchers/case_insensitive.rs b/old_nushell/crates/nu-completion/src/matchers/case_insensitive.rs similarity index 100% rename from crates/nu-completion/src/matchers/case_insensitive.rs rename to old_nushell/crates/nu-completion/src/matchers/case_insensitive.rs diff --git a/crates/nu-completion/src/matchers/case_sensitive.rs b/old_nushell/crates/nu-completion/src/matchers/case_sensitive.rs similarity index 100% rename from crates/nu-completion/src/matchers/case_sensitive.rs rename to old_nushell/crates/nu-completion/src/matchers/case_sensitive.rs diff --git a/crates/nu-completion/src/matchers/mod.rs b/old_nushell/crates/nu-completion/src/matchers/mod.rs similarity index 100% rename from crates/nu-completion/src/matchers/mod.rs rename to old_nushell/crates/nu-completion/src/matchers/mod.rs diff --git a/crates/nu-completion/src/path.rs b/old_nushell/crates/nu-completion/src/path.rs similarity index 100% rename from crates/nu-completion/src/path.rs rename to old_nushell/crates/nu-completion/src/path.rs diff --git a/crates/nu-completion/src/variable.rs b/old_nushell/crates/nu-completion/src/variable.rs similarity index 100% rename from crates/nu-completion/src/variable.rs rename to old_nushell/crates/nu-completion/src/variable.rs diff --git a/crates/nu-data/Cargo.toml b/old_nushell/crates/nu-data/Cargo.toml similarity index 100% rename from crates/nu-data/Cargo.toml rename to old_nushell/crates/nu-data/Cargo.toml diff --git a/crates/nu-data/src/base.rs b/old_nushell/crates/nu-data/src/base.rs similarity index 100% rename from crates/nu-data/src/base.rs rename to old_nushell/crates/nu-data/src/base.rs diff --git a/crates/nu-data/src/base/shape.rs b/old_nushell/crates/nu-data/src/base/shape.rs similarity index 100% rename from crates/nu-data/src/base/shape.rs rename to old_nushell/crates/nu-data/src/base/shape.rs diff --git a/crates/nu-data/src/config.rs b/old_nushell/crates/nu-data/src/config.rs similarity index 100% rename from crates/nu-data/src/config.rs rename to old_nushell/crates/nu-data/src/config.rs diff --git a/crates/nu-data/src/config/conf.rs b/old_nushell/crates/nu-data/src/config/conf.rs similarity index 100% rename from crates/nu-data/src/config/conf.rs rename to old_nushell/crates/nu-data/src/config/conf.rs diff --git a/crates/nu-data/src/config/config_trust.rs b/old_nushell/crates/nu-data/src/config/config_trust.rs similarity index 100% rename from crates/nu-data/src/config/config_trust.rs rename to old_nushell/crates/nu-data/src/config/config_trust.rs diff --git a/crates/nu-data/src/config/local_config.rs b/old_nushell/crates/nu-data/src/config/local_config.rs similarity index 100% rename from crates/nu-data/src/config/local_config.rs rename to old_nushell/crates/nu-data/src/config/local_config.rs diff --git a/crates/nu-data/src/config/nuconfig.rs b/old_nushell/crates/nu-data/src/config/nuconfig.rs similarity index 100% rename from crates/nu-data/src/config/nuconfig.rs rename to old_nushell/crates/nu-data/src/config/nuconfig.rs diff --git a/crates/nu-data/src/config/path.rs b/old_nushell/crates/nu-data/src/config/path.rs similarity index 100% rename from crates/nu-data/src/config/path.rs rename to old_nushell/crates/nu-data/src/config/path.rs diff --git a/crates/nu-data/src/config/tests.rs b/old_nushell/crates/nu-data/src/config/tests.rs similarity index 100% rename from crates/nu-data/src/config/tests.rs rename to old_nushell/crates/nu-data/src/config/tests.rs diff --git a/crates/nu-data/src/dict.rs b/old_nushell/crates/nu-data/src/dict.rs similarity index 100% rename from crates/nu-data/src/dict.rs rename to old_nushell/crates/nu-data/src/dict.rs diff --git a/crates/nu-data/src/keybinding.rs b/old_nushell/crates/nu-data/src/keybinding.rs similarity index 100% rename from crates/nu-data/src/keybinding.rs rename to old_nushell/crates/nu-data/src/keybinding.rs diff --git a/crates/nu-data/src/lib.rs b/old_nushell/crates/nu-data/src/lib.rs similarity index 100% rename from crates/nu-data/src/lib.rs rename to old_nushell/crates/nu-data/src/lib.rs diff --git a/crates/nu-data/src/primitive.rs b/old_nushell/crates/nu-data/src/primitive.rs similarity index 100% rename from crates/nu-data/src/primitive.rs rename to old_nushell/crates/nu-data/src/primitive.rs diff --git a/crates/nu-data/src/utils/group.rs b/old_nushell/crates/nu-data/src/utils/group.rs similarity index 100% rename from crates/nu-data/src/utils/group.rs rename to old_nushell/crates/nu-data/src/utils/group.rs diff --git a/crates/nu-data/src/utils/internal.rs b/old_nushell/crates/nu-data/src/utils/internal.rs similarity index 100% rename from crates/nu-data/src/utils/internal.rs rename to old_nushell/crates/nu-data/src/utils/internal.rs diff --git a/crates/nu-data/src/utils/mod.rs b/old_nushell/crates/nu-data/src/utils/mod.rs similarity index 100% rename from crates/nu-data/src/utils/mod.rs rename to old_nushell/crates/nu-data/src/utils/mod.rs diff --git a/crates/nu-data/src/utils/split.rs b/old_nushell/crates/nu-data/src/utils/split.rs similarity index 100% rename from crates/nu-data/src/utils/split.rs rename to old_nushell/crates/nu-data/src/utils/split.rs diff --git a/crates/nu-data/src/value.rs b/old_nushell/crates/nu-data/src/value.rs similarity index 100% rename from crates/nu-data/src/value.rs rename to old_nushell/crates/nu-data/src/value.rs diff --git a/old_nushell/crates/nu-engine/Cargo.toml b/old_nushell/crates/nu-engine/Cargo.toml new file mode 100644 index 0000000000..29f0adcff8 --- /dev/null +++ b/old_nushell/crates/nu-engine/Cargo.toml @@ -0,0 +1,59 @@ +[package] +authors = ["The Nu Project Contributors"] +description = "Core commands for nushell" +edition = "2018" +license = "MIT" +name = "nu-engine" +version = "0.43.0" + +[dependencies] +nu-data = { version = "0.43.0", path="../nu-data" } +nu-errors = { version = "0.43.0", path="../nu-errors" } +nu-parser = { version = "0.43.0", path="../nu-parser" } +nu-plugin = { version = "0.43.0", path="../nu-plugin" } +nu-protocol = { version = "0.43.0", path="../nu-protocol" } +nu-source = { version = "0.43.0", path="../nu-source" } +nu-stream = { version = "0.43.0", path="../nu-stream" } +nu-value-ext = { version = "0.43.0", path="../nu-value-ext" } +nu-ansi-term = { version = "0.43.0", path="../nu-ansi-term" } +nu-test-support = { version = "0.43.0", path="../nu-test-support" } +nu-path = { version = "0.43.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" +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.43.0", path="../nu-test-support" } +hamcrest2 = "0.3.0" + +[features] +rustyline-support = [] +trash-support = ["trash"] +dataframe = ["nu-protocol/dataframe"] diff --git a/crates/nu-engine/README.md b/old_nushell/crates/nu-engine/README.md similarity index 100% rename from crates/nu-engine/README.md rename to old_nushell/crates/nu-engine/README.md diff --git a/crates/nu-engine/src/call_info.rs b/old_nushell/crates/nu-engine/src/call_info.rs similarity index 100% rename from crates/nu-engine/src/call_info.rs rename to old_nushell/crates/nu-engine/src/call_info.rs diff --git a/crates/nu-engine/src/command_args.rs b/old_nushell/crates/nu-engine/src/command_args.rs similarity index 100% rename from crates/nu-engine/src/command_args.rs rename to old_nushell/crates/nu-engine/src/command_args.rs diff --git a/crates/nu-engine/src/config_holder.rs b/old_nushell/crates/nu-engine/src/config_holder.rs similarity index 100% rename from crates/nu-engine/src/config_holder.rs rename to old_nushell/crates/nu-engine/src/config_holder.rs diff --git a/old_nushell/crates/nu-engine/src/documentation.rs b/old_nushell/crates/nu-engine/src/documentation.rs new file mode 100644 index 0000000000..bd8f1df742 --- /dev/null +++ b/old_nushell/crates/nu-engine/src/documentation.rs @@ -0,0 +1,313 @@ +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 std::collections::HashMap; + +const COMMANDS_DOCS_DIR: &str = "docs/commands"; + +#[derive(Default)] +pub struct DocumentationConfig { + no_subcommands: bool, + 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) + .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(), + ) + }); + row_entries.insert( + "documentation".to_owned(), + UntaggedValue::string(get_documentation( + command.stream_command(), + scope, + &DocumentationConfig { + no_subcommands: true, + no_color: true, + brief: false, + }, + )) + .into_untagged_value(), + ); + row_entries +} + +// 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(); + + // 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) { + let sub_names = cmap + .get_mut(*parent_name) + .expect("Expected an entry for parent"); + sub_names.push(name.to_owned()); + } + } else { + cmap.insert(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 { + // Must be a sub-command, skip since it's being handled underneath when we hit the parent command + if !cmap.contains_key(name) { + continue; + } + let mut row_entries = generate_doc(name, scope); + // 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()); + } + + if !sub_table.is_empty() { + row_entries.insert( + "subcommands".to_owned(), + UntaggedValue::table(&sub_table).into_untagged_value(), + ); + } + table.push(UntaggedValue::row(row_entries).into_untagged_value()); + } + UntaggedValue::table(&table).into_untagged_value() +} + +fn retrieve_doc_link(name: &str) -> Option { + let doc_name = name.split_whitespace().join("_"); // Because .replace(" ", "_") didn't work + let mut entries = + std::fs::read_dir(COMMANDS_DOCS_DIR).expect("Directory for command docs are missing!"); + entries.find_map(|r| { + r.map_or(None, |de| { + if de.file_name().to_string_lossy() == format!("{}.{}", &doc_name, "md") { + Some(format!("/commands/{}.{}", &doc_name, "html")) + } else { + None + } + }) + }) +} + +#[allow(clippy::cognitive_complexity)] +pub fn get_documentation( + cmd: &dyn WholeStreamCommand, + scope: &Scope, + config: &DocumentationConfig, +) -> String { + let cmd_name = cmd.name(); + let signature = cmd.signature(); + let mut long_desc = String::new(); + + let usage = &cmd.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() }; + if !extra_usage.is_empty() { + long_desc.push_str(extra_usage); + long_desc.push_str("\n\n"); + } + + 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 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)); + + if !subcommands.is_empty() { + long_desc.push_str("\nSubcommands:\n"); + subcommands.sort(); + long_desc.push_str(&subcommands.join("\n")); + long_desc.push('\n'); + } + + if !signature.positional.is_empty() || signature.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)); + } + } + } + + if let Some(rest_positional) = &signature.rest_positional { + long_desc.push_str(&format!(" ...args: {}\n", rest_positional.2)); + } + } + 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(" "); + long_desc.push_str(example.description); + + if config.no_color { + 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('\n'); + + long_desc +} + +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 { + format!( + " -{}, --{}{} {}\n", + c, + flag, + if !ty.1.is_empty() { ":" } else { "" }, + ty.1 + ) + } else { + format!( + " --{}{} {}\n", + flag, + if !ty.1.is_empty() { ":" } else { "" }, + ty.1 + ) + } + } + 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 + ) + } + } + }; + long_desc.push_str(&msg); + } + long_desc +} + +pub fn get_brief_help(cmd: &dyn WholeStreamCommand, scope: &Scope) -> String { + get_documentation( + cmd, + scope, + &DocumentationConfig { + no_subcommands: false, + no_color: false, + brief: true, + }, + ) +} + +pub fn get_full_help(cmd: &dyn WholeStreamCommand, scope: &Scope) -> String { + get_documentation(cmd, scope, &DocumentationConfig::default()) +} diff --git a/crates/nu-engine/src/env/basic_host.rs b/old_nushell/crates/nu-engine/src/env/basic_host.rs similarity index 100% rename from crates/nu-engine/src/env/basic_host.rs rename to old_nushell/crates/nu-engine/src/env/basic_host.rs diff --git a/crates/nu-engine/src/env/host.rs b/old_nushell/crates/nu-engine/src/env/host.rs similarity index 100% rename from crates/nu-engine/src/env/host.rs rename to old_nushell/crates/nu-engine/src/env/host.rs diff --git a/crates/nu-engine/src/env/mod.rs b/old_nushell/crates/nu-engine/src/env/mod.rs similarity index 100% rename from crates/nu-engine/src/env/mod.rs rename to old_nushell/crates/nu-engine/src/env/mod.rs diff --git a/crates/nu-engine/src/evaluate/block.rs b/old_nushell/crates/nu-engine/src/evaluate/block.rs similarity index 100% rename from crates/nu-engine/src/evaluate/block.rs rename to old_nushell/crates/nu-engine/src/evaluate/block.rs diff --git a/crates/nu-engine/src/evaluate/envvar.rs b/old_nushell/crates/nu-engine/src/evaluate/envvar.rs similarity index 100% rename from crates/nu-engine/src/evaluate/envvar.rs rename to old_nushell/crates/nu-engine/src/evaluate/envvar.rs diff --git a/crates/nu-engine/src/evaluate/evaluate_args.rs b/old_nushell/crates/nu-engine/src/evaluate/evaluate_args.rs similarity index 100% rename from crates/nu-engine/src/evaluate/evaluate_args.rs rename to old_nushell/crates/nu-engine/src/evaluate/evaluate_args.rs diff --git a/crates/nu-engine/src/evaluate/evaluator.rs b/old_nushell/crates/nu-engine/src/evaluate/evaluator.rs similarity index 100% rename from crates/nu-engine/src/evaluate/evaluator.rs rename to old_nushell/crates/nu-engine/src/evaluate/evaluator.rs diff --git a/crates/nu-engine/src/evaluate/expr.rs b/old_nushell/crates/nu-engine/src/evaluate/expr.rs similarity index 100% rename from crates/nu-engine/src/evaluate/expr.rs rename to old_nushell/crates/nu-engine/src/evaluate/expr.rs diff --git a/crates/nu-engine/src/evaluate/internal.rs b/old_nushell/crates/nu-engine/src/evaluate/internal.rs similarity index 100% rename from crates/nu-engine/src/evaluate/internal.rs rename to old_nushell/crates/nu-engine/src/evaluate/internal.rs diff --git a/crates/nu-engine/src/evaluate/lang.rs b/old_nushell/crates/nu-engine/src/evaluate/lang.rs similarity index 100% rename from crates/nu-engine/src/evaluate/lang.rs rename to old_nushell/crates/nu-engine/src/evaluate/lang.rs diff --git a/crates/nu-engine/src/evaluate/mod.rs b/old_nushell/crates/nu-engine/src/evaluate/mod.rs similarity index 100% rename from crates/nu-engine/src/evaluate/mod.rs rename to old_nushell/crates/nu-engine/src/evaluate/mod.rs diff --git a/crates/nu-engine/src/evaluate/operator.rs b/old_nushell/crates/nu-engine/src/evaluate/operator.rs similarity index 100% rename from crates/nu-engine/src/evaluate/operator.rs rename to old_nushell/crates/nu-engine/src/evaluate/operator.rs diff --git a/crates/nu-engine/src/evaluate/scope.rs b/old_nushell/crates/nu-engine/src/evaluate/scope.rs similarity index 100% rename from crates/nu-engine/src/evaluate/scope.rs rename to old_nushell/crates/nu-engine/src/evaluate/scope.rs diff --git a/crates/nu-engine/src/evaluate/variables.rs b/old_nushell/crates/nu-engine/src/evaluate/variables.rs similarity index 100% rename from crates/nu-engine/src/evaluate/variables.rs rename to old_nushell/crates/nu-engine/src/evaluate/variables.rs diff --git a/crates/nu-engine/src/evaluation_context.rs b/old_nushell/crates/nu-engine/src/evaluation_context.rs similarity index 100% rename from crates/nu-engine/src/evaluation_context.rs rename to old_nushell/crates/nu-engine/src/evaluation_context.rs diff --git a/crates/nu-engine/src/example.rs b/old_nushell/crates/nu-engine/src/example.rs similarity index 100% rename from crates/nu-engine/src/example.rs rename to old_nushell/crates/nu-engine/src/example.rs diff --git a/crates/nu-engine/src/filesystem/dir_info.rs b/old_nushell/crates/nu-engine/src/filesystem/dir_info.rs similarity index 100% rename from crates/nu-engine/src/filesystem/dir_info.rs rename to old_nushell/crates/nu-engine/src/filesystem/dir_info.rs diff --git a/crates/nu-engine/src/filesystem/filesystem_shell.rs b/old_nushell/crates/nu-engine/src/filesystem/filesystem_shell.rs similarity index 100% rename from crates/nu-engine/src/filesystem/filesystem_shell.rs rename to old_nushell/crates/nu-engine/src/filesystem/filesystem_shell.rs diff --git a/crates/nu-engine/src/filesystem/mod.rs b/old_nushell/crates/nu-engine/src/filesystem/mod.rs similarity index 100% rename from crates/nu-engine/src/filesystem/mod.rs rename to old_nushell/crates/nu-engine/src/filesystem/mod.rs diff --git a/crates/nu-engine/src/filesystem/utils.rs b/old_nushell/crates/nu-engine/src/filesystem/utils.rs similarity index 100% rename from crates/nu-engine/src/filesystem/utils.rs rename to old_nushell/crates/nu-engine/src/filesystem/utils.rs diff --git a/crates/nu-engine/src/from_value.rs b/old_nushell/crates/nu-engine/src/from_value.rs similarity index 100% rename from crates/nu-engine/src/from_value.rs rename to old_nushell/crates/nu-engine/src/from_value.rs diff --git a/old_nushell/crates/nu-engine/src/lib.rs b/old_nushell/crates/nu-engine/src/lib.rs new file mode 100644 index 0000000000..607ab63e11 --- /dev/null +++ b/old_nushell/crates/nu-engine/src/lib.rs @@ -0,0 +1,40 @@ +mod call_info; +mod command_args; +mod config_holder; +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 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}; diff --git a/crates/nu-engine/src/maybe_text_codec.rs b/old_nushell/crates/nu-engine/src/maybe_text_codec.rs similarity index 100% rename from crates/nu-engine/src/maybe_text_codec.rs rename to old_nushell/crates/nu-engine/src/maybe_text_codec.rs diff --git a/crates/nu-engine/src/plugin/build_plugin.rs b/old_nushell/crates/nu-engine/src/plugin/build_plugin.rs similarity index 100% rename from crates/nu-engine/src/plugin/build_plugin.rs rename to old_nushell/crates/nu-engine/src/plugin/build_plugin.rs diff --git a/crates/nu-engine/src/plugin/mod.rs b/old_nushell/crates/nu-engine/src/plugin/mod.rs similarity index 100% rename from crates/nu-engine/src/plugin/mod.rs rename to old_nushell/crates/nu-engine/src/plugin/mod.rs diff --git a/crates/nu-engine/src/plugin/run_plugin.rs b/old_nushell/crates/nu-engine/src/plugin/run_plugin.rs similarity index 100% rename from crates/nu-engine/src/plugin/run_plugin.rs rename to old_nushell/crates/nu-engine/src/plugin/run_plugin.rs diff --git a/crates/nu-engine/src/print.rs b/old_nushell/crates/nu-engine/src/print.rs similarity index 100% rename from crates/nu-engine/src/print.rs rename to old_nushell/crates/nu-engine/src/print.rs diff --git a/crates/nu-engine/src/script.rs b/old_nushell/crates/nu-engine/src/script.rs similarity index 100% rename from crates/nu-engine/src/script.rs rename to old_nushell/crates/nu-engine/src/script.rs diff --git a/crates/nu-engine/src/shell/mod.rs b/old_nushell/crates/nu-engine/src/shell/mod.rs similarity index 100% rename from crates/nu-engine/src/shell/mod.rs rename to old_nushell/crates/nu-engine/src/shell/mod.rs diff --git a/crates/nu-engine/src/shell/painter.rs b/old_nushell/crates/nu-engine/src/shell/painter.rs similarity index 100% rename from crates/nu-engine/src/shell/painter.rs rename to old_nushell/crates/nu-engine/src/shell/painter.rs diff --git a/crates/nu-engine/src/shell/palette.rs b/old_nushell/crates/nu-engine/src/shell/palette.rs similarity index 100% rename from crates/nu-engine/src/shell/palette.rs rename to old_nushell/crates/nu-engine/src/shell/palette.rs diff --git a/crates/nu-engine/src/shell/shell_args.rs b/old_nushell/crates/nu-engine/src/shell/shell_args.rs similarity index 100% rename from crates/nu-engine/src/shell/shell_args.rs rename to old_nushell/crates/nu-engine/src/shell/shell_args.rs diff --git a/crates/nu-engine/src/shell/shell_manager.rs b/old_nushell/crates/nu-engine/src/shell/shell_manager.rs similarity index 100% rename from crates/nu-engine/src/shell/shell_manager.rs rename to old_nushell/crates/nu-engine/src/shell/shell_manager.rs diff --git a/crates/nu-engine/src/shell/value_shell.rs b/old_nushell/crates/nu-engine/src/shell/value_shell.rs similarity index 100% rename from crates/nu-engine/src/shell/value_shell.rs rename to old_nushell/crates/nu-engine/src/shell/value_shell.rs diff --git a/crates/nu-engine/src/types.rs b/old_nushell/crates/nu-engine/src/types.rs similarity index 100% rename from crates/nu-engine/src/types.rs rename to old_nushell/crates/nu-engine/src/types.rs diff --git a/crates/nu-engine/src/types/deduction.rs b/old_nushell/crates/nu-engine/src/types/deduction.rs similarity index 100% rename from crates/nu-engine/src/types/deduction.rs rename to old_nushell/crates/nu-engine/src/types/deduction.rs diff --git a/crates/nu-engine/src/whole_stream_command.rs b/old_nushell/crates/nu-engine/src/whole_stream_command.rs similarity index 100% rename from crates/nu-engine/src/whole_stream_command.rs rename to old_nushell/crates/nu-engine/src/whole_stream_command.rs diff --git a/crates/nu-engine/tests/evaluate/mod.rs b/old_nushell/crates/nu-engine/tests/evaluate/mod.rs similarity index 100% rename from crates/nu-engine/tests/evaluate/mod.rs rename to old_nushell/crates/nu-engine/tests/evaluate/mod.rs diff --git a/crates/nu-engine/tests/evaluate/operator.rs b/old_nushell/crates/nu-engine/tests/evaluate/operator.rs similarity index 100% rename from crates/nu-engine/tests/evaluate/operator.rs rename to old_nushell/crates/nu-engine/tests/evaluate/operator.rs diff --git a/crates/nu-engine/tests/evaluate/subexpression.rs b/old_nushell/crates/nu-engine/tests/evaluate/subexpression.rs similarity index 100% rename from crates/nu-engine/tests/evaluate/subexpression.rs rename to old_nushell/crates/nu-engine/tests/evaluate/subexpression.rs diff --git a/crates/nu-engine/tests/evaluate/variables.rs b/old_nushell/crates/nu-engine/tests/evaluate/variables.rs similarity index 100% rename from crates/nu-engine/tests/evaluate/variables.rs rename to old_nushell/crates/nu-engine/tests/evaluate/variables.rs diff --git a/crates/nu-engine/tests/main.rs b/old_nushell/crates/nu-engine/tests/main.rs similarity index 100% rename from crates/nu-engine/tests/main.rs rename to old_nushell/crates/nu-engine/tests/main.rs diff --git a/crates/nu-errors/Cargo.toml b/old_nushell/crates/nu-errors/Cargo.toml similarity index 100% rename from crates/nu-errors/Cargo.toml rename to old_nushell/crates/nu-errors/Cargo.toml diff --git a/crates/nu-errors/src/lib.rs b/old_nushell/crates/nu-errors/src/lib.rs similarity index 100% rename from crates/nu-errors/src/lib.rs rename to old_nushell/crates/nu-errors/src/lib.rs diff --git a/old_nushell/crates/nu-json/Cargo.toml b/old_nushell/crates/nu-json/Cargo.toml new file mode 100644 index 0000000000..739e8f8b8a --- /dev/null +++ b/old_nushell/crates/nu-json/Cargo.toml @@ -0,0 +1,25 @@ +[package] +authors = ["The Nu Project Contributors", "Christian Zangl "] +description = "Fork of serde-hjson" +edition = "2018" +license = "MIT" +name = "nu-json" +version = "0.43.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +preserve_order = ["linked-hash-map", "linked-hash-map/serde_impl"] +default = ["preserve_order"] + +[dependencies] +serde = "1.0" +num-traits = "0.2.14" +regex = "^1.0" +lazy_static = "1" +linked-hash-map = { version="0.5", optional=true } + +[dev-dependencies] +nu-path = { version = "0.43.0", path="../nu-path" } +nu-test-support = { version = "0.43.0", path="../nu-test-support" } +serde_json = "1.0.39" diff --git a/crates/nu-json/LICENSE b/old_nushell/crates/nu-json/LICENSE similarity index 100% rename from crates/nu-json/LICENSE rename to old_nushell/crates/nu-json/LICENSE diff --git a/crates/nu-json/src/builder.rs b/old_nushell/crates/nu-json/src/builder.rs similarity index 100% rename from crates/nu-json/src/builder.rs rename to old_nushell/crates/nu-json/src/builder.rs diff --git a/crates/nu-json/src/de.rs b/old_nushell/crates/nu-json/src/de.rs similarity index 100% rename from crates/nu-json/src/de.rs rename to old_nushell/crates/nu-json/src/de.rs diff --git a/crates/nu-json/src/error.rs b/old_nushell/crates/nu-json/src/error.rs similarity index 100% rename from crates/nu-json/src/error.rs rename to old_nushell/crates/nu-json/src/error.rs diff --git a/old_nushell/crates/nu-json/src/lib.rs b/old_nushell/crates/nu-json/src/lib.rs new file mode 100644 index 0000000000..6a196c5273 --- /dev/null +++ b/old_nushell/crates/nu-json/src/lib.rs @@ -0,0 +1,13 @@ +pub use self::de::{ + from_iter, from_reader, from_slice, from_str, Deserializer, StreamDeserializer, +}; +pub use self::error::{Error, ErrorCode, Result}; +pub use self::ser::{to_string, to_vec, to_writer, Serializer}; +pub use self::value::{from_value, to_value, Map, Value}; + +pub mod builder; +pub mod de; +pub mod error; +pub mod ser; +mod util; +pub mod value; diff --git a/old_nushell/crates/nu-json/src/ser.rs b/old_nushell/crates/nu-json/src/ser.rs new file mode 100644 index 0000000000..200b90f1e4 --- /dev/null +++ b/old_nushell/crates/nu-json/src/ser.rs @@ -0,0 +1,1017 @@ +//! Hjson Serialization +//! +//! This module provides for Hjson serialization with the type `Serializer`. + +use std::fmt::{Display, LowerExp}; +use std::io; +use std::num::FpCategory; + +use super::error::{Error, ErrorCode, Result}; +use serde::ser; + +use super::util::ParseNumber; + +use regex::Regex; + +use lazy_static::lazy_static; + +/// A structure for serializing Rust values into Hjson. +pub struct Serializer { + writer: W, + formatter: F, +} + +impl<'a, W> Serializer> +where + W: io::Write, +{ + /// Creates a new Hjson serializer. + #[inline] + pub fn new(writer: W) -> Self { + Serializer::with_formatter(writer, HjsonFormatter::new()) + } +} + +impl Serializer +where + W: io::Write, + F: Formatter, +{ + /// Creates a new Hjson visitor whose output will be written to the writer + /// specified. + #[inline] + pub fn with_formatter(writer: W, formatter: F) -> Self { + Serializer { writer, formatter } + } + + /// Unwrap the `Writer` from the `Serializer`. + #[inline] + pub fn into_inner(self) -> W { + self.writer + } +} + +#[doc(hidden)] +#[derive(Eq, PartialEq)] +pub enum State { + Empty, + First, + Rest, +} + +#[doc(hidden)] +pub struct Compound<'a, W, F> { + ser: &'a mut Serializer, + state: State, +} + +impl<'a, W, F> ser::Serializer for &'a mut Serializer +where + W: io::Write, + F: Formatter, +{ + type Ok = (); + type Error = Error; + + type SerializeSeq = Compound<'a, W, F>; + type SerializeTuple = Compound<'a, W, F>; + type SerializeTupleStruct = Compound<'a, W, F>; + type SerializeTupleVariant = Compound<'a, W, F>; + type SerializeMap = Compound<'a, W, F>; + type SerializeStruct = Compound<'a, W, F>; + type SerializeStructVariant = Compound<'a, W, F>; + + #[inline] + fn serialize_bool(self, value: bool) -> Result<()> { + self.formatter.start_value(&mut self.writer)?; + if value { + self.writer.write_all(b"true").map_err(From::from) + } else { + self.writer.write_all(b"false").map_err(From::from) + } + } + + #[inline] + fn serialize_i8(self, value: i8) -> Result<()> { + self.formatter.start_value(&mut self.writer)?; + write!(&mut self.writer, "{}", value).map_err(From::from) + } + + #[inline] + fn serialize_i16(self, value: i16) -> Result<()> { + self.formatter.start_value(&mut self.writer)?; + write!(&mut self.writer, "{}", value).map_err(From::from) + } + + #[inline] + fn serialize_i32(self, value: i32) -> Result<()> { + self.formatter.start_value(&mut self.writer)?; + write!(&mut self.writer, "{}", value).map_err(From::from) + } + + #[inline] + fn serialize_i64(self, value: i64) -> Result<()> { + self.formatter.start_value(&mut self.writer)?; + write!(&mut self.writer, "{}", value).map_err(From::from) + } + + #[inline] + fn serialize_u8(self, value: u8) -> Result<()> { + self.formatter.start_value(&mut self.writer)?; + write!(&mut self.writer, "{}", value).map_err(From::from) + } + + #[inline] + fn serialize_u16(self, value: u16) -> Result<()> { + self.formatter.start_value(&mut self.writer)?; + write!(&mut self.writer, "{}", value).map_err(From::from) + } + + #[inline] + fn serialize_u32(self, value: u32) -> Result<()> { + self.formatter.start_value(&mut self.writer)?; + write!(&mut self.writer, "{}", value).map_err(From::from) + } + + #[inline] + fn serialize_u64(self, value: u64) -> Result<()> { + self.formatter.start_value(&mut self.writer)?; + write!(&mut self.writer, "{}", value).map_err(From::from) + } + + #[inline] + fn serialize_f32(self, value: f32) -> Result<()> { + self.formatter.start_value(&mut self.writer)?; + fmt_f32_or_null(&mut self.writer, if value == -0f32 { 0f32 } else { value }) + .map_err(From::from) + } + + #[inline] + fn serialize_f64(self, value: f64) -> Result<()> { + self.formatter.start_value(&mut self.writer)?; + fmt_f64_or_null(&mut self.writer, if value == -0f64 { 0f64 } else { value }) + .map_err(From::from) + } + + #[inline] + fn serialize_char(self, value: char) -> Result<()> { + self.formatter.start_value(&mut self.writer)?; + escape_char(&mut self.writer, value).map_err(From::from) + } + + #[inline] + fn serialize_str(self, value: &str) -> Result<()> { + quote_str(&mut self.writer, &mut self.formatter, value).map_err(From::from) + } + + #[inline] + fn serialize_bytes(self, value: &[u8]) -> Result<()> { + let mut seq = self.serialize_seq(Some(value.len()))?; + for byte in value { + ser::SerializeSeq::serialize_element(&mut seq, byte)? + } + ser::SerializeSeq::end(seq) + } + + #[inline] + fn serialize_unit(self) -> Result<()> { + self.formatter.start_value(&mut self.writer)?; + self.writer.write_all(b"null").map_err(From::from) + } + + #[inline] + fn serialize_unit_struct(self, _name: &'static str) -> Result<()> { + self.serialize_unit() + } + + #[inline] + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + ) -> Result<()> { + self.serialize_str(variant) + } + + /// Serialize newtypes without an object wrapper. + #[inline] + fn serialize_newtype_struct(self, _name: &'static str, value: &T) -> Result<()> + where + T: ?Sized + ser::Serialize, + { + value.serialize(self) + } + + #[inline] + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + value: &T, + ) -> Result<()> + where + T: ?Sized + ser::Serialize, + { + self.formatter.open(&mut self.writer, b'{')?; + self.formatter.comma(&mut self.writer, true)?; + escape_key(&mut self.writer, variant)?; + self.formatter.colon(&mut self.writer)?; + value.serialize(&mut *self)?; + self.formatter.close(&mut self.writer, b'}') + } + + #[inline] + fn serialize_none(self) -> Result<()> { + self.serialize_unit() + } + + #[inline] + fn serialize_some(self, value: &V) -> Result<()> + where + V: ?Sized + ser::Serialize, + { + value.serialize(self) + } + + #[inline] + fn serialize_seq(self, len: Option) -> Result { + let state = if len == Some(0) { + self.formatter.start_value(&mut self.writer)?; + self.writer.write_all(b"[]")?; + State::Empty + } else { + self.formatter.open(&mut self.writer, b'[')?; + State::First + }; + Ok(Compound { ser: self, state }) + } + + #[inline] + fn serialize_tuple(self, len: usize) -> Result { + self.serialize_seq(Some(len)) + } + + #[inline] + fn serialize_tuple_struct( + self, + _name: &'static str, + len: usize, + ) -> Result { + self.serialize_seq(Some(len)) + } + + #[inline] + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + len: usize, + ) -> Result { + self.formatter.open(&mut self.writer, b'{')?; + self.formatter.comma(&mut self.writer, true)?; + escape_key(&mut self.writer, variant)?; + self.formatter.colon(&mut self.writer)?; + self.serialize_seq(Some(len)) + } + + #[inline] + fn serialize_map(self, len: Option) -> Result { + let state = if len == Some(0) { + self.formatter.start_value(&mut self.writer)?; + self.writer.write_all(b"{}")?; + State::Empty + } else { + self.formatter.open(&mut self.writer, b'{')?; + State::First + }; + Ok(Compound { ser: self, state }) + } + + #[inline] + fn serialize_struct(self, _name: &'static str, len: usize) -> Result { + self.serialize_map(Some(len)) + } + + #[inline] + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + len: usize, + ) -> Result { + self.formatter.open(&mut self.writer, b'{')?; + self.formatter.comma(&mut self.writer, true)?; + escape_key(&mut self.writer, variant)?; + self.formatter.colon(&mut self.writer)?; + self.serialize_map(Some(len)) + } +} + +impl<'a, W, F> ser::SerializeSeq for Compound<'a, W, F> +where + W: io::Write, + F: Formatter, +{ + type Ok = (); + type Error = Error; + + fn serialize_element(&mut self, value: &T) -> Result<()> + where + T: serde::Serialize, + { + self.ser + .formatter + .comma(&mut self.ser.writer, self.state == State::First)?; + self.state = State::Rest; + value.serialize(&mut *self.ser) + } + + fn end(self) -> Result { + match self.state { + State::Empty => Ok(()), + _ => self.ser.formatter.close(&mut self.ser.writer, b']'), + } + } +} + +impl<'a, W, F> ser::SerializeTuple for Compound<'a, W, F> +where + W: io::Write, + F: Formatter, +{ + type Ok = (); + type Error = Error; + + fn serialize_element(&mut self, value: &T) -> Result<()> + where + T: serde::Serialize, + { + ser::SerializeSeq::serialize_element(self, value) + } + + fn end(self) -> Result { + ser::SerializeSeq::end(self) + } +} + +impl<'a, W, F> ser::SerializeTupleStruct for Compound<'a, W, F> +where + W: io::Write, + F: Formatter, +{ + type Ok = (); + type Error = Error; + + fn serialize_field(&mut self, value: &T) -> Result<()> + where + T: serde::Serialize, + { + ser::SerializeSeq::serialize_element(self, value) + } + + fn end(self) -> Result { + ser::SerializeSeq::end(self) + } +} + +impl<'a, W, F> ser::SerializeTupleVariant for Compound<'a, W, F> +where + W: io::Write, + F: Formatter, +{ + type Ok = (); + type Error = Error; + + fn serialize_field(&mut self, value: &T) -> Result<()> + where + T: serde::Serialize, + { + ser::SerializeSeq::serialize_element(self, value) + } + + fn end(self) -> Result { + match self.state { + State::Empty => {} + _ => self.ser.formatter.close(&mut self.ser.writer, b']')?, + } + self.ser.formatter.close(&mut self.ser.writer, b'}') + } +} + +impl<'a, W, F> ser::SerializeMap for Compound<'a, W, F> +where + W: io::Write, + F: Formatter, +{ + type Ok = (); + type Error = Error; + + fn serialize_key(&mut self, key: &T) -> Result<()> + where + T: serde::Serialize, + { + self.ser + .formatter + .comma(&mut self.ser.writer, self.state == State::First)?; + self.state = State::Rest; + + key.serialize(MapKeySerializer { ser: self.ser })?; + + self.ser.formatter.colon(&mut self.ser.writer) + } + + fn serialize_value(&mut self, value: &T) -> Result<()> + where + T: serde::Serialize, + { + value.serialize(&mut *self.ser) + } + + fn end(self) -> Result { + match self.state { + State::Empty => Ok(()), + _ => self.ser.formatter.close(&mut self.ser.writer, b'}'), + } + } +} + +impl<'a, W, F> ser::SerializeStruct for Compound<'a, W, F> +where + W: io::Write, + F: Formatter, +{ + type Ok = (); + type Error = Error; + + fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<()> + where + T: serde::Serialize, + { + ser::SerializeMap::serialize_entry(self, key, value) + } + + fn end(self) -> Result { + ser::SerializeMap::end(self) + } +} + +impl<'a, W, F> ser::SerializeStructVariant for Compound<'a, W, F> +where + W: io::Write, + F: Formatter, +{ + type Ok = (); + type Error = Error; + + fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<()> + where + T: serde::Serialize, + { + ser::SerializeStruct::serialize_field(self, key, value) + } + + fn end(self) -> Result { + match self.state { + State::Empty => {} + _ => self.ser.formatter.close(&mut self.ser.writer, b'}')?, + } + self.ser.formatter.close(&mut self.ser.writer, b'}') + } +} + +struct MapKeySerializer<'a, W: 'a, F: 'a> { + ser: &'a mut Serializer, +} + +impl<'a, W, F> ser::Serializer for MapKeySerializer<'a, W, F> +where + W: io::Write, + F: Formatter, +{ + type Ok = (); + type Error = Error; + + #[inline] + fn serialize_str(self, value: &str) -> Result<()> { + escape_key(&mut self.ser.writer, value).map_err(From::from) + } + + type SerializeSeq = ser::Impossible<(), Error>; + type SerializeTuple = ser::Impossible<(), Error>; + type SerializeTupleStruct = ser::Impossible<(), Error>; + type SerializeTupleVariant = ser::Impossible<(), Error>; + type SerializeMap = ser::Impossible<(), Error>; + type SerializeStruct = ser::Impossible<(), Error>; + type SerializeStructVariant = ser::Impossible<(), Error>; + + fn serialize_bool(self, _value: bool) -> Result<()> { + Err(Error::Syntax(ErrorCode::KeyMustBeAString, 0, 0)) + } + + fn serialize_i8(self, _value: i8) -> Result<()> { + Err(Error::Syntax(ErrorCode::KeyMustBeAString, 0, 0)) + } + + fn serialize_i16(self, _value: i16) -> Result<()> { + Err(Error::Syntax(ErrorCode::KeyMustBeAString, 0, 0)) + } + + fn serialize_i32(self, _value: i32) -> Result<()> { + Err(Error::Syntax(ErrorCode::KeyMustBeAString, 0, 0)) + } + + fn serialize_i64(self, _value: i64) -> Result<()> { + Err(Error::Syntax(ErrorCode::KeyMustBeAString, 0, 0)) + } + + fn serialize_u8(self, _value: u8) -> Result<()> { + Err(Error::Syntax(ErrorCode::KeyMustBeAString, 0, 0)) + } + + fn serialize_u16(self, _value: u16) -> Result<()> { + Err(Error::Syntax(ErrorCode::KeyMustBeAString, 0, 0)) + } + + fn serialize_u32(self, _value: u32) -> Result<()> { + Err(Error::Syntax(ErrorCode::KeyMustBeAString, 0, 0)) + } + + fn serialize_u64(self, _value: u64) -> Result<()> { + Err(Error::Syntax(ErrorCode::KeyMustBeAString, 0, 0)) + } + + fn serialize_f32(self, _value: f32) -> Result<()> { + Err(Error::Syntax(ErrorCode::KeyMustBeAString, 0, 0)) + } + + fn serialize_f64(self, _value: f64) -> Result<()> { + Err(Error::Syntax(ErrorCode::KeyMustBeAString, 0, 0)) + } + + fn serialize_char(self, _value: char) -> Result<()> { + Err(Error::Syntax(ErrorCode::KeyMustBeAString, 0, 0)) + } + + fn serialize_bytes(self, _value: &[u8]) -> Result<()> { + Err(Error::Syntax(ErrorCode::KeyMustBeAString, 0, 0)) + } + + fn serialize_unit(self) -> Result<()> { + Err(Error::Syntax(ErrorCode::KeyMustBeAString, 0, 0)) + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result<()> { + Err(Error::Syntax(ErrorCode::KeyMustBeAString, 0, 0)) + } + + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + ) -> Result<()> { + Err(Error::Syntax(ErrorCode::KeyMustBeAString, 0, 0)) + } + + fn serialize_newtype_struct(self, _name: &'static str, _value: &T) -> Result<()> + where + T: ?Sized + ser::Serialize, + { + Err(Error::Syntax(ErrorCode::KeyMustBeAString, 0, 0)) + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result<()> + where + T: ?Sized + ser::Serialize, + { + Err(Error::Syntax(ErrorCode::KeyMustBeAString, 0, 0)) + } + + fn serialize_none(self) -> Result<()> { + Err(Error::Syntax(ErrorCode::KeyMustBeAString, 0, 0)) + } + + fn serialize_some(self, _value: &T) -> Result<()> + where + T: ?Sized + ser::Serialize, + { + Err(Error::Syntax(ErrorCode::KeyMustBeAString, 0, 0)) + } + + fn serialize_seq(self, _len: Option) -> Result { + Err(Error::Syntax(ErrorCode::KeyMustBeAString, 0, 0)) + } + + fn serialize_tuple(self, _len: usize) -> Result { + Err(Error::Syntax(ErrorCode::KeyMustBeAString, 0, 0)) + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(Error::Syntax(ErrorCode::KeyMustBeAString, 0, 0)) + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(Error::Syntax(ErrorCode::KeyMustBeAString, 0, 0)) + } + + fn serialize_map(self, _len: Option) -> Result { + Err(Error::Syntax(ErrorCode::KeyMustBeAString, 0, 0)) + } + + fn serialize_struct(self, _name: &'static str, _len: usize) -> Result { + Err(Error::Syntax(ErrorCode::KeyMustBeAString, 0, 0)) + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(Error::Syntax(ErrorCode::KeyMustBeAString, 0, 0)) + } +} + +/// This trait abstracts away serializing the JSON control characters +pub trait Formatter { + /// Called when serializing a '{' or '['. + fn open(&mut self, writer: &mut W, ch: u8) -> Result<()> + where + W: io::Write; + + /// Called when serializing a ','. + fn comma(&mut self, writer: &mut W, first: bool) -> Result<()> + where + W: io::Write; + + /// Called when serializing a ':'. + fn colon(&mut self, writer: &mut W) -> Result<()> + where + W: io::Write; + + /// Called when serializing a '}' or ']'. + fn close(&mut self, writer: &mut W, ch: u8) -> Result<()> + where + W: io::Write; + + /// Newline with indent. + fn newline(&mut self, writer: &mut W, add_indent: i32) -> Result<()> + where + W: io::Write; + + /// Start a value. + fn start_value(&mut self, writer: &mut W) -> Result<()> + where + W: io::Write; +} + +struct HjsonFormatter<'a> { + current_indent: usize, + current_is_array: bool, + stack: Vec, + at_colon: bool, + indent: &'a [u8], + braces_same_line: bool, +} + +impl<'a> HjsonFormatter<'a> { + /// Construct a formatter that defaults to using two spaces for indentation. + pub fn new() -> Self { + HjsonFormatter::with_indent(b" ") + } + + /// Construct a formatter that uses the `indent` string for indentation. + pub fn with_indent(indent: &'a [u8]) -> Self { + HjsonFormatter { + current_indent: 0, + current_is_array: false, + stack: Vec::new(), + at_colon: false, + indent, + braces_same_line: false, + } + } +} + +impl<'a> Formatter for HjsonFormatter<'a> { + fn open(&mut self, writer: &mut W, ch: u8) -> Result<()> + where + W: io::Write, + { + if self.current_indent > 0 && !self.current_is_array && !self.braces_same_line { + self.newline(writer, 0)?; + } else { + self.start_value(writer)?; + } + self.current_indent += 1; + self.stack.push(self.current_is_array); + self.current_is_array = ch == b'['; + writer.write_all(&[ch]).map_err(From::from) + } + + fn comma(&mut self, writer: &mut W, _: bool) -> Result<()> + where + W: io::Write, + { + writer.write_all(b"\n")?; + indent(writer, self.current_indent, self.indent) + } + + fn colon(&mut self, writer: &mut W) -> Result<()> + where + W: io::Write, + { + self.at_colon = !self.braces_same_line; + writer + .write_all(if self.braces_same_line { b": " } else { b":" }) + .map_err(From::from) + } + + fn close(&mut self, writer: &mut W, ch: u8) -> Result<()> + where + W: io::Write, + { + self.current_indent -= 1; + self.current_is_array = self.stack.pop().expect("Internal error: json parsing"); + writer.write_all(b"\n")?; + indent(writer, self.current_indent, self.indent)?; + writer.write_all(&[ch]).map_err(From::from) + } + + fn newline(&mut self, writer: &mut W, add_indent: i32) -> Result<()> + where + W: io::Write, + { + self.at_colon = false; + writer.write_all(b"\n")?; + let ii = self.current_indent as i32 + add_indent; + indent(writer, if ii < 0 { 0 } else { ii as usize }, self.indent) + } + + fn start_value(&mut self, writer: &mut W) -> Result<()> + where + W: io::Write, + { + if self.at_colon { + self.at_colon = false; + writer.write_all(b" ")? + } + Ok(()) + } +} + +/// Serializes and escapes a `&[u8]` into a Hjson string. +#[inline] +pub fn escape_bytes(wr: &mut W, bytes: &[u8]) -> Result<()> +where + W: io::Write, +{ + wr.write_all(b"\"")?; + + let mut start = 0; + + for (i, byte) in bytes.iter().enumerate() { + let escaped = match *byte { + b'"' => b"\\\"", + b'\\' => b"\\\\", + b'\x08' => b"\\b", + b'\x0c' => b"\\f", + b'\n' => b"\\n", + b'\r' => b"\\r", + b'\t' => b"\\t", + _ => { + continue; + } + }; + + if start < i { + wr.write_all(&bytes[start..i])?; + } + + wr.write_all(escaped)?; + + start = i + 1; + } + + if start != bytes.len() { + wr.write_all(&bytes[start..])?; + } + + wr.write_all(b"\"")?; + Ok(()) +} + +/// Serializes and escapes a `&str` into a Hjson string. +#[inline] +pub fn quote_str(wr: &mut W, formatter: &mut F, value: &str) -> Result<()> +where + W: io::Write, + F: Formatter, +{ + lazy_static! { + // NEEDS_ESCAPE tests if the string can be written without escapes + static ref NEEDS_ESCAPE: Regex = Regex::new("[\\\\\"\x00-\x1f\x7f-\u{9f}\u{00ad}\u{0600}-\u{0604}\u{070f}\u{17b4}\u{17b5}\u{200c}-\u{200f}\u{2028}-\u{202f}\u{2060}-\u{206f}\u{feff}\u{fff0}-\u{ffff}]").expect("Internal error: json parsing"); + // NEEDS_QUOTES tests if the string can be written as a quoteless string (includes needsEscape but without \\ and \") + static ref NEEDS_QUOTES: Regex = Regex::new("^\\s|^\"|^'''|^#|^/\\*|^//|^\\{|^\\}|^\\[|^\\]|^:|^,|\\s$|[\x00-\x1f\x7f-\u{9f}\u{00ad}\u{0600}-\u{0604}\u{070f}\u{17b4}\u{17b5}\u{200c}-\u{200f}\u{2028}-\u{202f}\u{2060}-\u{206f}\u{feff}\u{fff0}-\u{ffff}]").expect("Internal error: json parsing"); + // NEEDS_ESCAPEML tests if the string can be written as a multiline string (includes needsEscape but without \n, \r, \\ and \") + static ref NEEDS_ESCAPEML: Regex = Regex::new("'''|[\x00-\x09\x0b\x0c\x0e-\x1f\x7f-\u{9f}\u{00ad}\u{0600}-\u{0604}\u{070f}\u{17b4}\u{17b5}\u{200c}-\u{200f}\u{2028}-\u{202f}\u{2060}-\u{206f}\u{feff}\u{fff0}-\u{ffff}]").expect("Internal error: json parsing"); + // starts with a keyword and optionally is followed by a comment + static ref STARTS_WITH_KEYWORD: Regex = Regex::new(r#"^(true|false|null)\s*((,|\]|\}|#|//|/\*).*)?$"#).expect("Internal error: json parsing"); + } + + if value.is_empty() { + formatter.start_value(wr)?; + return escape_bytes(wr, value.as_bytes()); + } + + // Check if we can insert this string without quotes + // see hjson syntax (must not parse as true, false, null or number) + + let mut pn = ParseNumber::new(value.bytes()); + let is_number = pn.parse(true).is_ok(); + + if is_number || NEEDS_QUOTES.is_match(value) || STARTS_WITH_KEYWORD.is_match(value) { + // First check if the string can be expressed in multiline format or + // we must replace the offending characters with safe escape sequences. + + if NEEDS_ESCAPE.is_match(value) && !NEEDS_ESCAPEML.is_match(value) + /* && !isRootObject */ + { + ml_str(wr, formatter, value) + } else { + formatter.start_value(wr)?; + escape_bytes(wr, value.as_bytes()) + } + } else { + // without quotes + formatter.start_value(wr)?; + wr.write_all(value.as_bytes()).map_err(From::from) + } +} + +/// Serializes and escapes a `&str` into a multiline Hjson string. +pub fn ml_str(wr: &mut W, formatter: &mut F, value: &str) -> Result<()> +where + W: io::Write, + F: Formatter, +{ + // wrap the string into the ''' (multiline) format + + let a: Vec<&str> = value.split('\n').collect(); + + if a.len() == 1 { + // The string contains only a single line. We still use the multiline + // format as it avoids escaping the \ character (e.g. when used in a + // regex). + formatter.start_value(wr)?; + wr.write_all(b"'''")?; + wr.write_all(a[0].as_bytes())?; + wr.write_all(b"'''")? + } else { + formatter.newline(wr, 1)?; + wr.write_all(b"'''")?; + for line in a { + formatter.newline(wr, if !line.is_empty() { 1 } else { -999 })?; + wr.write_all(line.as_bytes())?; + } + formatter.newline(wr, 1)?; + wr.write_all(b"'''")?; + } + Ok(()) +} + +/// Serializes and escapes a `&str` into a Hjson key. +#[inline] +pub fn escape_key(wr: &mut W, value: &str) -> Result<()> +where + W: io::Write, +{ + lazy_static! { + static ref NEEDS_ESCAPE_NAME: Regex = + Regex::new(r#"[,\{\[\}\]\s:#"]|//|/\*|'''|^$"#).expect("Internal error: json parsing"); + } + + // Check if we can insert this name without quotes + if NEEDS_ESCAPE_NAME.is_match(value) { + escape_bytes(wr, value.as_bytes()).map_err(From::from) + } else { + wr.write_all(value.as_bytes()).map_err(From::from) + } +} + +#[inline] +fn escape_char(wr: &mut W, value: char) -> Result<()> +where + W: io::Write, +{ + let mut scratch = [0_u8; 4]; + escape_bytes(wr, value.encode_utf8(&mut scratch).as_bytes()) +} + +fn fmt_f32_or_null(wr: &mut W, value: f32) -> Result<()> +where + W: io::Write, +{ + match value.classify() { + FpCategory::Nan | FpCategory::Infinite => wr.write_all(b"null")?, + _ => wr.write_all(fmt_small(value).as_bytes())?, + } + + Ok(()) +} + +fn fmt_f64_or_null(wr: &mut W, value: f64) -> Result<()> +where + W: io::Write, +{ + match value.classify() { + FpCategory::Nan | FpCategory::Infinite => wr.write_all(b"null")?, + _ => wr.write_all(fmt_small(value).as_bytes())?, + } + + Ok(()) +} + +fn indent(wr: &mut W, n: usize, s: &[u8]) -> Result<()> +where + W: io::Write, +{ + for _ in 0..n { + wr.write_all(s)?; + } + + Ok(()) +} + +// format similar to es6 +fn fmt_small(value: N) -> String +where + N: Display + LowerExp, +{ + let f1 = value.to_string(); + let f2 = format!("{:e}", value); + if f1.len() <= f2.len() + 1 { + f1 + } else if !f2.contains("e-") { + f2.replace("e", "e+") + } else { + f2 + } +} + +/// Encode the specified struct into a Hjson `[u8]` writer. +#[inline] +pub fn to_writer(writer: &mut W, value: &T) -> Result<()> +where + W: io::Write, + T: ser::Serialize, +{ + let mut ser = Serializer::new(writer); + value.serialize(&mut ser)?; + Ok(()) +} + +/// Encode the specified struct into a Hjson `[u8]` buffer. +#[inline] +pub fn to_vec(value: &T) -> Result> +where + T: ser::Serialize, +{ + // We are writing to a Vec, which doesn't fail. So we can ignore + // the error. + let mut writer = Vec::with_capacity(128); + to_writer(&mut writer, value)?; + Ok(writer) +} + +/// Encode the specified struct into a Hjson `String` buffer. +#[inline] +pub fn to_string(value: &T) -> Result +where + T: ser::Serialize, +{ + let vec = to_vec(value)?; + let string = String::from_utf8(vec)?; + Ok(string) +} diff --git a/crates/nu-json/src/util.rs b/old_nushell/crates/nu-json/src/util.rs similarity index 100% rename from crates/nu-json/src/util.rs rename to old_nushell/crates/nu-json/src/util.rs diff --git a/crates/nu-json/src/value.rs b/old_nushell/crates/nu-json/src/value.rs similarity index 100% rename from crates/nu-json/src/value.rs rename to old_nushell/crates/nu-json/src/value.rs diff --git a/old_nushell/crates/nu-json/tests/main.rs b/old_nushell/crates/nu-json/tests/main.rs new file mode 100644 index 0000000000..9b20a6b49c --- /dev/null +++ b/old_nushell/crates/nu-json/tests/main.rs @@ -0,0 +1,213 @@ +extern crate nu_json; +extern crate nu_test_support; +extern crate serde; +extern crate serde_json; + +use nu_json::Value; +use regex::Regex; +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; + +fn txt(text: &str) -> String { + let out = String::from_utf8_lossy(text.as_bytes()); + + #[cfg(windows)] + { + out.replace("\r\n", "").replace("\n", "") + } + + #[cfg(not(windows))] + { + out.to_string() + } +} + +fn hjson_expectations() -> PathBuf { + let assets = nu_test_support::fs::assets().join("nu_json"); + + nu_path::canonicalize(assets.clone()).unwrap_or_else(|e| { + panic!( + "Couldn't canonicalize hjson assets path {}: {:?}", + assets.display(), + e + ) + }) +} + +fn get_test_content(name: &str) -> io::Result { + let expectations = hjson_expectations(); + + let mut p = format!("{}/{}_test.hjson", expectations.display(), name); + + if !Path::new(&p).exists() { + p = format!("{}/{}_test.json", expectations.display(), name); + } + + fs::read_to_string(&p) +} + +fn get_result_content(name: &str) -> io::Result<(String, String)> { + let expectations = hjson_expectations(); + + let p1 = format!("{}/{}_result.json", expectations.display(), name); + let p2 = format!("{}/{}_result.hjson", expectations.display(), name); + + Ok((fs::read_to_string(&p1)?, fs::read_to_string(&p2)?)) +} + +macro_rules! run_test { + // {{ is a workaround for rust stable + ($v: ident, $list: expr, $fix: expr) => {{ + let name = stringify!($v); + $list.push(format!("{}_test", name)); + println!("- running {}", name); + let should_fail = name.starts_with("fail"); + let test_content = get_test_content(name).unwrap(); + let data: nu_json::Result = nu_json::from_str(&test_content); + assert!(should_fail == data.is_err()); + + if !should_fail { + let udata = data.unwrap(); + let (rjson, rhjson) = get_result_content(name).unwrap(); + let rjson = txt(&rjson); + let rhjson = txt(&rhjson); + let actual_hjson = nu_json::to_string(&udata).unwrap(); + let actual_hjson = txt(&actual_hjson); + let actual_json = $fix(serde_json::to_string_pretty(&udata).unwrap()); + let actual_json = txt(&actual_json); + if rhjson != actual_hjson { + println!( + "{:?}\n---hjson expected\n{}\n---hjson actual\n{}\n---\n", + name, rhjson, actual_hjson + ); + } + if rjson != actual_json { + println!( + "{:?}\n---json expected\n{}\n---json actual\n{}\n---\n", + name, rjson, actual_json + ); + } + assert!(rhjson == actual_hjson && rjson == actual_json); + } + }}; +} + +// add fixes where rust's json differs from javascript + +fn std_fix(json: String) -> String { + // serde_json serializes integers with a superfluous .0 suffix + let re = Regex::new(r"(?m)(?P\d)\.0(?P,?)$").unwrap(); + re.replace_all(&json, "$d$s").to_string() +} + +fn fix_kan(json: String) -> String { + std_fix(json).replace(" -0,", " 0,") +} + +fn fix_pass1(json: String) -> String { + std_fix(json) + .replace("1.23456789e34", "1.23456789e+34") + .replace("2.3456789012e76", "2.3456789012e+76") +} + +#[test] +fn test_hjson() { + let mut done: Vec = Vec::new(); + + println!(); + run_test!(charset, done, std_fix); + run_test!(comments, done, std_fix); + run_test!(empty, done, std_fix); + run_test!(failCharset1, done, std_fix); + run_test!(failJSON02, done, std_fix); + run_test!(failJSON05, done, std_fix); + run_test!(failJSON06, done, std_fix); + run_test!(failJSON07, done, std_fix); + run_test!(failJSON08, done, std_fix); + run_test!(failJSON10, done, std_fix); + run_test!(failJSON11, done, std_fix); + run_test!(failJSON12, done, std_fix); + run_test!(failJSON13, done, std_fix); + run_test!(failJSON14, done, std_fix); + run_test!(failJSON15, done, std_fix); + run_test!(failJSON16, done, std_fix); + run_test!(failJSON17, done, std_fix); + run_test!(failJSON19, done, std_fix); + run_test!(failJSON20, done, std_fix); + run_test!(failJSON21, done, std_fix); + run_test!(failJSON22, done, std_fix); + run_test!(failJSON23, done, std_fix); + run_test!(failJSON24, done, std_fix); + run_test!(failJSON26, done, std_fix); + run_test!(failJSON28, done, std_fix); + run_test!(failJSON29, done, std_fix); + run_test!(failJSON30, done, std_fix); + run_test!(failJSON31, done, std_fix); + run_test!(failJSON32, done, std_fix); + run_test!(failJSON33, done, std_fix); + run_test!(failJSON34, done, std_fix); + run_test!(failKey1, done, std_fix); + run_test!(failKey2, done, std_fix); + run_test!(failKey3, done, std_fix); + run_test!(failKey4, done, std_fix); + run_test!(failMLStr1, done, std_fix); + run_test!(failObj1, done, std_fix); + run_test!(failObj2, done, std_fix); + run_test!(failObj3, done, std_fix); + run_test!(failStr1a, done, std_fix); + run_test!(failStr1b, done, std_fix); + run_test!(failStr1c, done, std_fix); + run_test!(failStr1d, done, std_fix); + run_test!(failStr2a, done, std_fix); + run_test!(failStr2b, done, std_fix); + run_test!(failStr2c, done, std_fix); + run_test!(failStr2d, done, std_fix); + run_test!(failStr3a, done, std_fix); + run_test!(failStr3b, done, std_fix); + run_test!(failStr3c, done, std_fix); + run_test!(failStr3d, done, std_fix); + run_test!(failStr4a, done, std_fix); + run_test!(failStr4b, done, std_fix); + run_test!(failStr4c, done, std_fix); + run_test!(failStr4d, done, std_fix); + run_test!(failStr5a, done, std_fix); + run_test!(failStr5b, done, std_fix); + run_test!(failStr5c, done, std_fix); + run_test!(failStr5d, done, std_fix); + run_test!(failStr6a, done, std_fix); + run_test!(failStr6b, done, std_fix); + run_test!(failStr6c, done, std_fix); + run_test!(failStr6d, done, std_fix); + run_test!(kan, done, fix_kan); + run_test!(keys, done, std_fix); + run_test!(oa, done, std_fix); + run_test!(pass1, done, fix_pass1); + run_test!(pass2, done, std_fix); + run_test!(pass3, done, std_fix); + run_test!(pass4, done, std_fix); + run_test!(passSingle, done, std_fix); + run_test!(root, done, std_fix); + run_test!(stringify1, done, std_fix); + run_test!(strings, done, std_fix); + run_test!(trail, done, std_fix); + + // check if we include all assets + let paths = fs::read_dir(hjson_expectations()).unwrap(); + + let all = paths + .map(|item| String::from(item.unwrap().path().file_stem().unwrap().to_str().unwrap())) + .filter(|x| x.contains("_test")); + + let missing = all + .into_iter() + .filter(|x| done.iter().find(|y| &x == y) == None) + .collect::>(); + + if !missing.is_empty() { + for item in missing { + println!("missing: {}", item); + } + panic!(); + } +} diff --git a/old_nushell/crates/nu-parser/Cargo.toml b/old_nushell/crates/nu-parser/Cargo.toml new file mode 100644 index 0000000000..eb937612ae --- /dev/null +++ b/old_nushell/crates/nu-parser/Cargo.toml @@ -0,0 +1,26 @@ +[package] +authors = ["The Nu Project Contributors"] +description = "Nushell parser" +edition = "2018" +license = "MIT" +name = "nu-parser" +version = "0.43.0" + +[dependencies] +bigdecimal = { package = "bigdecimal", version = "0.3.0", features = ["serde"] } +derive-new = "0.5.8" +indexmap = { version="1.6.1", features=["serde-1"] } +log = "0.4" +num-bigint = { version="0.4.3", features=["serde"] } +itertools = "0.10.0" +smart-default = "0.6.0" + +nu-errors = { version = "0.43.0", path="../nu-errors" } +nu-data = { version = "0.43.0", path="../nu-data" } +nu-path = { version = "0.43.0", path="../nu-path" } +nu-protocol = { version = "0.43.0", path="../nu-protocol" } +nu-source = { version = "0.43.0", path="../nu-source" } +nu-test-support = { version = "0.43.0", path="../nu-test-support" } + +[features] +stable = [] diff --git a/old_nushell/crates/nu-parser/src/errors.rs b/old_nushell/crates/nu-parser/src/errors.rs new file mode 100644 index 0000000000..cb576ebcf7 --- /dev/null +++ b/old_nushell/crates/nu-parser/src/errors.rs @@ -0,0 +1,18 @@ +// use std::fmt::Debug; + +// A combination of an informative parse error, and what has been successfully parsed so far +// #[derive(Debug)] +// pub struct ParseError { +// /// An informative cause for this parse error +// pub cause: nu_errors::ParseError, +// // /// What has been successfully parsed, if anything +// // pub partial: Option, +// } + +// pub type ParseResult = Result>; + +// impl From> for nu_errors::ShellError { +// fn from(e: ParseError) -> Self { +// e.cause.into() +// } +// } diff --git a/crates/nu-parser/src/flag.rs b/old_nushell/crates/nu-parser/src/flag.rs similarity index 100% rename from crates/nu-parser/src/flag.rs rename to old_nushell/crates/nu-parser/src/flag.rs diff --git a/crates/nu-parser/src/lex/lexer.rs b/old_nushell/crates/nu-parser/src/lex/lexer.rs similarity index 100% rename from crates/nu-parser/src/lex/lexer.rs rename to old_nushell/crates/nu-parser/src/lex/lexer.rs diff --git a/crates/nu-parser/src/lex/mod.rs b/old_nushell/crates/nu-parser/src/lex/mod.rs similarity index 100% rename from crates/nu-parser/src/lex/mod.rs rename to old_nushell/crates/nu-parser/src/lex/mod.rs diff --git a/crates/nu-parser/src/lex/tests.rs b/old_nushell/crates/nu-parser/src/lex/tests.rs similarity index 100% rename from crates/nu-parser/src/lex/tests.rs rename to old_nushell/crates/nu-parser/src/lex/tests.rs diff --git a/crates/nu-parser/src/lex/token_group.rs b/old_nushell/crates/nu-parser/src/lex/token_group.rs similarity index 100% rename from crates/nu-parser/src/lex/token_group.rs rename to old_nushell/crates/nu-parser/src/lex/token_group.rs diff --git a/crates/nu-parser/src/lex/tokens.rs b/old_nushell/crates/nu-parser/src/lex/tokens.rs similarity index 100% rename from crates/nu-parser/src/lex/tokens.rs rename to old_nushell/crates/nu-parser/src/lex/tokens.rs diff --git a/old_nushell/crates/nu-parser/src/lib.rs b/old_nushell/crates/nu-parser/src/lib.rs new file mode 100644 index 0000000000..ee120446ca --- /dev/null +++ b/old_nushell/crates/nu-parser/src/lib.rs @@ -0,0 +1,15 @@ +#[macro_use] +extern crate derive_new; + +mod errors; +mod flag; +mod lex; +mod parse; +mod scope; +mod shapes; + +pub use lex::lexer::{lex, parse_block, NewlineMode}; +pub use lex::tokens::{LiteBlock, LiteCommand, LiteGroup, LitePipeline}; +pub use parse::{classify_block, garbage, parse, parse_full_column_path, parse_math_expression}; +pub use scope::ParserScope; +pub use shapes::shapes; diff --git a/crates/nu-parser/src/parse.rs b/old_nushell/crates/nu-parser/src/parse.rs similarity index 100% rename from crates/nu-parser/src/parse.rs rename to old_nushell/crates/nu-parser/src/parse.rs diff --git a/crates/nu-parser/src/parse/def.rs b/old_nushell/crates/nu-parser/src/parse/def.rs similarity index 100% rename from crates/nu-parser/src/parse/def.rs rename to old_nushell/crates/nu-parser/src/parse/def.rs diff --git a/crates/nu-parser/src/parse/def/data_structs.rs b/old_nushell/crates/nu-parser/src/parse/def/data_structs.rs similarity index 100% rename from crates/nu-parser/src/parse/def/data_structs.rs rename to old_nushell/crates/nu-parser/src/parse/def/data_structs.rs diff --git a/crates/nu-parser/src/parse/def/primitives.rs b/old_nushell/crates/nu-parser/src/parse/def/primitives.rs similarity index 100% rename from crates/nu-parser/src/parse/def/primitives.rs rename to old_nushell/crates/nu-parser/src/parse/def/primitives.rs diff --git a/crates/nu-parser/src/parse/def/signature.rs b/old_nushell/crates/nu-parser/src/parse/def/signature.rs similarity index 100% rename from crates/nu-parser/src/parse/def/signature.rs rename to old_nushell/crates/nu-parser/src/parse/def/signature.rs diff --git a/crates/nu-parser/src/parse/def/tests.rs b/old_nushell/crates/nu-parser/src/parse/def/tests.rs similarity index 100% rename from crates/nu-parser/src/parse/def/tests.rs rename to old_nushell/crates/nu-parser/src/parse/def/tests.rs diff --git a/crates/nu-parser/src/parse/source.rs b/old_nushell/crates/nu-parser/src/parse/source.rs similarity index 100% rename from crates/nu-parser/src/parse/source.rs rename to old_nushell/crates/nu-parser/src/parse/source.rs diff --git a/crates/nu-parser/src/parse/util.rs b/old_nushell/crates/nu-parser/src/parse/util.rs similarity index 100% rename from crates/nu-parser/src/parse/util.rs rename to old_nushell/crates/nu-parser/src/parse/util.rs diff --git a/crates/nu-parser/src/scope.rs b/old_nushell/crates/nu-parser/src/scope.rs similarity index 100% rename from crates/nu-parser/src/scope.rs rename to old_nushell/crates/nu-parser/src/scope.rs diff --git a/crates/nu-parser/src/shapes.rs b/old_nushell/crates/nu-parser/src/shapes.rs similarity index 100% rename from crates/nu-parser/src/shapes.rs rename to old_nushell/crates/nu-parser/src/shapes.rs diff --git a/crates/nu-parser/tests/main.rs b/old_nushell/crates/nu-parser/tests/main.rs similarity index 100% rename from crates/nu-parser/tests/main.rs rename to old_nushell/crates/nu-parser/tests/main.rs diff --git a/old_nushell/crates/nu-path/Cargo.toml b/old_nushell/crates/nu-path/Cargo.toml new file mode 100644 index 0000000000..df7b77163b --- /dev/null +++ b/old_nushell/crates/nu-path/Cargo.toml @@ -0,0 +1,12 @@ +[package] +authors = ["The Nu Project Contributors"] +description = "Path handling library for Nushell" +edition = "2018" +license = "MIT" +name = "nu-path" +version = "0.43.0" + +[dependencies] +dirs-next = "2.0.0" +dunce = "1.0.1" + diff --git a/crates/nu-path/README.md b/old_nushell/crates/nu-path/README.md similarity index 100% rename from crates/nu-path/README.md rename to old_nushell/crates/nu-path/README.md diff --git a/old_nushell/crates/nu-path/src/dots.rs b/old_nushell/crates/nu-path/src/dots.rs new file mode 100644 index 0000000000..0439b36d95 --- /dev/null +++ b/old_nushell/crates/nu-path/src/dots.rs @@ -0,0 +1,259 @@ +use std::path::{is_separator, Component, Path, PathBuf}; + +const EXPAND_STR: &str = if cfg!(windows) { r"..\" } else { "../" }; + +fn handle_dots_push(string: &mut String, count: u8) { + if count < 1 { + return; + } + + if count == 1 { + string.push('.'); + return; + } + + for _ in 0..(count - 1) { + string.push_str(EXPAND_STR); + } + + string.pop(); // remove last '/' +} + +/// Expands any occurrence of more than two dots into a sequence of ../ (or ..\ on windows), e.g., +/// "..." into "../..", "...." into "../../../", etc. +pub fn expand_ndots(path: impl AsRef) -> PathBuf { + // Check if path is valid UTF-8 and if not, return it as it is to avoid breaking it via string + // conversion. + let path_str = match path.as_ref().to_str() { + Some(s) => s, + None => return path.as_ref().into(), + }; + + // find if we need to expand any >2 dot paths and early exit if not + let mut dots_count = 0u8; + let ndots_present = { + for chr in path_str.chars() { + if chr == '.' { + dots_count += 1; + } else { + if is_separator(chr) && (dots_count > 2) { + // this path component had >2 dots + break; + } + + dots_count = 0; + } + } + + dots_count > 2 + }; + + if !ndots_present { + return path.as_ref().into(); + } + + let mut dots_count = 0u8; + let mut expanded = String::new(); + for chr in path_str.chars() { + if chr == '.' { + dots_count += 1; + } else { + if is_separator(chr) { + // check for dots expansion only at path component boundaries + handle_dots_push(&mut expanded, dots_count); + dots_count = 0; + } else { + // got non-dot within path component => do not expand any dots + while dots_count > 0 { + expanded.push('.'); + dots_count -= 1; + } + } + expanded.push(chr); + } + } + + handle_dots_push(&mut expanded, dots_count); + + expanded.into() +} + +/// Expand "." and ".." into nothing and parent directory, respectively. +pub fn expand_dots(path: impl AsRef) -> PathBuf { + let path = path.as_ref(); + + // Early-exit if path does not contain '.' or '..' + if !path + .components() + .any(|c| std::matches!(c, Component::CurDir | Component::ParentDir)) + { + return path.into(); + } + + let mut result = PathBuf::with_capacity(path.as_os_str().len()); + + // Only pop/skip path elements if the previous one was an actual path element + let prev_is_normal = |p: &Path| -> bool { + p.components() + .next_back() + .map(|c| std::matches!(c, Component::Normal(_))) + .unwrap_or(false) + }; + + path.components().for_each(|component| match component { + Component::ParentDir if prev_is_normal(&result) => { + result.pop(); + } + Component::CurDir if prev_is_normal(&result) => {} + _ => result.push(component), + }); + + dunce::simplified(&result).to_path_buf() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn expand_two_dots() { + let path = Path::new("/foo/bar/.."); + + assert_eq!( + PathBuf::from("/foo"), // missing path + expand_dots(path) + ); + } + + #[test] + fn expand_dots_with_curdir() { + let path = Path::new("/foo/./bar/./baz"); + + assert_eq!(PathBuf::from("/foo/bar/baz"), expand_dots(path)); + } + + fn check_ndots_expansion(expected: &str, s: &str) { + let expanded = expand_ndots(Path::new(s)); + assert_eq!(Path::new(expected), &expanded); + } + + // common tests + #[test] + fn string_without_ndots() { + check_ndots_expansion("../hola", "../hola"); + } + + #[test] + fn string_with_three_ndots_and_chars() { + check_ndots_expansion("a...b", "a...b"); + } + + #[test] + fn string_with_two_ndots_and_chars() { + check_ndots_expansion("a..b", "a..b"); + } + + #[test] + fn string_with_one_dot_and_chars() { + check_ndots_expansion("a.b", "a.b"); + } + + #[test] + fn expand_dots_double_dots_no_change() { + // Can't resolve this as we don't know our parent dir + assert_eq!(Path::new(".."), expand_dots(Path::new(".."))); + } + + #[test] + fn expand_dots_single_dot_no_change() { + // Can't resolve this as we don't know our current dir + assert_eq!(Path::new("."), expand_dots(Path::new("."))); + } + + #[test] + fn expand_dots_multi_single_dots_no_change() { + assert_eq!(Path::new("././."), expand_dots(Path::new("././."))); + } + + #[test] + fn expand_multi_double_dots_no_change() { + assert_eq!(Path::new("../../../"), expand_dots(Path::new("../../../"))); + } + + #[test] + fn expand_dots_no_change_with_dirs() { + // Can't resolve this as we don't know our parent dir + assert_eq!( + Path::new("../../../dir1/dir2/"), + expand_dots(Path::new("../../../dir1/dir2")) + ); + } + + #[test] + fn expand_dots_simple() { + assert_eq!(Path::new("/foo"), expand_dots(Path::new("/foo/bar/.."))); + } + + #[test] + fn expand_dots_complex() { + assert_eq!( + Path::new("/test"), + expand_dots(Path::new("/foo/./bar/../../test/././test2/../")) + ); + } + + #[cfg(windows)] + mod windows { + use super::*; + + #[test] + fn string_with_three_ndots() { + check_ndots_expansion(r"..\..", "..."); + } + + #[test] + fn string_with_mixed_ndots_and_chars() { + check_ndots_expansion( + r"a...b/./c..d/../e.f/..\..\..//.", + "a...b/./c..d/../e.f/....//.", + ); + } + + #[test] + fn string_with_three_ndots_and_final_slash() { + check_ndots_expansion(r"..\../", ".../"); + } + + #[test] + fn string_with_three_ndots_and_garbage() { + check_ndots_expansion(r"ls ..\../ garbage.*[", "ls .../ garbage.*["); + } + } + + #[cfg(not(windows))] + mod non_windows { + use super::*; + #[test] + fn string_with_three_ndots() { + check_ndots_expansion(r"../..", "..."); + } + + #[test] + fn string_with_mixed_ndots_and_chars() { + check_ndots_expansion( + "a...b/./c..d/../e.f/../../..//.", + "a...b/./c..d/../e.f/....//.", + ); + } + + #[test] + fn string_with_three_ndots_and_final_slash() { + check_ndots_expansion("../../", ".../"); + } + + #[test] + fn string_with_three_ndots_and_garbage() { + check_ndots_expansion("ls ../../ garbage.*[", "ls .../ garbage.*["); + } + } +} diff --git a/old_nushell/crates/nu-path/src/expansions.rs b/old_nushell/crates/nu-path/src/expansions.rs new file mode 100644 index 0000000000..3393a5793f --- /dev/null +++ b/old_nushell/crates/nu-path/src/expansions.rs @@ -0,0 +1,75 @@ +use std::io; +use std::path::{Path, PathBuf}; + +use super::dots::{expand_dots, expand_ndots}; +use super::tilde::expand_tilde; + +// Join a path relative to another path. Paths starting with tilde are considered as absolute. +fn join_path_relative(path: P, relative_to: Q) -> PathBuf +where + P: AsRef, + Q: AsRef, +{ + let path = path.as_ref(); + let relative_to = relative_to.as_ref(); + + if path == Path::new(".") { + // Joining a Path with '.' appends a '.' at the end, making the prompt + // more ugly - so we don't do anything, which should result in an equal + // path on all supported systems. + relative_to.into() + } else if path.starts_with("~") { + // do not end up with "/some/path/~" + path.into() + } else { + relative_to.join(path) + } +} + +/// Resolve all symbolic links and all components (tilde, ., .., ...+) and return the path in its +/// absolute form. +/// +/// Fails under the same conditions as +/// [std::fs::canonicalize](https://doc.rust-lang.org/std/fs/fn.canonicalize.html). +pub fn canonicalize(path: impl AsRef) -> io::Result { + let path = expand_tilde(path); + let path = expand_ndots(path); + + dunce::canonicalize(path) +} + +/// Same as canonicalize() but the input path is specified relative to another path +pub fn canonicalize_with(path: P, relative_to: Q) -> io::Result +where + P: AsRef, + Q: AsRef, +{ + let path = join_path_relative(path, relative_to); + + canonicalize(path) +} + +/// Resolve only path components (tilde, ., .., ...+), if possible. +/// +/// The function works in a "best effort" mode: It does not fail but rather returns the unexpanded +/// version if the expansion is not possible. +/// +/// Furthermore, unlike canonicalize(), it does not use sys calls (such as readlink). +/// +/// Does not convert to absolute form nor does it resolve symlinks. +pub fn expand_path(path: impl AsRef) -> PathBuf { + let path = expand_tilde(path); + let path = expand_ndots(path); + expand_dots(path) +} + +/// Same as expand_path() but the input path is specified relative to another path +pub fn expand_path_with(path: P, relative_to: Q) -> PathBuf +where + P: AsRef, + Q: AsRef, +{ + let path = join_path_relative(path, relative_to); + + expand_path(path) +} diff --git a/old_nushell/crates/nu-path/src/lib.rs b/old_nushell/crates/nu-path/src/lib.rs new file mode 100644 index 0000000000..9606bc15cf --- /dev/null +++ b/old_nushell/crates/nu-path/src/lib.rs @@ -0,0 +1,8 @@ +mod dots; +mod expansions; +mod tilde; +mod util; + +pub use expansions::{canonicalize, canonicalize_with, expand_path, expand_path_with}; +pub use tilde::expand_tilde; +pub use util::trim_trailing_slash; diff --git a/old_nushell/crates/nu-path/src/tilde.rs b/old_nushell/crates/nu-path/src/tilde.rs new file mode 100644 index 0000000000..e1c7ec56a3 --- /dev/null +++ b/old_nushell/crates/nu-path/src/tilde.rs @@ -0,0 +1,85 @@ +use std::path::{Path, PathBuf}; + +fn expand_tilde_with(path: impl AsRef, home: Option) -> PathBuf { + let path = path.as_ref(); + + if !path.starts_with("~") { + return path.into(); + } + + match home { + None => path.into(), + Some(mut h) => { + if h == Path::new("/") { + // Corner case: `h` is a root directory; + // don't prepend extra `/`, just drop the tilde. + path.strip_prefix("~").unwrap_or(path).into() + } else { + if let Ok(p) = path.strip_prefix("~/") { + h.push(p) + } + h + } + } + } +} + +/// Expand tilde ("~") into a home directory if it is the first path component +pub fn expand_tilde(path: impl AsRef) -> PathBuf { + // TODO: Extend this to work with "~user" style of home paths + expand_tilde_with(path, dirs_next::home_dir()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn check_expanded(s: &str) { + let home = Path::new("/home"); + let buf = Some(PathBuf::from(home)); + assert!(expand_tilde_with(Path::new(s), buf).starts_with(&home)); + + // Tests the special case in expand_tilde for "/" as home + let home = Path::new("/"); + let buf = Some(PathBuf::from(home)); + assert!(!expand_tilde_with(Path::new(s), buf).starts_with("//")); + } + + fn check_not_expanded(s: &str) { + let home = PathBuf::from("/home"); + let expanded = expand_tilde_with(Path::new(s), Some(home)); + assert!(expanded == Path::new(s)); + } + + #[test] + fn string_with_tilde() { + check_expanded("~"); + } + + #[test] + fn string_with_tilde_forward_slash() { + check_expanded("~/test/"); + } + + #[test] + fn string_with_tilde_double_forward_slash() { + check_expanded("~//test/"); + } + + #[test] + fn does_not_expand_tilde_if_tilde_is_not_first_character() { + check_not_expanded("1~1"); + } + + #[cfg(windows)] + #[test] + fn string_with_tilde_backslash() { + check_expanded("~\\test/test2/test3"); + } + + #[cfg(windows)] + #[test] + fn string_with_double_tilde_backslash() { + check_expanded("~\\\\test\\test2/test3"); + } +} diff --git a/crates/nu-path/src/util.rs b/old_nushell/crates/nu-path/src/util.rs similarity index 100% rename from crates/nu-path/src/util.rs rename to old_nushell/crates/nu-path/src/util.rs diff --git a/crates/nu-path/tests/mod.rs b/old_nushell/crates/nu-path/tests/mod.rs similarity index 100% rename from crates/nu-path/tests/mod.rs rename to old_nushell/crates/nu-path/tests/mod.rs diff --git a/crates/nu-path/tests/util.rs b/old_nushell/crates/nu-path/tests/util.rs similarity index 100% rename from crates/nu-path/tests/util.rs rename to old_nushell/crates/nu-path/tests/util.rs diff --git a/old_nushell/crates/nu-plugin/Cargo.toml b/old_nushell/crates/nu-plugin/Cargo.toml new file mode 100644 index 0000000000..87c640d5bd --- /dev/null +++ b/old_nushell/crates/nu-plugin/Cargo.toml @@ -0,0 +1,22 @@ +[package] +authors = ["The Nu Project Contributors"] +description = "Nushell Plugin" +edition = "2018" +license = "MIT" +name = "nu-plugin" +version = "0.43.0" + +[lib] +doctest = false + +[dependencies] +nu-errors = { path="../nu-errors", version = "0.43.0" } +nu-protocol = { path="../nu-protocol", version = "0.43.0" } +nu-source = { path="../nu-source", version = "0.43.0" } +nu-test-support = { path="../nu-test-support", version = "0.43.0" } +nu-value-ext = { path="../nu-value-ext", version = "0.43.0" } +indexmap = { version="1.6.1", features=["serde-1"] } +serde = { version="1.0", features=["derive"] } +serde_json = "1.0" + +[build-dependencies] diff --git a/crates/nu-plugin/src/jsonrpc.rs b/old_nushell/crates/nu-plugin/src/jsonrpc.rs similarity index 100% rename from crates/nu-plugin/src/jsonrpc.rs rename to old_nushell/crates/nu-plugin/src/jsonrpc.rs diff --git a/old_nushell/crates/nu-plugin/src/lib.rs b/old_nushell/crates/nu-plugin/src/lib.rs new file mode 100644 index 0000000000..8b6d5122a7 --- /dev/null +++ b/old_nushell/crates/nu-plugin/src/lib.rs @@ -0,0 +1,6 @@ +pub mod jsonrpc; +mod plugin; + +pub mod test_helpers; + +pub use crate::plugin::{serve_plugin, Plugin}; diff --git a/crates/nu-plugin/src/plugin.rs b/old_nushell/crates/nu-plugin/src/plugin.rs similarity index 100% rename from crates/nu-plugin/src/plugin.rs rename to old_nushell/crates/nu-plugin/src/plugin.rs diff --git a/crates/nu-plugin/src/test_helpers.rs b/old_nushell/crates/nu-plugin/src/test_helpers.rs similarity index 100% rename from crates/nu-plugin/src/test_helpers.rs rename to old_nushell/crates/nu-plugin/src/test_helpers.rs diff --git a/crates/nu-pretty-hex/Cargo.lock b/old_nushell/crates/nu-pretty-hex/Cargo.lock similarity index 100% rename from crates/nu-pretty-hex/Cargo.lock rename to old_nushell/crates/nu-pretty-hex/Cargo.lock diff --git a/old_nushell/crates/nu-pretty-hex/Cargo.toml b/old_nushell/crates/nu-pretty-hex/Cargo.toml new file mode 100644 index 0000000000..cff77a5bc5 --- /dev/null +++ b/old_nushell/crates/nu-pretty-hex/Cargo.toml @@ -0,0 +1,27 @@ +[package] +authors = ["Andrei Volnin ", "The Nu Project Contributors"] +description = "Pretty hex dump of bytes slice in the common style." +edition = "2018" +license = "MIT" +name = "nu-pretty-hex" +version = "0.43.0" + +[lib] +doctest = false +name = "nu_pretty_hex" +path = "src/lib.rs" + +[[bin]] +name = "nu_pretty_hex" +path = "src/main.rs" + +[dependencies] +nu-ansi-term = { path="../nu-ansi-term", version = "0.43.0" } +rand = "0.8.3" + +[dev-dependencies] +heapless = { version = "0.7.8", default-features = false } + +# [features] +# default = ["alloc"] +# alloc = [] diff --git a/crates/nu-pretty-hex/LICENSE b/old_nushell/crates/nu-pretty-hex/LICENSE similarity index 100% rename from crates/nu-pretty-hex/LICENSE rename to old_nushell/crates/nu-pretty-hex/LICENSE diff --git a/crates/nu-pretty-hex/README.md b/old_nushell/crates/nu-pretty-hex/README.md similarity index 100% rename from crates/nu-pretty-hex/README.md rename to old_nushell/crates/nu-pretty-hex/README.md diff --git a/crates/nu-pretty-hex/src/lib.rs b/old_nushell/crates/nu-pretty-hex/src/lib.rs similarity index 100% rename from crates/nu-pretty-hex/src/lib.rs rename to old_nushell/crates/nu-pretty-hex/src/lib.rs diff --git a/old_nushell/crates/nu-pretty-hex/src/main.rs b/old_nushell/crates/nu-pretty-hex/src/main.rs new file mode 100644 index 0000000000..4156cb5c2a --- /dev/null +++ b/old_nushell/crates/nu-pretty-hex/src/main.rs @@ -0,0 +1,50 @@ +use nu_pretty_hex::*; + +fn main() { + let config = HexConfig { + title: true, + ascii: true, + width: 16, + group: 4, + chunk: 1, + skip: Some(10), + // length: Some(5), + // length: None, + length: Some(50), + }; + + let my_string = "Darren Schroeder 😉"; + println!("ConfigHex\n{}\n", config_hex(&my_string, config)); + println!("SimpleHex\n{}\n", simple_hex(&my_string)); + println!("PrettyHex\n{}\n", pretty_hex(&my_string)); + println!("ConfigHex\n{}\n", config_hex(&my_string, config)); + + // let mut my_str = String::new(); + // for x in 0..256 { + // my_str.push(x as u8); + // } + let mut v: Vec = vec![]; + for x in 0..=127 { + v.push(x); + } + let my_str = String::from_utf8_lossy(&v[..]); + + println!("First128\n{}\n", pretty_hex(&my_str.as_bytes())); + println!( + "First128-Param\n{}\n", + config_hex(&my_str.as_bytes(), config) + ); + + let mut r_str = String::new(); + for _ in 0..=127 { + r_str.push(rand::random::() as char); + } + + println!("Random127\n{}\n", pretty_hex(&r_str)); +} + +//chunk 0 44617272656e20536368726f65646572 Darren Schroeder +//chunk 1 44 61 72 72 65 6e 20 53 63 68 72 6f 65 64 65 72 Darren Schroeder +//chunk 2 461 7272 656e 2053 6368 726f 6564 6572 Darren Schroeder +//chunk 3 46172 72656e 205363 68726f 656465 72 Darren Schroeder +//chunk 4 44617272 656e2053 6368726f 65646572 Darren Schroeder diff --git a/old_nushell/crates/nu-pretty-hex/src/pretty_hex.rs b/old_nushell/crates/nu-pretty-hex/src/pretty_hex.rs new file mode 100644 index 0000000000..99ebaf022c --- /dev/null +++ b/old_nushell/crates/nu-pretty-hex/src/pretty_hex.rs @@ -0,0 +1,299 @@ +use core::primitive::str; +use core::{default::Default, fmt}; +use nu_ansi_term::{Color, Style}; + +/// Returns a one-line hexdump of `source` grouped in default format without header +/// and ASCII column. +pub fn simple_hex>(source: &T) -> String { + let mut writer = String::new(); + hex_write(&mut writer, source, HexConfig::simple(), None).unwrap_or(()); + writer +} + +/// Dump `source` as hex octets in default format without header and ASCII column to the `writer`. +pub fn simple_hex_write(writer: &mut W, source: &T) -> fmt::Result +where + T: AsRef<[u8]>, + W: fmt::Write, +{ + hex_write(writer, source, HexConfig::simple(), None) +} + +/// Return a multi-line hexdump in default format complete with addressing, hex digits, +/// and ASCII representation. +pub fn pretty_hex>(source: &T) -> String { + let mut writer = String::new(); + hex_write(&mut writer, source, HexConfig::default(), Some(true)).unwrap_or(()); + writer +} + +/// Write multi-line hexdump in default format complete with addressing, hex digits, +/// and ASCII representation to the writer. +pub fn pretty_hex_write(writer: &mut W, source: &T) -> fmt::Result +where + T: AsRef<[u8]>, + W: fmt::Write, +{ + hex_write(writer, source, HexConfig::default(), Some(true)) +} + +/// Return a hexdump of `source` in specified format. +pub fn config_hex>(source: &T, cfg: HexConfig) -> String { + let mut writer = String::new(); + hex_write(&mut writer, source, cfg, Some(true)).unwrap_or(()); + writer +} + +/// Configuration parameters for hexdump. +#[derive(Clone, Copy, Debug)] +pub struct HexConfig { + /// Write first line header with data length. + pub title: bool, + /// Append ASCII representation column. + pub ascii: bool, + /// Source bytes per row. 0 for single row without address prefix. + pub width: usize, + /// Chunks count per group. 0 for single group (column). + pub group: usize, + /// Source bytes per chunk (word). 0 for single word. + pub chunk: usize, + /// Bytes from 0 to skip + pub skip: Option, + /// Length to return + pub length: Option, +} + +/// Default configuration with `title`, `ascii`, 16 source bytes `width` grouped to 4 separate +/// hex bytes. Using in `pretty_hex`, `pretty_hex_write` and `fmt::Debug` implementation. +impl Default for HexConfig { + fn default() -> HexConfig { + HexConfig { + title: true, + ascii: true, + width: 16, + group: 4, + chunk: 1, + skip: None, + length: None, + } + } +} + +impl HexConfig { + /// Returns configuration for `simple_hex`, `simple_hex_write` and `fmt::Display` implementation. + pub fn simple() -> Self { + HexConfig::default().to_simple() + } + + fn delimiter(&self, i: usize) -> &'static str { + if i > 0 && self.chunk > 0 && i % self.chunk == 0 { + if self.group > 0 && i % (self.group * self.chunk) == 0 { + " " + } else { + " " + } + } else { + "" + } + } + + fn to_simple(self) -> Self { + HexConfig { + title: false, + ascii: false, + width: 0, + ..self + } + } +} + +fn categorize_byte(byte: &u8) -> (Style, Option) { + // This section is here so later we can configure these items + let null_char_style = Style::default().fg(Color::Fixed(242)); + let null_char = Some('0'); + let ascii_printable_style = Style::default().fg(Color::Cyan).bold(); + let ascii_printable = None; + let ascii_space_style = Style::default().fg(Color::Green).bold(); + let ascii_space = Some(' '); + let ascii_white_space_style = Style::default().fg(Color::Green).bold(); + let ascii_white_space = Some('_'); + let ascii_other_style = Style::default().fg(Color::Purple).bold(); + let ascii_other = Some('•'); + let non_ascii_style = Style::default().fg(Color::Yellow).bold(); + let non_ascii = Some('×'); // or Some('.') + + if byte == &0 { + (null_char_style, null_char) + } else if byte.is_ascii_graphic() { + (ascii_printable_style, ascii_printable) + } else if byte.is_ascii_whitespace() { + // 0x20 == 32 decimal - replace with a real space + if byte == &32 { + (ascii_space_style, ascii_space) + } else { + (ascii_white_space_style, ascii_white_space) + } + } else if byte.is_ascii() { + (ascii_other_style, ascii_other) + } else { + (non_ascii_style, non_ascii) + } +} + +/// Write hex dump in specified format. +pub fn hex_write( + writer: &mut W, + source: &T, + cfg: HexConfig, + with_color: Option, +) -> fmt::Result +where + T: AsRef<[u8]>, + W: fmt::Write, +{ + let use_color = with_color.unwrap_or(false); + + if source.as_ref().is_empty() { + return Ok(()); + } + + let amount = match cfg.length { + Some(len) => len, + None => source.as_ref().len(), + }; + + let skip = cfg.skip.unwrap_or(0); + + let source_part_vec: Vec = source + .as_ref() + .iter() + .skip(skip) + .take(amount) + .map(|&x| x as u8) + .collect(); + + if cfg.title { + if use_color { + writeln!( + writer, + "Length: {0} (0x{0:x}) bytes | {1}printable {2}whitespace {3}ascii_other {4}non_ascii{5}", + source_part_vec.len(), + Style::default().fg(Color::Cyan).bold().prefix(), + Style::default().fg(Color::Green).bold().prefix(), + Style::default().fg(Color::Purple).bold().prefix(), + Style::default().fg(Color::Yellow).bold().prefix(), + Style::default().fg(Color::Yellow).suffix() + )?; + } else { + writeln!(writer, "Length: {0} (0x{0:x}) bytes", source_part_vec.len(),)?; + } + } + + let lines = source_part_vec.chunks(if cfg.width > 0 { + cfg.width + } else { + source_part_vec.len() + }); + + let lines_len = lines.len(); + + for (i, row) in lines.enumerate() { + if cfg.width > 0 { + let style = Style::default().fg(Color::Cyan); + if use_color { + write!( + writer, + "{}{:08x}{}: ", + style.prefix(), + i * cfg.width + skip, + style.suffix() + )?; + } else { + write!(writer, "{:08x}: ", i * cfg.width + skip,)?; + } + } + for (i, x) in row.as_ref().iter().enumerate() { + if use_color { + let (style, _char) = categorize_byte(x); + write!( + writer, + "{}{}{:02x}{}", + cfg.delimiter(i), + style.prefix(), + x, + style.suffix() + )?; + } else { + write!(writer, "{}{:02x}", cfg.delimiter(i), x,)?; + } + } + if cfg.ascii { + for j in row.len()..cfg.width { + write!(writer, "{} ", cfg.delimiter(j))?; + } + write!(writer, " ")?; + for x in row { + let (style, a_char) = categorize_byte(x); + let replacement_char = match a_char { + Some(c) => c, + None => *x as char, + }; + if use_color { + write!( + writer, + "{}{}{}", + style.prefix(), + replacement_char, + style.suffix() + )?; + } else { + write!(writer, "{}", replacement_char,)?; + } + } + } + if i + 1 < lines_len { + writeln!(writer)?; + } + } + Ok(()) +} + +/// Reference wrapper for use in arguments formatting. +pub struct Hex<'a, T: 'a>(&'a T, HexConfig); + +impl<'a, T: 'a + AsRef<[u8]>> fmt::Display for Hex<'a, T> { + /// Formats the value by `simple_hex_write` using the given formatter. + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + hex_write(f, self.0, self.1.to_simple(), None) + } +} + +impl<'a, T: 'a + AsRef<[u8]>> fmt::Debug for Hex<'a, T> { + /// Formats the value by `pretty_hex_write` using the given formatter. + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + hex_write(f, self.0, self.1, None) + } +} + +/// Allows generates hex dumps to a formatter. +pub trait PrettyHex: Sized { + /// Wrap self reference for use in `std::fmt::Display` and `std::fmt::Debug` + /// formatting as hex dumps. + fn hex_dump(&self) -> Hex; + + /// Wrap self reference for use in `std::fmt::Display` and `std::fmt::Debug` + /// formatting as hex dumps in specified format. + fn hex_conf(&self, cfg: HexConfig) -> Hex; +} + +impl PrettyHex for T +where + T: AsRef<[u8]>, +{ + fn hex_dump(&self) -> Hex { + Hex(self, HexConfig::default()) + } + fn hex_conf(&self, cfg: HexConfig) -> Hex { + Hex(self, cfg) + } +} diff --git a/crates/nu-pretty-hex/tests/256.txt b/old_nushell/crates/nu-pretty-hex/tests/256.txt similarity index 100% rename from crates/nu-pretty-hex/tests/256.txt rename to old_nushell/crates/nu-pretty-hex/tests/256.txt diff --git a/crates/nu-pretty-hex/tests/data b/old_nushell/crates/nu-pretty-hex/tests/data similarity index 100% rename from crates/nu-pretty-hex/tests/data rename to old_nushell/crates/nu-pretty-hex/tests/data diff --git a/crates/nu-pretty-hex/tests/tests.rs b/old_nushell/crates/nu-pretty-hex/tests/tests.rs similarity index 100% rename from crates/nu-pretty-hex/tests/tests.rs rename to old_nushell/crates/nu-pretty-hex/tests/tests.rs diff --git a/old_nushell/crates/nu-protocol/Cargo.toml b/old_nushell/crates/nu-protocol/Cargo.toml new file mode 100644 index 0000000000..bb15832600 --- /dev/null +++ b/old_nushell/crates/nu-protocol/Cargo.toml @@ -0,0 +1,38 @@ +[package] +authors = ["The Nu Project Contributors"] +description = "Core values and protocols for Nushell" +edition = "2018" +license = "MIT" +name = "nu-protocol" +version = "0.43.0" + +[lib] +doctest = false + +[dependencies] +bigdecimal = { package = "bigdecimal", version = "0.3.0", features = ["serde"] } +byte-unit = "4.0.9" +chrono = { version="0.4.19", features=["serde"] } +chrono-humanize = "0.2.1" +derive-new = "0.5.8" +getset = "0.1.1" +indexmap = { version="1.6.1", features=["serde-1"] } +log = "0.4.14" +nu-errors = { path="../nu-errors", version = "0.43.0" } +nu-source = { path="../nu-source", version = "0.43.0" } +num-bigint = { version = "0.4.3", features = ["serde"] } +num-integer = "0.1.44" +num-traits = "0.2.14" +serde = { version="1.0", features=["derive"] } +serde_bytes = "0.11.5" + +[dependencies.polars] +version = "0.17.0" +optional = true +default-features = false +features = ["docs", "zip_with", "csv-file", "temporal", "performant", "pretty_fmt", "dtype-slim", "serde", "rows", "strings", "checked_arithmetic", "object", "dtype-date", "dtype-datetime", "dtype-time"] + +[features] +dataframe = ["polars"] + +[build-dependencies] diff --git a/crates/nu-protocol/src/call_info.rs b/old_nushell/crates/nu-protocol/src/call_info.rs similarity index 100% rename from crates/nu-protocol/src/call_info.rs rename to old_nushell/crates/nu-protocol/src/call_info.rs diff --git a/crates/nu-protocol/src/config_path.rs b/old_nushell/crates/nu-protocol/src/config_path.rs similarity index 100% rename from crates/nu-protocol/src/config_path.rs rename to old_nushell/crates/nu-protocol/src/config_path.rs diff --git a/crates/nu-protocol/src/dataframe/compute_between.rs b/old_nushell/crates/nu-protocol/src/dataframe/compute_between.rs similarity index 100% rename from crates/nu-protocol/src/dataframe/compute_between.rs rename to old_nushell/crates/nu-protocol/src/dataframe/compute_between.rs diff --git a/crates/nu-protocol/src/dataframe/conversion.rs b/old_nushell/crates/nu-protocol/src/dataframe/conversion.rs similarity index 100% rename from crates/nu-protocol/src/dataframe/conversion.rs rename to old_nushell/crates/nu-protocol/src/dataframe/conversion.rs diff --git a/crates/nu-protocol/src/dataframe/mod.rs b/old_nushell/crates/nu-protocol/src/dataframe/mod.rs similarity index 100% rename from crates/nu-protocol/src/dataframe/mod.rs rename to old_nushell/crates/nu-protocol/src/dataframe/mod.rs diff --git a/crates/nu-protocol/src/dataframe/nu_dataframe.rs b/old_nushell/crates/nu-protocol/src/dataframe/nu_dataframe.rs similarity index 100% rename from crates/nu-protocol/src/dataframe/nu_dataframe.rs rename to old_nushell/crates/nu-protocol/src/dataframe/nu_dataframe.rs diff --git a/crates/nu-protocol/src/dataframe/nu_groupby.rs b/old_nushell/crates/nu-protocol/src/dataframe/nu_groupby.rs similarity index 100% rename from crates/nu-protocol/src/dataframe/nu_groupby.rs rename to old_nushell/crates/nu-protocol/src/dataframe/nu_groupby.rs diff --git a/crates/nu-protocol/src/dataframe/operations.rs b/old_nushell/crates/nu-protocol/src/dataframe/operations.rs similarity index 100% rename from crates/nu-protocol/src/dataframe/operations.rs rename to old_nushell/crates/nu-protocol/src/dataframe/operations.rs diff --git a/crates/nu-protocol/src/hir.rs b/old_nushell/crates/nu-protocol/src/hir.rs similarity index 100% rename from crates/nu-protocol/src/hir.rs rename to old_nushell/crates/nu-protocol/src/hir.rs diff --git a/old_nushell/crates/nu-protocol/src/lib.rs b/old_nushell/crates/nu-protocol/src/lib.rs new file mode 100644 index 0000000000..e9d7346359 --- /dev/null +++ b/old_nushell/crates/nu-protocol/src/lib.rs @@ -0,0 +1,35 @@ +#[macro_use] +mod macros; + +mod call_info; +pub mod config_path; +pub mod hir; +mod maybe_owned; +mod registry; +mod return_value; +mod signature; +mod syntax_shape; +mod type_name; +mod type_shape; +pub mod value; + +#[cfg(feature = "dataframe")] +pub mod dataframe; + +pub use crate::call_info::{CallInfo, EvaluatedArgs}; +pub use crate::config_path::ConfigPath; +pub use crate::maybe_owned::MaybeOwned; +pub use crate::registry::{SignatureRegistry, VariableRegistry}; +pub use crate::return_value::{CommandAction, ReturnSuccess, ReturnValue}; +pub use crate::signature::{NamedType, PositionalType, Signature}; +pub use crate::syntax_shape::SyntaxShape; +pub use crate::type_name::{PrettyType, ShellTypeName, SpannedTypeName}; +pub use crate::type_shape::{Row as RowType, Type}; +pub use crate::value::column_path::{ColumnPath, PathMember, UnspannedPathMember}; +pub use crate::value::dict::{Dictionary, TaggedDictBuilder}; +pub use crate::value::did_you_mean::did_you_mean; +pub use crate::value::primitive::Primitive; +pub use crate::value::primitive::{format_date, format_duration, format_primitive}; +pub use crate::value::range::{Range, RangeInclusion}; +pub use crate::value::value_structure::{ValueResource, ValueStructure}; +pub use crate::value::{merge_descriptors, UntaggedValue, Value}; diff --git a/crates/nu-protocol/src/macros.rs b/old_nushell/crates/nu-protocol/src/macros.rs similarity index 100% rename from crates/nu-protocol/src/macros.rs rename to old_nushell/crates/nu-protocol/src/macros.rs diff --git a/crates/nu-protocol/src/maybe_owned.rs b/old_nushell/crates/nu-protocol/src/maybe_owned.rs similarity index 100% rename from crates/nu-protocol/src/maybe_owned.rs rename to old_nushell/crates/nu-protocol/src/maybe_owned.rs diff --git a/crates/nu-protocol/src/registry.rs b/old_nushell/crates/nu-protocol/src/registry.rs similarity index 100% rename from crates/nu-protocol/src/registry.rs rename to old_nushell/crates/nu-protocol/src/registry.rs diff --git a/crates/nu-protocol/src/return_value.rs b/old_nushell/crates/nu-protocol/src/return_value.rs similarity index 100% rename from crates/nu-protocol/src/return_value.rs rename to old_nushell/crates/nu-protocol/src/return_value.rs diff --git a/old_nushell/crates/nu-protocol/src/signature.rs b/old_nushell/crates/nu-protocol/src/signature.rs new file mode 100644 index 0000000000..1969931241 --- /dev/null +++ b/old_nushell/crates/nu-protocol/src/signature.rs @@ -0,0 +1,383 @@ +use crate::syntax_shape::SyntaxShape; +use crate::type_shape::Type; +use indexmap::IndexMap; +use nu_source::{DbgDocBldr, DebugDocBuilder, PrettyDebug, PrettyDebugWithSource}; +use serde::{Deserialize, Serialize}; + +/// The types of named parameter that a command can have +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub enum NamedType { + /// A flag without any associated argument. eg) `foo --bar, foo -b` + Switch(Option), + /// A mandatory flag, with associated argument. eg) `foo --required xyz, foo -r xyz` + Mandatory(Option, SyntaxShape), + /// An optional flag, with associated argument. eg) `foo --optional abc, foo -o abc` + Optional(Option, SyntaxShape), +} + +impl NamedType { + pub fn get_short(&self) -> Option { + match self { + NamedType::Switch(s) => *s, + NamedType::Mandatory(s, _) => *s, + NamedType::Optional(s, _) => *s, + } + } + + pub fn get_type_description(&self) -> (String, String, String) { + let empty_string = ("".to_string(), "".to_string(), "".to_string()); + match self { + NamedType::Switch(f) => match f { + Some(flag) => ("switch_flag".to_string(), flag.to_string(), "".to_string()), + None => empty_string, + }, + NamedType::Mandatory(f, shape) => match f { + Some(flag) => ( + "mandatory_flag".to_string(), + flag.to_string(), + shape.syntax_shape_name().to_string(), + ), + None => empty_string, + }, + NamedType::Optional(f, shape) => match f { + Some(flag) => ( + "optional_flag".to_string(), + flag.to_string(), + shape.syntax_shape_name().to_string(), + ), + None => empty_string, + }, + } + } +} + +/// The type of positional arguments +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum PositionalType { + /// A mandatory positional argument with the expected shape of the value + Mandatory(String, SyntaxShape), + /// An optional positional argument with the expected shape of the value + Optional(String, SyntaxShape), +} + +impl PrettyDebug for PositionalType { + /// Prepare the PositionalType for pretty-printing + fn pretty(&self) -> DebugDocBuilder { + match self { + PositionalType::Mandatory(string, shape) => { + DbgDocBldr::description(string) + + DbgDocBldr::delimit("(", shape.pretty(), ")") + .into_kind() + .group() + } + PositionalType::Optional(string, shape) => { + DbgDocBldr::description(string) + + DbgDocBldr::operator("?") + + DbgDocBldr::delimit("(", shape.pretty(), ")") + .into_kind() + .group() + } + } + } +} + +impl PositionalType { + /// Helper to create a mandatory positional argument type + pub fn mandatory(name: &str, ty: SyntaxShape) -> PositionalType { + PositionalType::Mandatory(name.to_string(), ty) + } + + /// Helper to create a mandatory positional argument with an "any" type + pub fn mandatory_any(name: &str) -> PositionalType { + PositionalType::Mandatory(name.to_string(), SyntaxShape::Any) + } + + /// Helper to create a mandatory positional argument with a block type + pub fn mandatory_block(name: &str) -> PositionalType { + PositionalType::Mandatory(name.to_string(), SyntaxShape::Block) + } + + /// Helper to create a optional positional argument type + pub fn optional(name: &str, ty: SyntaxShape) -> PositionalType { + PositionalType::Optional(name.to_string(), ty) + } + + /// Helper to create a optional positional argument with an "any" type + pub fn optional_any(name: &str) -> PositionalType { + PositionalType::Optional(name.to_string(), SyntaxShape::Any) + } + + /// Gets the name of the positional argument + pub fn name(&self) -> &str { + match self { + PositionalType::Mandatory(s, _) => s, + PositionalType::Optional(s, _) => s, + } + } + + /// Gets the expected type of a positional argument + pub fn syntax_type(&self) -> SyntaxShape { + match *self { + PositionalType::Mandatory(_, t) => t, + PositionalType::Optional(_, t) => t, + } + } + + pub fn get_type_description(&self) -> (String, String) { + match &self { + PositionalType::Mandatory(c, s) => (c.to_string(), s.syntax_shape_name().to_string()), + PositionalType::Optional(c, s) => (c.to_string(), s.syntax_shape_name().to_string()), + } + } +} + +type Description = String; + +/// The full signature of a command. All commands have a signature similar to a function signature. +/// Commands will use this information to register themselves with Nu's core engine so that the command +/// can be invoked, help can be displayed, and calls to the command can be error-checked. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Signature { + /// The name of the command. Used when calling the command + pub name: String, + /// Usage instructions about the command + pub usage: String, + /// Longer or more verbose usage statement + pub extra_usage: String, + /// The list of positional arguments, both required and optional, and their corresponding types and help text + pub positional: Vec<(PositionalType, Description)>, + /// After the positional arguments, a catch-all for the rest of the arguments that might follow, their type, and help text + pub rest_positional: Option<(String, SyntaxShape, Description)>, + /// The named flags with corresponding type and help text + pub named: IndexMap, + /// The type of values being sent out from the command into the pipeline, if any + pub yields: Option, + /// The type of values being read in from the pipeline into the command, if any + pub input: Option, + /// If the command is expected to filter data, or to consume it (as a sink) + pub is_filter: bool, +} + +impl PartialEq for Signature { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + && self.usage == other.usage + && self.positional == other.positional + && self.rest_positional == other.rest_positional + && self.is_filter == other.is_filter + } +} + +impl Eq for Signature {} + +impl Signature { + pub fn shift_positional(&mut self) { + self.positional = Vec::from(&self.positional[1..]); + } + + pub fn remove_named(&mut self, name: &str) { + self.named.remove(name); + } + + pub fn allowed(&self) -> Vec { + let mut allowed = indexmap::IndexSet::new(); + + for (name, (t, _)) in &self.named { + if let Some(c) = t.get_short() { + allowed.insert(format!("-{}", c)); + } + allowed.insert(format!("--{}", name)); + } + + for (ty, _) in &self.positional { + let shape = ty.syntax_type(); + allowed.insert(shape.display()); + } + + if let Some((_, shape, _)) = &self.rest_positional { + allowed.insert(shape.display()); + } + + allowed.into_iter().collect() + } +} + +impl PrettyDebugWithSource for Signature { + /// Prepare a Signature for pretty-printing + fn pretty_debug(&self, source: &str) -> DebugDocBuilder { + DbgDocBldr::typed( + "signature", + DbgDocBldr::description(&self.name) + + DbgDocBldr::preceded( + DbgDocBldr::space(), + DbgDocBldr::intersperse( + self.positional + .iter() + .map(|(ty, _)| ty.pretty_debug(source)), + DbgDocBldr::space(), + ), + ), + ) + } +} + +impl Signature { + /// Create a new command signature with the given name + pub fn new(name: impl Into) -> Signature { + Signature { + name: name.into(), + usage: String::new(), + extra_usage: String::new(), + positional: vec![], + rest_positional: None, + named: indexmap::indexmap! {"help".into() => (NamedType::Switch(Some('h')), "Display this help message".into())}, + is_filter: false, + yields: None, + input: None, + } + } + + /// Create a new signature + pub fn build(name: impl Into) -> Signature { + Signature::new(name.into()) + } + + /// Add a description to the signature + pub fn desc(mut self, usage: impl Into) -> Signature { + self.usage = usage.into(); + self + } + + /// Add a required positional argument to the signature + pub fn required( + mut self, + name: impl Into, + ty: impl Into, + desc: impl Into, + ) -> Signature { + self.positional.push(( + PositionalType::Mandatory(name.into(), ty.into()), + desc.into(), + )); + + self + } + + /// Add an optional positional argument to the signature + pub fn optional( + mut self, + name: impl Into, + ty: impl Into, + desc: impl Into, + ) -> Signature { + self.positional.push(( + PositionalType::Optional(name.into(), ty.into()), + desc.into(), + )); + + self + } + + /// Add an optional named flag argument to the signature + pub fn named( + mut self, + name: impl Into, + ty: impl Into, + desc: impl Into, + short: Option, + ) -> Signature { + let s = short.map(|c| { + debug_assert!(!self.get_shorts().contains(&c)); + c + }); + self.named.insert( + name.into(), + (NamedType::Optional(s, ty.into()), desc.into()), + ); + + self + } + + /// Add a required named flag argument to the signature + pub fn required_named( + mut self, + name: impl Into, + ty: impl Into, + desc: impl Into, + short: Option, + ) -> Signature { + let s = short.map(|c| { + debug_assert!(!self.get_shorts().contains(&c)); + c + }); + + self.named.insert( + name.into(), + (NamedType::Mandatory(s, ty.into()), desc.into()), + ); + + self + } + + /// Add a switch to the signature + pub fn switch( + mut self, + name: impl Into, + desc: impl Into, + short: Option, + ) -> Signature { + let s = short.map(|c| { + debug_assert!( + !self.get_shorts().contains(&c), + "There may be duplicate short flags, such as -h" + ); + c + }); + + self.named + .insert(name.into(), (NamedType::Switch(s), desc.into())); + self + } + + /// Set the filter flag for the signature + pub fn filter(mut self) -> Signature { + self.is_filter = true; + self + } + + /// Set the type for the "rest" of the positional arguments + /// Note: Not naming the field in your struct holding the rest values "rest", can + /// cause errors when deserializing + pub fn rest( + mut self, + name: impl Into, + ty: SyntaxShape, + desc: impl Into, + ) -> Signature { + self.rest_positional = Some((name.into(), ty, desc.into())); + self + } + + /// Add a type for the output of the command to the signature + pub fn yields(mut self, ty: Type) -> Signature { + self.yields = Some(ty); + self + } + + /// Add a type for the input of the command to the signature + pub fn input(mut self, ty: Type) -> Signature { + self.input = Some(ty); + self + } + + /// Get list of the short-hand flags + pub fn get_shorts(&self) -> Vec { + let mut shorts = Vec::new(); + for (_, (t, _)) in &self.named { + if let Some(c) = t.get_short() { + shorts.push(c); + } + } + shorts + } +} diff --git a/old_nushell/crates/nu-protocol/src/syntax_shape.rs b/old_nushell/crates/nu-protocol/src/syntax_shape.rs new file mode 100644 index 0000000000..0cea4011b4 --- /dev/null +++ b/old_nushell/crates/nu-protocol/src/syntax_shape.rs @@ -0,0 +1,70 @@ +use nu_source::{DbgDocBldr, DebugDocBuilder, PrettyDebug}; +use serde::{Deserialize, Serialize}; + +/// The syntactic shapes that values must match to be passed into a command. You can think of this as the type-checking that occurs when you call a function. +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub enum SyntaxShape { + /// Any syntactic form is allowed + Any, + /// Strings and string-like bare words are allowed + String, + /// A dotted path to navigate the table + ColumnPath, + /// A dotted path to navigate the table (including variable) + FullColumnPath, + /// Only a numeric (integer or decimal) value is allowed + Number, + /// A range is allowed (eg, `1..3`) + Range, + /// Only an integer value is allowed + Int, + /// A filepath is allowed + FilePath, + /// A glob pattern is allowed, eg `foo*` + GlobPattern, + /// A block is allowed, eg `{start this thing}` + Block, + /// A table is allowed, eg `[first second]` + Table, + /// A filesize value is allowed, eg `10kb` + Filesize, + /// A duration value is allowed, eg `19day` + Duration, + /// An operator + Operator, + /// A math expression which expands shorthand forms on the lefthand side, eg `foo > 1` + /// The shorthand allows us to more easily reach columns inside of the row being passed in + RowCondition, + /// A general math expression, eg the `1 + 2` of `= 1 + 2` + MathExpression, +} + +impl SyntaxShape { + pub fn syntax_shape_name(&self) -> &str { + match self { + SyntaxShape::Any => "any", + SyntaxShape::String => "string", + SyntaxShape::FullColumnPath => "column path (with variable)", + SyntaxShape::ColumnPath => "column path", + SyntaxShape::Number => "number", + SyntaxShape::Range => "range", + SyntaxShape::Int => "integer", + SyntaxShape::FilePath => "file path", + SyntaxShape::GlobPattern => "pattern", + SyntaxShape::Block => "block", + SyntaxShape::Table => "table", + SyntaxShape::Duration => "duration", + SyntaxShape::Filesize => "filesize", + SyntaxShape::Operator => "operator", + SyntaxShape::RowCondition => "condition", + SyntaxShape::MathExpression => "math expression", + } + } +} + +impl PrettyDebug for SyntaxShape { + /// Prepare SyntaxShape for pretty-printing + fn pretty(&self) -> DebugDocBuilder { + DbgDocBldr::kind(self.syntax_shape_name().to_string()) + } +} diff --git a/crates/nu-protocol/src/type_name.rs b/old_nushell/crates/nu-protocol/src/type_name.rs similarity index 100% rename from crates/nu-protocol/src/type_name.rs rename to old_nushell/crates/nu-protocol/src/type_name.rs diff --git a/crates/nu-protocol/src/type_shape.rs b/old_nushell/crates/nu-protocol/src/type_shape.rs similarity index 100% rename from crates/nu-protocol/src/type_shape.rs rename to old_nushell/crates/nu-protocol/src/type_shape.rs diff --git a/crates/nu-protocol/src/value.rs b/old_nushell/crates/nu-protocol/src/value.rs similarity index 100% rename from crates/nu-protocol/src/value.rs rename to old_nushell/crates/nu-protocol/src/value.rs diff --git a/crates/nu-protocol/src/value/column_path.rs b/old_nushell/crates/nu-protocol/src/value/column_path.rs similarity index 100% rename from crates/nu-protocol/src/value/column_path.rs rename to old_nushell/crates/nu-protocol/src/value/column_path.rs diff --git a/crates/nu-protocol/src/value/convert.rs b/old_nushell/crates/nu-protocol/src/value/convert.rs similarity index 100% rename from crates/nu-protocol/src/value/convert.rs rename to old_nushell/crates/nu-protocol/src/value/convert.rs diff --git a/crates/nu-protocol/src/value/debug.rs b/old_nushell/crates/nu-protocol/src/value/debug.rs similarity index 100% rename from crates/nu-protocol/src/value/debug.rs rename to old_nushell/crates/nu-protocol/src/value/debug.rs diff --git a/crates/nu-protocol/src/value/dict.rs b/old_nushell/crates/nu-protocol/src/value/dict.rs similarity index 100% rename from crates/nu-protocol/src/value/dict.rs rename to old_nushell/crates/nu-protocol/src/value/dict.rs diff --git a/crates/nu-protocol/src/value/did_you_mean.rs b/old_nushell/crates/nu-protocol/src/value/did_you_mean.rs similarity index 100% rename from crates/nu-protocol/src/value/did_you_mean.rs rename to old_nushell/crates/nu-protocol/src/value/did_you_mean.rs diff --git a/crates/nu-protocol/src/value/iter.rs b/old_nushell/crates/nu-protocol/src/value/iter.rs similarity index 100% rename from crates/nu-protocol/src/value/iter.rs rename to old_nushell/crates/nu-protocol/src/value/iter.rs diff --git a/crates/nu-protocol/src/value/primitive.rs b/old_nushell/crates/nu-protocol/src/value/primitive.rs similarity index 100% rename from crates/nu-protocol/src/value/primitive.rs rename to old_nushell/crates/nu-protocol/src/value/primitive.rs diff --git a/old_nushell/crates/nu-protocol/src/value/range.rs b/old_nushell/crates/nu-protocol/src/value/range.rs new file mode 100644 index 0000000000..77b27b9965 --- /dev/null +++ b/old_nushell/crates/nu-protocol/src/value/range.rs @@ -0,0 +1,154 @@ +use crate::value::Primitive; +use derive_new::new; +use nu_errors::ShellError; +use nu_source::{DbgDocBldr, DebugDocBuilder, Spanned}; +use serde::{Deserialize, Serialize}; + +/// The two types of ways to include a range end. Inclusive means to include the value (eg 1..3 inclusive would include the 3 value). +/// Exclusive excludes the value (eg 1..3 exclusive does not include 3 value) +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Serialize, Deserialize)] +pub enum RangeInclusion { + Inclusive, + Exclusive, +} + +impl RangeInclusion { + /// Get a RangeInclusion left bracket ready for pretty printing + pub fn debug_left_bracket(self) -> DebugDocBuilder { + DbgDocBldr::delimiter(match self { + RangeInclusion::Exclusive => "(", + RangeInclusion::Inclusive => "[", + }) + } + + /// Get a RangeInclusion right bracket ready for pretty printing + pub fn debug_right_bracket(self) -> DebugDocBuilder { + DbgDocBldr::delimiter(match self { + RangeInclusion::Exclusive => ")", + RangeInclusion::Inclusive => "]", + }) + } +} + +/// The range definition, holding the starting and end point of the range +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Serialize, Deserialize, new)] +pub struct Range { + pub from: (Spanned, RangeInclusion), + pub to: (Spanned, RangeInclusion), +} + +impl Range { + pub fn min_u64(&self) -> Result { + let (from, range_incl) = &self.from; + + let minval = if let Primitive::Nothing = from.item { + u64::MIN + } else { + from.item.as_u64(from.span)? + }; + + match range_incl { + RangeInclusion::Inclusive => Ok(minval), + RangeInclusion::Exclusive => Ok(minval.saturating_add(1)), + } + } + + pub fn max_u64(&self) -> Result { + let (to, range_incl) = &self.to; + + let maxval = if let Primitive::Nothing = to.item { + u64::MAX + } else { + to.item.as_u64(to.span)? + }; + + match range_incl { + RangeInclusion::Inclusive => Ok(maxval), + RangeInclusion::Exclusive => Ok(maxval.saturating_sub(1)), + } + } + + pub fn min_i64(&self) -> Result { + let (from, range_incl) = &self.from; + + let minval = if let Primitive::Nothing = from.item { + 0 + } else { + from.item.as_i64(from.span)? + }; + + match range_incl { + RangeInclusion::Inclusive => Ok(minval), + RangeInclusion::Exclusive => Ok(minval.saturating_add(1)), + } + } + + pub fn max_i64(&self) -> Result { + let (to, range_incl) = &self.to; + + let maxval = if let Primitive::Nothing = to.item { + i64::MAX + } else { + to.item.as_i64(to.span)? + }; + + match range_incl { + RangeInclusion::Inclusive => Ok(maxval), + RangeInclusion::Exclusive => Ok(maxval.saturating_sub(1)), + } + } + + pub fn min_usize(&self) -> Result { + let (from, range_incl) = &self.from; + + let minval = if let Primitive::Nothing = from.item { + usize::MIN + } else { + from.item.as_usize(from.span)? + }; + + match range_incl { + RangeInclusion::Inclusive => Ok(minval), + RangeInclusion::Exclusive => Ok(minval.saturating_add(1)), + } + } + + pub fn max_usize(&self) -> Result { + let (to, range_incl) = &self.to; + + let maxval = if let Primitive::Nothing = to.item { + usize::MAX + } else { + to.item.as_usize(to.span)? + }; + + match range_incl { + RangeInclusion::Inclusive => Ok(maxval), + RangeInclusion::Exclusive => Ok(maxval.saturating_sub(1)), + } + } + + pub fn min_f64(&self) -> Result { + let from = &self.from.0; + + if let Primitive::Nothing = from.item { + Ok(f64::MIN) + } else { + Ok(from.item.as_f64(from.span)?) + } + + // How would inclusive vs. exclusive range work here? + } + + pub fn max_f64(&self) -> Result { + let to = &self.to.0; + + if let Primitive::Nothing = to.item { + Ok(f64::MAX) + } else { + Ok(to.item.as_f64(to.span)?) + } + + // How would inclusive vs. exclusive range work here? + } +} diff --git a/crates/nu-protocol/src/value/serde_bigdecimal.rs b/old_nushell/crates/nu-protocol/src/value/serde_bigdecimal.rs similarity index 100% rename from crates/nu-protocol/src/value/serde_bigdecimal.rs rename to old_nushell/crates/nu-protocol/src/value/serde_bigdecimal.rs diff --git a/crates/nu-protocol/src/value/serde_bigint.rs b/old_nushell/crates/nu-protocol/src/value/serde_bigint.rs similarity index 100% rename from crates/nu-protocol/src/value/serde_bigint.rs rename to old_nushell/crates/nu-protocol/src/value/serde_bigint.rs diff --git a/old_nushell/crates/nu-protocol/src/value/unit.rs b/old_nushell/crates/nu-protocol/src/value/unit.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/nu-protocol/src/value/value_structure.rs b/old_nushell/crates/nu-protocol/src/value/value_structure.rs similarity index 100% rename from crates/nu-protocol/src/value/value_structure.rs rename to old_nushell/crates/nu-protocol/src/value/value_structure.rs diff --git a/crates/nu-serde/Cargo.toml b/old_nushell/crates/nu-serde/Cargo.toml similarity index 100% rename from crates/nu-serde/Cargo.toml rename to old_nushell/crates/nu-serde/Cargo.toml diff --git a/crates/nu-serde/README.md b/old_nushell/crates/nu-serde/README.md similarity index 100% rename from crates/nu-serde/README.md rename to old_nushell/crates/nu-serde/README.md diff --git a/crates/nu-serde/src/lib.rs b/old_nushell/crates/nu-serde/src/lib.rs similarity index 100% rename from crates/nu-serde/src/lib.rs rename to old_nushell/crates/nu-serde/src/lib.rs diff --git a/crates/nu-serde/src/snapshots/nu_serde__test__it_serializes_return_value_list.snap b/old_nushell/crates/nu-serde/src/snapshots/nu_serde__test__it_serializes_return_value_list.snap similarity index 100% rename from crates/nu-serde/src/snapshots/nu_serde__test__it_serializes_return_value_list.snap rename to old_nushell/crates/nu-serde/src/snapshots/nu_serde__test__it_serializes_return_value_list.snap diff --git a/crates/nu-serde/src/snapshots/nu_serde__test__it_works_with_complex_structs.snap b/old_nushell/crates/nu-serde/src/snapshots/nu_serde__test__it_works_with_complex_structs.snap similarity index 100% rename from crates/nu-serde/src/snapshots/nu_serde__test__it_works_with_complex_structs.snap rename to old_nushell/crates/nu-serde/src/snapshots/nu_serde__test__it_works_with_complex_structs.snap diff --git a/crates/nu-serde/src/snapshots/nu_serde__test__it_works_with_lists_of_values.snap b/old_nushell/crates/nu-serde/src/snapshots/nu_serde__test__it_works_with_lists_of_values.snap similarity index 100% rename from crates/nu-serde/src/snapshots/nu_serde__test__it_works_with_lists_of_values.snap rename to old_nushell/crates/nu-serde/src/snapshots/nu_serde__test__it_works_with_lists_of_values.snap diff --git a/crates/nu-serde/src/snapshots/nu_serde__test__it_works_with_single_integers.snap b/old_nushell/crates/nu-serde/src/snapshots/nu_serde__test__it_works_with_single_integers.snap similarity index 100% rename from crates/nu-serde/src/snapshots/nu_serde__test__it_works_with_single_integers.snap rename to old_nushell/crates/nu-serde/src/snapshots/nu_serde__test__it_works_with_single_integers.snap diff --git a/crates/nu-serde/src/test.rs b/old_nushell/crates/nu-serde/src/test.rs similarity index 100% rename from crates/nu-serde/src/test.rs rename to old_nushell/crates/nu-serde/src/test.rs diff --git a/crates/nu-source/Cargo.toml b/old_nushell/crates/nu-source/Cargo.toml similarity index 100% rename from crates/nu-source/Cargo.toml rename to old_nushell/crates/nu-source/Cargo.toml diff --git a/crates/nu-source/README.md b/old_nushell/crates/nu-source/README.md similarity index 100% rename from crates/nu-source/README.md rename to old_nushell/crates/nu-source/README.md diff --git a/crates/nu-source/src/lib.rs b/old_nushell/crates/nu-source/src/lib.rs similarity index 100% rename from crates/nu-source/src/lib.rs rename to old_nushell/crates/nu-source/src/lib.rs diff --git a/crates/nu-source/src/meta.rs b/old_nushell/crates/nu-source/src/meta.rs similarity index 100% rename from crates/nu-source/src/meta.rs rename to old_nushell/crates/nu-source/src/meta.rs diff --git a/crates/nu-source/src/pretty.rs b/old_nushell/crates/nu-source/src/pretty.rs similarity index 100% rename from crates/nu-source/src/pretty.rs rename to old_nushell/crates/nu-source/src/pretty.rs diff --git a/crates/nu-source/src/term_colored.rs b/old_nushell/crates/nu-source/src/term_colored.rs similarity index 100% rename from crates/nu-source/src/term_colored.rs rename to old_nushell/crates/nu-source/src/term_colored.rs diff --git a/crates/nu-source/src/text.rs b/old_nushell/crates/nu-source/src/text.rs similarity index 100% rename from crates/nu-source/src/text.rs rename to old_nushell/crates/nu-source/src/text.rs diff --git a/crates/nu-stream/Cargo.toml b/old_nushell/crates/nu-stream/Cargo.toml similarity index 100% rename from crates/nu-stream/Cargo.toml rename to old_nushell/crates/nu-stream/Cargo.toml diff --git a/crates/nu-stream/src/input.rs b/old_nushell/crates/nu-stream/src/input.rs similarity index 100% rename from crates/nu-stream/src/input.rs rename to old_nushell/crates/nu-stream/src/input.rs diff --git a/crates/nu-stream/src/interruptible.rs b/old_nushell/crates/nu-stream/src/interruptible.rs similarity index 100% rename from crates/nu-stream/src/interruptible.rs rename to old_nushell/crates/nu-stream/src/interruptible.rs diff --git a/crates/nu-stream/src/lib.rs b/old_nushell/crates/nu-stream/src/lib.rs similarity index 100% rename from crates/nu-stream/src/lib.rs rename to old_nushell/crates/nu-stream/src/lib.rs diff --git a/crates/nu-stream/src/output.rs b/old_nushell/crates/nu-stream/src/output.rs similarity index 100% rename from crates/nu-stream/src/output.rs rename to old_nushell/crates/nu-stream/src/output.rs diff --git a/crates/nu-stream/src/prelude.rs b/old_nushell/crates/nu-stream/src/prelude.rs similarity index 100% rename from crates/nu-stream/src/prelude.rs rename to old_nushell/crates/nu-stream/src/prelude.rs diff --git a/old_nushell/crates/nu-table/.gitignore b/old_nushell/crates/nu-table/.gitignore new file mode 100644 index 0000000000..4c234e523b --- /dev/null +++ b/old_nushell/crates/nu-table/.gitignore @@ -0,0 +1,22 @@ +/target +/scratch +**/*.rs.bk +history.txt +tests/fixtures/nuplayground +crates/*/target + +# Debian/Ubuntu +debian/.debhelper/ +debian/debhelper-build-stamp +debian/files +debian/nu.substvars +debian/nu/ + +# macOS junk +.DS_Store + +# JetBrains' IDE items +.idea/* + +# VSCode's IDE items +.vscode/* diff --git a/old_nushell/crates/nu-table/Cargo.toml b/old_nushell/crates/nu-table/Cargo.toml new file mode 100644 index 0000000000..1a6e2565a0 --- /dev/null +++ b/old_nushell/crates/nu-table/Cargo.toml @@ -0,0 +1,20 @@ +[package] +authors = ["The Nu Project Contributors"] +description = "Nushell table printing" +edition = "2018" +license = "MIT" +name = "nu-table" +version = "0.43.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[[bin]] +name = "table" +path = "src/main.rs" + +[dependencies] +atty = "0.2.14" +nu-ansi-term = { version = "0.43.0", path="../nu-ansi-term" } + +regex = "1.4" +strip-ansi-escapes = "0.1.1" +unicode-width = "0.1.8" diff --git a/crates/nu-table/src/lib.rs b/old_nushell/crates/nu-table/src/lib.rs similarity index 100% rename from crates/nu-table/src/lib.rs rename to old_nushell/crates/nu-table/src/lib.rs diff --git a/old_nushell/crates/nu-table/src/main.rs b/old_nushell/crates/nu-table/src/main.rs new file mode 100644 index 0000000000..bb464ca069 --- /dev/null +++ b/old_nushell/crates/nu-table/src/main.rs @@ -0,0 +1,96 @@ +use nu_table::{draw_table, StyledString, Table, TextStyle, Theme}; +use std::collections::HashMap; + +fn main() { + let args: Vec<_> = std::env::args().collect(); + let mut width = 0; + + if args.len() > 1 { + // Width in terminal characters + width = args[1].parse::().expect("Need a width in columns"); + } + + if width < 4 { + println!("Width must be greater than or equal to 4, setting width to 80"); + width = 80; + } + + // The mocked up table data + let (table_headers, row_data) = make_table_data(); + // The table headers + let headers = vec_of_str_to_vec_of_styledstr(&table_headers, true); + // The table rows + let rows = vec_of_str_to_vec_of_styledstr(&row_data, false); + // The table itself + let table = Table::new(headers, vec![rows; 3], Theme::rounded()); + // FIXME: Config isn't available from here so just put these here to compile + let color_hm: HashMap = HashMap::new(); + // Capture the table as a string + let output_table = draw_table(&table, width, &color_hm); + + if atty::is(atty::Stream::Stdout) { + // Draw the table with ansi colors + println!("{}", output_table) + } else { + // Draw the table without ansi colors + if let Ok(bytes) = strip_ansi_escapes::strip(&output_table) { + println!("{}", String::from_utf8_lossy(&bytes)) + } else { + println!("{}", output_table) + } + } +} + +fn make_table_data() -> (Vec<&'static str>, Vec<&'static str>) { + let table_headers = vec![ + "category", + "description", + "emoji", + "ios_version", + "unicode_version", + "aliases", + "tags", + "category2", + "description2", + "emoji2", + "ios_version2", + "unicode_version2", + "aliases2", + "tags2", + ]; + + let row_data = vec![ + "Smileys & Emotion", + "grinning face", + "😀", + "6", + "6.1", + "grinning", + "smile", + "Smileys & Emotion", + "grinning face", + "😀", + "6", + "6.1", + "grinning", + "smile", + ]; + + (table_headers, row_data) +} + +fn vec_of_str_to_vec_of_styledstr(data: &[&str], is_header: bool) -> Vec { + let mut v = vec![]; + + for x in data { + if is_header { + v.push(StyledString::new( + String::from(*x), + TextStyle::default_header(), + )) + } else { + v.push(StyledString::new(String::from(*x), TextStyle::basic_left())) + } + } + v +} diff --git a/old_nushell/crates/nu-table/src/table.rs b/old_nushell/crates/nu-table/src/table.rs new file mode 100644 index 0000000000..38f826552b --- /dev/null +++ b/old_nushell/crates/nu-table/src/table.rs @@ -0,0 +1,1269 @@ +use crate::wrap::{column_width, split_sublines, wrap, Alignment, Subline, WrappedCell}; +use nu_ansi_term::{Color, Style}; +use std::collections::HashMap; +use std::fmt::Write; + +enum SeparatorPosition { + Top, + Middle, + Bottom, +} + +#[derive(Debug)] +pub struct Table { + pub headers: Vec, + pub data: Vec>, + pub theme: Theme, +} + +#[derive(Debug, Clone)] +pub struct StyledString { + pub contents: String, + pub style: TextStyle, +} + +impl StyledString { + pub fn new(contents: String, style: TextStyle) -> StyledString { + StyledString { contents, style } + } + + pub fn set_style(&mut self, style: TextStyle) { + self.style = style; + } +} + +#[derive(Debug, Clone, Copy)] +pub struct TextStyle { + pub alignment: Alignment, + pub color_style: Option