diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2193598686..07e0b2b891 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -26,6 +26,13 @@ updates: patterns: - "polars" - "polars-*" + # uutils/coreutils also versions all their workspace crates the same at the moment + # Most of them have bleeding edge version requirements (some not) + # see: https://github.com/uutils/coreutils/blob/main/Cargo.toml + uutils: + patterns: + - "uucore" + - "uu_*" - package-ecosystem: "github-actions" directory: "/" schedule: diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index a0295b3b4d..694ff6e1bb 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -19,7 +19,7 @@ jobs: # Prevent sudden announcement of a new advisory from failing ci: continue-on-error: true steps: - - uses: actions/checkout@v4.1.6 + - uses: actions/checkout@v4.1.7 - uses: rustsec/audit-check@v1.4.1 with: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9cb92b0902..25fcfde55e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,10 +33,10 @@ jobs: runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v4.1.6 + - uses: actions/checkout@v4.1.7 - name: Setup Rust toolchain and cache - uses: actions-rust-lang/setup-rust-toolchain@v1.8.0 + uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 - name: cargo fmt run: cargo fmt --all -- --check @@ -66,10 +66,10 @@ jobs: runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v4.1.6 + - uses: actions/checkout@v4.1.7 - name: Setup Rust toolchain and cache - uses: actions-rust-lang/setup-rust-toolchain@v1.8.0 + uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 - name: Tests run: cargo test --workspace --profile ci --exclude nu_plugin_* ${{ matrix.default-flags }} @@ -95,10 +95,10 @@ jobs: runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v4.1.6 + - uses: actions/checkout@v4.1.7 - name: Setup Rust toolchain and cache - uses: actions-rust-lang/setup-rust-toolchain@v1.8.0 + uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 - name: Install Nushell run: cargo install --path . --locked --no-default-features @@ -146,10 +146,10 @@ jobs: runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v4.1.6 + - uses: actions/checkout@v4.1.7 - name: Setup Rust toolchain and cache - uses: actions-rust-lang/setup-rust-toolchain@v1.8.0 + uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 - name: Clippy run: cargo clippy --package nu_plugin_* -- $CLIPPY_OPTIONS diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml index 251cffd3a1..9b7f44feba 100644 --- a/.github/workflows/nightly-build.yml +++ b/.github/workflows/nightly-build.yml @@ -27,7 +27,7 @@ jobs: # if: github.repository == 'nushell/nightly' steps: - name: Checkout - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 if: github.repository == 'nushell/nightly' with: ref: main @@ -36,10 +36,10 @@ jobs: token: ${{ secrets.WORKFLOW_TOKEN }} - name: Setup Nushell - uses: hustcer/setup-nu@v3.11 + uses: hustcer/setup-nu@v3.12 if: github.repository == 'nushell/nightly' with: - version: 0.93.0 + version: 0.95.0 # Synchronize the main branch of nightly repo with the main branch of Nushell official repo - name: Prepare for Nightly Release @@ -99,20 +99,20 @@ jobs: extra: msi os: windows-latest - target: x86_64-unknown-linux-gnu - os: ubuntu-20.04 + os: ubuntu-22.04 - target: x86_64-unknown-linux-musl - os: ubuntu-20.04 + os: ubuntu-22.04 - target: aarch64-unknown-linux-gnu - os: ubuntu-20.04 + os: ubuntu-22.04 - target: armv7-unknown-linux-gnueabihf - os: ubuntu-20.04 + os: ubuntu-22.04 - target: riscv64gc-unknown-linux-gnu os: ubuntu-latest runs-on: ${{matrix.os}} steps: - - uses: actions/checkout@v4.1.6 + - uses: actions/checkout@v4.1.7 with: ref: main fetch-depth: 0 @@ -122,15 +122,15 @@ jobs: echo "targets = ['${{matrix.target}}']" >> rust-toolchain.toml - name: Setup Rust toolchain and cache - uses: actions-rust-lang/setup-rust-toolchain@v1.8.0 + uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 # WARN: Keep the rustflags to prevent from the winget submission error: `CAQuietExec: Error 0xc0000135` with: rustflags: '' - name: Setup Nushell - uses: hustcer/setup-nu@v3.11 + uses: hustcer/setup-nu@v3.12 with: - version: 0.93.0 + version: 0.95.0 - name: Release Nu Binary id: nu @@ -161,7 +161,7 @@ jobs: # REF: https://github.com/marketplace/actions/gh-release # Create a release only in nushell/nightly repo - name: Publish Archive - uses: softprops/action-gh-release@v2.0.5 + uses: softprops/action-gh-release@v2.0.6 if: ${{ startsWith(github.repository, 'nushell/nightly') }} with: prerelease: true @@ -181,14 +181,14 @@ jobs: - name: Waiting for Release run: sleep 1800 - - uses: actions/checkout@v4.1.6 + - uses: actions/checkout@v4.1.7 with: ref: main - name: Setup Nushell - uses: hustcer/setup-nu@v3.11 + uses: hustcer/setup-nu@v3.12 with: - version: 0.93.0 + version: 0.95.0 # Keep the last a few releases - name: Delete Older Releases diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6aca9578d4..34cad24fbf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,36 +49,36 @@ jobs: extra: msi os: windows-latest - target: x86_64-unknown-linux-gnu - os: ubuntu-20.04 + os: ubuntu-22.04 - target: x86_64-unknown-linux-musl - os: ubuntu-20.04 + os: ubuntu-22.04 - target: aarch64-unknown-linux-gnu - os: ubuntu-20.04 + os: ubuntu-22.04 - target: armv7-unknown-linux-gnueabihf - os: ubuntu-20.04 + os: ubuntu-22.04 - target: riscv64gc-unknown-linux-gnu os: ubuntu-latest runs-on: ${{matrix.os}} steps: - - uses: actions/checkout@v4.1.6 + - uses: actions/checkout@v4.1.7 - name: Update Rust Toolchain Target run: | echo "targets = ['${{matrix.target}}']" >> rust-toolchain.toml - name: Setup Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@v1.8.0 + uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 # WARN: Keep the rustflags to prevent from the winget submission error: `CAQuietExec: Error 0xc0000135` with: cache: false rustflags: '' - name: Setup Nushell - uses: hustcer/setup-nu@v3.11 + uses: hustcer/setup-nu@v3.12 with: - version: 0.93.0 + version: 0.95.0 - name: Release Nu Binary id: nu @@ -91,7 +91,7 @@ jobs: # REF: https://github.com/marketplace/actions/gh-release - name: Publish Archive - uses: softprops/action-gh-release@v2.0.5 + uses: softprops/action-gh-release@v2.0.6 if: ${{ startsWith(github.ref, 'refs/tags/') }} with: draft: true diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml index 1142b2dbaf..c7c153983d 100644 --- a/.github/workflows/typos.yml +++ b/.github/workflows/typos.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Actions Repository - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Check spelling - uses: crate-ci/typos@v1.22.1 + uses: crate-ci/typos@v1.23.2 diff --git a/Cargo.lock b/Cargo.lock index 3087267458..49770a9a0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -920,6 +920,15 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1108,6 +1117,36 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "curl" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e2161dd6eba090ff1594084e95fd67aeccf04382ffea77999ea94ed42ec67b6" +dependencies = [ + "curl-sys", + "libc", + "openssl-probe", + "openssl-sys", + "schannel", + "socket2", + "windows-sys 0.52.0", +] + +[[package]] +name = "curl-sys" +version = "0.4.73+curl-8.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "450ab250ecf17227c39afb9a2dd9261dc0035cb80f2612472fc0c4aac2dcb84d" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", + "windows-sys 0.52.0", +] + [[package]] name = "deranged" version = "0.3.11" @@ -1139,6 +1178,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "deunicode" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" + [[package]] name = "dialoguer" version = "0.11.0" @@ -1174,24 +1219,24 @@ dependencies = [ ] [[package]] -name = "dirs-next" -version = "2.0.0" +name = "dirs" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ - "cfg-if", - "dirs-sys-next", + "dirs-sys", ] [[package]] -name = "dirs-sys-next" -version = "0.1.2" +name = "dirs-sys" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", + "option-ext", "redox_users", - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -1218,6 +1263,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "doctest-file" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" + [[package]] name = "downcast-rs" version = "1.2.1" @@ -1315,6 +1366,16 @@ dependencies = [ "syn 2.0.60", ] +[[package]] +name = "env_filter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +dependencies = [ + "log", + "regex", +] + [[package]] name = "env_logger" version = "0.8.4" @@ -1325,6 +1386,19 @@ dependencies = [ "regex", ] +[[package]] +name = "env_logger" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -1678,9 +1752,9 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "git2" -version = "0.18.3" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70" +checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" dependencies = [ "bitflags 2.5.0", "libc", @@ -1839,12 +1913,26 @@ checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" dependencies = [ "log", "mac", - "markup5ever", + "markup5ever 0.11.0", "proc-macro2", "quote", "syn 1.0.109", ] +[[package]] +name = "html5ever" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" +dependencies = [ + "log", + "mac", + "markup5ever 0.12.1", + "proc-macro2", + "quote", + "syn 2.0.60", +] + [[package]] name = "http" version = "0.2.12" @@ -1891,6 +1979,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.14.28" @@ -1980,12 +2074,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "indoc" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" - [[package]] name = "inotify" version = "0.9.6" @@ -2017,10 +2105,11 @@ dependencies = [ [[package]] name = "interprocess" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4d0250d41da118226e55b3d50ca3f0d9e0a0f6829b92f543ac0054aeea1572" +checksum = "67bafc2f5dbdad79a6d925649758d5472647b416028099f0b829d1b67fdd47d3" dependencies = [ + "doctest-file", "libc", "recvmsg", "widestring", @@ -2253,9 +2342,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.154" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libflate" @@ -2279,9 +2368,9 @@ dependencies = [ [[package]] name = "libgit2-sys" -version = "0.16.2+1.7.2" +version = "0.17.0+1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8" +checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224" dependencies = [ "cc", "libc", @@ -2293,9 +2382,9 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" dependencies = [ "cfg-if", "windows-targets 0.52.5", @@ -2507,6 +2596,32 @@ dependencies = [ "tendril", ] +[[package]] +name = "markup5ever" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" +dependencies = [ + "log", + "phf 0.11.2", + "phf_codegen 0.11.2", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "markup5ever_rcdom" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18" +dependencies = [ + "html5ever 0.27.0", + "markup5ever 0.12.1", + "tendril", + "xml5ever", +] + [[package]] name = "md-5" version = "0.10.6" @@ -2753,12 +2868,12 @@ dependencies = [ [[package]] name = "nu" -version = "0.94.3" +version = "0.95.1" dependencies = [ "assert_cmd", "crossterm", "ctrlc", - "dirs-next", + "dirs", "log", "miette", "mimalloc", @@ -2785,6 +2900,7 @@ dependencies = [ "openssl", "pretty_assertions", "reedline", + "regex", "rstest", "serde_json", "serial_test", @@ -2806,7 +2922,7 @@ dependencies = [ [[package]] name = "nu-cli" -version = "0.94.3" +version = "0.95.1" dependencies = [ "chrono", "crossterm", @@ -2841,7 +2957,7 @@ dependencies = [ [[package]] name = "nu-cmd-base" -version = "0.94.3" +version = "0.95.1" dependencies = [ "indexmap", "miette", @@ -2853,7 +2969,7 @@ dependencies = [ [[package]] name = "nu-cmd-extra" -version = "0.94.3" +version = "0.95.1" dependencies = [ "fancy-regex", "heck 0.5.0", @@ -2878,7 +2994,7 @@ dependencies = [ [[package]] name = "nu-cmd-lang" -version = "0.94.3" +version = "0.95.1" dependencies = [ "itertools 0.12.1", "nu-engine", @@ -2890,7 +3006,7 @@ dependencies = [ [[package]] name = "nu-cmd-plugin" -version = "0.94.3" +version = "0.95.1" dependencies = [ "itertools 0.12.1", "nu-engine", @@ -2901,7 +3017,7 @@ dependencies = [ [[package]] name = "nu-color-config" -version = "0.94.3" +version = "0.95.1" dependencies = [ "nu-ansi-term", "nu-engine", @@ -2913,7 +3029,7 @@ dependencies = [ [[package]] name = "nu-command" -version = "0.94.3" +version = "0.95.1" dependencies = [ "alphanumeric-sort", "base64 0.22.1", @@ -2928,9 +3044,10 @@ dependencies = [ "chrono-tz 0.8.6", "crossterm", "csv", + "deunicode", "dialoguer", "digest", - "dirs-next", + "dirs", "dtparse", "encoding_rs", "fancy-regex", @@ -3011,7 +3128,7 @@ dependencies = [ "uu_mv", "uu_uname", "uu_whoami", - "uucore 0.0.25", + "uucore", "uuid", "v_htmlescape", "wax", @@ -3021,9 +3138,21 @@ dependencies = [ ] [[package]] -name = "nu-engine" -version = "0.94.3" +name = "nu-derive-value" +version = "0.95.1" dependencies = [ + "convert_case", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.60", +] + +[[package]] +name = "nu-engine" +version = "0.95.1" +dependencies = [ + "log", "nu-glob", "nu-path", "nu-protocol", @@ -3032,7 +3161,7 @@ dependencies = [ [[package]] name = "nu-explore" -version = "0.94.3" +version = "0.95.1" dependencies = [ "ansi-str", "anyhow", @@ -3057,16 +3186,19 @@ dependencies = [ [[package]] name = "nu-glob" -version = "0.94.3" +version = "0.95.1" dependencies = [ "doc-comment", ] [[package]] name = "nu-json" -version = "0.94.3" +version = "0.95.1" dependencies = [ + "fancy-regex", "linked-hash-map", + "nu-path", + "nu-test-support", "num-traits", "serde", "serde_json", @@ -3074,7 +3206,7 @@ dependencies = [ [[package]] name = "nu-lsp" -version = "0.94.3" +version = "0.95.1" dependencies = [ "assert-json-diff", "crossbeam-channel", @@ -3095,7 +3227,7 @@ dependencies = [ [[package]] name = "nu-parser" -version = "0.94.3" +version = "0.95.1" dependencies = [ "bytesize", "chrono", @@ -3111,16 +3243,16 @@ dependencies = [ [[package]] name = "nu-path" -version = "0.94.3" +version = "0.95.1" dependencies = [ - "dirs-next", + "dirs", "omnipath", "pwd", ] [[package]] name = "nu-plugin" -version = "0.94.3" +version = "0.95.1" dependencies = [ "log", "nix", @@ -3128,6 +3260,7 @@ dependencies = [ "nu-plugin-core", "nu-plugin-protocol", "nu-protocol", + "nu-utils", "serde", "thiserror", "typetag", @@ -3135,7 +3268,7 @@ dependencies = [ [[package]] name = "nu-plugin-core" -version = "0.94.3" +version = "0.95.1" dependencies = [ "interprocess", "log", @@ -3149,7 +3282,7 @@ dependencies = [ [[package]] name = "nu-plugin-engine" -version = "0.94.3" +version = "0.95.1" dependencies = [ "log", "nu-engine", @@ -3157,6 +3290,7 @@ dependencies = [ "nu-plugin-protocol", "nu-protocol", "nu-system", + "nu-utils", "serde", "typetag", "windows 0.54.0", @@ -3164,7 +3298,7 @@ dependencies = [ [[package]] name = "nu-plugin-protocol" -version = "0.94.3" +version = "0.95.1" dependencies = [ "bincode", "nu-protocol", @@ -3176,7 +3310,7 @@ dependencies = [ [[package]] name = "nu-plugin-test-support" -version = "0.94.3" +version = "0.95.1" dependencies = [ "nu-ansi-term", "nu-cmd-lang", @@ -3194,7 +3328,7 @@ dependencies = [ [[package]] name = "nu-pretty-hex" -version = "0.94.3" +version = "0.95.1" dependencies = [ "heapless", "nu-ansi-term", @@ -3203,17 +3337,20 @@ dependencies = [ [[package]] name = "nu-protocol" -version = "0.94.3" +version = "0.95.1" dependencies = [ "brotli", "byte-unit", "chrono", "chrono-humanize", + "convert_case", "fancy-regex", "indexmap", + "log", "lru", "miette", "nix", + "nu-derive-value", "nu-path", "nu-system", "nu-test-support", @@ -3234,7 +3371,7 @@ dependencies = [ [[package]] name = "nu-std" -version = "0.94.3" +version = "0.95.1" dependencies = [ "log", "miette", @@ -3245,7 +3382,7 @@ dependencies = [ [[package]] name = "nu-system" -version = "0.94.3" +version = "0.95.1" dependencies = [ "chrono", "itertools 0.12.1", @@ -3263,7 +3400,7 @@ dependencies = [ [[package]] name = "nu-table" -version = "0.94.3" +version = "0.95.1" dependencies = [ "fancy-regex", "nu-ansi-term", @@ -3277,7 +3414,7 @@ dependencies = [ [[package]] name = "nu-term-grid" -version = "0.94.3" +version = "0.95.1" dependencies = [ "nu-utils", "unicode-width", @@ -3285,7 +3422,7 @@ dependencies = [ [[package]] name = "nu-test-support" -version = "0.94.3" +version = "0.95.1" dependencies = [ "nu-glob", "nu-path", @@ -3297,7 +3434,7 @@ dependencies = [ [[package]] name = "nu-utils" -version = "0.94.3" +version = "0.95.1" dependencies = [ "crossterm_winapi", "log", @@ -3323,7 +3460,7 @@ dependencies = [ [[package]] name = "nu_plugin_example" -version = "0.94.3" +version = "0.95.1" dependencies = [ "nu-cmd-lang", "nu-plugin", @@ -3333,7 +3470,7 @@ dependencies = [ [[package]] name = "nu_plugin_formats" -version = "0.94.3" +version = "0.95.1" dependencies = [ "eml-parser", "ical", @@ -3346,7 +3483,7 @@ dependencies = [ [[package]] name = "nu_plugin_gstat" -version = "0.94.3" +version = "0.95.1" dependencies = [ "git2", "nu-plugin", @@ -3355,7 +3492,7 @@ dependencies = [ [[package]] name = "nu_plugin_inc" -version = "0.94.3" +version = "0.95.1" dependencies = [ "nu-plugin", "nu-protocol", @@ -3364,12 +3501,14 @@ dependencies = [ [[package]] name = "nu_plugin_polars" -version = "0.94.3" +version = "0.95.1" dependencies = [ "chrono", "chrono-tz 0.9.0", + "env_logger 0.11.3", "fancy-regex", "indexmap", + "log", "mimalloc", "nu-cmd-lang", "nu-command", @@ -3379,6 +3518,7 @@ dependencies = [ "nu-plugin", "nu-plugin-test-support", "nu-protocol", + "nu-utils", "num", "polars", "polars-arrow", @@ -3387,7 +3527,7 @@ dependencies = [ "polars-plan", "polars-utils", "serde", - "sqlparser 0.47.0", + "sqlparser", "tempfile", "typetag", "uuid", @@ -3395,19 +3535,22 @@ dependencies = [ [[package]] name = "nu_plugin_query" -version = "0.94.3" +version = "0.95.1" dependencies = [ "gjson", "nu-plugin", "nu-protocol", "scraper", + "serde", + "serde_json", "sxd-document", "sxd-xpath", + "webpage", ] [[package]] name = "nu_plugin_stress_internals" -version = "0.94.3" +version = "0.95.1" dependencies = [ "interprocess", "serde", @@ -3533,7 +3676,7 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "nuon" -version = "0.94.3" +version = "0.95.1" dependencies = [ "chrono", "fancy-regex", @@ -3630,9 +3773,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "open" -version = "5.1.2" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "449f0ff855d85ddbf1edd5b646d65249ead3f5e422aaa86b7d2d0b049b103e32" +checksum = "61a877bf6abd716642a53ef1b89fb498923a4afca5c754f9050b4d081c05c4b3" dependencies = [ "is-wsl", "libc", @@ -3693,6 +3836,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "ordered-multimap" version = "0.7.3" @@ -3997,9 +4146,9 @@ dependencies = [ [[package]] name = "polars" -version = "0.40.0" +version = "0.41.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e148396dca5496566880fa19374f3f789a29db94e3eb458afac1497b4bac5442" +checksum = "ce49e10a756f68eb99c102c6b2a0cbc0c583a0fa7263536ad0913d94be878d2d" dependencies = [ "getrandom", "polars-arrow", @@ -4017,9 +4166,9 @@ dependencies = [ [[package]] name = "polars-arrow" -version = "0.40.0" +version = "0.41.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cb5e11cd0752ae022fa6ca3afa50a14b0301b7ce53c0135828fbb0f4fa8303e" +checksum = "b436f83f62e864f0d91871e26528f2c5552c7cf07c8d77547f1b8e3fde22bd27" dependencies = [ "ahash 0.8.11", "atoi", @@ -4065,9 +4214,9 @@ dependencies = [ [[package]] name = "polars-compute" -version = "0.40.0" +version = "0.41.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89fc4578f826234cdecb782952aa9c479dc49373f81694a7b439c70b6f609ba0" +checksum = "f6758f834f07e622a2f859bebb542b2b7f8879b8704dbb2b2bbab460ddcdca4b" dependencies = [ "bytemuck", "either", @@ -4081,9 +4230,9 @@ dependencies = [ [[package]] name = "polars-core" -version = "0.40.0" +version = "0.41.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e490c6bace1366a558feea33d1846f749a8ca90bd72a6748752bc65bb4710b2a" +checksum = "7ed262e9bdda15a12a9bfcfc9200bec5253335633dbd86cf5b94fda0194244b3" dependencies = [ "ahash 0.8.11", "bitflags 2.5.0", @@ -4115,9 +4264,9 @@ dependencies = [ [[package]] name = "polars-error" -version = "0.40.0" +version = "0.41.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08888f58e61599b00f5ea0c2ccdc796b54b9859559cc0d4582733509451fa01a" +checksum = "53e1707a17475ba5e74c349154b415e3148a1a275e395965427971b5e53ad621" dependencies = [ "avro-schema", "polars-arrow-format", @@ -4128,9 +4277,9 @@ dependencies = [ [[package]] name = "polars-expr" -version = "0.40.0" +version = "0.41.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4173591920fe56ad55af025f92eb0d08421ca85705c326a640c43856094e3484" +checksum = "31a9688d5842e7a7fbad88e67a174778794a91d97d3bba1b3c09dd1656fee3b2" dependencies = [ "ahash 0.8.11", "bitflags 2.5.0", @@ -4148,9 +4297,9 @@ dependencies = [ [[package]] name = "polars-io" -version = "0.40.0" +version = "0.41.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5842896aea46d975b425d63f156f412aed3cfde4c257b64fb1f43ceea288074e" +checksum = "18798dacd94fb9263f65f63f0feab0908675422646d6f7fc37043b85ff6dca35" dependencies = [ "ahash 0.8.11", "async-trait", @@ -4189,9 +4338,9 @@ dependencies = [ [[package]] name = "polars-json" -version = "0.40.0" +version = "0.41.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "160cbad0145b93ac6a88639aadfa6f7d7c769d05a8674f9b7e895b398cae9901" +checksum = "044ea319f667efbf8007c4c38171c2956e0e7f9b078eb66e31e82f80d1e14b51" dependencies = [ "ahash 0.8.11", "chrono", @@ -4210,19 +4359,21 @@ dependencies = [ [[package]] name = "polars-lazy" -version = "0.40.0" +version = "0.41.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e805ea2ebbc6b7749b0afb31b7fc5d32b42b57ba29b984549d43d3a16114c4a5" +checksum = "74a11994c2211f2e99d9ac31776fd7c2c0607d5fe62d5b5db9e396f7d663f3d5" dependencies = [ "ahash 0.8.11", "bitflags 2.5.0", "glob", + "memchr", "once_cell", "polars-arrow", "polars-core", "polars-expr", "polars-io", "polars-json", + "polars-mem-engine", "polars-ops", "polars-pipe", "polars-plan", @@ -4234,10 +4385,29 @@ dependencies = [ ] [[package]] -name = "polars-ops" -version = "0.40.0" +name = "polars-mem-engine" +version = "0.41.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0aed7e169c81b98457641cf82b251f52239a668916c2e683abd1f38df00d58" +checksum = "5acd5fde6fadaddfcae3227ec5b64121007928f8e68870c80653438e20c1c587" +dependencies = [ + "polars-arrow", + "polars-core", + "polars-error", + "polars-expr", + "polars-io", + "polars-json", + "polars-ops", + "polars-plan", + "polars-time", + "polars-utils", + "rayon", +] + +[[package]] +name = "polars-ops" +version = "0.41.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4170c59e974727941edfb722f6d430ed623be9e7f30581ee00832c907f1b9fd" dependencies = [ "ahash 0.8.11", "argminmax", @@ -4271,9 +4441,9 @@ dependencies = [ [[package]] name = "polars-parquet" -version = "0.40.0" +version = "0.41.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c70670a9e51cac66d0e77fd20b5cc957dbcf9f2660d410633862bb72f846d5b8" +checksum = "c684638c36c60c691d707d414249fe8af4a19a35a39d418464b140fe23732e5d" dependencies = [ "ahash 0.8.11", "async-stream", @@ -4286,9 +4456,11 @@ dependencies = [ "num-traits", "parquet-format-safe", "polars-arrow", + "polars-compute", "polars-error", "polars-utils", "seq-macro", + "serde", "simdutf8", "snap", "streaming-decompression", @@ -4297,9 +4469,9 @@ dependencies = [ [[package]] name = "polars-pipe" -version = "0.40.0" +version = "0.41.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a40ae1b3c74ee07e2d1f7cbf56c5d6e15969e45d9b6f0903bd2acaf783ba436" +checksum = "832af9fbebc4c074d95fb19e1ef9e1bf37c343641238c2476febff296a7028ea" dependencies = [ "crossbeam-channel", "crossbeam-queue", @@ -4323,9 +4495,9 @@ dependencies = [ [[package]] name = "polars-plan" -version = "0.40.0" +version = "0.41.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8daa3541ae7e9af311a4389bc2b21f83349c34c723cc67fa524cdefdaa172d90" +checksum = "801390ea815c05c9cf8337f3148090c9c10c9595a839fa0706b77cc2405b4466" dependencies = [ "ahash 0.8.11", "bytemuck", @@ -4353,9 +4525,9 @@ dependencies = [ [[package]] name = "polars-row" -version = "0.40.0" +version = "0.41.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb285f2f3a65b00dd06bef16bb9f712dbb5478f941dab5cf74f9f016d382e40" +checksum = "dee955e91b605fc91db4d0a8ea02609d3a09ff79256d905214a2a6f758cd6f7b" dependencies = [ "bytemuck", "polars-arrow", @@ -4365,9 +4537,9 @@ dependencies = [ [[package]] name = "polars-sql" -version = "0.40.0" +version = "0.41.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a724f699d194cb02c25124d3832f7d4d77f387f1a89ee42f6b9e88ec561d4ad9" +checksum = "d89c00a4b399501d5bd478e8e8022b9391047fe8570324ecba20c4e4833c0e87" dependencies = [ "hex", "once_cell", @@ -4375,18 +4547,20 @@ dependencies = [ "polars-core", "polars-error", "polars-lazy", + "polars-ops", "polars-plan", + "polars-time", "rand", "serde", "serde_json", - "sqlparser 0.39.0", + "sqlparser", ] [[package]] name = "polars-time" -version = "0.40.0" +version = "0.41.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ebec238d8b6200d9f0c3ce411c8441e950bd5a7df7806b8172d06c1d5a4b97" +checksum = "9689b3aff99d64befe300495528bdc44c36d2656c3a8b242a790d4f43df027fc" dependencies = [ "atoi", "bytemuck", @@ -4406,9 +4580,9 @@ dependencies = [ [[package]] name = "polars-utils" -version = "0.40.0" +version = "0.41.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34e1a907c63abf71e5f21467e2e4ff748896c28196746f631c6c25512ec6102c" +checksum = "12081e346983a91e26f395597e1d53dea1b4ecd694653aee1cc402d2fae01f04" dependencies = [ "ahash 0.8.11", "bytemuck", @@ -4644,7 +4818,7 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" dependencies = [ - "env_logger", + "env_logger 0.8.4", "log", "rand", ] @@ -4723,21 +4897,21 @@ dependencies = [ [[package]] name = "ratatui" -version = "0.26.2" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a564a852040e82671dc50a37d88f3aa83bbc690dfc6844cfe7a2591620206a80" +checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" dependencies = [ "bitflags 2.5.0", "cassowary", "compact_str", "crossterm", - "indoc", "itertools 0.12.1", "lru", "paste", "stability", "strum", "unicode-segmentation", + "unicode-truncate", "unicode-width", ] @@ -4828,7 +5002,7 @@ dependencies = [ [[package]] name = "reedline" version = "0.32.0" -source = "git+https://github.com/ysthakur/reedline?branch=fuzzy-select-underline#fe7cbb85feb5236673d89702409ca59362c3e338" +source = "git+https://github.com/nushell/reedline?branch=main#480059a3f52cf919341cda88e8c544edd846bc73" dependencies = [ "arboard", "chrono", @@ -5042,9 +5216,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.4.0" +version = "8.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19549741604902eb99a7ed0ee177a0663ee1eda51a29f71401f166e47e77806a" +checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -5195,7 +5369,7 @@ dependencies = [ "ahash 0.8.11", "cssparser", "ego-tree", - "html5ever", + "html5ever 0.26.0", "once_cell", "selectors", "tendril", @@ -5411,9 +5585,9 @@ dependencies = [ [[package]] name = "shadow-rs" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d75516bdaee8f640543ad1f6e292448c23ce57143f812c3736ab4b0874383df" +checksum = "0a600f795d0894cda22235b44eea4b85c2a35b405f65523645ac8e35b306817a" dependencies = [ "const_format", "is_debug", @@ -5558,15 +5732,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "sqlparser" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743b4dc2cbde11890ccb254a8fc9d537fa41b36da00de2a1c5e9848c9bc42bd7" -dependencies = [ - "log", -] - [[package]] name = "sqlparser" version = "0.47.0" @@ -6284,6 +6449,16 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +[[package]] +name = "unicode-truncate" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5fbabedabe362c618c714dbefda9927b5afc8e2a8102f47f081089a9019226" +dependencies = [ + "itertools 0.12.1", + "unicode-width", +] + [[package]] name = "unicode-width" version = "0.1.12" @@ -6304,9 +6479,9 @@ checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" [[package]] name = "ureq" -version = "2.9.7" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d11a831e3c0b56e438a28308e7c810799e3c118417f342d30ecec080105395cd" +checksum = "72139d247e5f97a3eff96229a7ae85ead5328a39efe76f8bf5a06313d505b6ea" dependencies = [ "base64 0.22.1", "encoding_rs", @@ -6351,98 +6526,82 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uu_cp" -version = "0.0.25" +version = "0.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcbe045dc92209114afdfd366bd18f7b95dbf999f3eaa85ad6dca910b0be3d56" +checksum = "6fb99d355ccb02e8c514e4a1d93e4aa4eedea9837de24635dfd24c165971444e" dependencies = [ "clap", "filetime", "indicatif", "libc", "quick-error 2.0.1", - "uucore 0.0.26", + "uucore", "walkdir", "xattr", ] [[package]] name = "uu_mkdir" -version = "0.0.25" +version = "0.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "040aa4584036b2f65e05387b0ea9ac468afce1db325743ce5f350689fd9ce4ae" +checksum = "219588fbc146f18188781208ac4034616c51cf151677b4e1f9caf63ca8a7f2cf" dependencies = [ "clap", - "uucore 0.0.26", + "uucore", ] [[package]] name = "uu_mktemp" -version = "0.0.25" +version = "0.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f240a99c36d768153874d198c43605a45c86996b576262689a0f18248cc3bc57" +checksum = "b1e79ad2c5911908fce23a6069c52ca82e1997e2ed4bf6abf2d867c79c3dc73f" dependencies = [ "clap", "rand", "tempfile", - "uucore 0.0.26", + "uucore", ] [[package]] name = "uu_mv" -version = "0.0.25" +version = "0.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c99fd7c75e6e85553c92537314be3d9a64b4927051aa1608513feea2f933022" +checksum = "cd57c8d02f8a99ed56ed9f6fddab403ee0e2bf9e8f3a5ca8f0f9e4d6e3e392a0" dependencies = [ "clap", "fs_extra", "indicatif", - "uucore 0.0.26", + "uucore", ] [[package]] name = "uu_uname" -version = "0.0.25" +version = "0.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5951832d73199636bde6c0d61cf960932b3c4450142c290375bc10c7abed6db5" +checksum = "ad1ca90f9b292bccaad0de70e6feccac5182c6713a5e1ca72d97bf3555b608b4" dependencies = [ "clap", "platform-info", - "uucore 0.0.26", + "uucore", ] [[package]] name = "uu_whoami" -version = "0.0.25" +version = "0.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b44166eb6335aeac42744ea368cc4c32d3f2287a4ff765a5ce44d927ab8bb4" +checksum = "bc7c52e42e0425710461700adc1063f468f2ba8a8ff83ee69ba661095ab7b77a" dependencies = [ "clap", "libc", - "uucore 0.0.26", + "uucore", "windows-sys 0.48.0", ] [[package]] name = "uucore" -version = "0.0.25" +version = "0.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23994a722acb43dbc56877e271c9723f167ae42c4c089f909b2d7dd106c3a9b4" -dependencies = [ - "clap", - "glob", - "libc", - "nix", - "once_cell", - "os_display", - "uucore_procs", - "wild", -] - -[[package]] -name = "uucore" -version = "0.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb2ea2f77699e5ff5c7e001af588ceb34cae8b5f9af5496bea5a6476aaa8e780" +checksum = "7b54aad02cf7e96f5fafabb6b836efa73eef934783b17530095a29ffd4fdc154" dependencies = [ "clap", "dunce", @@ -6479,9 +6638,9 @@ checksum = "425a23c7b7145bc7620c9c445817c37b1f78b6790aee9f208133f3c028975b60" [[package]] name = "uuid" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom", "serde", @@ -6724,6 +6883,20 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "webpage" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70862efc041d46e6bbaa82bb9c34ae0596d090e86cbd14bd9e93b36ee6802eac" +dependencies = [ + "curl", + "html5ever 0.27.0", + "markup5ever_rcdom", + "serde", + "serde_json", + "url", +] + [[package]] name = "which" version = "6.0.1" @@ -7136,6 +7309,17 @@ dependencies = [ "rustix", ] +[[package]] +name = "xml5ever" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69" +dependencies = [ + "log", + "mac", + "markup5ever 0.12.1", +] + [[package]] name = "xxhash-rust" version = "0.8.10" diff --git a/Cargo.toml b/Cargo.toml index aa15a51f54..335e2f0384 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ license = "MIT" name = "nu" repository = "https://github.com/nushell/nushell" rust-version = "1.77.2" -version = "0.94.3" +version = "0.95.1" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -39,6 +39,7 @@ members = [ "crates/nu-lsp", "crates/nu-pretty-hex", "crates/nu-protocol", + "crates/nu-derive-value", "crates/nu-plugin", "crates/nu-plugin-core", "crates/nu-plugin-engine", @@ -74,13 +75,15 @@ chardetng = "0.1.17" chrono = { default-features = false, version = "0.4.34" } chrono-humanize = "0.2.3" chrono-tz = "0.8" +convert_case = "0.6" crossbeam-channel = "0.5.8" crossterm = "0.27" csv = "1.3" ctrlc = "3.4" +deunicode = "1.6.0" dialoguer = { default-features = false, version = "0.11" } digest = { default-features = false, version = "0.10" } -dirs-next = "2.0" +dirs = "5.0" dtparse = "2.0" encoding_rs = "0.8" fancy-regex = "0.13" @@ -93,7 +96,7 @@ heck = "0.5.0" human-date-parser = "0.1.1" indexmap = "2.2" indicatif = "0.17" -interprocess = "2.1.0" +interprocess = "2.2.0" is_executable = "1.0" itertools = "0.12" libc = "0.2" @@ -117,17 +120,20 @@ num-format = "0.4" num-traits = "0.2" omnipath = "0.1" once_cell = "1.18" -open = "5.1" +open = "5.3" os_pipe = { version = "1.2", features = ["io_safety"] } pathdiff = "0.2" percent-encoding = "2" pretty_assertions = "1.4" print-positions = "0.6" +proc-macro-error = { version = "1.0", default-features = false } +proc-macro2 = "1.0" procfs = "0.16.0" pwd = "1.3" quick-xml = "0.31.0" quickcheck = "1.0" quickcheck_macros = "1.0" +quote = "1.0" rand = "0.8" ratatui = "0.26" rayon = "1.10" @@ -139,7 +145,7 @@ ropey = "1.6.1" roxmltree = "0.19" rstest = { version = "0.18", default-features = false } rusqlite = "0.31" -rust-embed = "8.4.0" +rust-embed = "8.5.0" same-file = "1.0" serde = { version = "1.0", default-features = false } serde_json = "1.0" @@ -147,6 +153,7 @@ serde_urlencoded = "0.7.1" serde_yaml = "0.9" sha2 = "0.10" strip-ansi-escapes = "0.2.0" +syn = "2.0" sysinfo = "0.30" tabled = { version = "0.14.0", default-features = false } tempfile = "3.10" @@ -157,16 +164,16 @@ trash = "3.3" umask = "2.1" unicode-segmentation = "1.11" unicode-width = "0.1" -ureq = { version = "2.9", default-features = false } +ureq = { version = "2.10", default-features = false } url = "2.2" -uu_cp = "0.0.25" -uu_mkdir = "0.0.25" -uu_mktemp = "0.0.25" -uu_mv = "0.0.25" -uu_whoami = "0.0.25" -uu_uname = "0.0.25" -uucore = "0.0.25" -uuid = "1.8.0" +uu_cp = "0.0.27" +uu_mkdir = "0.0.27" +uu_mktemp = "0.0.27" +uu_mv = "0.0.27" +uu_whoami = "0.0.27" +uu_uname = "0.0.27" +uucore = "0.0.27" +uuid = "1.10.0" v_htmlescape = "0.15.0" wax = "0.6" which = "6.0.0" @@ -174,27 +181,27 @@ windows = "0.54" winreg = "0.52" [dependencies] -nu-cli = { path = "./crates/nu-cli", version = "0.94.3" } -nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.94.3" } -nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.94.3" } -nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.94.3", optional = true } -nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.94.3" } -nu-command = { path = "./crates/nu-command", version = "0.94.3" } -nu-engine = { path = "./crates/nu-engine", version = "0.94.3" } -nu-explore = { path = "./crates/nu-explore", version = "0.94.3" } -nu-lsp = { path = "./crates/nu-lsp/", version = "0.94.3" } -nu-parser = { path = "./crates/nu-parser", version = "0.94.3" } -nu-path = { path = "./crates/nu-path", version = "0.94.3" } -nu-plugin-engine = { path = "./crates/nu-plugin-engine", optional = true, version = "0.94.3" } -nu-protocol = { path = "./crates/nu-protocol", version = "0.94.3" } -nu-std = { path = "./crates/nu-std", version = "0.94.3" } -nu-system = { path = "./crates/nu-system", version = "0.94.3" } -nu-utils = { path = "./crates/nu-utils", version = "0.94.3" } - +nu-cli = { path = "./crates/nu-cli", version = "0.95.1" } +nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.95.1" } +nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.95.1" } +nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.95.1", optional = true } +nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.95.1" } +nu-command = { path = "./crates/nu-command", version = "0.95.1" } +nu-engine = { path = "./crates/nu-engine", version = "0.95.1" } +nu-explore = { path = "./crates/nu-explore", version = "0.95.1" } +nu-lsp = { path = "./crates/nu-lsp/", version = "0.95.1" } +nu-parser = { path = "./crates/nu-parser", version = "0.95.1" } +nu-path = { path = "./crates/nu-path", version = "0.95.1" } +nu-plugin-engine = { path = "./crates/nu-plugin-engine", optional = true, version = "0.95.1" } +nu-protocol = { path = "./crates/nu-protocol", version = "0.95.1" } +nu-std = { path = "./crates/nu-std", version = "0.95.1" } +nu-system = { path = "./crates/nu-system", version = "0.95.1" } +nu-utils = { path = "./crates/nu-utils", version = "0.95.1" } reedline = { workspace = true, features = ["bashisms", "sqlite"] } crossterm = { workspace = true } ctrlc = { workspace = true } +dirs = { workspace = true } log = { workspace = true } miette = { workspace = true, features = ["fancy-no-backtrace", "fancy"] } mimalloc = { version = "0.1.42", default-features = false, optional = true } @@ -218,13 +225,14 @@ nix = { workspace = true, default-features = false, features = [ ] } [dev-dependencies] -nu-test-support = { path = "./crates/nu-test-support", version = "0.94.3" } -nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.94.3" } -nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.94.3" } +nu-test-support = { path = "./crates/nu-test-support", version = "0.95.1" } +nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.95.1" } +nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.95.1" } assert_cmd = "2.0" -dirs-next = { workspace = true } +dirs = { workspace = true } tango-bench = "0.5" pretty_assertions = { workspace = true } +regex = { workspace = true } rstest = { workspace = true, default-features = false } serial_test = "3.1" tempfile = { workspace = true } @@ -244,7 +252,6 @@ default = ["default-no-clipboard", "system-clipboard"] # See https://github.com/nushell/nushell/pull/11535 default-no-clipboard = [ "plugin", - "which-support", "trash-support", "sqlite", "mimalloc", @@ -264,7 +271,6 @@ system-clipboard = [ ] # Stable (Default) -which-support = ["nu-command/which-support", "nu-cmd-lang/which-support"] trash-support = ["nu-command/trash-support", "nu-cmd-lang/trash-support"] # SQLite commands for nushell @@ -298,7 +304,7 @@ bench = false # To use a development version of a dependency please use a global override here # changing versions in each sub-crate of the workspace is tedious [patch.crates-io] -reedline = { git = "https://github.com/ysthakur/reedline", branch = "fuzzy-select-underline" } +reedline = { git = "https://github.com/nushell/reedline", branch = "main" } # nu-ansi-term = {git = "https://github.com/nushell/nu-ansi-term.git", branch = "main"} # Run all benchmarks with `cargo bench` diff --git a/README.md b/README.md index 0bfb7fc0c3..9b88a92528 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ To use `Nu` in GitHub Action, check [setup-nu](https://github.com/marketplace/ac Detailed installation instructions can be found in the [installation chapter of the book](https://www.nushell.sh/book/installation.html). Nu is available via many package managers: -[![Packaging status](https://repology.org/badge/vertical-allrepos/nushell.svg)](https://repology.org/project/nushell/versions) +[![Packaging status](https://repology.org/badge/vertical-allrepos/nushell.svg?columns=3)](https://repology.org/project/nushell/versions) For details about which platforms the Nushell team actively supports, see [our platform support policy](devdocs/PLATFORM_SUPPORT.md). @@ -222,6 +222,7 @@ Please submit an issue or PR to be added to this list. - [clap](https://github.com/clap-rs/clap/tree/master/clap_complete_nushell) - [Dorothy](http://github.com/bevry/dorothy) - [Direnv](https://github.com/direnv/direnv/blob/master/docs/hook.md#nushell) +- [x-cmd](https://x-cmd.com/mod/nu) ## Contributing diff --git a/benches/benchmarks.rs b/benches/benchmarks.rs index 84552daef9..4efb666507 100644 --- a/benches/benchmarks.rs +++ b/benches/benchmarks.rs @@ -4,11 +4,14 @@ use nu_plugin_protocol::{PluginCallResponse, PluginOutput}; use nu_protocol::{ engine::{EngineState, Stack}, - PipelineData, Span, Spanned, Value, + PipelineData, Signals, Span, Spanned, Value, }; use nu_std::load_standard_library; use nu_utils::{get_default_config, get_default_env}; -use std::rc::Rc; +use std::{ + rc::Rc, + sync::{atomic::AtomicBool, Arc}, +}; use std::hint::black_box; @@ -42,13 +45,16 @@ fn setup_stack_and_engine_from_command(command: &str) -> (Stack, EngineState) { }; let mut stack = Stack::new(); + + // Support running benchmarks with IR mode + stack.use_ir = std::env::var_os("NU_USE_IR").is_some(); + evaluate_commands( &commands, &mut engine, &mut stack, PipelineData::empty(), - None, - false, + Default::default(), ) .unwrap(); @@ -90,8 +96,7 @@ fn bench_command( &mut engine, &mut stack, PipelineData::empty(), - None, - false, + Default::default(), ) .unwrap(), ); @@ -250,14 +255,12 @@ fn bench_eval_interleave(n: i32) -> impl IntoBenchmarks { ) } -fn bench_eval_interleave_with_ctrlc(n: i32) -> impl IntoBenchmarks { +fn bench_eval_interleave_with_interrupt(n: i32) -> impl IntoBenchmarks { let mut engine = setup_engine(); - engine.ctrlc = Some(std::sync::Arc::new(std::sync::atomic::AtomicBool::new( - false, - ))); + engine.set_signals(Signals::new(Arc::new(AtomicBool::new(false)))); let stack = Stack::new(); bench_command( - &format!("eval_interleave_with_ctrlc_{n}"), + &format!("eval_interleave_with_interrupt_{n}"), &format!("seq 1 {n} | wrap a | interleave {{ seq 1 {n} | wrap b }} | ignore"), stack, engine, @@ -445,9 +448,9 @@ tango_benchmarks!( bench_eval_interleave(100), bench_eval_interleave(1_000), bench_eval_interleave(10_000), - bench_eval_interleave_with_ctrlc(100), - bench_eval_interleave_with_ctrlc(1_000), - bench_eval_interleave_with_ctrlc(10_000), + bench_eval_interleave_with_interrupt(100), + bench_eval_interleave_with_interrupt(1_000), + bench_eval_interleave_with_interrupt(10_000), // For bench_eval_for(1), bench_eval_for(10), diff --git a/crates/nu-cli/Cargo.toml b/crates/nu-cli/Cargo.toml index c2ca2c9704..2ed504d502 100644 --- a/crates/nu-cli/Cargo.toml +++ b/crates/nu-cli/Cargo.toml @@ -5,27 +5,27 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cli" edition = "2021" license = "MIT" name = "nu-cli" -version = "0.94.3" +version = "0.95.1" [lib] bench = false [dev-dependencies] -nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.94.3" } -nu-command = { path = "../nu-command", version = "0.94.3" } -nu-test-support = { path = "../nu-test-support", version = "0.94.3" } +nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.95.1" } +nu-command = { path = "../nu-command", version = "0.95.1" } +nu-test-support = { path = "../nu-test-support", version = "0.95.1" } rstest = { workspace = true, default-features = false } tempfile = { workspace = true } [dependencies] -nu-cmd-base = { path = "../nu-cmd-base", version = "0.94.3" } -nu-engine = { path = "../nu-engine", version = "0.94.3" } -nu-path = { path = "../nu-path", version = "0.94.3" } -nu-parser = { path = "../nu-parser", version = "0.94.3" } -nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.94.3", optional = true } -nu-protocol = { path = "../nu-protocol", version = "0.94.3" } -nu-utils = { path = "../nu-utils", version = "0.94.3" } -nu-color-config = { path = "../nu-color-config", version = "0.94.3" } +nu-cmd-base = { path = "../nu-cmd-base", version = "0.95.1" } +nu-engine = { path = "../nu-engine", version = "0.95.1" } +nu-path = { path = "../nu-path", version = "0.95.1" } +nu-parser = { path = "../nu-parser", version = "0.95.1" } +nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.95.1", optional = true } +nu-protocol = { path = "../nu-protocol", version = "0.95.1" } +nu-utils = { path = "../nu-utils", version = "0.95.1" } +nu-color-config = { path = "../nu-color-config", version = "0.95.1" } nu-ansi-term = { workspace = true } reedline = { workspace = true, features = ["bashisms", "sqlite"] } @@ -46,4 +46,4 @@ which = { workspace = true } [features] plugin = ["nu-plugin-engine"] -system-clipboard = ["reedline/system_clipboard"] +system-clipboard = ["reedline/system_clipboard"] \ No newline at end of file diff --git a/crates/nu-cli/README.md b/crates/nu-cli/README.md new file mode 100644 index 0000000000..e0eca3c9bd --- /dev/null +++ b/crates/nu-cli/README.md @@ -0,0 +1,7 @@ +This crate implements the core functionality of the interactive Nushell REPL and interfaces with `reedline`. +Currently implements the syntax highlighting and completions logic. +Furthermore includes a few commands that are specific to `reedline` + +## Internal Nushell crate + +This crate implements components of Nushell and is not designed to support plugin authors or other users directly. diff --git a/crates/nu-cli/src/commands/history/history_.rs b/crates/nu-cli/src/commands/history/history_.rs index 8b0714216e..cdf85eea72 100644 --- a/crates/nu-cli/src/commands/history/history_.rs +++ b/crates/nu-cli/src/commands/history/history_.rs @@ -47,7 +47,7 @@ impl Command for History { if let Some(config_path) = nu_path::config_dir() { let clear = call.has_flag(engine_state, stack, "clear")?; let long = call.has_flag(engine_state, stack, "long")?; - let ctrlc = engine_state.ctrlc.clone(); + let signals = engine_state.signals().clone(); let mut history_path = config_path; history_path.push("nushell"); @@ -107,7 +107,7 @@ impl Command for History { file: history_path.display().to_string(), span: head, })? - .into_pipeline_data(head, ctrlc)), + .into_pipeline_data(head, signals)), HistoryFileFormat::Sqlite => Ok(history_reader .and_then(|h| { h.search(SearchQuery::everything(SearchDirection::Forward, None)) @@ -122,7 +122,7 @@ impl Command for History { file: history_path.display().to_string(), span: head, })? - .into_pipeline_data(head, ctrlc)), + .into_pipeline_data(head, signals)), } } } else { diff --git a/crates/nu-cli/src/commands/keybindings_list.rs b/crates/nu-cli/src/commands/keybindings_list.rs index f4450c0c23..350df7b820 100644 --- a/crates/nu-cli/src/commands/keybindings_list.rs +++ b/crates/nu-cli/src/commands/keybindings_list.rs @@ -49,22 +49,24 @@ impl Command for KeybindingsList { fn run( &self, - _engine_state: &EngineState, - _stack: &mut Stack, + engine_state: &EngineState, + stack: &mut Stack, call: &Call, _input: PipelineData, ) -> Result { - let records = if call.named_len() == 0 { - let all_options = ["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() - }; + let all_options = ["modifiers", "keycodes", "edits", "modes", "events"]; + + let presence = all_options + .iter() + .map(|option| call.has_flag(engine_state, stack, option)) + .collect::, ShellError>>()?; + + let records = all_options + .iter() + .zip(presence) + .filter(|(_, present)| *present) + .flat_map(|(option, _)| get_records(option, call.head)) + .collect(); Ok(Value::list(records, call.head).into_pipeline_data()) } diff --git a/crates/nu-cli/src/completions/base.rs b/crates/nu-cli/src/completions/base.rs index 0debabe688..cf13dae68c 100644 --- a/crates/nu-cli/src/completions/base.rs +++ b/crates/nu-cli/src/completions/base.rs @@ -1,13 +1,12 @@ -use crate::completions::{CompletionOptions, SortBy}; +use crate::completions::CompletionOptions; use nu_protocol::{ engine::{Stack, StateWorkingSet}, - levenshtein_distance, Span, + Span, }; use reedline::Suggestion; -// Completer trait represents the three stages of the completion -// fetch, filter and sort pub trait Completer { + /// Fetch, filter, and sort completions #[allow(clippy::too_many_arguments)] fn fetch( &mut self, @@ -19,32 +18,6 @@ pub trait Completer { pos: usize, options: &CompletionOptions, ) -> Vec; - - fn get_sort_by(&self) -> SortBy { - SortBy::Ascending - } - - fn sort(&self, items: Vec, prefix: Vec) -> Vec { - let prefix_str = String::from_utf8_lossy(&prefix).to_string(); - let mut filtered_items = items; - - // Sort items - match self.get_sort_by() { - SortBy::LevenshteinDistance => { - filtered_items.sort_by(|a, b| { - let a_distance = levenshtein_distance(&prefix_str, &a.suggestion.value); - let b_distance = levenshtein_distance(&prefix_str, &b.suggestion.value); - a_distance.cmp(&b_distance) - }); - } - SortBy::Ascending => { - filtered_items.sort_by(|a, b| a.suggestion.value.cmp(&b.suggestion.value)); - } - SortBy::None => {} - }; - - filtered_items - } } #[derive(Debug, Default, PartialEq)] diff --git a/crates/nu-cli/src/completions/command_completions.rs b/crates/nu-cli/src/completions/command_completions.rs index 4a95202d30..9f6155ae55 100644 --- a/crates/nu-cli/src/completions/command_completions.rs +++ b/crates/nu-cli/src/completions/command_completions.rs @@ -9,7 +9,7 @@ use nu_protocol::{ }; use reedline::Suggestion; -use super::SemanticSuggestion; +use super::{completion_common::sort_suggestions, SemanticSuggestion}; pub struct CommandCompletion { flattened: Vec<(Span, FlatShape)>, @@ -164,7 +164,7 @@ impl Completer for CommandCompletion { &mut self, working_set: &StateWorkingSet, _stack: &Stack, - _prefix: Vec, + prefix: Vec, span: Span, offset: usize, pos: usize, @@ -201,7 +201,11 @@ impl Completer for CommandCompletion { }; if !subcommands.is_empty() { - return subcommands; + return sort_suggestions( + &String::from_utf8_lossy(&prefix), + subcommands, + SortBy::LevenshteinDistance, + ); } let config = working_set.get_config(); @@ -226,11 +230,11 @@ impl Completer for CommandCompletion { vec![] }; - subcommands.into_iter().chain(commands).collect::>() - } - - fn get_sort_by(&self) -> SortBy { - SortBy::LevenshteinDistance + sort_suggestions( + &String::from_utf8_lossy(&prefix), + commands, + SortBy::LevenshteinDistance, + ) } } diff --git a/crates/nu-cli/src/completions/completer.rs b/crates/nu-cli/src/completions/completer.rs index 5a7806a8d2..b58988c26a 100644 --- a/crates/nu-cli/src/completions/completer.rs +++ b/crates/nu-cli/src/completions/completer.rs @@ -51,8 +51,7 @@ impl NuCompleter { ..Default::default() }; - // Fetch - let mut suggestions = completer.fetch( + completer.fetch( working_set, &self.stack, prefix.clone(), @@ -60,12 +59,7 @@ impl NuCompleter { offset, pos, &options, - ); - - // Sort - suggestions = completer.sort(suggestions, prefix); - - suggestions + ) } fn external_completion( diff --git a/crates/nu-cli/src/completions/completion_common.rs b/crates/nu-cli/src/completions/completion_common.rs index 26d48a8675..b3bca778a9 100644 --- a/crates/nu-cli/src/completions/completion_common.rs +++ b/crates/nu-cli/src/completions/completion_common.rs @@ -1,16 +1,21 @@ -use crate::completions::{matches, CompletionOptions}; +use crate::{ + completions::{matches, CompletionOptions}, + SemanticSuggestion, +}; use nu_ansi_term::Style; use nu_engine::env_to_string; use nu_path::{expand_to_real_path, home_dir}; use nu_protocol::{ engine::{EngineState, Stack, StateWorkingSet}, - Span, + levenshtein_distance, Span, }; use nu_utils::get_ls_colors; use std::path::{ is_separator, Component, Path, PathBuf, MAIN_SEPARATOR as SEP, MAIN_SEPARATOR_STR, }; +use super::SortBy; + #[derive(Clone, Default)] pub struct PathBuiltFromString { parts: Vec, @@ -45,6 +50,7 @@ fn complete_rec( return completions; }; + let mut entries = Vec::new(); for entry in result.filter_map(|e| e.ok()) { let entry_name = entry.file_name().to_string_lossy().into_owned(); let entry_isdir = entry.path().is_dir(); @@ -53,20 +59,26 @@ fn complete_rec( built.isdir = entry_isdir; if !dir || entry_isdir { - match partial.split_first() { - Some((base, rest)) => { - if matches(base, &entry_name, options) { - if !rest.is_empty() || isdir { - completions - .extend(complete_rec(rest, &built, cwd, options, dir, isdir)); - } else { - completions.push(built); - } + entries.push((entry_name, built)); + } + } + + let prefix = partial.first().unwrap_or(&""); + let sorted_entries = sort_completions(prefix, entries, SortBy::Ascending, |(entry, _)| entry); + + for (entry_name, built) in sorted_entries { + match partial.split_first() { + Some((base, rest)) => { + if matches(base, &entry_name, options) { + if !rest.is_empty() || isdir { + completions.extend(complete_rec(rest, &built, cwd, options, dir, isdir)); + } else { + completions.push(built); } } - None => { - completions.push(built); - } + } + None => { + completions.push(built); } } } @@ -256,3 +268,38 @@ pub fn adjust_if_intermediate( readjusted, } } + +/// Convenience function to sort suggestions using [`sort_completions`] +pub fn sort_suggestions( + prefix: &str, + items: Vec, + sort_by: SortBy, +) -> Vec { + sort_completions(prefix, items, sort_by, |it| &it.suggestion.value) +} + +/// # Arguments +/// * `prefix` - What the user's typed, for sorting by Levenshtein distance +pub fn sort_completions( + prefix: &str, + mut items: Vec, + sort_by: SortBy, + get_value: fn(&T) -> &str, +) -> Vec { + // Sort items + match sort_by { + SortBy::LevenshteinDistance => { + items.sort_by(|a, b| { + let a_distance = levenshtein_distance(prefix, get_value(a)); + let b_distance = levenshtein_distance(prefix, get_value(b)); + a_distance.cmp(&b_distance) + }); + } + SortBy::Ascending => { + items.sort_by(|a, b| get_value(a).cmp(get_value(b))); + } + SortBy::None => {} + }; + + items +} diff --git a/crates/nu-cli/src/completions/custom_completions.rs b/crates/nu-cli/src/completions/custom_completions.rs index f14971d466..0d2c674ef9 100644 --- a/crates/nu-cli/src/completions/custom_completions.rs +++ b/crates/nu-cli/src/completions/custom_completions.rs @@ -12,6 +12,8 @@ use nu_protocol::{ use nu_utils::IgnoreCaseExt; use std::collections::HashMap; +use super::completion_common::sort_suggestions; + pub struct CustomCompletion { stack: Stack, decl_id: usize, @@ -122,15 +124,12 @@ impl Completer for CustomCompletion { }) .unwrap_or_default(); - if let Some(custom_completion_options) = custom_completion_options { + let suggestions = if let Some(custom_completion_options) = custom_completion_options { filter(&prefix, suggestions, &custom_completion_options) } else { filter(&prefix, suggestions, completion_options) - } - } - - fn get_sort_by(&self) -> SortBy { - self.sort_by + }; + sort_suggestions(&String::from_utf8_lossy(&prefix), suggestions, self.sort_by) } } diff --git a/crates/nu-cli/src/completions/directory_completions.rs b/crates/nu-cli/src/completions/directory_completions.rs index 5bef87fe45..71aa0309a8 100644 --- a/crates/nu-cli/src/completions/directory_completions.rs +++ b/crates/nu-cli/src/completions/directory_completions.rs @@ -1,14 +1,14 @@ use crate::completions::{ completion_common::{adjust_if_intermediate, complete_item, AdjustView}, - Completer, CompletionOptions, SortBy, + Completer, CompletionOptions, }; use nu_ansi_term::Style; use nu_protocol::{ engine::{EngineState, Stack, StateWorkingSet}, - levenshtein_distance, Span, + Span, }; use reedline::Suggestion; -use std::path::{Path, MAIN_SEPARATOR as SEP}; +use std::path::Path; use super::SemanticSuggestion; @@ -36,7 +36,7 @@ impl Completer for DirectoryCompletion { // Filter only the folders #[allow(deprecated)] - let output: Vec<_> = directory_completion( + let items: Vec<_> = directory_completion( span, &prefix, &working_set.permanent_state.current_work_dir(), @@ -63,41 +63,11 @@ impl Completer for DirectoryCompletion { }) .collect(); - output - } - - // Sort results prioritizing the non hidden folders - fn sort(&self, items: Vec, prefix: Vec) -> Vec { - let prefix_str = String::from_utf8_lossy(&prefix).to_string(); - - // Sort items - let mut sorted_items = items; - - match self.get_sort_by() { - SortBy::Ascending => { - sorted_items.sort_by(|a, b| { - // Ignore trailing slashes in folder names when sorting - a.suggestion - .value - .trim_end_matches(SEP) - .cmp(b.suggestion.value.trim_end_matches(SEP)) - }); - } - SortBy::LevenshteinDistance => { - sorted_items.sort_by(|a, b| { - let a_distance = levenshtein_distance(&prefix_str, &a.suggestion.value); - let b_distance = levenshtein_distance(&prefix_str, &b.suggestion.value); - a_distance.cmp(&b_distance) - }); - } - _ => (), - } - // Separate the results between hidden and non hidden let mut hidden: Vec = vec![]; let mut non_hidden: Vec = vec![]; - for item in sorted_items.into_iter() { + for item in items.into_iter() { let item_path = Path::new(&item.suggestion.value); if let Some(value) = item_path.file_name() { diff --git a/crates/nu-cli/src/completions/dotnu_completions.rs b/crates/nu-cli/src/completions/dotnu_completions.rs index 4221c82588..464df7e9cf 100644 --- a/crates/nu-cli/src/completions/dotnu_completions.rs +++ b/crates/nu-cli/src/completions/dotnu_completions.rs @@ -1,4 +1,4 @@ -use crate::completions::{file_path_completion, Completer, CompletionOptions, SortBy}; +use crate::completions::{file_path_completion, Completer, CompletionOptions}; use nu_protocol::{ engine::{Stack, StateWorkingSet}, Span, @@ -6,7 +6,7 @@ use nu_protocol::{ use reedline::Suggestion; use std::path::{is_separator, Path, MAIN_SEPARATOR as SEP, MAIN_SEPARATOR_STR}; -use super::SemanticSuggestion; +use super::{completion_common::sort_suggestions, SemanticSuggestion, SortBy}; #[derive(Clone, Default)] pub struct DotNuCompletion {} @@ -132,10 +132,6 @@ impl Completer for DotNuCompletion { }) .collect(); - output - } - - fn get_sort_by(&self) -> SortBy { - SortBy::LevenshteinDistance + sort_suggestions(&prefix_str, output, SortBy::Ascending) } } diff --git a/crates/nu-cli/src/completions/file_completions.rs b/crates/nu-cli/src/completions/file_completions.rs index 96cc50858c..4a0ffed002 100644 --- a/crates/nu-cli/src/completions/file_completions.rs +++ b/crates/nu-cli/src/completions/file_completions.rs @@ -1,15 +1,15 @@ use crate::completions::{ completion_common::{adjust_if_intermediate, complete_item, AdjustView}, - Completer, CompletionOptions, SortBy, + Completer, CompletionOptions, }; use nu_ansi_term::Style; use nu_protocol::{ engine::{EngineState, Stack, StateWorkingSet}, - levenshtein_distance, Span, + Span, }; use nu_utils::IgnoreCaseExt; use reedline::Suggestion; -use std::path::{Path, MAIN_SEPARATOR as SEP}; +use std::path::Path; use super::SemanticSuggestion; @@ -40,7 +40,7 @@ impl Completer for FileCompletion { } = adjust_if_intermediate(&prefix, working_set, span); #[allow(deprecated)] - let output: Vec<_> = complete_item( + let items: Vec<_> = complete_item( readjusted, span, &prefix, @@ -68,41 +68,13 @@ impl Completer for FileCompletion { }) .collect(); - output - } - - // Sort results prioritizing the non hidden folders - fn sort(&self, items: Vec, prefix: Vec) -> Vec { - let prefix_str = String::from_utf8_lossy(&prefix).to_string(); - - // Sort items - let mut sorted_items = items; - - match self.get_sort_by() { - SortBy::Ascending => { - sorted_items.sort_by(|a, b| { - // Ignore trailing slashes in folder names when sorting - a.suggestion - .value - .trim_end_matches(SEP) - .cmp(b.suggestion.value.trim_end_matches(SEP)) - }); - } - SortBy::LevenshteinDistance => { - sorted_items.sort_by(|a, b| { - let a_distance = levenshtein_distance(&prefix_str, &a.suggestion.value); - let b_distance = levenshtein_distance(&prefix_str, &b.suggestion.value); - a_distance.cmp(&b_distance) - }); - } - _ => (), - } + // Sort results prioritizing the non hidden folders // Separate the results between hidden and non hidden let mut hidden: Vec = vec![]; let mut non_hidden: Vec = vec![]; - for item in sorted_items.into_iter() { + for item in items.into_iter() { let item_path = Path::new(&item.suggestion.value); if let Some(value) = item_path.file_name() { diff --git a/crates/nu-cli/src/completions/flag_completions.rs b/crates/nu-cli/src/completions/flag_completions.rs index 9f10787175..04a91b68c8 100644 --- a/crates/nu-cli/src/completions/flag_completions.rs +++ b/crates/nu-cli/src/completions/flag_completions.rs @@ -1,4 +1,6 @@ -use crate::completions::{Completer, CompletionOptions}; +use crate::completions::{ + completion_common::sort_suggestions, Completer, CompletionOptions, SortBy, +}; use nu_protocol::{ ast::{Expr, Expression}, engine::{Stack, StateWorkingSet}, @@ -92,7 +94,7 @@ impl Completer for FlagCompletion { } } - return output; + return sort_suggestions(&String::from_utf8_lossy(&prefix), output, SortBy::Ascending); } vec![] diff --git a/crates/nu-cli/src/completions/variable_completions.rs b/crates/nu-cli/src/completions/variable_completions.rs index ba91218c05..3866953dd2 100644 --- a/crates/nu-cli/src/completions/variable_completions.rs +++ b/crates/nu-cli/src/completions/variable_completions.rs @@ -9,6 +9,8 @@ use nu_protocol::{ use reedline::Suggestion; use std::str; +use super::{completion_common::sort_suggestions, SortBy}; + #[derive(Clone)] pub struct VariableCompletion { var_context: (Vec, Vec>), // tuple with $var and the sublevels (.b.c.d) @@ -40,6 +42,7 @@ impl Completer for VariableCompletion { end: span.end - offset, }; let sublevels_count = self.var_context.1.len(); + let prefix_str = String::from_utf8_lossy(&prefix); // Completions for the given variable if !var_str.is_empty() { @@ -69,7 +72,7 @@ impl Completer for VariableCompletion { } } - return output; + return sort_suggestions(&prefix_str, output, SortBy::Ascending); } } else { // No nesting provided, return all env vars @@ -94,7 +97,7 @@ impl Completer for VariableCompletion { } } - return output; + return sort_suggestions(&prefix_str, output, SortBy::Ascending); } } @@ -118,7 +121,7 @@ impl Completer for VariableCompletion { } } - return output; + return sort_suggestions(&prefix_str, output, SortBy::Ascending); } } @@ -140,7 +143,7 @@ impl Completer for VariableCompletion { } } - return output; + return sort_suggestions(&prefix_str, output, SortBy::Ascending); } } } @@ -230,6 +233,8 @@ impl Completer for VariableCompletion { } } + output = sort_suggestions(&prefix_str, output, SortBy::Ascending); + output.dedup(); // TODO: Removes only consecutive duplicates, is it intended? output diff --git a/crates/nu-cli/src/config_files.rs b/crates/nu-cli/src/config_files.rs index ec7ad2f412..3a02f75e86 100644 --- a/crates/nu-cli/src/config_files.rs +++ b/crates/nu-cli/src/config_files.rs @@ -8,7 +8,7 @@ use nu_protocol::{ report_error_new, HistoryFileFormat, PipelineData, }; #[cfg(feature = "plugin")] -use nu_utils::utils::perf; +use nu_utils::perf; use std::path::PathBuf; #[cfg(feature = "plugin")] @@ -53,13 +53,10 @@ pub fn read_plugin_file( // Reading signatures from plugin registry file // The plugin.msgpackz file stores the parsed signature collected from each registered plugin add_plugin_file(engine_state, plugin_file.clone(), storage_path); - perf( + perf!( "add plugin file to engine_state", start_time, - file!(), - line!(), - column!(), - engine_state.get_config().use_ansi_coloring, + engine_state.get_config().use_ansi_coloring ); start_time = std::time::Instant::now(); @@ -137,13 +134,10 @@ pub fn read_plugin_file( } }; - perf( + perf!( &format!("read plugin file {}", plugin_path.display()), start_time, - file!(), - line!(), - column!(), - engine_state.get_config().use_ansi_coloring, + engine_state.get_config().use_ansi_coloring ); start_time = std::time::Instant::now(); @@ -156,13 +150,10 @@ pub fn read_plugin_file( return; } - perf( + perf!( &format!("load plugin file {}", plugin_path.display()), start_time, - file!(), - line!(), - column!(), - engine_state.get_config().use_ansi_coloring, + engine_state.get_config().use_ansi_coloring ); } } @@ -344,7 +335,10 @@ pub fn migrate_old_plugin_file(engine_state: &EngineState, storage_path: &str) - name: identity.name().to_owned(), filename: identity.filename().to_owned(), shell: identity.shell().map(|p| p.to_owned()), - data: PluginRegistryItemData::Valid { commands }, + data: PluginRegistryItemData::Valid { + metadata: Default::default(), + commands, + }, }); } @@ -378,13 +372,10 @@ pub fn migrate_old_plugin_file(engine_state: &EngineState, storage_path: &str) - ); } - perf( + perf!( "migrate old plugin file", start_time, - file!(), - line!(), - column!(), - engine_state.get_config().use_ansi_coloring, + engine_state.get_config().use_ansi_coloring ); true } diff --git a/crates/nu-cli/src/eval_cmds.rs b/crates/nu-cli/src/eval_cmds.rs index 8fa3bf30e5..1459f1ef0d 100644 --- a/crates/nu-cli/src/eval_cmds.rs +++ b/crates/nu-cli/src/eval_cmds.rs @@ -8,24 +8,53 @@ use nu_protocol::{ }; use std::sync::Arc; +#[derive(Default)] +pub struct EvaluateCommandsOpts { + pub table_mode: Option, + pub error_style: Option, + pub no_newline: bool, +} + /// Run a command (or commands) given to us by the user pub fn evaluate_commands( commands: &Spanned, engine_state: &mut EngineState, stack: &mut Stack, input: PipelineData, - table_mode: Option, - no_newline: bool, + opts: EvaluateCommandsOpts, ) -> Result<(), ShellError> { + let EvaluateCommandsOpts { + table_mode, + error_style, + no_newline, + } = opts; + + // Handle the configured error style early + if let Some(e_style) = error_style { + match e_style.coerce_str()?.parse() { + Ok(e_style) => { + Arc::make_mut(&mut engine_state.config).error_style = e_style; + } + Err(err) => { + return Err(ShellError::GenericError { + error: "Invalid value for `--error-style`".into(), + msg: err.into(), + span: Some(e_style.span()), + help: None, + inner: vec![], + }); + } + } + } + // Translate environment variables from Strings to Values convert_env_values(engine_state, stack)?; // Parse the source code let (block, delta) = { if let Some(ref t_mode) = table_mode { - let mut config = engine_state.get_config().clone(); - config.table_mode = t_mode.coerce_str()?.parse().unwrap_or_default(); - engine_state.set_config(config); + Arc::make_mut(&mut engine_state.config).table_mode = + t_mode.coerce_str()?.parse().unwrap_or_default(); } let mut working_set = StateWorkingSet::new(engine_state); @@ -40,6 +69,11 @@ pub fn evaluate_commands( std::process::exit(1); } + if let Some(err) = working_set.compile_errors.first() { + report_error(&working_set, err); + // Not a fatal error, for now + } + (output, working_set.render()) }; diff --git a/crates/nu-cli/src/eval_file.rs b/crates/nu-cli/src/eval_file.rs index ff6ba36fe3..bac4378d1b 100644 --- a/crates/nu-cli/src/eval_file.rs +++ b/crates/nu-cli/src/eval_file.rs @@ -76,12 +76,21 @@ pub fn evaluate_file( trace!("parsing file: {}", file_path_str); let block = parse(&mut working_set, Some(file_path_str), &file, false); + if let Some(warning) = working_set.parse_warnings.first() { + report_error(&working_set, warning); + } + // If any parse errors were found, report the first error and exit. if let Some(err) = working_set.parse_errors.first() { report_error(&working_set, err); std::process::exit(1); } + if let Some(err) = working_set.compile_errors.first() { + report_error(&working_set, err); + // Not a fatal error, for now + } + // Look for blocks whose name starts with "main" and replace it with the filename. for block in working_set.delta.blocks.iter_mut().map(Arc::make_mut) { if block.signature.name == "main" { diff --git a/crates/nu-cli/src/lib.rs b/crates/nu-cli/src/lib.rs index c4342dc3a0..d584802cfb 100644 --- a/crates/nu-cli/src/lib.rs +++ b/crates/nu-cli/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] mod commands; mod completions; mod config_files; @@ -17,7 +18,7 @@ mod validation; pub use commands::add_cli_context; pub use completions::{FileCompletion, NuCompleter, SemanticSuggestion, SuggestionKind}; pub use config_files::eval_config_contents; -pub use eval_cmds::evaluate_commands; +pub use eval_cmds::{evaluate_commands, EvaluateCommandsOpts}; pub use eval_file::evaluate_file; pub use menus::NuHelpCompleter; pub use nu_cmd_base::util::get_init_cwd; diff --git a/crates/nu-cli/src/menus/help_completions.rs b/crates/nu-cli/src/menus/help_completions.rs index 3fb93fdb73..f0effb2075 100644 --- a/crates/nu-cli/src/menus/help_completions.rs +++ b/crates/nu-cli/src/menus/help_completions.rs @@ -1,25 +1,31 @@ use nu_engine::documentation::get_flags_section; -use nu_protocol::{engine::EngineState, levenshtein_distance}; +use nu_protocol::{engine::EngineState, levenshtein_distance, Config}; use nu_utils::IgnoreCaseExt; use reedline::{Completer, Suggestion}; use std::{fmt::Write, sync::Arc}; -pub struct NuHelpCompleter(Arc); +pub struct NuHelpCompleter { + engine_state: Arc, + config: Arc, +} impl NuHelpCompleter { - pub fn new(engine_state: Arc) -> Self { - Self(engine_state) + pub fn new(engine_state: Arc, config: Arc) -> Self { + Self { + engine_state, + config, + } } fn completion_helper(&self, line: &str, pos: usize) -> Vec { let folded_line = line.to_folded_case(); let mut commands = self - .0 + .engine_state .get_decls_sorted(false) .into_iter() .filter_map(|(_, decl_id)| { - let decl = self.0.get_decl(decl_id); + let decl = self.engine_state.get_decl(decl_id); (decl.name().to_folded_case().contains(&folded_line) || decl.usage().to_folded_case().contains(&folded_line) || decl @@ -54,9 +60,12 @@ impl NuHelpCompleter { let _ = write!(long_desc, "Usage:\r\n > {}\r\n", sig.call_signature()); if !sig.named.is_empty() { - long_desc.push_str(&get_flags_section(Some(&*self.0.clone()), &sig, |v| { - v.to_parsable_string(", ", &self.0.config) - })) + long_desc.push_str(&get_flags_section( + Some(&self.engine_state), + Some(&self.config), + &sig, + |v| v.to_parsable_string(", ", &self.config), + )) } if !sig.required_positional.is_empty() @@ -71,7 +80,7 @@ impl NuHelpCompleter { let opt_suffix = if let Some(value) = &positional.default_value { format!( " (optional, default: {})", - &value.to_parsable_string(", ", &self.0.config), + &value.to_parsable_string(", ", &self.config), ) } else { (" (optional)").to_string() @@ -139,7 +148,8 @@ mod test { ) { let engine_state = nu_command::add_shell_command_context(nu_cmd_lang::create_default_context()); - let mut completer = NuHelpCompleter::new(engine_state.into()); + let config = engine_state.get_config().clone(); + let mut completer = NuHelpCompleter::new(engine_state.into(), config); let suggestions = completer.complete(line, end); assert_eq!( diff --git a/crates/nu-cli/src/nu_highlight.rs b/crates/nu-cli/src/nu_highlight.rs index 07084c6258..0b5f211838 100644 --- a/crates/nu-cli/src/nu_highlight.rs +++ b/crates/nu-cli/src/nu_highlight.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use nu_engine::command_prelude::*; use reedline::{Highlighter, StyledText}; @@ -32,14 +34,11 @@ impl Command for NuHighlight { ) -> Result { let head = call.head; - let ctrlc = engine_state.ctrlc.clone(); - let engine_state = std::sync::Arc::new(engine_state.clone()); - let config = engine_state.get_config().clone(); + let signals = engine_state.signals(); let highlighter = crate::NuHighlighter { - engine_state, - stack: std::sync::Arc::new(stack.clone()), - config, + engine_state: Arc::new(engine_state.clone()), + stack: Arc::new(stack.clone()), }; input.map( @@ -50,7 +49,7 @@ impl Command for NuHighlight { } Err(err) => Value::error(err, head), }, - ctrlc, + signals, ) } diff --git a/crates/nu-cli/src/reedline_config.rs b/crates/nu-cli/src/reedline_config.rs index 9c08265dfa..d5d3272bc1 100644 --- a/crates/nu-cli/src/reedline_config.rs +++ b/crates/nu-cli/src/reedline_config.rs @@ -75,15 +75,21 @@ const DEFAULT_HELP_MENU: &str = r#" // Adds all menus to line editor pub(crate) fn add_menus( mut line_editor: Reedline, - engine_state: Arc, + engine_state_ref: Arc, stack: &Stack, - config: &Config, + config: Arc, ) -> Result { //log::trace!("add_menus: config: {:#?}", &config); line_editor = line_editor.clear_menus(); for menu in &config.menus { - line_editor = add_menu(line_editor, menu, engine_state.clone(), stack, config)? + line_editor = add_menu( + line_editor, + menu, + engine_state_ref.clone(), + stack, + config.clone(), + )? } // Checking if the default menus have been added from the config file @@ -93,13 +99,16 @@ pub(crate) fn add_menus( ("help_menu", DEFAULT_HELP_MENU), ]; + let mut engine_state = (*engine_state_ref).clone(); + let mut menu_eval_results = vec![]; + for (name, definition) in default_menus { if !config .menus .iter() - .any(|menu| menu.name.to_expanded_string("", config) == name) + .any(|menu| menu.name.to_expanded_string("", &config) == name) { - let (block, _) = { + let (block, delta) = { let mut working_set = StateWorkingSet::new(&engine_state); let output = parse( &mut working_set, @@ -111,15 +120,31 @@ pub(crate) fn add_menus( (output, working_set.render()) }; + engine_state.merge_delta(delta)?; + let mut temp_stack = Stack::new().capture(); let input = PipelineData::Empty; - let res = eval_block::(&engine_state, &mut temp_stack, &block, input)?; + menu_eval_results.push(eval_block::( + &engine_state, + &mut temp_stack, + &block, + input, + )?); + } + } - if let PipelineData::Value(value, None) = res { - for menu in create_menus(&value)? { - line_editor = - add_menu(line_editor, &menu, engine_state.clone(), stack, config)?; - } + let new_engine_state_ref = Arc::new(engine_state); + + for res in menu_eval_results.into_iter() { + if let PipelineData::Value(value, None) = res { + for menu in create_menus(&value)? { + line_editor = add_menu( + line_editor, + &menu, + new_engine_state_ref.clone(), + stack, + config.clone(), + )?; } } } @@ -132,27 +157,27 @@ fn add_menu( menu: &ParsedMenu, engine_state: Arc, stack: &Stack, - config: &Config, + config: Arc, ) -> Result { let span = menu.menu_type.span(); if let Value::Record { val, .. } = &menu.menu_type { - let layout = extract_value("layout", val, span)?.to_expanded_string("", config); + let layout = extract_value("layout", val, span)?.to_expanded_string("", &config); match layout.as_str() { - "columnar" => add_columnar_menu(line_editor, menu, engine_state, stack, config), + "columnar" => add_columnar_menu(line_editor, menu, engine_state, stack, &config), "list" => add_list_menu(line_editor, menu, engine_state, stack, config), "ide" => add_ide_menu(line_editor, menu, engine_state, stack, config), "description" => add_description_menu(line_editor, menu, engine_state, stack, config), _ => Err(ShellError::UnsupportedConfigValue { expected: "columnar, list, ide or description".to_string(), - value: menu.menu_type.to_abbreviated_string(config), + value: menu.menu_type.to_abbreviated_string(&config), span: menu.menu_type.span(), }), } } else { Err(ShellError::UnsupportedConfigValue { expected: "only record type".to_string(), - value: menu.menu_type.to_abbreviated_string(config), + value: menu.menu_type.to_abbreviated_string(&config), span: menu.menu_type.span(), }) } @@ -263,9 +288,9 @@ pub(crate) fn add_list_menu( menu: &ParsedMenu, engine_state: Arc, stack: &Stack, - config: &Config, + config: Arc, ) -> Result { - let name = menu.name.to_expanded_string("", config); + let name = menu.name.to_expanded_string("", &config); let mut list_menu = ListMenu::default().with_name(&name); let span = menu.menu_type.span(); @@ -292,7 +317,7 @@ pub(crate) fn add_list_menu( } } - let marker = menu.marker.to_expanded_string("", config); + let marker = menu.marker.to_expanded_string("", &config); list_menu = list_menu.with_marker(&marker); let only_buffer_difference = menu.only_buffer_difference.as_bool()?; @@ -318,7 +343,7 @@ pub(crate) fn add_list_menu( } _ => Err(ShellError::UnsupportedConfigValue { expected: "block or omitted value".to_string(), - value: menu.source.to_abbreviated_string(config), + value: menu.source.to_abbreviated_string(&config), span: menu.source.span(), }), } @@ -330,10 +355,10 @@ pub(crate) fn add_ide_menu( menu: &ParsedMenu, engine_state: Arc, stack: &Stack, - config: &Config, + config: Arc, ) -> Result { let span = menu.menu_type.span(); - let name = menu.name.to_expanded_string("", config); + let name = menu.name.to_expanded_string("", &config); let mut ide_menu = IdeMenu::default().with_name(&name); if let Value::Record { val, .. } = &menu.menu_type { @@ -398,7 +423,7 @@ pub(crate) fn add_ide_menu( } else { return Err(ShellError::UnsupportedConfigValue { expected: "bool or record".to_string(), - value: border.to_abbreviated_string(config), + value: border.to_abbreviated_string(&config), span: border.span(), }); } @@ -422,7 +447,7 @@ pub(crate) fn add_ide_menu( _ => { return Err(ShellError::UnsupportedConfigValue { expected: "\"left\", \"right\" or \"prefer_right\"".to_string(), - value: description_mode.to_abbreviated_string(config), + value: description_mode.to_abbreviated_string(&config), span: description_mode.span(), }); } @@ -490,7 +515,7 @@ pub(crate) fn add_ide_menu( } } - let marker = menu.marker.to_expanded_string("", config); + let marker = menu.marker.to_expanded_string("", &config); ide_menu = ide_menu.with_marker(&marker); let only_buffer_difference = menu.only_buffer_difference.as_bool()?; @@ -516,7 +541,7 @@ pub(crate) fn add_ide_menu( } _ => Err(ShellError::UnsupportedConfigValue { expected: "block or omitted value".to_string(), - value: menu.source.to_abbreviated_string(config), + value: menu.source.to_abbreviated_string(&config), span, }), } @@ -528,9 +553,9 @@ pub(crate) fn add_description_menu( menu: &ParsedMenu, engine_state: Arc, stack: &Stack, - config: &Config, + config: Arc, ) -> Result { - let name = menu.name.to_expanded_string("", config); + let name = menu.name.to_expanded_string("", &config); let mut description_menu = DescriptionMenu::default().with_name(&name); let span = menu.menu_type.span(); @@ -589,7 +614,7 @@ pub(crate) fn add_description_menu( } } - let marker = menu.marker.to_expanded_string("", config); + let marker = menu.marker.to_expanded_string("", &config); description_menu = description_menu.with_marker(&marker); let only_buffer_difference = menu.only_buffer_difference.as_bool()?; @@ -598,7 +623,7 @@ pub(crate) fn add_description_menu( let span = menu.source.span(); match &menu.source { Value::Nothing { .. } => { - let completer = Box::new(NuHelpCompleter::new(engine_state)); + let completer = Box::new(NuHelpCompleter::new(engine_state, config)); Ok(line_editor.with_menu(ReedlineMenu::WithCompleter { menu: Box::new(description_menu), completer, @@ -619,7 +644,7 @@ pub(crate) fn add_description_menu( } _ => Err(ShellError::UnsupportedConfigValue { expected: "closure or omitted value".to_string(), - value: menu.source.to_abbreviated_string(config), + value: menu.source.to_abbreviated_string(&config), span: menu.source.span(), }), } diff --git a/crates/nu-cli/src/repl.rs b/crates/nu-cli/src/repl.rs index d9aef32bbb..817950a445 100644 --- a/crates/nu-cli/src/repl.rs +++ b/crates/nu-cli/src/repl.rs @@ -31,7 +31,7 @@ use nu_protocol::{ }; use nu_utils::{ filesystem::{have_permission, PermissionResult}, - utils::perf, + perf, }; use reedline::{ CursorConfig, CwdAwareHinter, DefaultCompleter, EditCommand, Emacs, FileBackedHistory, @@ -43,7 +43,7 @@ use std::{ io::{self, IsTerminal, Write}, panic::{catch_unwind, AssertUnwindSafe}, path::{Path, PathBuf}, - sync::{atomic::Ordering, Arc}, + sync::Arc, time::{Duration, Instant}, }; use sysinfo::System; @@ -89,14 +89,7 @@ pub fn evaluate_repl( if let Err(e) = convert_env_values(engine_state, &unique_stack) { report_error_new(engine_state, &e); } - perf( - "translate env vars", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("translate env vars", start_time, use_color); // seed env vars unique_stack.add_env_var( @@ -225,28 +218,14 @@ fn get_line_editor( // Now that reedline is created, get the history session id and store it in engine_state store_history_id_in_engine(engine_state, &line_editor); - perf( - "setup reedline", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("setup reedline", start_time, use_color); if let Some(history) = engine_state.history_config() { start_time = std::time::Instant::now(); line_editor = setup_history(nushell_path, engine_state, line_editor, history)?; - perf( - "setup history", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("setup history", start_time, use_color); } Ok(line_editor) } @@ -289,28 +268,14 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) { if let Err(err) = engine_state.merge_env(&mut stack, cwd) { report_error_new(engine_state, &err); } - perf( - "merge env", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + // Check whether $env.NU_USE_IR is set, so that the user can change it in the REPL + // Temporary while IR eval is optional + stack.use_ir = stack.has_env_var(engine_state, "NU_USE_IR"); + perf!("merge env", start_time, use_color); start_time = std::time::Instant::now(); - // Reset the ctrl-c handler - if let Some(ctrlc) = &mut engine_state.ctrlc { - ctrlc.store(false, Ordering::SeqCst); - } - perf( - "reset ctrlc", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + engine_state.reset_signals(); + perf!("reset signals", start_time, use_color); start_time = std::time::Instant::now(); // Right before we start our prompt and take input from the user, @@ -320,14 +285,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) { report_error_new(engine_state, &err); } } - perf( - "pre-prompt hook", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("pre-prompt hook", start_time, use_color); start_time = std::time::Instant::now(); // Next, check all the environment variables they ask for @@ -336,17 +294,10 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) { if let Err(error) = hook::eval_env_change_hook(env_change, engine_state, &mut stack) { report_error_new(engine_state, &error) } - perf( - "env-change hook", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("env-change hook", start_time, use_color); let engine_reference = Arc::new(engine_state.clone()); - let config = engine_state.get_config(); + let config = stack.get_config(engine_state); start_time = std::time::Instant::now(); // Find the configured cursor shapes for each mode @@ -355,14 +306,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) { vi_normal: map_nucursorshape_to_cursorshape(config.cursor_shape_vi_normal), emacs: map_nucursorshape_to_cursorshape(config.cursor_shape_emacs), }; - perf( - "get config/cursor config", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("get config/cursor config", start_time, use_color); start_time = std::time::Instant::now(); // at this line we have cloned the state for the completer and the transient prompt @@ -379,7 +323,6 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) { engine_state: engine_reference.clone(), // STACK-REFERENCE 1 stack: stack_arc.clone(), - config: config.clone(), })) .with_validator(Box::new(NuValidator { engine_state: engine_reference.clone(), @@ -392,16 +335,17 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) { .with_quick_completions(config.quick_completions) .with_partial_completions(config.partial_completions) .with_ansi_colors(config.use_ansi_coloring) + .with_cwd(Some( + engine_state + .cwd(None) + .map(|cwd| cwd.into_std_path_buf()) + .unwrap_or_default() + .to_string_lossy() + .to_string(), + )) .with_cursor_config(cursor_config); - perf( - "reedline builder", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("reedline builder", start_time, use_color); let style_computer = StyleComputer::from_config(engine_state, &stack_arc); @@ -416,14 +360,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) { line_editor.disable_hints() }; - perf( - "reedline coloring/style_computer", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("reedline coloring/style_computer", start_time, use_color); start_time = std::time::Instant::now(); trace!("adding menus"); @@ -433,14 +370,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) { Reedline::create() }); - perf( - "reedline adding menus", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("reedline adding menus", start_time, use_color); start_time = std::time::Instant::now(); let buffer_editor = get_editor(engine_state, &stack_arc, Span::unknown()); @@ -457,14 +387,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) { line_editor }; - perf( - "reedline buffer_editor", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("reedline buffer_editor", start_time, use_color); if let Some(history) = engine_state.history_config() { start_time = std::time::Instant::now(); @@ -474,28 +397,14 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) { } } - perf( - "sync_history", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("sync_history", start_time, use_color); } start_time = std::time::Instant::now(); // Changing the line editor based on the found keybindings line_editor = setup_keybindings(engine_state, line_editor); - perf( - "keybindings", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("keybindings", start_time, use_color); start_time = std::time::Instant::now(); let config = &engine_state.get_config().clone(); @@ -512,14 +421,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) { nu_prompt, ); - perf( - "update_prompt", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("update_prompt", start_time, use_color); *entry_num += 1; @@ -546,14 +448,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) { // so we should avoid it or making stack cheaper to clone. let mut stack = Arc::unwrap_or_clone(stack_arc); - perf( - "line_editor setup", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("line_editor setup", start_time, use_color); let line_editor_input_time = std::time::Instant::now(); match input { @@ -590,14 +485,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) { } } - perf( - "pre_execution_hook", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("pre_execution_hook", start_time, use_color); let mut repl = engine_state.repl_state.lock().expect("repl state mutex"); repl.cursor_pos = line_editor.current_insertion_point(); @@ -612,26 +500,20 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) { run_ansi_sequence(VSCODE_PRE_EXECUTION_MARKER); - perf( + perf!( "pre_execute_marker (633;C) ansi escape sequence", start_time, - file!(), - line!(), - column!(), - use_color, + use_color ); } else if shell_integration_osc133 { start_time = Instant::now(); run_ansi_sequence(PRE_EXECUTION_MARKER); - perf( + perf!( "pre_execute_marker (133;C) ansi escape sequence", start_time, - file!(), - line!(), - column!(), - use_color, + use_color ); } } else if shell_integration_osc133 { @@ -639,13 +521,10 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) { run_ansi_sequence(PRE_EXECUTION_MARKER); - perf( + perf!( "pre_execute_marker (133;C) ansi escape sequence", start_time, - file!(), - line!(), - column!(), - use_color, + use_color ); } @@ -769,22 +648,16 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) { ); } } - perf( + perf!( "processing line editor input", line_editor_input_time, - file!(), - line!(), - column!(), - use_color, + use_color ); - perf( + perf!( "time between prompts in line editor loop", loop_start_time, - file!(), - line!(), - column!(), - use_color, + use_color ); (true, stack, line_editor) @@ -800,13 +673,14 @@ fn prepare_history_metadata( line_editor: &mut Reedline, ) { if !s.is_empty() && line_editor.has_last_command_context() { - #[allow(deprecated)] let result = line_editor .update_last_command_context(&|mut c| { c.start_timestamp = Some(chrono::Utc::now()); c.hostname = hostname.map(str::to_string); - - c.cwd = Some(StateWorkingSet::new(engine_state).get_cwd()); + c.cwd = engine_state + .cwd(None) + .ok() + .map(|path| path.to_string_lossy().to_string()); c }) .into_diagnostic(); @@ -1061,14 +935,7 @@ fn run_shell_integration_osc2( // ESC]2;stringBEL -- Set window title to string run_ansi_sequence(&format!("\x1b]2;{title}\x07")); - perf( - "set title with command osc2", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("set title with command osc2", start_time, use_color); } } @@ -1093,13 +960,10 @@ fn run_shell_integration_osc7( percent_encoding::utf8_percent_encode(&path, percent_encoding::CONTROLS) )); - perf( + perf!( "communicate path to terminal with osc7", start_time, - file!(), - line!(), - column!(), - use_color, + use_color ); } } @@ -1116,13 +980,10 @@ fn run_shell_integration_osc9_9(engine_state: &EngineState, stack: &mut Stack, u percent_encoding::utf8_percent_encode(&path, percent_encoding::CONTROLS) )); - perf( + perf!( "communicate path to terminal with osc9;9", start_time, - file!(), - line!(), - column!(), - use_color, + use_color ); } } @@ -1142,13 +1003,10 @@ fn run_shell_integration_osc633(engine_state: &EngineState, stack: &mut Stack, u VSCODE_CWD_PROPERTY_MARKER_PREFIX, path, VSCODE_CWD_PROPERTY_MARKER_SUFFIX )); - perf( + perf!( "communicate path to terminal with osc633;P", start_time, - file!(), - line!(), - column!(), - use_color, + use_color ); } } @@ -1371,13 +1229,10 @@ fn run_finaliziation_ansi_sequence( shell_integration_osc133, )); - perf( + perf!( "post_execute_marker (633;D) ansi escape sequences", start_time, - file!(), - line!(), - column!(), - use_color, + use_color ); } else if shell_integration_osc133 { let start_time = Instant::now(); @@ -1389,13 +1244,10 @@ fn run_finaliziation_ansi_sequence( shell_integration_osc133, )); - perf( + perf!( "post_execute_marker (133;D) ansi escape sequences", start_time, - file!(), - line!(), - column!(), - use_color, + use_color ); } } else if shell_integration_osc133 { @@ -1408,13 +1260,10 @@ fn run_finaliziation_ansi_sequence( shell_integration_osc133, )); - perf( + perf!( "post_execute_marker (133;D) ansi escape sequences", start_time, - file!(), - line!(), - column!(), - use_color, + use_color ); } } diff --git a/crates/nu-cli/src/syntax_highlight.rs b/crates/nu-cli/src/syntax_highlight.rs index e296943af6..ebd8315190 100644 --- a/crates/nu-cli/src/syntax_highlight.rs +++ b/crates/nu-cli/src/syntax_highlight.rs @@ -6,7 +6,7 @@ use nu_parser::{flatten_block, parse, FlatShape}; use nu_protocol::{ ast::{Block, Expr, Expression, PipelineRedirection, RecordItem}, engine::{EngineState, Stack, StateWorkingSet}, - Config, Span, + Span, }; use reedline::{Highlighter, StyledText}; use std::sync::Arc; @@ -14,15 +14,14 @@ use std::sync::Arc; pub struct NuHighlighter { pub engine_state: Arc, pub stack: Arc, - pub config: Config, } impl Highlighter for NuHighlighter { fn highlight(&self, line: &str, _cursor: usize) -> StyledText { trace!("highlighting: {}", line); - let highlight_resolved_externals = - self.engine_state.get_config().highlight_resolved_externals; + let config = self.stack.get_config(&self.engine_state); + let highlight_resolved_externals = config.highlight_resolved_externals; let mut working_set = StateWorkingSet::new(&self.engine_state); let block = parse(&mut working_set, None, line.as_bytes(), false); let (shapes, global_span_offset) = { @@ -88,7 +87,7 @@ impl Highlighter for NuHighlighter { .to_string(); let mut add_colored_token = |shape: &FlatShape, text: String| { - output.push((get_shape_color(shape.as_str(), &self.config), text)); + output.push((get_shape_color(shape.as_str(), &config), text)); }; match shape.1 { @@ -128,9 +127,9 @@ impl Highlighter for NuHighlighter { let start = part.start - span.start; let end = part.end - span.start; let text = next_token[start..end].to_string(); - let mut style = get_shape_color(shape.as_str(), &self.config); + let mut style = get_shape_color(shape.as_str(), &config); if highlight { - style = get_matching_brackets_style(style, &self.config); + style = get_matching_brackets_style(style, &config); } output.push((style, text)); } @@ -138,6 +137,7 @@ impl Highlighter for NuHighlighter { FlatShape::Filepath => add_colored_token(&shape.1, next_token), FlatShape::Directory => add_colored_token(&shape.1, next_token), + FlatShape::GlobInterpolation => add_colored_token(&shape.1, next_token), FlatShape::GlobPattern => add_colored_token(&shape.1, next_token), FlatShape::Variable(_) | FlatShape::VarDecl(_) => { add_colored_token(&shape.1, next_token) @@ -429,6 +429,14 @@ fn find_matching_block_end_in_expr( ) }), + Expr::Collect(_, expr) => find_matching_block_end_in_expr( + line, + working_set, + expr, + global_span_offset, + global_cursor_offset, + ), + Expr::Block(block_id) | Expr::Closure(block_id) | Expr::RowCondition(block_id) @@ -452,15 +460,17 @@ fn find_matching_block_end_in_expr( } } - Expr::StringInterpolation(exprs) => exprs.iter().find_map(|expr| { - find_matching_block_end_in_expr( - line, - working_set, - expr, - global_span_offset, - global_cursor_offset, - ) - }), + Expr::StringInterpolation(exprs) | Expr::GlobInterpolation(exprs, _) => { + exprs.iter().find_map(|expr| { + find_matching_block_end_in_expr( + line, + working_set, + expr, + global_span_offset, + global_cursor_offset, + ) + }) + } Expr::List(list) => { if expr_last == global_cursor_offset { diff --git a/crates/nu-cli/src/util.rs b/crates/nu-cli/src/util.rs index e4912e012f..bcee53c9b0 100644 --- a/crates/nu-cli/src/util.rs +++ b/crates/nu-cli/src/util.rs @@ -8,7 +8,7 @@ use nu_protocol::{ }; #[cfg(windows)] use nu_utils::enable_vt_processing; -use nu_utils::utils::perf; +use nu_utils::perf; use std::path::Path; // This will collect environment variables from std::env and adds them to a stack. @@ -228,13 +228,10 @@ pub fn eval_source( let _ = enable_vt_processing(); } - perf( + perf!( &format!("eval_source {}", &fname), start_time, - file!(), - line!(), - column!(), - engine_state.get_config().use_ansi_coloring, + engine_state.get_config().use_ansi_coloring ); exit_code @@ -265,6 +262,11 @@ fn evaluate_source( return Ok(Some(1)); } + if let Some(err) = working_set.compile_errors.first() { + report_error(&working_set, err); + // Not a fatal error, for now + } + (output, working_set.render()) }; diff --git a/crates/nu-cli/tests/completions/mod.rs b/crates/nu-cli/tests/completions/mod.rs index a5b0b13aa8..590979251e 100644 --- a/crates/nu-cli/tests/completions/mod.rs +++ b/crates/nu-cli/tests/completions/mod.rs @@ -11,7 +11,7 @@ use std::{ sync::Arc, }; use support::{ - completions_helpers::{new_partial_engine, new_quote_engine}, + completions_helpers::{new_dotnu_engine, new_partial_engine, new_quote_engine}, file, folder, match_suggestions, new_engine, }; @@ -85,8 +85,29 @@ fn custom_completer() -> NuCompleter { NuCompleter::new(Arc::new(engine), Arc::new(stack)) } +#[fixture] +fn subcommand_completer() -> NuCompleter { + // Create a new engine + let (dir, _, mut engine, mut stack) = new_engine(); + + // Use fuzzy matching, because subcommands are sorted by Levenshtein distance, + // and that's not very useful with prefix matching + let commands = r#" + $env.config.completions.algorithm = "fuzzy" + def foo [] {} + def "foo bar" [] {} + def "foo abaz" [] {} + def "foo aabrr" [] {} + def food [] {} + "#; + assert!(support::merge_input(commands.as_bytes(), &mut engine, &mut stack, dir).is_ok()); + + // Instantiate a new completer + NuCompleter::new(Arc::new(engine), Arc::new(stack)) +} + #[test] -fn variables_dollar_sign_with_varialblecompletion() { +fn variables_dollar_sign_with_variablecompletion() { let (_, _, engine, stack) = new_engine(); let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); @@ -138,43 +159,42 @@ fn variables_customcompletion_subcommands_with_customcompletion_2( #[test] fn dotnu_completions() { // Create a new engine - let (_, _, engine, stack) = new_engine(); + let (_, _, engine, stack) = new_dotnu_engine(); // Instantiate a new completer let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); + let expected = vec![ + "asdf.nu".into(), + "bar.nu".into(), + "bat.nu".into(), + "baz.nu".into(), + #[cfg(windows)] + "dir_module\\".into(), + #[cfg(not(windows))] + "dir_module/".into(), + "foo.nu".into(), + "spam.nu".into(), + "xyzzy.nu".into(), + ]; + // Test source completion let completion_str = "source-env ".to_string(); let suggestions = completer.complete(&completion_str, completion_str.len()); - assert_eq!(2, suggestions.len()); - assert_eq!("custom_completion.nu", suggestions.first().unwrap().value); - #[cfg(windows)] - assert_eq!("directory_completion\\", suggestions.get(1).unwrap().value); - #[cfg(not(windows))] - assert_eq!("directory_completion/", suggestions.get(1).unwrap().value); + match_suggestions(expected.clone(), suggestions); // Test use completion let completion_str = "use ".to_string(); let suggestions = completer.complete(&completion_str, completion_str.len()); - assert_eq!(2, suggestions.len()); - assert_eq!("custom_completion.nu", suggestions.first().unwrap().value); - #[cfg(windows)] - assert_eq!("directory_completion\\", suggestions.get(1).unwrap().value); - #[cfg(not(windows))] - assert_eq!("directory_completion/", suggestions.get(1).unwrap().value); + match_suggestions(expected.clone(), suggestions); // Test overlay use completion let completion_str = "overlay use ".to_string(); let suggestions = completer.complete(&completion_str, completion_str.len()); - assert_eq!(2, suggestions.len()); - assert_eq!("custom_completion.nu", suggestions.first().unwrap().value); - #[cfg(windows)] - assert_eq!("directory_completion\\", suggestions.get(1).unwrap().value); - #[cfg(not(windows))] - assert_eq!("directory_completion/", suggestions.get(1).unwrap().value); + match_suggestions(expected, suggestions); } #[test] @@ -276,9 +296,10 @@ fn partial_completions() { // Create the expected values let expected_paths: Vec = vec![ - folder(dir.join("partial_a")), - folder(dir.join("partial_b")), - folder(dir.join("partial_c")), + folder(dir.join("partial")), + folder(dir.join("partial-a")), + folder(dir.join("partial-b")), + folder(dir.join("partial-c")), ]; // Match the results @@ -292,13 +313,14 @@ fn partial_completions() { // Create the expected values let expected_paths: Vec = vec![ - file(dir.join("partial_a").join("have_ext.exe")), - file(dir.join("partial_a").join("have_ext.txt")), - file(dir.join("partial_a").join("hello")), - file(dir.join("partial_a").join("hola")), - file(dir.join("partial_b").join("hello_b")), - file(dir.join("partial_b").join("hi_b")), - file(dir.join("partial_c").join("hello_c")), + file(dir.join("partial").join("hello.txt")), + file(dir.join("partial-a").join("have_ext.exe")), + file(dir.join("partial-a").join("have_ext.txt")), + file(dir.join("partial-a").join("hello")), + file(dir.join("partial-a").join("hola")), + file(dir.join("partial-b").join("hello_b")), + file(dir.join("partial-b").join("hi_b")), + file(dir.join("partial-c").join("hello_c")), ]; // Match the results @@ -311,14 +333,15 @@ fn partial_completions() { // Create the expected values let expected_paths: Vec = vec![ - file(dir.join("partial_a").join("anotherfile")), - file(dir.join("partial_a").join("have_ext.exe")), - file(dir.join("partial_a").join("have_ext.txt")), - file(dir.join("partial_a").join("hello")), - file(dir.join("partial_a").join("hola")), - file(dir.join("partial_b").join("hello_b")), - file(dir.join("partial_b").join("hi_b")), - file(dir.join("partial_c").join("hello_c")), + file(dir.join("partial").join("hello.txt")), + file(dir.join("partial-a").join("anotherfile")), + file(dir.join("partial-a").join("have_ext.exe")), + file(dir.join("partial-a").join("have_ext.txt")), + file(dir.join("partial-a").join("hello")), + file(dir.join("partial-a").join("hola")), + file(dir.join("partial-b").join("hello_b")), + file(dir.join("partial-b").join("hi_b")), + file(dir.join("partial-c").join("hello_c")), ]; // Match the results @@ -343,19 +366,25 @@ fn partial_completions() { // Create the expected values let expected_paths: Vec = vec![ file( - dir.join("partial_a") + dir.join("partial") .join("..") .join("final_partial") .join("somefile"), ), file( - dir.join("partial_b") + dir.join("partial-a") .join("..") .join("final_partial") .join("somefile"), ), file( - dir.join("partial_c") + dir.join("partial-b") + .join("..") + .join("final_partial") + .join("somefile"), + ), + file( + dir.join("partial-c") .join("..") .join("final_partial") .join("somefile"), @@ -366,28 +395,28 @@ fn partial_completions() { match_suggestions(expected_paths, suggestions); // Test completion for all files under directories whose names begin with "pa" - let file_str = file(dir.join("partial_a").join("have")); + let file_str = file(dir.join("partial-a").join("have")); let target_file = format!("rm {file_str}"); let suggestions = completer.complete(&target_file, target_file.len()); // Create the expected values let expected_paths: Vec = vec![ - file(dir.join("partial_a").join("have_ext.exe")), - file(dir.join("partial_a").join("have_ext.txt")), + file(dir.join("partial-a").join("have_ext.exe")), + file(dir.join("partial-a").join("have_ext.txt")), ]; // Match the results match_suggestions(expected_paths, suggestions); // Test completion for all files under directories whose names begin with "pa" - let file_str = file(dir.join("partial_a").join("have_ext.")); + let file_str = file(dir.join("partial-a").join("have_ext.")); let file_dir = format!("rm {file_str}"); let suggestions = completer.complete(&file_dir, file_dir.len()); // Create the expected values let expected_paths: Vec = vec![ - file(dir.join("partial_a").join("have_ext.exe")), - file(dir.join("partial_a").join("have_ext.txt")), + file(dir.join("partial-a").join("have_ext.exe")), + file(dir.join("partial-a").join("have_ext.txt")), ]; // Match the results @@ -652,6 +681,27 @@ fn command_watch_with_filecompletion() { match_suggestions(expected_paths, suggestions) } +#[rstest] +fn subcommand_completions(mut subcommand_completer: NuCompleter) { + let prefix = "foo br"; + let suggestions = subcommand_completer.complete(prefix, prefix.len()); + match_suggestions( + vec!["foo bar".to_string(), "foo aabrr".to_string()], + suggestions, + ); + + let prefix = "foo b"; + let suggestions = subcommand_completer.complete(prefix, prefix.len()); + match_suggestions( + vec![ + "foo bar".to_string(), + "foo abaz".to_string(), + "foo aabrr".to_string(), + ], + suggestions, + ); +} + #[test] fn file_completion_quoted() { let (_, _, engine, stack) = new_quote_engine(); @@ -662,16 +712,16 @@ fn file_completion_quoted() { let suggestions = completer.complete(target_dir, target_dir.len()); let expected_paths: Vec = vec![ - "\'[a] bc.txt\'".to_string(), "`--help`".to_string(), "`-42`".to_string(), "`-inf`".to_string(), "`4.2`".to_string(), + "\'[a] bc.txt\'".to_string(), "`te st.txt`".to_string(), "`te#st.txt`".to_string(), "`te'st.txt`".to_string(), "`te(st).txt`".to_string(), - format!("`{}`", folder("test dir".into())), + format!("`{}`", folder("test dir")), ]; match_suggestions(expected_paths, suggestions); @@ -763,11 +813,13 @@ fn variables_completions() { // Test completions for $nu let suggestions = completer.complete("$nu.", 4); - assert_eq!(15, suggestions.len()); + assert_eq!(18, suggestions.len()); let expected: Vec = vec![ + "cache-dir".into(), "config-path".into(), "current-exe".into(), + "data-dir".into(), "default-config-dir".into(), "env-path".into(), "history-enabled".into(), @@ -781,6 +833,7 @@ fn variables_completions() { "plugin-path".into(), "startup-time".into(), "temp-path".into(), + "vendor-autoload-dir".into(), ]; // Match results @@ -854,6 +907,11 @@ fn variables_completions() { // Match results match_suggestions(expected, suggestions); + + let suggestions = completer.complete("$", 1); + let expected: Vec = vec!["$actor".into(), "$env".into(), "$in".into(), "$nu".into()]; + + match_suggestions(expected, suggestions); } #[test] diff --git a/crates/nu-cli/tests/completions/support/completions_helpers.rs b/crates/nu-cli/tests/completions/support/completions_helpers.rs index 47f46ab00e..623d6fbc5a 100644 --- a/crates/nu-cli/tests/completions/support/completions_helpers.rs +++ b/crates/nu-cli/tests/completions/support/completions_helpers.rs @@ -1,5 +1,6 @@ use nu_engine::eval_block; use nu_parser::parse; +use nu_path::{AbsolutePathBuf, PathBuf}; use nu_protocol::{ debugger::WithoutDebug, engine::{EngineState, Stack, StateWorkingSet}, @@ -7,14 +8,14 @@ use nu_protocol::{ }; use nu_test_support::fs; use reedline::Suggestion; -use std::path::{PathBuf, MAIN_SEPARATOR}; +use std::path::MAIN_SEPARATOR; fn create_default_context() -> EngineState { nu_command::add_shell_command_context(nu_cmd_lang::create_default_context()) } // creates a new engine with the current path into the completions fixtures folder -pub fn new_engine() -> (PathBuf, String, EngineState, Stack) { +pub fn new_engine() -> (AbsolutePathBuf, String, EngineState, Stack) { // Target folder inside assets let dir = fs::fixtures().join("completions"); let dir_str = dir @@ -68,7 +69,53 @@ pub fn new_engine() -> (PathBuf, String, EngineState, Stack) { (dir, dir_str, engine_state, stack) } -pub fn new_quote_engine() -> (PathBuf, String, EngineState, Stack) { +// creates a new engine with the current path into the completions fixtures folder +pub fn new_dotnu_engine() -> (AbsolutePathBuf, String, EngineState, Stack) { + // Target folder inside assets + let dir = fs::fixtures().join("dotnu_completions"); + let dir_str = dir + .clone() + .into_os_string() + .into_string() + .unwrap_or_default(); + let dir_span = nu_protocol::Span::new(0, dir_str.len()); + + // Create a new engine with default context + let mut engine_state = create_default_context(); + + // Add $nu + engine_state.generate_nu_constant(); + + // New stack + let mut stack = Stack::new(); + + // Add pwd as env var + stack.add_env_var("PWD".to_string(), Value::string(dir_str.clone(), dir_span)); + stack.add_env_var( + "TEST".to_string(), + Value::string("NUSHELL".to_string(), dir_span), + ); + + stack.add_env_var( + "NU_LIB_DIRS".to_string(), + Value::List { + vals: vec![ + Value::string(file(dir.join("lib-dir1")), dir_span), + Value::string(file(dir.join("lib-dir2")), dir_span), + Value::string(file(dir.join("lib-dir3")), dir_span), + ], + internal_span: dir_span, + }, + ); + + // Merge environment into the permanent state + let merge_result = engine_state.merge_env(&mut stack, &dir); + assert!(merge_result.is_ok()); + + (dir, dir_str, engine_state, stack) +} + +pub fn new_quote_engine() -> (AbsolutePathBuf, String, EngineState, Stack) { // Target folder inside assets let dir = fs::fixtures().join("quoted_completions"); let dir_str = dir @@ -103,7 +150,7 @@ pub fn new_quote_engine() -> (PathBuf, String, EngineState, Stack) { (dir, dir_str, engine_state, stack) } -pub fn new_partial_engine() -> (PathBuf, String, EngineState, Stack) { +pub fn new_partial_engine() -> (AbsolutePathBuf, String, EngineState, Stack) { // Target folder inside assets let dir = fs::fixtures().join("partial_completions"); let dir_str = dir @@ -149,22 +196,25 @@ pub fn match_suggestions(expected: Vec, suggestions: Vec) { Expected: {expected:#?}\n" ) } - expected.iter().zip(suggestions).for_each(|it| { - assert_eq!(it.0, &it.1.value); - }); + assert_eq!( + expected, + suggestions + .into_iter() + .map(|it| it.value) + .collect::>() + ); } // append the separator to the converted path -pub fn folder(path: PathBuf) -> String { +pub fn folder(path: impl Into) -> String { let mut converted_path = file(path); converted_path.push(MAIN_SEPARATOR); - converted_path } // convert a given path to string -pub fn file(path: PathBuf) -> String { - path.into_os_string().into_string().unwrap_or_default() +pub fn file(path: impl Into) -> String { + path.into().into_os_string().into_string().unwrap() } // merge_input executes the given input into the engine @@ -173,7 +223,7 @@ pub fn merge_input( input: &[u8], engine_state: &mut EngineState, stack: &mut Stack, - dir: PathBuf, + dir: AbsolutePathBuf, ) -> Result<(), ShellError> { let (block, delta) = { let mut working_set = StateWorkingSet::new(engine_state); diff --git a/crates/nu-cmd-base/Cargo.toml b/crates/nu-cmd-base/Cargo.toml index fb6a977ab9..2fe8610f49 100644 --- a/crates/nu-cmd-base/Cargo.toml +++ b/crates/nu-cmd-base/Cargo.toml @@ -5,17 +5,17 @@ edition = "2021" license = "MIT" name = "nu-cmd-base" repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-base" -version = "0.94.3" +version = "0.95.1" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -nu-engine = { path = "../nu-engine", version = "0.94.3" } -nu-parser = { path = "../nu-parser", version = "0.94.3" } -nu-path = { path = "../nu-path", version = "0.94.3" } -nu-protocol = { path = "../nu-protocol", version = "0.94.3" } +nu-engine = { path = "../nu-engine", version = "0.95.1" } +nu-parser = { path = "../nu-parser", version = "0.95.1" } +nu-path = { path = "../nu-path", version = "0.95.1" } +nu-protocol = { path = "../nu-protocol", version = "0.95.1" } indexmap = { workspace = true } miette = { workspace = true } -[dev-dependencies] +[dev-dependencies] \ No newline at end of file diff --git a/crates/nu-cmd-base/README.md b/crates/nu-cmd-base/README.md new file mode 100644 index 0000000000..28d299928a --- /dev/null +++ b/crates/nu-cmd-base/README.md @@ -0,0 +1,5 @@ +Utilities used by the different `nu-command`/`nu-cmd-*` crates, should not contain any full `Command` implementations. + +## Internal Nushell crate + +This crate implements components of Nushell and is not designed to support plugin authors or other users directly. diff --git a/crates/nu-cmd-base/src/hook.rs b/crates/nu-cmd-base/src/hook.rs index 76c13bd5c3..cef5348618 100644 --- a/crates/nu-cmd-base/src/hook.rs +++ b/crates/nu-cmd-base/src/hook.rs @@ -194,7 +194,7 @@ pub fn eval_hook( let Some(follow) = val.get("code") else { return Err(ShellError::CantFindColumn { col_name: "code".into(), - span, + span: Some(span), src_span: span, }); }; diff --git a/crates/nu-cmd-base/src/input_handler.rs b/crates/nu-cmd-base/src/input_handler.rs index d81193e190..7d61f90cb0 100644 --- a/crates/nu-cmd-base/src/input_handler.rs +++ b/crates/nu-cmd-base/src/input_handler.rs @@ -1,5 +1,5 @@ -use nu_protocol::{ast::CellPath, PipelineData, ShellError, Span, Value}; -use std::sync::{atomic::AtomicBool, Arc}; +use nu_protocol::{ast::CellPath, PipelineData, ShellError, Signals, Span, Value}; +use std::sync::Arc; pub trait CmdArgument { fn take_cell_paths(&mut self) -> Option>; @@ -40,7 +40,7 @@ pub fn operate( mut arg: A, input: PipelineData, span: Span, - ctrlc: Option>, + signals: &Signals, ) -> Result where A: CmdArgument + Send + Sync + 'static, @@ -55,7 +55,7 @@ where _ => cmd(&v, &arg, span), } }, - ctrlc, + signals, ), Some(column_paths) => { let arg = Arc::new(arg); @@ -79,7 +79,7 @@ where } v }, - ctrlc, + signals, ) } } diff --git a/crates/nu-cmd-base/src/lib.rs b/crates/nu-cmd-base/src/lib.rs index 250a57d741..c2608f8466 100644 --- a/crates/nu-cmd-base/src/lib.rs +++ b/crates/nu-cmd-base/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] pub mod formats; pub mod hook; pub mod input_handler; diff --git a/crates/nu-cmd-base/src/util.rs b/crates/nu-cmd-base/src/util.rs index 559161329b..e485243877 100644 --- a/crates/nu-cmd-base/src/util.rs +++ b/crates/nu-cmd-base/src/util.rs @@ -1,3 +1,4 @@ +use nu_path::AbsolutePathBuf; use nu_protocol::{ engine::{EngineState, Stack}, Range, ShellError, Span, Value, @@ -15,18 +16,20 @@ pub fn get_init_cwd() -> PathBuf { pub fn get_guaranteed_cwd(engine_state: &EngineState, stack: &Stack) -> PathBuf { engine_state .cwd(Some(stack)) + .map(AbsolutePathBuf::into_std_path_buf) .unwrap_or(crate::util::get_init_cwd()) } type MakeRangeError = fn(&str, Span) -> ShellError; +/// Returns a inclusive pair of boundary in given `range`. pub fn process_range(range: &Range) -> Result<(isize, isize), MakeRangeError> { match range { Range::IntRange(range) => { let start = range.start().try_into().unwrap_or(0); let end = match range.end() { - Bound::Included(v) => (v + 1) as isize, - Bound::Excluded(v) => v as isize, + Bound::Included(v) => v as isize, + Bound::Excluded(v) => (v - 1) as isize, Bound::Unbounded => isize::MAX, }; Ok((start, end)) diff --git a/crates/nu-cmd-extra/Cargo.toml b/crates/nu-cmd-extra/Cargo.toml index 39a705f47c..8609adb44c 100644 --- a/crates/nu-cmd-extra/Cargo.toml +++ b/crates/nu-cmd-extra/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" license = "MIT" name = "nu-cmd-extra" repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-extra" -version = "0.94.3" +version = "0.95.1" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -13,13 +13,13 @@ version = "0.94.3" bench = false [dependencies] -nu-cmd-base = { path = "../nu-cmd-base", version = "0.94.3" } -nu-engine = { path = "../nu-engine", version = "0.94.3" } -nu-json = { version = "0.94.3", path = "../nu-json" } -nu-parser = { path = "../nu-parser", version = "0.94.3" } -nu-pretty-hex = { version = "0.94.3", path = "../nu-pretty-hex" } -nu-protocol = { path = "../nu-protocol", version = "0.94.3" } -nu-utils = { path = "../nu-utils", version = "0.94.3" } +nu-cmd-base = { path = "../nu-cmd-base", version = "0.95.1" } +nu-engine = { path = "../nu-engine", version = "0.95.1" } +nu-json = { version = "0.95.1", path = "../nu-json" } +nu-parser = { path = "../nu-parser", version = "0.95.1" } +nu-pretty-hex = { version = "0.95.1", path = "../nu-pretty-hex" } +nu-protocol = { path = "../nu-protocol", version = "0.95.1" } +nu-utils = { path = "../nu-utils", version = "0.95.1" } # Potential dependencies for extras heck = { workspace = true } @@ -32,11 +32,7 @@ serde_urlencoded = { workspace = true } v_htmlescape = { workspace = true } itertools = { workspace = true } -[features] -extra = ["default"] -default = [] - [dev-dependencies] -nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.94.3" } -nu-command = { path = "../nu-command", version = "0.94.3" } -nu-test-support = { path = "../nu-test-support", version = "0.94.3" } +nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.95.1" } +nu-command = { path = "../nu-command", version = "0.95.1" } +nu-test-support = { path = "../nu-test-support", version = "0.95.1" } \ No newline at end of file diff --git a/crates/nu-cmd-extra/src/extra/bits/and.rs b/crates/nu-cmd-extra/src/extra/bits/and.rs index 538cf6e60f..234b4e5cc1 100644 --- a/crates/nu-cmd-extra/src/extra/bits/and.rs +++ b/crates/nu-cmd-extra/src/extra/bits/and.rs @@ -79,7 +79,7 @@ impl Command for BitsAnd { input.map( move |value| binary_op(&value, &target, little_endian, |(l, r)| l & r, head), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-cmd-extra/src/extra/bits/into.rs b/crates/nu-cmd-extra/src/extra/bits/into.rs index cf85f92ac5..9ff8bd0a05 100644 --- a/crates/nu-cmd-extra/src/extra/bits/into.rs +++ b/crates/nu-cmd-extra/src/extra/bits/into.rs @@ -1,6 +1,9 @@ +use std::io::{self, Read, Write}; + use nu_cmd_base::input_handler::{operate, CmdArgument}; use nu_engine::command_prelude::*; +use nu_protocol::Signals; use num_traits::ToPrimitive; pub struct Arguments { @@ -118,12 +121,43 @@ fn into_bits( let cell_paths = call.rest(engine_state, stack, 0)?; let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths); - if let PipelineData::ByteStream(stream, ..) = input { - // TODO: in the future, we may want this to stream out, converting each to bytes - Ok(Value::binary(stream.into_bytes()?, head).into_pipeline_data()) + if let PipelineData::ByteStream(stream, metadata) = input { + Ok(PipelineData::ByteStream( + byte_stream_to_bits(stream, head), + metadata, + )) } else { let args = Arguments { cell_paths }; - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) + } +} + +fn byte_stream_to_bits(stream: ByteStream, head: Span) -> ByteStream { + if let Some(mut reader) = stream.reader() { + let mut is_first = true; + ByteStream::from_fn( + head, + Signals::empty(), + ByteStreamType::String, + move |buffer| { + let mut byte = [0]; + if reader.read(&mut byte[..]).err_span(head)? > 0 { + // Format the byte as bits + if is_first { + is_first = false; + } else { + buffer.push(b' '); + } + write!(buffer, "{:08b}", byte[0]).expect("format failed"); + Ok(true) + } else { + // EOF + Ok(false) + } + }, + ) + } else { + ByteStream::read(io::empty(), head, Signals::empty(), ByteStreamType::String) } } diff --git a/crates/nu-cmd-extra/src/extra/bits/not.rs b/crates/nu-cmd-extra/src/extra/bits/not.rs index e4c344f137..405cc79d7e 100644 --- a/crates/nu-cmd-extra/src/extra/bits/not.rs +++ b/crates/nu-cmd-extra/src/extra/bits/not.rs @@ -82,7 +82,7 @@ impl Command for BitsNot { number_size, }; - operate(action, args, input, head, engine_state.ctrlc.clone()) + operate(action, args, input, head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-cmd-extra/src/extra/bits/or.rs b/crates/nu-cmd-extra/src/extra/bits/or.rs index 2352d65c23..a0af2dc8d0 100644 --- a/crates/nu-cmd-extra/src/extra/bits/or.rs +++ b/crates/nu-cmd-extra/src/extra/bits/or.rs @@ -80,7 +80,7 @@ impl Command for BitsOr { input.map( move |value| binary_op(&value, &target, little_endian, |(l, r)| l | r, head), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-cmd-extra/src/extra/bits/rotate_left.rs b/crates/nu-cmd-extra/src/extra/bits/rotate_left.rs index cbd9d17eb5..5bb9e42f6b 100644 --- a/crates/nu-cmd-extra/src/extra/bits/rotate_left.rs +++ b/crates/nu-cmd-extra/src/extra/bits/rotate_left.rs @@ -86,7 +86,7 @@ impl Command for BitsRol { bits, }; - operate(action, args, input, head, engine_state.ctrlc.clone()) + operate(action, args, input, head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-cmd-extra/src/extra/bits/rotate_right.rs b/crates/nu-cmd-extra/src/extra/bits/rotate_right.rs index 0aea603ce1..31e17891a3 100644 --- a/crates/nu-cmd-extra/src/extra/bits/rotate_right.rs +++ b/crates/nu-cmd-extra/src/extra/bits/rotate_right.rs @@ -86,7 +86,7 @@ impl Command for BitsRor { bits, }; - operate(action, args, input, head, engine_state.ctrlc.clone()) + operate(action, args, input, head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-cmd-extra/src/extra/bits/shift_left.rs b/crates/nu-cmd-extra/src/extra/bits/shift_left.rs index 049408c24a..6a67a45e0e 100644 --- a/crates/nu-cmd-extra/src/extra/bits/shift_left.rs +++ b/crates/nu-cmd-extra/src/extra/bits/shift_left.rs @@ -88,7 +88,7 @@ impl Command for BitsShl { bits, }; - operate(action, args, input, head, engine_state.ctrlc.clone()) + operate(action, args, input, head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-cmd-extra/src/extra/bits/shift_right.rs b/crates/nu-cmd-extra/src/extra/bits/shift_right.rs index d66db68ee5..e45e10ac94 100644 --- a/crates/nu-cmd-extra/src/extra/bits/shift_right.rs +++ b/crates/nu-cmd-extra/src/extra/bits/shift_right.rs @@ -88,7 +88,7 @@ impl Command for BitsShr { bits, }; - operate(action, args, input, head, engine_state.ctrlc.clone()) + operate(action, args, input, head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-cmd-extra/src/extra/bits/xor.rs b/crates/nu-cmd-extra/src/extra/bits/xor.rs index 65c3be4e1a..4a71487137 100644 --- a/crates/nu-cmd-extra/src/extra/bits/xor.rs +++ b/crates/nu-cmd-extra/src/extra/bits/xor.rs @@ -80,7 +80,7 @@ impl Command for BitsXor { input.map( move |value| binary_op(&value, &target, little_endian, |(l, r)| l ^ r, head), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-cmd-extra/src/extra/conversions/fmt.rs b/crates/nu-cmd-extra/src/extra/conversions/fmt.rs index fec0745dac..3b682291e7 100644 --- a/crates/nu-cmd-extra/src/extra/conversions/fmt.rs +++ b/crates/nu-cmd-extra/src/extra/conversions/fmt.rs @@ -59,7 +59,7 @@ fn fmt( ) -> Result { let cell_paths: Vec = call.rest(engine_state, stack, 0)?; let args = CellPathOnlyArgs::from(cell_paths); - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn action(input: &Value, _args: &CellPathOnlyArgs, span: Span) -> Value { diff --git a/crates/nu-cmd-extra/src/extra/filters/each_while.rs b/crates/nu-cmd-extra/src/extra/filters/each_while.rs index 58679c8eea..1f30e5b9b9 100644 --- a/crates/nu-cmd-extra/src/extra/filters/each_while.rs +++ b/crates/nu-cmd-extra/src/extra/filters/each_while.rs @@ -89,7 +89,7 @@ impl Command for EachWhile { } }) .fuse() - .into_pipeline_data(head, engine_state.ctrlc.clone())) + .into_pipeline_data(head, engine_state.signals().clone())) } PipelineData::ByteStream(stream, ..) => { let span = stream.span(); @@ -107,7 +107,7 @@ impl Command for EachWhile { } }) .fuse() - .into_pipeline_data(head, engine_state.ctrlc.clone())) + .into_pipeline_data(head, engine_state.signals().clone())) } else { Ok(PipelineData::Empty) } diff --git a/crates/nu-cmd-extra/src/extra/filters/update_cells.rs b/crates/nu-cmd-extra/src/extra/filters/update_cells.rs index c90e933410..b102c13c0d 100644 --- a/crates/nu-cmd-extra/src/extra/filters/update_cells.rs +++ b/crates/nu-cmd-extra/src/extra/filters/update_cells.rs @@ -108,7 +108,7 @@ impl Command for UpdateCells { columns, span: head, } - .into_pipeline_data(head, engine_state.ctrlc.clone()) + .into_pipeline_data(head, engine_state.signals().clone()) .set_metadata(metadata)) } } diff --git a/crates/nu-cmd-extra/src/extra/formats/to/html.rs b/crates/nu-cmd-extra/src/extra/formats/to/html.rs index c2ca80e2c6..f02f11cf84 100644 --- a/crates/nu-cmd-extra/src/extra/formats/to/html.rs +++ b/crates/nu-cmd-extra/src/extra/formats/to/html.rs @@ -239,7 +239,7 @@ fn to_html( let partial = call.has_flag(engine_state, stack, "partial")?; let list = call.has_flag(engine_state, stack, "list")?; let theme: Option> = call.get_flag(engine_state, stack, "theme")?; - let config = engine_state.get_config(); + let config = &stack.get_config(engine_state); let vec_of_values = input.into_iter().collect::>(); let headers = merge_descriptors(&vec_of_values); @@ -368,6 +368,7 @@ fn theme_demo(span: Span) -> PipelineData { .collect(); Value::list(result, span).into_pipeline_data_with_metadata(PipelineMetadata { data_source: DataSource::HtmlThemes, + content_type: None, }) } diff --git a/crates/nu-cmd-extra/src/extra/math/arccos.rs b/crates/nu-cmd-extra/src/extra/math/arccos.rs index 120fc4df98..5d0a659380 100644 --- a/crates/nu-cmd-extra/src/extra/math/arccos.rs +++ b/crates/nu-cmd-extra/src/extra/math/arccos.rs @@ -45,7 +45,7 @@ impl Command for SubCommand { } input.map( move |value| operate(value, head, use_degrees), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-cmd-extra/src/extra/math/arccosh.rs b/crates/nu-cmd-extra/src/extra/math/arccosh.rs index 30e0d2cfb6..532a388b3f 100644 --- a/crates/nu-cmd-extra/src/extra/math/arccosh.rs +++ b/crates/nu-cmd-extra/src/extra/math/arccosh.rs @@ -41,10 +41,7 @@ impl Command for SubCommand { if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map( - move |value| operate(value, head), - engine_state.ctrlc.clone(), - ) + input.map(move |value| operate(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-cmd-extra/src/extra/math/arcsin.rs b/crates/nu-cmd-extra/src/extra/math/arcsin.rs index a68e0648ef..a85c438367 100644 --- a/crates/nu-cmd-extra/src/extra/math/arcsin.rs +++ b/crates/nu-cmd-extra/src/extra/math/arcsin.rs @@ -45,7 +45,7 @@ impl Command for SubCommand { } input.map( move |value| operate(value, head, use_degrees), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-cmd-extra/src/extra/math/arcsinh.rs b/crates/nu-cmd-extra/src/extra/math/arcsinh.rs index 67addfdba2..91cb814a32 100644 --- a/crates/nu-cmd-extra/src/extra/math/arcsinh.rs +++ b/crates/nu-cmd-extra/src/extra/math/arcsinh.rs @@ -41,10 +41,7 @@ impl Command for SubCommand { if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map( - move |value| operate(value, head), - engine_state.ctrlc.clone(), - ) + input.map(move |value| operate(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-cmd-extra/src/extra/math/arctan.rs b/crates/nu-cmd-extra/src/extra/math/arctan.rs index 9c14203312..f52c8bd40c 100644 --- a/crates/nu-cmd-extra/src/extra/math/arctan.rs +++ b/crates/nu-cmd-extra/src/extra/math/arctan.rs @@ -45,7 +45,7 @@ impl Command for SubCommand { } input.map( move |value| operate(value, head, use_degrees), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-cmd-extra/src/extra/math/arctanh.rs b/crates/nu-cmd-extra/src/extra/math/arctanh.rs index 920e56eeb6..7791b56948 100644 --- a/crates/nu-cmd-extra/src/extra/math/arctanh.rs +++ b/crates/nu-cmd-extra/src/extra/math/arctanh.rs @@ -41,10 +41,7 @@ impl Command for SubCommand { if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map( - move |value| operate(value, head), - engine_state.ctrlc.clone(), - ) + input.map(move |value| operate(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-cmd-extra/src/extra/math/cos.rs b/crates/nu-cmd-extra/src/extra/math/cos.rs index 633c131b8b..252c7dbbd6 100644 --- a/crates/nu-cmd-extra/src/extra/math/cos.rs +++ b/crates/nu-cmd-extra/src/extra/math/cos.rs @@ -44,7 +44,7 @@ impl Command for SubCommand { } input.map( move |value| operate(value, head, use_degrees), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-cmd-extra/src/extra/math/cosh.rs b/crates/nu-cmd-extra/src/extra/math/cosh.rs index a772540b5c..e46d3c4df8 100644 --- a/crates/nu-cmd-extra/src/extra/math/cosh.rs +++ b/crates/nu-cmd-extra/src/extra/math/cosh.rs @@ -41,10 +41,7 @@ impl Command for SubCommand { if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map( - move |value| operate(value, head), - engine_state.ctrlc.clone(), - ) + input.map(move |value| operate(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-cmd-extra/src/extra/math/exp.rs b/crates/nu-cmd-extra/src/extra/math/exp.rs index b89d6f553f..d8f6a52899 100644 --- a/crates/nu-cmd-extra/src/extra/math/exp.rs +++ b/crates/nu-cmd-extra/src/extra/math/exp.rs @@ -41,10 +41,7 @@ impl Command for SubCommand { if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map( - move |value| operate(value, head), - engine_state.ctrlc.clone(), - ) + input.map(move |value| operate(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-cmd-extra/src/extra/math/ln.rs b/crates/nu-cmd-extra/src/extra/math/ln.rs index dd9782b467..694192bc8e 100644 --- a/crates/nu-cmd-extra/src/extra/math/ln.rs +++ b/crates/nu-cmd-extra/src/extra/math/ln.rs @@ -41,10 +41,7 @@ impl Command for SubCommand { if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map( - move |value| operate(value, head), - engine_state.ctrlc.clone(), - ) + input.map(move |value| operate(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-cmd-extra/src/extra/math/sin.rs b/crates/nu-cmd-extra/src/extra/math/sin.rs index 883007d1ed..0caedbabe7 100644 --- a/crates/nu-cmd-extra/src/extra/math/sin.rs +++ b/crates/nu-cmd-extra/src/extra/math/sin.rs @@ -44,7 +44,7 @@ impl Command for SubCommand { } input.map( move |value| operate(value, head, use_degrees), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-cmd-extra/src/extra/math/sinh.rs b/crates/nu-cmd-extra/src/extra/math/sinh.rs index c768dba739..d40db3bcb5 100644 --- a/crates/nu-cmd-extra/src/extra/math/sinh.rs +++ b/crates/nu-cmd-extra/src/extra/math/sinh.rs @@ -41,10 +41,7 @@ impl Command for SubCommand { if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map( - move |value| operate(value, head), - engine_state.ctrlc.clone(), - ) + input.map(move |value| operate(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-cmd-extra/src/extra/math/tan.rs b/crates/nu-cmd-extra/src/extra/math/tan.rs index e10807279d..97c5d2ff93 100644 --- a/crates/nu-cmd-extra/src/extra/math/tan.rs +++ b/crates/nu-cmd-extra/src/extra/math/tan.rs @@ -44,7 +44,7 @@ impl Command for SubCommand { } input.map( move |value| operate(value, head, use_degrees), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-cmd-extra/src/extra/math/tanh.rs b/crates/nu-cmd-extra/src/extra/math/tanh.rs index 4d09f93cf4..6679d04fe5 100644 --- a/crates/nu-cmd-extra/src/extra/math/tanh.rs +++ b/crates/nu-cmd-extra/src/extra/math/tanh.rs @@ -41,10 +41,7 @@ impl Command for SubCommand { if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map( - move |value| operate(value, head), - engine_state.ctrlc.clone(), - ) + input.map(move |value| operate(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-cmd-extra/src/extra/platform/ansi/gradient.rs b/crates/nu-cmd-extra/src/extra/platform/ansi/gradient.rs index 21c7e42a61..5934b57a5d 100644 --- a/crates/nu-cmd-extra/src/extra/platform/ansi/gradient.rs +++ b/crates/nu-cmd-extra/src/extra/platform/ansi/gradient.rs @@ -140,7 +140,7 @@ fn operate( ret } }, - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-cmd-extra/src/extra/strings/encode_decode/hex.rs b/crates/nu-cmd-extra/src/extra/strings/encode_decode/hex.rs index 7628d6e240..be681b382a 100644 --- a/crates/nu-cmd-extra/src/extra/strings/encode_decode/hex.rs +++ b/crates/nu-cmd-extra/src/extra/strings/encode_decode/hex.rs @@ -88,7 +88,7 @@ pub fn operate( cell_paths, }; - general_operate(action, args, input, call.head, engine_state.ctrlc.clone()) + general_operate(action, args, input, call.head, engine_state.signals()) } fn action( diff --git a/crates/nu-cmd-extra/src/extra/strings/format/command.rs b/crates/nu-cmd-extra/src/extra/strings/format/command.rs index 50e89e61e3..ef9b6f2baf 100644 --- a/crates/nu-cmd-extra/src/extra/strings/format/command.rs +++ b/crates/nu-cmd-extra/src/extra/strings/format/command.rs @@ -1,5 +1,5 @@ use nu_engine::command_prelude::*; -use nu_protocol::{ast::PathMember, engine::StateWorkingSet, ListStream}; +use nu_protocol::{ast::PathMember, engine::StateWorkingSet, Config, ListStream}; #[derive(Clone)] pub struct FormatPattern; @@ -43,6 +43,8 @@ impl Command for FormatPattern { let it_id = working_set.add_variable(b"$it".to_vec(), call.head, Type::Any, false); stack.add_var(it_id, input_val.clone()); + let config = stack.get_config(engine_state); + match specified_pattern { Err(e) => Err(e), Ok(pattern) => { @@ -56,7 +58,7 @@ impl Command for FormatPattern { string_span.start + 1, )?; - format(input_val, &ops, engine_state, call.head) + format(input_val, &ops, engine_state, &config, call.head) } } } @@ -181,33 +183,30 @@ fn format( input_data: Value, format_operations: &[FormatOperation], engine_state: &EngineState, + config: &Config, head_span: Span, ) -> Result { let data_as_value = input_data; // We can only handle a Record or a List of Records match data_as_value { - Value::Record { .. } => { - match format_record(format_operations, &data_as_value, engine_state) { - Ok(value) => Ok(PipelineData::Value(Value::string(value, head_span), None)), - Err(value) => Err(value), - } - } + Value::Record { .. } => match format_record(format_operations, &data_as_value, config) { + Ok(value) => Ok(PipelineData::Value(Value::string(value, head_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, engine_state) { - Ok(value) => { - list.push(Value::string(value, head_span)); - } - Err(value) => { - return Err(value); - } + Value::Record { .. } => match format_record(format_operations, val, config) { + Ok(value) => { + list.push(Value::string(value, head_span)); } - } + Err(value) => { + return Err(value); + } + }, Value::Error { error, .. } => return Err(*error.clone()), _ => { return Err(ShellError::OnlySupportsThisInputType { @@ -220,7 +219,7 @@ fn format( } } - Ok(ListStream::new(list.into_iter(), head_span, engine_state.ctrlc.clone()).into()) + Ok(ListStream::new(list.into_iter(), head_span, engine_state.signals().clone()).into()) } // Unwrapping this ShellError is a bit unfortunate. // Ideally, its Span would be preserved. @@ -237,9 +236,8 @@ fn format( fn format_record( format_operations: &[FormatOperation], data_as_value: &Value, - engine_state: &EngineState, + config: &Config, ) -> Result { - let config = engine_state.get_config(); let mut output = String::new(); for op in format_operations { diff --git a/crates/nu-cmd-extra/src/extra/strings/str_/case/mod.rs b/crates/nu-cmd-extra/src/extra/strings/str_/case/mod.rs index 980a1b83fc..bfa54921e2 100644 --- a/crates/nu-cmd-extra/src/extra/strings/str_/case/mod.rs +++ b/crates/nu-cmd-extra/src/extra/strings/str_/case/mod.rs @@ -44,7 +44,7 @@ where case_operation, cell_paths, }; - general_operate(action, args, input, call.head, engine_state.ctrlc.clone()) + general_operate(action, args, input, call.head, engine_state.signals()) } fn action(input: &Value, args: &Arguments, head: Span) -> Value diff --git a/crates/nu-cmd-extra/src/lib.rs b/crates/nu-cmd-extra/src/lib.rs index 2a3d70e6cd..f6f97c2f96 100644 --- a/crates/nu-cmd-extra/src/lib.rs +++ b/crates/nu-cmd-extra/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] mod example_test; pub mod extra; pub use extra::*; diff --git a/crates/nu-cmd-extra/tests/commands/bits/into.rs b/crates/nu-cmd-extra/tests/commands/bits/into.rs new file mode 100644 index 0000000000..b7e7700583 --- /dev/null +++ b/crates/nu-cmd-extra/tests/commands/bits/into.rs @@ -0,0 +1,13 @@ +use nu_test_support::nu; + +#[test] +fn byte_stream_into_bits() { + let result = nu!("[0x[01] 0x[02 03]] | bytes collect | into bits"); + assert_eq!("00000001 00000010 00000011", result.out); +} + +#[test] +fn byte_stream_into_bits_is_stream() { + let result = nu!("[0x[01] 0x[02 03]] | bytes collect | into bits | describe"); + assert_eq!("string (stream)", result.out); +} diff --git a/crates/nu-cmd-extra/tests/commands/bits/mod.rs b/crates/nu-cmd-extra/tests/commands/bits/mod.rs new file mode 100644 index 0000000000..0d4ee04b0d --- /dev/null +++ b/crates/nu-cmd-extra/tests/commands/bits/mod.rs @@ -0,0 +1 @@ +mod into; diff --git a/crates/nu-cmd-extra/tests/commands/mod.rs b/crates/nu-cmd-extra/tests/commands/mod.rs index 2354122e35..fd216cecb6 100644 --- a/crates/nu-cmd-extra/tests/commands/mod.rs +++ b/crates/nu-cmd-extra/tests/commands/mod.rs @@ -1 +1,2 @@ +mod bits; mod bytes; diff --git a/crates/nu-cmd-lang/Cargo.toml b/crates/nu-cmd-lang/Cargo.toml index 22e1cad374..16ac1b893a 100644 --- a/crates/nu-cmd-lang/Cargo.toml +++ b/crates/nu-cmd-lang/Cargo.toml @@ -6,27 +6,26 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-lang" edition = "2021" license = "MIT" name = "nu-cmd-lang" -version = "0.94.3" +version = "0.95.1" [lib] bench = false [dependencies] -nu-engine = { path = "../nu-engine", version = "0.94.3" } -nu-parser = { path = "../nu-parser", version = "0.94.3" } -nu-protocol = { path = "../nu-protocol", version = "0.94.3" } -nu-utils = { path = "../nu-utils", version = "0.94.3" } +nu-engine = { path = "../nu-engine", version = "0.95.1" } +nu-parser = { path = "../nu-parser", version = "0.95.1" } +nu-protocol = { path = "../nu-protocol", version = "0.95.1" } +nu-utils = { path = "../nu-utils", version = "0.95.1" } itertools = { workspace = true } -shadow-rs = { version = "0.28", default-features = false } +shadow-rs = { version = "0.29", default-features = false } [build-dependencies] -shadow-rs = { version = "0.28", default-features = false } +shadow-rs = { version = "0.29", default-features = false } [features] mimalloc = [] -which-support = [] trash-support = [] sqlite = [] static-link-openssl = [] -system-clipboard = [] +system-clipboard = [] \ No newline at end of file diff --git a/crates/nu-cmd-lang/src/core_commands/break_.rs b/crates/nu-cmd-lang/src/core_commands/break_.rs index 4698f12c34..90cc1a73f2 100644 --- a/crates/nu-cmd-lang/src/core_commands/break_.rs +++ b/crates/nu-cmd-lang/src/core_commands/break_.rs @@ -1,4 +1,5 @@ use nu_engine::command_prelude::*; +use nu_protocol::engine::CommandType; #[derive(Clone)] pub struct Break; @@ -18,6 +19,15 @@ impl Command for Break { .category(Category::Core) } + fn extra_usage(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + fn run( &self, _engine_state: &EngineState, diff --git a/crates/nu-cmd-lang/src/core_commands/collect.rs b/crates/nu-cmd-lang/src/core_commands/collect.rs index 1c28646548..d6282eec35 100644 --- a/crates/nu-cmd-lang/src/core_commands/collect.rs +++ b/crates/nu-cmd-lang/src/core_commands/collect.rs @@ -50,6 +50,7 @@ is particularly large, this can cause high memory usage."# // check where some input came from. Some(PipelineMetadata { data_source: DataSource::FilePath(_), + content_type: None, }) => None, other => other, }; diff --git a/crates/nu-cmd-lang/src/core_commands/const_.rs b/crates/nu-cmd-lang/src/core_commands/const_.rs index f780c5ada9..5b3d03443a 100644 --- a/crates/nu-cmd-lang/src/core_commands/const_.rs +++ b/crates/nu-cmd-lang/src/core_commands/const_.rs @@ -46,6 +46,9 @@ impl Command for Const { call: &Call, _input: PipelineData, ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + let call = call.assert_ast_call()?; let var_id = if let Some(id) = call.positional_nth(0).and_then(|pos| pos.as_var()) { id } else { diff --git a/crates/nu-cmd-lang/src/core_commands/continue_.rs b/crates/nu-cmd-lang/src/core_commands/continue_.rs index f65a983269..cfa3e38335 100644 --- a/crates/nu-cmd-lang/src/core_commands/continue_.rs +++ b/crates/nu-cmd-lang/src/core_commands/continue_.rs @@ -1,4 +1,5 @@ use nu_engine::command_prelude::*; +use nu_protocol::engine::CommandType; #[derive(Clone)] pub struct Continue; @@ -18,6 +19,14 @@ impl Command for Continue { .category(Category::Core) } + fn extra_usage(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } fn run( &self, _engine_state: &EngineState, diff --git a/crates/nu-cmd-lang/src/core_commands/def.rs b/crates/nu-cmd-lang/src/core_commands/def.rs index e4a169ffcc..913f74803c 100644 --- a/crates/nu-cmd-lang/src/core_commands/def.rs +++ b/crates/nu-cmd-lang/src/core_commands/def.rs @@ -67,8 +67,8 @@ impl Command for Def { }, Example { description: "Define a custom wrapper for an external command", - example: r#"def --wrapped my-echo [...rest] { echo $rest }; my-echo spam"#, - result: Some(Value::test_list(vec![Value::test_string("spam")])), + example: r#"def --wrapped my-echo [...rest] { ^echo ...$rest }; my-echo -e 'spam\tspam'"#, + result: Some(Value::test_string("spam\tspam")), }, ] } diff --git a/crates/nu-cmd-lang/src/core_commands/do_.rs b/crates/nu-cmd-lang/src/core_commands/do_.rs index 8ca3fbac56..bf29a2159a 100644 --- a/crates/nu-cmd-lang/src/core_commands/do_.rs +++ b/crates/nu-cmd-lang/src/core_commands/do_.rs @@ -23,11 +23,7 @@ impl Command for Do { fn signature(&self) -> Signature { Signature::build("do") - .required( - "closure", - SyntaxShape::OneOf(vec![SyntaxShape::Closure(None), SyntaxShape::Any]), - "The closure to run.", - ) + .required("closure", SyntaxShape::Closure(None), "The closure to run.") .input_output_types(vec![(Type::Any, Type::Any)]) .switch( "ignore-errors", @@ -85,6 +81,10 @@ impl Command for Do { bind_args_to(&mut callee_stack, &block.signature, rest, head)?; let eval_block_with_early_return = get_eval_block_with_early_return(engine_state); + + // Applies to all block evaluation once set true + callee_stack.use_ir = caller_stack.has_env_var(engine_state, "NU_USE_IR"); + let result = eval_block_with_early_return(engine_state, &mut callee_stack, block, input); if has_env { @@ -229,14 +229,24 @@ impl Command for Do { result: None, }, Example { - description: "Run the closure, with a positional parameter", - example: r#"do {|x| 100 + $x } 77"#, + description: "Run the closure with a positional, type-checked parameter", + example: r#"do {|x:int| 100 + $x } 77"#, result: Some(Value::test_int(177)), }, Example { - description: "Run the closure, with input", - example: r#"77 | do {|x| 100 + $in }"#, - result: None, // TODO: returns 177 + description: "Run the closure with pipeline input", + example: r#"77 | do { 100 + $in }"#, + result: Some(Value::test_int(177)), + }, + Example { + description: "Run the closure with a default parameter value", + example: r#"77 | do {|x=100| $x + $in }"#, + result: Some(Value::test_int(177)), + }, + Example { + description: "Run the closure with two positional parameters", + example: r#"do {|x,y| $x + $y } 77 100"#, + result: Some(Value::test_int(177)), }, Example { description: "Run the closure and keep changes to the environment", diff --git a/crates/nu-cmd-lang/src/core_commands/error_make.rs b/crates/nu-cmd-lang/src/core_commands/error_make.rs index 987e083cbc..07efcd7885 100644 --- a/crates/nu-cmd-lang/src/core_commands/error_make.rs +++ b/crates/nu-cmd-lang/src/core_commands/error_make.rs @@ -56,16 +56,7 @@ impl Command for ErrorMake { Example { description: "Create a simple custom error", example: r#"error make {msg: "my custom error message"}"#, - result: Some(Value::error( - ShellError::GenericError { - error: "my custom error message".into(), - msg: "".into(), - span: None, - help: None, - inner: vec![], - }, - Span::unknown(), - )), + result: None, }, Example { description: "Create a more complex custom error", @@ -82,16 +73,7 @@ impl Command for ErrorMake { } help: "A help string, suggesting a fix to the user" # optional }"#, - result: Some(Value::error( - ShellError::GenericError { - error: "my custom error message".into(), - msg: "my custom label text".into(), - span: Some(Span::new(123, 456)), - help: Some("A help string, suggesting a fix to the user".into()), - inner: vec![], - }, - Span::unknown(), - )), + result: None, }, Example { description: diff --git a/crates/nu-cmd-lang/src/core_commands/for_.rs b/crates/nu-cmd-lang/src/core_commands/for_.rs index 387af45282..36df743e5f 100644 --- a/crates/nu-cmd-lang/src/core_commands/for_.rs +++ b/crates/nu-cmd-lang/src/core_commands/for_.rs @@ -1,5 +1,5 @@ use nu_engine::{command_prelude::*, get_eval_block, get_eval_expression}; -use nu_protocol::engine::CommandType; +use nu_protocol::{engine::CommandType, Signals}; #[derive(Clone)] pub struct For; @@ -28,11 +28,6 @@ impl Command for For { "Range of the loop.", ) .required("block", SyntaxShape::Block, "The block to run.") - .switch( - "numbered", - "return a numbered item ($it.index and $it.item)", - Some('n'), - ) .creates_scope() .category(Category::Core) } @@ -53,6 +48,9 @@ impl Command for For { call: &Call, _input: PipelineData, ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + let call = call.assert_ast_call()?; let head = call.head; let var_id = call .positional_nth(0) @@ -77,9 +75,6 @@ impl Command for For { let value = eval_expression(engine_state, stack, keyword_expr)?; - let numbered = call.has_flag(engine_state, stack, "numbered")?; - - let ctrlc = engine_state.ctrlc.clone(); let engine_state = engine_state.clone(); let block = engine_state.get_block(block_id); @@ -88,29 +83,14 @@ impl Command for For { let span = value.span(); match value { Value::List { vals, .. } => { - for (idx, x) in vals.into_iter().enumerate() { - if nu_utils::ctrl_c::was_pressed(&ctrlc) { - break; - } + for x in vals.into_iter() { + engine_state.signals().check(head)?; // with_env() is used here to ensure that each iteration uses // a different set of environment variables. // Hence, a 'cd' in the first loop won't affect the next loop. - stack.add_var( - var_id, - if numbered { - Value::record( - record! { - "index" => Value::int(idx as i64, head), - "item" => x, - }, - head, - ) - } else { - x - }, - ); + stack.add_var(var_id, x); match eval_block(&engine_state, stack, block, PipelineData::empty()) { Err(ShellError::Break { .. }) => { @@ -136,21 +116,9 @@ impl Command for For { } } Value::Range { val, .. } => { - for (idx, x) in val.into_range_iter(span, ctrlc).enumerate() { - stack.add_var( - var_id, - if numbered { - Value::record( - record! { - "index" => Value::int(idx as i64, head), - "item" => x, - }, - head, - ) - } else { - x - }, - ); + for x in val.into_range_iter(span, Signals::empty()) { + engine_state.signals().check(head)?; + stack.add_var(var_id, x); match eval_block(&engine_state, stack, block, PipelineData::empty()) { Err(ShellError::Break { .. }) => { @@ -198,8 +166,7 @@ impl Command for For { }, Example { description: "Number each item and print a message", - example: - "for $it in ['bob' 'fred'] --numbered { print $\"($it.index) is ($it.item)\" }", + example: r#"for $it in (['bob' 'fred'] | enumerate) { print $"($it.index) is ($it.item)" }"#, result: None, }, ] diff --git a/crates/nu-cmd-lang/src/core_commands/if_.rs b/crates/nu-cmd-lang/src/core_commands/if_.rs index 83808c8e06..8667843770 100644 --- a/crates/nu-cmd-lang/src/core_commands/if_.rs +++ b/crates/nu-cmd-lang/src/core_commands/if_.rs @@ -2,7 +2,7 @@ use nu_engine::{ command_prelude::*, get_eval_block, get_eval_expression, get_eval_expression_with_input, }; use nu_protocol::{ - engine::StateWorkingSet, + engine::{CommandType, StateWorkingSet}, eval_const::{eval_const_subexpression, eval_constant, eval_constant_with_input}, }; @@ -41,6 +41,15 @@ impl Command for If { .category(Category::Core) } + fn extra_usage(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + fn is_const(&self) -> bool { true } @@ -51,6 +60,9 @@ impl Command for If { call: &Call, input: PipelineData, ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + let call = call.assert_ast_call()?; let cond = call.positional_nth(0).expect("checked through parser"); let then_block = call .positional_nth(1) @@ -90,6 +102,9 @@ impl Command for If { call: &Call, input: PipelineData, ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + let call = call.assert_ast_call()?; let cond = call.positional_nth(0).expect("checked through parser"); let then_block = call .positional_nth(1) @@ -122,6 +137,10 @@ impl Command for If { } } + fn search_terms(&self) -> Vec<&str> { + vec!["else", "conditional"] + } + fn examples(&self) -> Vec { vec![ Example { diff --git a/crates/nu-cmd-lang/src/core_commands/let_.rs b/crates/nu-cmd-lang/src/core_commands/let_.rs index f2da628c31..46324ef39e 100644 --- a/crates/nu-cmd-lang/src/core_commands/let_.rs +++ b/crates/nu-cmd-lang/src/core_commands/let_.rs @@ -46,6 +46,9 @@ impl Command for Let { call: &Call, input: PipelineData, ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + let call = call.assert_ast_call()?; let var_id = call .positional_nth(0) .expect("checked through parser") diff --git a/crates/nu-cmd-lang/src/core_commands/loop_.rs b/crates/nu-cmd-lang/src/core_commands/loop_.rs index 9b1e36a057..f495c8d3ae 100644 --- a/crates/nu-cmd-lang/src/core_commands/loop_.rs +++ b/crates/nu-cmd-lang/src/core_commands/loop_.rs @@ -1,4 +1,5 @@ use nu_engine::{command_prelude::*, get_eval_block}; +use nu_protocol::engine::CommandType; #[derive(Clone)] pub struct Loop; @@ -20,6 +21,15 @@ impl Command for Loop { .category(Category::Core) } + fn extra_usage(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + fn run( &self, engine_state: &EngineState, @@ -27,6 +37,10 @@ impl Command for Loop { call: &Call, _input: PipelineData, ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + let call = call.assert_ast_call()?; + let head = call.head; let block_id = call .positional_nth(0) .expect("checked through parser") @@ -39,9 +53,7 @@ impl Command for Loop { let stack = &mut stack.push_redirection(None, None); loop { - if nu_utils::ctrl_c::was_pressed(&engine_state.ctrlc) { - break; - } + engine_state.signals().check(head)?; match eval_block(engine_state, stack, block, PipelineData::empty()) { Err(ShellError::Break { .. }) => { diff --git a/crates/nu-cmd-lang/src/core_commands/match_.rs b/crates/nu-cmd-lang/src/core_commands/match_.rs index 41b5c24702..c3a3d61216 100644 --- a/crates/nu-cmd-lang/src/core_commands/match_.rs +++ b/crates/nu-cmd-lang/src/core_commands/match_.rs @@ -1,7 +1,7 @@ use nu_engine::{ command_prelude::*, get_eval_block, get_eval_expression, get_eval_expression_with_input, }; -use nu_protocol::engine::Matcher; +use nu_protocol::engine::{CommandType, Matcher}; #[derive(Clone)] pub struct Match; @@ -27,6 +27,15 @@ impl Command for Match { .category(Category::Core) } + fn extra_usage(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + fn run( &self, engine_state: &EngineState, @@ -34,6 +43,9 @@ impl Command for Match { call: &Call, input: PipelineData, ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + let call = call.assert_ast_call()?; let value: Value = call.req(engine_state, stack, 0)?; let matches = call .positional_nth(1) diff --git a/crates/nu-cmd-lang/src/core_commands/mut_.rs b/crates/nu-cmd-lang/src/core_commands/mut_.rs index 5db3c929af..b729590027 100644 --- a/crates/nu-cmd-lang/src/core_commands/mut_.rs +++ b/crates/nu-cmd-lang/src/core_commands/mut_.rs @@ -46,6 +46,9 @@ impl Command for Mut { call: &Call, input: PipelineData, ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + let call = call.assert_ast_call()?; let var_id = call .positional_nth(0) .expect("checked through parser") diff --git a/crates/nu-cmd-lang/src/core_commands/overlay/use_.rs b/crates/nu-cmd-lang/src/core_commands/overlay/use_.rs index e8b51fb59b..d6d3ae745a 100644 --- a/crates/nu-cmd-lang/src/core_commands/overlay/use_.rs +++ b/crates/nu-cmd-lang/src/core_commands/overlay/use_.rs @@ -65,9 +65,9 @@ impl Command for OverlayUse { name_arg.item = trim_quotes_str(&name_arg.item).to_string(); let maybe_origin_module_id = - if let Some(overlay_expr) = call.get_parser_info("overlay_expr") { + if let Some(overlay_expr) = call.get_parser_info(caller_stack, "overlay_expr") { if let Expr::Overlay(module_id) = &overlay_expr.expr { - module_id + *module_id } else { return Err(ShellError::NushellFailedSpanned { msg: "Not an overlay".to_string(), @@ -110,7 +110,7 @@ impl Command for OverlayUse { // a) adding a new overlay // b) refreshing an active overlay (the origin module changed) - let module = engine_state.get_module(*module_id); + let module = engine_state.get_module(module_id); // Evaluate the export-env block (if any) and keep its environment if let Some(block_id) = module.env_block { @@ -118,7 +118,7 @@ impl Command for OverlayUse { &name_arg.item, engine_state, caller_stack, - get_dirs_var_from_call(call), + get_dirs_var_from_call(caller_stack, call), )?; let block = engine_state.get_block(block_id); diff --git a/crates/nu-cmd-lang/src/core_commands/scope/command.rs b/crates/nu-cmd-lang/src/core_commands/scope/command.rs index 98439226cf..cc52a8a16f 100644 --- a/crates/nu-cmd-lang/src/core_commands/scope/command.rs +++ b/crates/nu-cmd-lang/src/core_commands/scope/command.rs @@ -1,5 +1,4 @@ use nu_engine::{command_prelude::*, get_full_help}; -use nu_protocol::engine::CommandType; #[derive(Clone)] pub struct Scope; @@ -20,10 +19,6 @@ impl Command for Scope { "Commands for getting info about what is in scope." } - fn command_type(&self) -> CommandType { - CommandType::Keyword - } - fn run( &self, engine_state: &EngineState, diff --git a/crates/nu-cmd-lang/src/core_commands/try_.rs b/crates/nu-cmd-lang/src/core_commands/try_.rs index 0b399e368a..2309897a1b 100644 --- a/crates/nu-cmd-lang/src/core_commands/try_.rs +++ b/crates/nu-cmd-lang/src/core_commands/try_.rs @@ -1,5 +1,5 @@ use nu_engine::{command_prelude::*, get_eval_block, EvalBlockFn}; -use nu_protocol::engine::Closure; +use nu_protocol::engine::{Closure, CommandType}; #[derive(Clone)] pub struct Try; @@ -10,7 +10,7 @@ impl Command for Try { } fn usage(&self) -> &str { - "Try to run a block, if it fails optionally run a catch block." + "Try to run a block, if it fails optionally run a catch closure." } fn signature(&self) -> nu_protocol::Signature { @@ -18,7 +18,7 @@ impl Command for Try { .input_output_types(vec![(Type::Any, Type::Any)]) .required("try_block", SyntaxShape::Block, "Block to run.") .optional( - "catch_block", + "catch_closure", SyntaxShape::Keyword( b"catch".to_vec(), Box::new(SyntaxShape::OneOf(vec![ @@ -26,11 +26,20 @@ impl Command for Try { SyntaxShape::Closure(Some(vec![SyntaxShape::Any])), ])), ), - "Block to run if try block fails.", + "Closure to run if try block fails.", ) .category(Category::Core) } + fn extra_usage(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + fn run( &self, engine_state: &EngineState, @@ -38,6 +47,9 @@ impl Command for Try { call: &Call, input: PipelineData, ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + let call = call.assert_ast_call()?; let try_block = call .positional_nth(0) .expect("checked through parser") @@ -85,9 +97,14 @@ impl Command for Try { }, Example { description: "Try to run a missing command", - example: "try { asdfasdf } catch { 'missing' } ", + example: "try { asdfasdf } catch { 'missing' }", result: Some(Value::test_string("missing")), }, + Example { + description: "Try to run a missing command and report the message", + example: "try { asdfasdf } catch { |err| $err.msg }", + result: None, + }, ] } } diff --git a/crates/nu-cmd-lang/src/core_commands/use_.rs b/crates/nu-cmd-lang/src/core_commands/use_.rs index b0f3648304..7f544fa5d4 100644 --- a/crates/nu-cmd-lang/src/core_commands/use_.rs +++ b/crates/nu-cmd-lang/src/core_commands/use_.rs @@ -57,7 +57,7 @@ This command is a parser keyword. For details, check: let Some(Expression { expr: Expr::ImportPattern(import_pattern), .. - }) = call.get_parser_info("import_pattern") + }) = call.get_parser_info(caller_stack, "import_pattern") else { return Err(ShellError::GenericError { error: "Unexpected import".into(), @@ -68,6 +68,9 @@ This command is a parser keyword. For details, check: }); }; + // Necessary so that we can modify the stack. + let import_pattern = import_pattern.clone(); + if let Some(module_id) = import_pattern.head.id { // Add constants for var_id in &import_pattern.constants { @@ -99,7 +102,7 @@ This command is a parser keyword. For details, check: &module_arg_str, engine_state, caller_stack, - get_dirs_var_from_call(call), + get_dirs_var_from_call(caller_stack, call), )?; let maybe_parent = maybe_file_path .as_ref() diff --git a/crates/nu-cmd-lang/src/core_commands/version.rs b/crates/nu-cmd-lang/src/core_commands/version.rs index 22ef1f2a08..5491db65fc 100644 --- a/crates/nu-cmd-lang/src/core_commands/version.rs +++ b/crates/nu-cmd-lang/src/core_commands/version.rs @@ -116,11 +116,18 @@ pub fn version(engine_state: &EngineState, span: Span) -> Result>(); record.push( @@ -160,11 +167,6 @@ fn features_enabled() -> Vec { // NOTE: There should be another way to know features on. - #[cfg(feature = "which-support")] - { - names.push("which".to_string()); - } - #[cfg(feature = "trash-support")] { names.push("trash".to_string()); diff --git a/crates/nu-cmd-lang/src/core_commands/while_.rs b/crates/nu-cmd-lang/src/core_commands/while_.rs index bf9076aa0c..a67c47fcab 100644 --- a/crates/nu-cmd-lang/src/core_commands/while_.rs +++ b/crates/nu-cmd-lang/src/core_commands/while_.rs @@ -1,4 +1,5 @@ use nu_engine::{command_prelude::*, get_eval_block, get_eval_expression}; +use nu_protocol::engine::CommandType; #[derive(Clone)] pub struct While; @@ -29,6 +30,15 @@ impl Command for While { vec!["loop"] } + fn extra_usage(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + fn run( &self, engine_state: &EngineState, @@ -36,6 +46,10 @@ impl Command for While { call: &Call, _input: PipelineData, ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + let call = call.assert_ast_call()?; + let head = call.head; let cond = call.positional_nth(0).expect("checked through parser"); let block_id = call .positional_nth(1) @@ -49,9 +63,7 @@ impl Command for While { let stack = &mut stack.push_redirection(None, None); loop { - if nu_utils::ctrl_c::was_pressed(&engine_state.ctrlc) { - break; - } + engine_state.signals().check(head)?; let result = eval_expression(engine_state, stack, cond)?; diff --git a/crates/nu-cmd-lang/src/example_support.rs b/crates/nu-cmd-lang/src/example_support.rs index bb03bbaf8c..72ea36972f 100644 --- a/crates/nu-cmd-lang/src/example_support.rs +++ b/crates/nu-cmd-lang/src/example_support.rs @@ -4,7 +4,7 @@ use nu_protocol::{ ast::Block, debugger::WithoutDebug, engine::{StateDelta, StateWorkingSet}, - Range, + report_error_new, Range, }; use std::{ sync::Arc, @@ -124,7 +124,10 @@ pub fn eval_block( nu_engine::eval_block::(engine_state, &mut stack, &block, input) .and_then(|data| data.into_value(Span::test_data())) - .unwrap_or_else(|err| panic!("test eval error in `{}`: {:?}", "TODO", err)) + .unwrap_or_else(|err| { + report_error_new(engine_state, &err); + panic!("test eval error in `{}`: {:?}", "TODO", err) + }) } pub fn check_example_evaluates_to_expected_output( diff --git a/crates/nu-cmd-lang/src/lib.rs b/crates/nu-cmd-lang/src/lib.rs index 427a535d5d..41022cc231 100644 --- a/crates/nu-cmd-lang/src/lib.rs +++ b/crates/nu-cmd-lang/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] mod core_commands; mod default_context; pub mod example_support; diff --git a/crates/nu-cmd-plugin/Cargo.toml b/crates/nu-cmd-plugin/Cargo.toml index d274f46a6a..7d26fe5ddd 100644 --- a/crates/nu-cmd-plugin/Cargo.toml +++ b/crates/nu-cmd-plugin/Cargo.toml @@ -5,16 +5,16 @@ edition = "2021" license = "MIT" name = "nu-cmd-plugin" repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-plugin" -version = "0.94.3" +version = "0.95.1" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -nu-engine = { path = "../nu-engine", version = "0.94.3" } -nu-path = { path = "../nu-path", version = "0.94.3" } -nu-protocol = { path = "../nu-protocol", version = "0.94.3", features = ["plugin"] } -nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.94.3" } +nu-engine = { path = "../nu-engine", version = "0.95.1" } +nu-path = { path = "../nu-path", version = "0.95.1" } +nu-protocol = { path = "../nu-protocol", version = "0.95.1", features = ["plugin"] } +nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.95.1" } itertools = { workspace = true } -[dev-dependencies] +[dev-dependencies] \ No newline at end of file diff --git a/crates/nu-cmd-plugin/src/commands/mod.rs b/crates/nu-cmd-plugin/src/commands/mod.rs index 3e927747f1..17ff32faa1 100644 --- a/crates/nu-cmd-plugin/src/commands/mod.rs +++ b/crates/nu-cmd-plugin/src/commands/mod.rs @@ -1,5 +1,3 @@ mod plugin; -mod register; pub use plugin::*; -pub use register::Register; diff --git a/crates/nu-cmd-plugin/src/commands/plugin/add.rs b/crates/nu-cmd-plugin/src/commands/plugin/add.rs index 225941db01..14f3541168 100644 --- a/crates/nu-cmd-plugin/src/commands/plugin/add.rs +++ b/crates/nu-cmd-plugin/src/commands/plugin/add.rs @@ -118,11 +118,12 @@ apparent the next time `nu` is next launched with that plugin registry file. }, )); let interface = plugin.clone().get_plugin(Some((engine_state, stack)))?; + let metadata = interface.get_metadata()?; let commands = interface.get_signature()?; modify_plugin_file(engine_state, stack, call.head, custom_path, |contents| { - // Update the file with the received signatures - let item = PluginRegistryItem::new(plugin.identity(), commands); + // Update the file with the received metadata and signatures + let item = PluginRegistryItem::new(plugin.identity(), metadata, commands); contents.upsert_plugin(item); Ok(()) })?; diff --git a/crates/nu-cmd-plugin/src/commands/plugin/list.rs b/crates/nu-cmd-plugin/src/commands/plugin/list.rs index 6b715a0001..030a3341d6 100644 --- a/crates/nu-cmd-plugin/src/commands/plugin/list.rs +++ b/crates/nu-cmd-plugin/src/commands/plugin/list.rs @@ -16,6 +16,7 @@ impl Command for PluginList { Type::Table( [ ("name".into(), Type::String), + ("version".into(), Type::String), ("is_running".into(), Type::Bool), ("pid".into(), Type::Int), ("filename".into(), Type::String), @@ -43,6 +44,7 @@ impl Command for PluginList { description: "List installed plugins.", result: Some(Value::test_list(vec![Value::test_record(record! { "name" => Value::test_string("inc"), + "version" => Value::test_string(env!("CARGO_PKG_VERSION")), "is_running" => Value::test_bool(true), "pid" => Value::test_int(106480), "filename" => if cfg!(windows) { @@ -98,8 +100,15 @@ impl Command for PluginList { .map(|s| Value::string(s.to_string_lossy(), head)) .unwrap_or(Value::nothing(head)); + let metadata = plugin.metadata(); + let version = metadata + .and_then(|m| m.version) + .map(|s| Value::string(s, head)) + .unwrap_or(Value::nothing(head)); + let record = record! { "name" => Value::string(plugin.identity().name(), head), + "version" => version, "is_running" => Value::bool(plugin.is_running(), head), "pid" => pid, "filename" => Value::string(plugin.identity().filename().to_string_lossy(), head), diff --git a/crates/nu-cmd-plugin/src/commands/plugin/stop.rs b/crates/nu-cmd-plugin/src/commands/plugin/stop.rs index d74ee59a3f..f90eac8411 100644 --- a/crates/nu-cmd-plugin/src/commands/plugin/stop.rs +++ b/crates/nu-cmd-plugin/src/commands/plugin/stop.rs @@ -72,7 +72,7 @@ impl Command for PluginStop { error: format!("Failed to stop the `{}` plugin", name.item), msg: "couldn't find a plugin with this name".into(), span: Some(name.span), - help: Some("you may need to `register` the plugin first".into()), + help: Some("you may need to `plugin add` the plugin first".into()), inner: vec![], }) } diff --git a/crates/nu-cmd-plugin/src/commands/register.rs b/crates/nu-cmd-plugin/src/commands/register.rs deleted file mode 100644 index 2c10456db7..0000000000 --- a/crates/nu-cmd-plugin/src/commands/register.rs +++ /dev/null @@ -1,80 +0,0 @@ -use nu_engine::command_prelude::*; -use nu_protocol::engine::CommandType; - -#[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") - .input_output_types(vec![(Type::Nothing, Type::Nothing)]) - .required( - "plugin", - SyntaxShape::Filepath, - "Path of executable for plugin.", - ) - .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::Plugin) - } - - fn extra_usage(&self) -> &str { - r#" -Deprecated in favor of `plugin add` and `plugin use`. - -This command is a parser keyword. For details, check: - https://www.nushell.sh/book/thinking_in_nu.html -"# - .trim() - } - - fn search_terms(&self) -> Vec<&str> { - vec!["add"] - } - - fn command_type(&self) -> CommandType { - CommandType::Keyword - } - - fn run( - &self, - _engine_state: &EngineState, - _stack: &mut Stack, - _call: &Call, - _input: PipelineData, - ) -> Result { - Ok(PipelineData::empty()) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Register `nu_plugin_query` plugin from ~/.cargo/bin/ dir", - example: r#"register ~/.cargo/bin/nu_plugin_query"#, - result: None, - }, - Example { - description: "Register `nu_plugin_query` plugin from `nu -c` (writes/updates $nu.plugin-path)", - example: r#"let plugin = ((which nu).path.0 | path dirname | path join 'nu_plugin_query'); nu -c $'register ($plugin); version'"#, - result: None, - }, - ] - } -} diff --git a/crates/nu-cmd-plugin/src/default_context.rs b/crates/nu-cmd-plugin/src/default_context.rs index 601dd52cfc..1ef6a905dd 100644 --- a/crates/nu-cmd-plugin/src/default_context.rs +++ b/crates/nu-cmd-plugin/src/default_context.rs @@ -18,7 +18,6 @@ pub fn add_plugin_command_context(mut engine_state: EngineState) -> EngineState PluginRm, PluginStop, PluginUse, - Register, ); working_set.render() diff --git a/crates/nu-cmd-plugin/src/util.rs b/crates/nu-cmd-plugin/src/util.rs index 312ab9e81a..7d5282235f 100644 --- a/crates/nu-cmd-plugin/src/util.rs +++ b/crates/nu-cmd-plugin/src/util.rs @@ -31,11 +31,20 @@ pub(crate) fn modify_plugin_file( })? }; + let file_span = custom_path.as_ref().map(|p| p.span).unwrap_or(span); + // Try to read the plugin file if it exists let mut contents = if fs::metadata(&plugin_registry_file_path).is_ok_and(|m| m.len() > 0) { PluginRegistryFile::read_from( - File::open(&plugin_registry_file_path).err_span(span)?, - Some(span), + File::open(&plugin_registry_file_path).map_err(|err| ShellError::IOErrorSpanned { + msg: format!( + "failed to read `{}`: {}", + plugin_registry_file_path.display(), + err + ), + span: file_span, + })?, + Some(file_span), )? } else { PluginRegistryFile::default() @@ -46,7 +55,14 @@ pub(crate) fn modify_plugin_file( // Save the modified file on success contents.write_to( - File::create(&plugin_registry_file_path).err_span(span)?, + File::create(&plugin_registry_file_path).map_err(|err| ShellError::IOErrorSpanned { + msg: format!( + "failed to create `{}`: {}", + plugin_registry_file_path.display(), + err + ), + span: file_span, + })?, Some(span), )?; diff --git a/crates/nu-color-config/Cargo.toml b/crates/nu-color-config/Cargo.toml index 95b06c1f77..23af09432c 100644 --- a/crates/nu-color-config/Cargo.toml +++ b/crates/nu-color-config/Cargo.toml @@ -5,18 +5,18 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-color-confi edition = "2021" license = "MIT" name = "nu-color-config" -version = "0.94.3" +version = "0.95.1" [lib] bench = false [dependencies] -nu-protocol = { path = "../nu-protocol", version = "0.94.3" } -nu-engine = { path = "../nu-engine", version = "0.94.3" } -nu-json = { path = "../nu-json", version = "0.94.3" } +nu-protocol = { path = "../nu-protocol", version = "0.95.1" } +nu-engine = { path = "../nu-engine", version = "0.95.1" } +nu-json = { path = "../nu-json", version = "0.95.1" } nu-ansi-term = { workspace = true } serde = { workspace = true, features = ["derive"] } [dev-dependencies] -nu-test-support = { path = "../nu-test-support", version = "0.94.3" } +nu-test-support = { path = "../nu-test-support", version = "0.95.1" } \ No newline at end of file diff --git a/crates/nu-color-config/README.md b/crates/nu-color-config/README.md new file mode 100644 index 0000000000..f03753685c --- /dev/null +++ b/crates/nu-color-config/README.md @@ -0,0 +1,5 @@ +Logic to resolve colors for syntax highlighting and output formatting + +## Internal Nushell crate + +This crate implements components of Nushell and is not designed to support plugin authors or other users directly. diff --git a/crates/nu-color-config/src/lib.rs b/crates/nu-color-config/src/lib.rs index 3bd4dc9e4d..77ec9d0e79 100644 --- a/crates/nu-color-config/src/lib.rs +++ b/crates/nu-color-config/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] mod color_config; mod matching_brackets_style; mod nu_style; diff --git a/crates/nu-color-config/src/shape_color.rs b/crates/nu-color-config/src/shape_color.rs index 72cc955e0b..8da397e914 100644 --- a/crates/nu-color-config/src/shape_color.rs +++ b/crates/nu-color-config/src/shape_color.rs @@ -20,6 +20,7 @@ pub fn default_shape_color(shape: &str) -> Style { "shape_flag" => Style::new().fg(Color::Blue).bold(), "shape_float" => Style::new().fg(Color::Purple).bold(), "shape_garbage" => Style::new().fg(Color::White).on(Color::Red).bold(), + "shape_glob_interpolation" => Style::new().fg(Color::Cyan).bold(), "shape_globpattern" => Style::new().fg(Color::Cyan).bold(), "shape_int" => Style::new().fg(Color::Purple).bold(), "shape_internalcall" => Style::new().fg(Color::Cyan).bold(), diff --git a/crates/nu-color-config/src/style_computer.rs b/crates/nu-color-config/src/style_computer.rs index 91907c1428..edc8786a53 100644 --- a/crates/nu-color-config/src/style_computer.rs +++ b/crates/nu-color-config/src/style_computer.rs @@ -1,6 +1,6 @@ use crate::{color_record_to_nustyle, lookup_ansi_color_style, text_style::Alignment, TextStyle}; use nu_ansi_term::{Color, Style}; -use nu_engine::{env::get_config, ClosureEvalOnce}; +use nu_engine::ClosureEvalOnce; use nu_protocol::{ cli_error::CliError, engine::{Closure, EngineState, Stack, StateWorkingSet}, @@ -114,7 +114,7 @@ impl<'a> StyleComputer<'a> { // The main constructor. pub fn from_config(engine_state: &'a EngineState, stack: &'a Stack) -> StyleComputer<'a> { - let config = get_config(engine_state, stack); + let config = stack.get_config(engine_state); // Create the hashmap #[rustfmt::skip] diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 4d69ce089a..ea501578b6 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" license = "MIT" name = "nu-command" repository = "https://github.com/nushell/nushell/tree/main/crates/nu-command" -version = "0.94.3" +version = "0.95.1" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -13,21 +13,21 @@ version = "0.94.3" bench = false [dependencies] -nu-cmd-base = { path = "../nu-cmd-base", version = "0.94.3" } -nu-color-config = { path = "../nu-color-config", version = "0.94.3" } -nu-engine = { path = "../nu-engine", version = "0.94.3" } -nu-glob = { path = "../nu-glob", version = "0.94.3" } -nu-json = { path = "../nu-json", version = "0.94.3" } -nu-parser = { path = "../nu-parser", version = "0.94.3" } -nu-path = { path = "../nu-path", version = "0.94.3" } -nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.94.3" } -nu-protocol = { path = "../nu-protocol", version = "0.94.3" } -nu-system = { path = "../nu-system", version = "0.94.3" } -nu-table = { path = "../nu-table", version = "0.94.3" } -nu-term-grid = { path = "../nu-term-grid", version = "0.94.3" } -nu-utils = { path = "../nu-utils", version = "0.94.3" } +nu-cmd-base = { path = "../nu-cmd-base", version = "0.95.1" } +nu-color-config = { path = "../nu-color-config", version = "0.95.1" } +nu-engine = { path = "../nu-engine", version = "0.95.1" } +nu-glob = { path = "../nu-glob", version = "0.95.1" } +nu-json = { path = "../nu-json", version = "0.95.1" } +nu-parser = { path = "../nu-parser", version = "0.95.1" } +nu-path = { path = "../nu-path", version = "0.95.1" } +nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.95.1" } +nu-protocol = { path = "../nu-protocol", version = "0.95.1" } +nu-system = { path = "../nu-system", version = "0.95.1" } +nu-table = { path = "../nu-table", version = "0.95.1" } +nu-term-grid = { path = "../nu-term-grid", version = "0.95.1" } +nu-utils = { path = "../nu-utils", version = "0.95.1" } nu-ansi-term = { workspace = true } -nuon = { path = "../nuon", version = "0.94.3" } +nuon = { path = "../nuon", version = "0.95.1" } alphanumeric-sort = { workspace = true } base64 = { workspace = true } @@ -42,6 +42,7 @@ chrono-humanize = { workspace = true } chrono-tz = { workspace = true } crossterm = { workspace = true } csv = { workspace = true } +deunicode = { workspace = true } dialoguer = { workspace = true, default-features = false, features = ["fuzzy-select"] } digest = { workspace = true, default-features = false } dtparse = { workspace = true } @@ -134,13 +135,12 @@ workspace = true plugin = ["nu-parser/plugin"] sqlite = ["rusqlite"] trash-support = ["trash"] -which-support = [] [dev-dependencies] -nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.94.3" } -nu-test-support = { path = "../nu-test-support", version = "0.94.3" } +nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.95.1" } +nu-test-support = { path = "../nu-test-support", version = "0.95.1" } -dirs-next = { workspace = true } +dirs = { workspace = true } mockito = { workspace = true, default-features = false } quickcheck = { workspace = true } quickcheck_macros = { workspace = true } diff --git a/crates/nu-command/README.md b/crates/nu-command/README.md new file mode 100644 index 0000000000..1cbb8cd9f2 --- /dev/null +++ b/crates/nu-command/README.md @@ -0,0 +1,7 @@ +This crate contains the majority of our commands + +We allow ourselves to move some of the commands in `nu-command` to `nu-cmd-*` crates as needed. + +## Internal Nushell crate + +This crate implements components of Nushell and is not designed to support plugin authors or other users directly. diff --git a/crates/nu-command/src/bytes/add.rs b/crates/nu-command/src/bytes/add.rs index 8514718cfd..ab31f74b12 100644 --- a/crates/nu-command/src/bytes/add.rs +++ b/crates/nu-command/src/bytes/add.rs @@ -78,7 +78,7 @@ impl Command for BytesAdd { end, cell_paths, }; - operate(add, arg, input, call.head, engine_state.ctrlc.clone()) + operate(add, arg, input, call.head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/bytes/at.rs b/crates/nu-command/src/bytes/at.rs index e90312603f..5e95f4fd62 100644 --- a/crates/nu-command/src/bytes/at.rs +++ b/crates/nu-command/src/bytes/at.rs @@ -83,7 +83,7 @@ impl Command for BytesAt { cell_paths, }; - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn examples(&self) -> Vec { @@ -128,48 +128,24 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value { let range = &args.indexes; match input { Value::Binary { val, .. } => { - use std::cmp::{self, Ordering}; let len = val.len() as isize; - let start = if range.0 < 0 { range.0 + len } else { range.0 }; + let end = if range.1 < 0 { range.1 + len } else { range.1 }; - let end = if range.1 < 0 { - cmp::max(range.1 + len, 0) - } else { - range.1 - }; - - if start < len && end >= 0 { - match start.cmp(&end) { - Ordering::Equal => Value::binary(vec![], head), - Ordering::Greater => Value::error( - ShellError::TypeMismatch { - err_message: "End must be greater than or equal to Start".to_string(), - span: head, - }, - head, - ), - Ordering::Less => Value::binary( - if end == isize::MAX { - val.iter() - .skip(start as usize) - .copied() - .collect::>() - } else { - val.iter() - .skip(start as usize) - .take((end - start) as usize) - .copied() - .collect() - }, - head, - ), - } - } else { + if start > end { Value::binary(vec![], head) + } else { + let val_iter = val.iter().skip(start as usize); + Value::binary( + if end == isize::MAX { + val_iter.copied().collect::>() + } else { + val_iter.take((end - start + 1) as usize).copied().collect() + }, + head, + ) } } - Value::Error { .. } => input.clone(), other => Value::error( diff --git a/crates/nu-command/src/bytes/build_.rs b/crates/nu-command/src/bytes/build_.rs index f6b1327621..9a3599a071 100644 --- a/crates/nu-command/src/bytes/build_.rs +++ b/crates/nu-command/src/bytes/build_.rs @@ -49,10 +49,8 @@ impl Command for BytesBuild { _input: PipelineData, ) -> Result { let mut output = vec![]; - for val in call.rest_iter_flattened(0, |expr| { - let eval_expression = get_eval_expression(engine_state); - eval_expression(engine_state, stack, expr) - })? { + let eval_expression = get_eval_expression(engine_state); + for val in call.rest_iter_flattened(engine_state, stack, eval_expression, 0)? { let val_span = val.span(); match val { Value::Binary { mut val, .. } => output.append(&mut val), diff --git a/crates/nu-command/src/bytes/collect.rs b/crates/nu-command/src/bytes/collect.rs index 74ea3e5d14..afb70bfb36 100644 --- a/crates/nu-command/src/bytes/collect.rs +++ b/crates/nu-command/src/bytes/collect.rs @@ -60,7 +60,12 @@ impl Command for BytesCollect { ) .flatten(); - let output = ByteStream::from_result_iter(iter, span, None, ByteStreamType::Binary); + let output = ByteStream::from_result_iter( + iter, + span, + engine_state.signals().clone(), + ByteStreamType::Binary, + ); Ok(PipelineData::ByteStream(output, metadata)) } diff --git a/crates/nu-command/src/bytes/ends_with.rs b/crates/nu-command/src/bytes/ends_with.rs index d6174a189c..8e3966716c 100644 --- a/crates/nu-command/src/bytes/ends_with.rs +++ b/crates/nu-command/src/bytes/ends_with.rs @@ -102,7 +102,7 @@ impl Command for BytesEndsWith { pattern, cell_paths, }; - operate(ends_with, arg, input, head, engine_state.ctrlc.clone()) + operate(ends_with, arg, input, head, engine_state.signals()) } } diff --git a/crates/nu-command/src/bytes/index_of.rs b/crates/nu-command/src/bytes/index_of.rs index bdf51b24d9..e10bd6c200 100644 --- a/crates/nu-command/src/bytes/index_of.rs +++ b/crates/nu-command/src/bytes/index_of.rs @@ -71,7 +71,7 @@ impl Command for BytesIndexOf { all: call.has_flag(engine_state, stack, "all")?, cell_paths, }; - operate(index_of, arg, input, call.head, engine_state.ctrlc.clone()) + operate(index_of, arg, input, call.head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/bytes/length.rs b/crates/nu-command/src/bytes/length.rs index aaaf23e0a5..78b3d31eac 100644 --- a/crates/nu-command/src/bytes/length.rs +++ b/crates/nu-command/src/bytes/length.rs @@ -46,7 +46,7 @@ impl Command for BytesLen { ) -> Result { let cell_paths: Vec = call.rest(engine_state, stack, 1)?; let arg = CellPathOnlyArgs::from(cell_paths); - operate(length, arg, input, call.head, engine_state.ctrlc.clone()) + operate(length, arg, input, call.head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/bytes/remove.rs b/crates/nu-command/src/bytes/remove.rs index 9afef07e8b..34d3c427f4 100644 --- a/crates/nu-command/src/bytes/remove.rs +++ b/crates/nu-command/src/bytes/remove.rs @@ -73,7 +73,7 @@ impl Command for BytesRemove { all: call.has_flag(engine_state, stack, "all")?, }; - operate(remove, arg, input, call.head, engine_state.ctrlc.clone()) + operate(remove, arg, input, call.head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/bytes/replace.rs b/crates/nu-command/src/bytes/replace.rs index ab7ede7588..db2b6fb790 100644 --- a/crates/nu-command/src/bytes/replace.rs +++ b/crates/nu-command/src/bytes/replace.rs @@ -73,7 +73,7 @@ impl Command for BytesReplace { all: call.has_flag(engine_state, stack, "all")?, }; - operate(replace, arg, input, call.head, engine_state.ctrlc.clone()) + operate(replace, arg, input, call.head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/bytes/reverse.rs b/crates/nu-command/src/bytes/reverse.rs index 171add213d..fd769eaa40 100644 --- a/crates/nu-command/src/bytes/reverse.rs +++ b/crates/nu-command/src/bytes/reverse.rs @@ -42,7 +42,7 @@ impl Command for BytesReverse { ) -> Result { let cell_paths: Vec = call.rest(engine_state, stack, 0)?; let arg = CellPathOnlyArgs::from(cell_paths); - operate(reverse, arg, input, call.head, engine_state.ctrlc.clone()) + operate(reverse, arg, input, call.head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/bytes/starts_with.rs b/crates/nu-command/src/bytes/starts_with.rs index 92cc16f02c..89cc4f7afc 100644 --- a/crates/nu-command/src/bytes/starts_with.rs +++ b/crates/nu-command/src/bytes/starts_with.rs @@ -79,7 +79,7 @@ impl Command for BytesStartsWith { pattern, cell_paths, }; - operate(starts_with, arg, input, head, engine_state.ctrlc.clone()) + operate(starts_with, arg, input, head, engine_state.signals()) } } diff --git a/crates/nu-command/src/charting/histogram.rs b/crates/nu-command/src/charting/histogram.rs index 52964b087d..29515c96c5 100755 --- a/crates/nu-command/src/charting/histogram.rs +++ b/crates/nu-command/src/charting/histogram.rs @@ -194,7 +194,7 @@ fn run_histogram( if inputs.is_empty() { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: head_span, + span: Some(head_span), src_span: list_span, }); } diff --git a/crates/nu-command/src/conversions/fill.rs b/crates/nu-command/src/conversions/fill.rs index 6507e4a368..eaf8c05da1 100644 --- a/crates/nu-command/src/conversions/fill.rs +++ b/crates/nu-command/src/conversions/fill.rs @@ -165,7 +165,7 @@ fn fill( cell_paths, }; - operate(action, arg, input, call.head, engine_state.ctrlc.clone()) + operate(action, arg, input, call.head, engine_state.signals()) } fn action(input: &Value, args: &Arguments, span: Span) -> Value { diff --git a/crates/nu-command/src/conversions/into/binary.rs b/crates/nu-command/src/conversions/into/binary.rs index 8eb7715754..e5c34ae6bc 100644 --- a/crates/nu-command/src/conversions/into/binary.rs +++ b/crates/nu-command/src/conversions/into/binary.rs @@ -138,7 +138,7 @@ fn into_binary( cell_paths, compact: call.has_flag(engine_state, stack, "compact")?, }; - operate(action, args, input, head, engine_state.ctrlc.clone()) + operate(action, args, input, head, engine_state.signals()) } } diff --git a/crates/nu-command/src/conversions/into/bool.rs b/crates/nu-command/src/conversions/into/bool.rs index b1d433cb93..0fcd33b4a3 100644 --- a/crates/nu-command/src/conversions/into/bool.rs +++ b/crates/nu-command/src/conversions/into/bool.rs @@ -107,7 +107,7 @@ fn into_bool( ) -> Result { let cell_paths: Vec = call.rest(engine_state, stack, 0)?; let args = CellPathOnlyArgs::from(cell_paths); - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn string_to_boolean(s: &str, span: Span) -> Result { diff --git a/crates/nu-command/src/conversions/into/cell_path.rs b/crates/nu-command/src/conversions/into/cell_path.rs index c05dad57ba..1342fb14c0 100644 --- a/crates/nu-command/src/conversions/into/cell_path.rs +++ b/crates/nu-command/src/conversions/into/cell_path.rs @@ -154,7 +154,7 @@ fn record_to_path_member( let Some(value) = record.get("value") else { return Err(ShellError::CantFindColumn { col_name: "value".into(), - span: val_span, + span: Some(val_span), src_span: span, }); }; diff --git a/crates/nu-command/src/conversions/into/datetime.rs b/crates/nu-command/src/conversions/into/datetime.rs index af10732aea..8dc0340ba1 100644 --- a/crates/nu-command/src/conversions/into/datetime.rs +++ b/crates/nu-command/src/conversions/into/datetime.rs @@ -1,5 +1,5 @@ use crate::{generate_strftime_list, parse_date_from_string}; -use chrono::{DateTime, FixedOffset, Local, NaiveTime, TimeZone, Utc}; +use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, NaiveTime, TimeZone, Utc}; use human_date_parser::{from_human_time, ParseResult}; use nu_cmd_base::input_handler::{operate, CmdArgument}; use nu_engine::command_prelude::*; @@ -141,7 +141,7 @@ impl Command for SubCommand { zone_options, cell_paths, }; - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } } @@ -162,24 +162,37 @@ impl Command for SubCommand { }; vec![ Example { - description: "Convert any standard timestamp string to datetime", + description: "Convert timestamp string to datetime with timezone offset", example: "'27.02.2021 1:55 pm +0000' | into datetime", #[allow(clippy::inconsistent_digit_grouping)] result: example_result_1(1614434100_000000000), }, Example { - description: "Convert any standard timestamp string to datetime", + description: "Convert standard timestamp string to datetime with timezone offset", example: "'2021-02-27T13:55:40.2246+00:00' | into datetime", #[allow(clippy::inconsistent_digit_grouping)] result: example_result_1(1614434140_224600000), }, Example { description: - "Convert non-standard timestamp string to datetime using a custom format", + "Convert non-standard timestamp string, with timezone offset, to datetime using a custom format", example: "'20210227_135540+0000' | into datetime --format '%Y%m%d_%H%M%S%z'", #[allow(clippy::inconsistent_digit_grouping)] result: example_result_1(1614434140_000000000), }, + Example { + description: "Convert non-standard timestamp string, without timezone offset, to datetime with custom formatting", + example: "'16.11.1984 8:00 am' | into datetime --format '%d.%m.%Y %H:%M %P'", + #[allow(clippy::inconsistent_digit_grouping)] + result: Some(Value::date( + DateTime::from_naive_utc_and_offset( + NaiveDateTime::parse_from_str("16.11.1984 8:00 am", "%d.%m.%Y %H:%M %P") + .expect("date calculation should not fail in test"), + *Local::now().offset(), + ), + Span::test_data(), + )), + }, Example { description: "Convert nanosecond-precision unix timestamp to a datetime with offset from UTC", @@ -372,10 +385,21 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value { Some(dt) => match DateTime::parse_from_str(val, &dt.0) { Ok(d) => Value::date ( d, head ), Err(reason) => { - Value::error ( - ShellError::CantConvert { to_type: format!("could not parse as datetime using format '{}'", dt.0), from_type: reason.to_string(), span: head, help: Some("you can use `into datetime` without a format string to enable flexible parsing".to_string()) }, - head, - ) + match NaiveDateTime::parse_from_str(val, &dt.0) { + Ok(d) => Value::date ( + DateTime::from_naive_utc_and_offset( + d, + *Local::now().offset(), + ), + head, + ), + Err(_) => { + Value::error ( + ShellError::CantConvert { to_type: format!("could not parse as datetime using format '{}'", dt.0), from_type: reason.to_string(), span: head, help: Some("you can use `into datetime` without a format string to enable flexible parsing".to_string()) }, + head, + ) + } + } } }, @@ -457,7 +481,7 @@ mod tests { } #[test] - fn takes_a_date_format() { + fn takes_a_date_format_with_timezone() { 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 args = Arguments { @@ -473,6 +497,26 @@ mod tests { assert_eq!(actual, expected) } + #[test] + fn takes_a_date_format_without_timezone() { + let date_str = Value::test_string("16.11.1984 8:00 am"); + let fmt_options = Some(DatetimeFormat("%d.%m.%Y %H:%M %P".to_string())); + let args = Arguments { + zone_options: None, + format_options: fmt_options, + cell_paths: None, + }; + let actual = action(&date_str, &args, Span::test_data()); + let expected = Value::date( + DateTime::from_naive_utc_and_offset( + NaiveDateTime::parse_from_str("16.11.1984 8:00 am", "%d.%m.%Y %H:%M %P").unwrap(), + *Local::now().offset(), + ), + 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"); diff --git a/crates/nu-command/src/conversions/into/duration.rs b/crates/nu-command/src/conversions/into/duration.rs index 21494f3bcc..b459dd04b1 100644 --- a/crates/nu-command/src/conversions/into/duration.rs +++ b/crates/nu-command/src/conversions/into/duration.rs @@ -166,7 +166,7 @@ fn into_duration( ret } }, - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/conversions/into/filesize.rs b/crates/nu-command/src/conversions/into/filesize.rs index 101a9fe6f0..5be167e30c 100644 --- a/crates/nu-command/src/conversions/into/filesize.rs +++ b/crates/nu-command/src/conversions/into/filesize.rs @@ -68,7 +68,7 @@ impl Command for SubCommand { ) -> Result { let cell_paths: Vec = call.rest(engine_state, stack, 0)?; let args = CellPathOnlyArgs::from(cell_paths); - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn examples(&self) -> Vec { @@ -122,7 +122,7 @@ pub fn action(input: &Value, _args: &CellPathOnlyArgs, span: Span) -> Value { Value::Filesize { .. } => input.clone(), Value::Int { val, .. } => Value::filesize(*val, value_span), Value::Float { val, .. } => Value::filesize(*val as i64, value_span), - Value::String { val, .. } => match int_from_string(val, value_span) { + Value::String { val, .. } => match i64_from_string(val, value_span) { Ok(val) => Value::filesize(val, value_span), Err(error) => Value::error(error, value_span), }, @@ -139,7 +139,7 @@ pub fn action(input: &Value, _args: &CellPathOnlyArgs, span: Span) -> Value { } } -fn int_from_string(a_string: &str, span: Span) -> Result { +fn i64_from_string(a_string: &str, span: Span) -> Result { // Get the Locale so we know what the thousands separator is let locale = get_system_locale(); @@ -151,24 +151,35 @@ fn int_from_string(a_string: &str, span: Span) -> Result { // Hadle negative file size if let Some(stripped_negative_string) = clean_string.strip_prefix('-') { match stripped_negative_string.parse::() { - Ok(n) => Ok(-(n.as_u64() as i64)), + Ok(n) => i64_from_byte_size(n, true, span), Err(_) => Err(string_convert_error(span)), } } else if let Some(stripped_positive_string) = clean_string.strip_prefix('+') { match stripped_positive_string.parse::() { Ok(n) if stripped_positive_string.starts_with(|c: char| c.is_ascii_digit()) => { - Ok(n.as_u64() as i64) + i64_from_byte_size(n, false, span) } _ => Err(string_convert_error(span)), } } else { match clean_string.parse::() { - Ok(n) => Ok(n.as_u64() as i64), + Ok(n) => i64_from_byte_size(n, false, span), Err(_) => Err(string_convert_error(span)), } } } +fn i64_from_byte_size( + byte_size: bytesize::ByteSize, + is_negative: bool, + span: Span, +) -> Result { + match i64::try_from(byte_size.as_u64()) { + Ok(n) => Ok(if is_negative { -n } else { n }), + Err(_) => Err(string_convert_error(span)), + } +} + fn string_convert_error(span: Span) -> ShellError { ShellError::CantConvert { to_type: "filesize".into(), diff --git a/crates/nu-command/src/conversions/into/float.rs b/crates/nu-command/src/conversions/into/float.rs index 9ccd7ea03f..43556bb5ef 100644 --- a/crates/nu-command/src/conversions/into/float.rs +++ b/crates/nu-command/src/conversions/into/float.rs @@ -49,7 +49,7 @@ impl Command for SubCommand { ) -> Result { let cell_paths: Vec = call.rest(engine_state, stack, 0)?; let args = CellPathOnlyArgs::from(cell_paths); - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/conversions/into/glob.rs b/crates/nu-command/src/conversions/into/glob.rs index e5d03093f4..ffc3655330 100644 --- a/crates/nu-command/src/conversions/into/glob.rs +++ b/crates/nu-command/src/conversions/into/glob.rs @@ -87,7 +87,7 @@ fn glob_helper( Ok(Value::glob(stream.into_string()?, false, head).into_pipeline_data()) } else { let args = Arguments { cell_paths }; - operate(action, args, input, head, engine_state.ctrlc.clone()) + operate(action, args, input, head, engine_state.signals()) } } diff --git a/crates/nu-command/src/conversions/into/int.rs b/crates/nu-command/src/conversions/into/int.rs index a3f1c92a4f..d4bfa61639 100644 --- a/crates/nu-command/src/conversions/into/int.rs +++ b/crates/nu-command/src/conversions/into/int.rs @@ -158,7 +158,7 @@ impl Command for SubCommand { signed, cell_paths, }; - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/conversions/into/record.rs b/crates/nu-command/src/conversions/into/record.rs index e867f06e15..1f332d9c85 100644 --- a/crates/nu-command/src/conversions/into/record.rs +++ b/crates/nu-command/src/conversions/into/record.rs @@ -125,7 +125,7 @@ fn into_record( ), }, Value::Range { val, .. } => Value::record( - val.into_range_iter(span, engine_state.ctrlc.clone()) + val.into_range_iter(span, engine_state.signals().clone()) .enumerate() .map(|(idx, val)| (format!("{idx}"), val)) .collect(), diff --git a/crates/nu-command/src/conversions/into/string.rs b/crates/nu-command/src/conversions/into/string.rs index 7c7d69cf4e..2b35272b3e 100644 --- a/crates/nu-command/src/conversions/into/string.rs +++ b/crates/nu-command/src/conversions/into/string.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use nu_cmd_base::input_handler::{operate, CmdArgument}; use nu_engine::command_prelude::*; use nu_protocol::{into_code, Config}; @@ -7,7 +9,7 @@ use num_format::ToFormattedString; struct Arguments { decimals_value: Option, cell_paths: Option>, - config: Config, + config: Arc, } impl CmdArgument for Arguments { @@ -174,13 +176,13 @@ fn string_helper( }) } } else { - let config = engine_state.get_config().clone(); + let config = stack.get_config(engine_state); let args = Arguments { decimals_value, cell_paths, config, }; - operate(action, args, input, head, engine_state.ctrlc.clone()) + operate(action, args, input, head, engine_state.signals()) } } diff --git a/crates/nu-command/src/conversions/into/value.rs b/crates/nu-command/src/conversions/into/value.rs index 4bf7e68f53..e8787e75bb 100644 --- a/crates/nu-command/src/conversions/into/value.rs +++ b/crates/nu-command/src/conversions/into/value.rs @@ -57,14 +57,12 @@ impl Command for IntoValue { call: &Call, input: PipelineData, ) -> Result { - let engine_state = engine_state.clone(); let metadata = input.metadata(); - let ctrlc = engine_state.ctrlc.clone(); let span = call.head; - let display_as_filesizes = call.has_flag(&engine_state, stack, "prefer-filesizes")?; + let display_as_filesizes = call.has_flag(engine_state, stack, "prefer-filesizes")?; // the columns to update - let columns: Option = call.get_flag(&engine_state, stack, "columns")?; + let columns: Option = call.get_flag(engine_state, stack, "columns")?; let columns: Option> = match columns { Some(val) => Some( val.into_list()? @@ -81,7 +79,7 @@ impl Command for IntoValue { display_as_filesizes, span, } - .into_pipeline_data(span, ctrlc) + .into_pipeline_data(span, engine_state.signals().clone()) .set_metadata(metadata)) } } diff --git a/crates/nu-command/src/database/commands/into_sqlite.rs b/crates/nu-command/src/database/commands/into_sqlite.rs index f3a2e3622a..88a6114ab3 100644 --- a/crates/nu-command/src/database/commands/into_sqlite.rs +++ b/crates/nu-command/src/database/commands/into_sqlite.rs @@ -2,13 +2,8 @@ use crate::database::values::sqlite::{open_sqlite_db, values_to_sql}; use nu_engine::command_prelude::*; use itertools::Itertools; -use std::{ - path::Path, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, -}; +use nu_protocol::Signals; +use std::path::Path; pub const DEFAULT_TABLE_NAME: &str = "main"; @@ -188,23 +183,18 @@ fn operate( let file_name: Spanned = call.req(engine_state, stack, 0)?; let table_name: Option> = call.get_flag(engine_state, stack, "table-name")?; let table = Table::new(&file_name, table_name)?; - let ctrl_c = engine_state.ctrlc.clone(); - - match action(input, table, span, ctrl_c) { - Ok(val) => Ok(val.into_pipeline_data()), - Err(e) => Err(e), - } + Ok(action(input, table, span, engine_state.signals())?.into_pipeline_data()) } fn action( input: PipelineData, table: Table, span: Span, - ctrl_c: Option>, + signals: &Signals, ) -> Result { match input { PipelineData::ListStream(stream, _) => { - insert_in_transaction(stream.into_iter(), span, table, ctrl_c) + insert_in_transaction(stream.into_iter(), span, table, signals) } PipelineData::Value( Value::List { @@ -212,9 +202,9 @@ fn action( internal_span, }, _, - ) => insert_in_transaction(vals.into_iter(), internal_span, table, ctrl_c), + ) => insert_in_transaction(vals.into_iter(), internal_span, table, signals), PipelineData::Value(val, _) => { - insert_in_transaction(std::iter::once(val), span, table, ctrl_c) + insert_in_transaction(std::iter::once(val), span, table, signals) } _ => Err(ShellError::OnlySupportsThisInputType { exp_input_type: "list".into(), @@ -229,7 +219,7 @@ fn insert_in_transaction( stream: impl Iterator, span: Span, mut table: Table, - ctrl_c: Option>, + signals: &Signals, ) -> Result { let mut stream = stream.peekable(); let first_val = match stream.peek() { @@ -251,17 +241,15 @@ fn insert_in_transaction( let tx = table.try_init(&first_val)?; for stream_value in stream { - if let Some(ref ctrlc) = ctrl_c { - if ctrlc.load(Ordering::Relaxed) { - tx.rollback().map_err(|e| ShellError::GenericError { - error: "Failed to rollback SQLite transaction".into(), - msg: e.to_string(), - span: None, - help: None, - inner: Vec::new(), - })?; - return Err(ShellError::InterruptedByUser { span: None }); - } + if let Err(err) = signals.check(span) { + tx.rollback().map_err(|e| ShellError::GenericError { + error: "Failed to rollback SQLite transaction".into(), + msg: e.to_string(), + span: None, + help: None, + inner: Vec::new(), + })?; + return Err(err); } let val = stream_value.as_record()?; diff --git a/crates/nu-command/src/database/values/sqlite.rs b/crates/nu-command/src/database/values/sqlite.rs index 483da7672e..f253ba1cbd 100644 --- a/crates/nu-command/src/database/values/sqlite.rs +++ b/crates/nu-command/src/database/values/sqlite.rs @@ -2,7 +2,7 @@ use super::definitions::{ db_column::DbColumn, db_constraint::DbConstraint, db_foreignkey::DbForeignKey, db_index::DbIndex, db_table::DbTable, }; -use nu_protocol::{CustomValue, PipelineData, Record, ShellError, Span, Spanned, Value}; +use nu_protocol::{CustomValue, PipelineData, Record, ShellError, Signals, Span, Spanned, Value}; use rusqlite::{ types::ValueRef, Connection, DatabaseName, Error as SqliteError, OpenFlags, Row, Statement, ToSql, @@ -12,7 +12,6 @@ use std::{ fs::File, io::Read, path::{Path, PathBuf}, - sync::{atomic::AtomicBool, Arc}, }; const SQLITE_MAGIC_BYTES: &[u8] = "SQLite format 3\0".as_bytes(); @@ -24,25 +23,21 @@ pub struct SQLiteDatabase { // 1) YAGNI, 2) it's not obvious how cloning a connection could work, 3) state // management gets tricky quick. Revisit this approach if we find a compelling use case. pub path: PathBuf, - #[serde(skip)] + #[serde(skip, default = "Signals::empty")] // this understandably can't be serialized. think that's OK, I'm not aware of a // reason why a CustomValue would be serialized outside of a plugin - ctrlc: Option>, + signals: Signals, } impl SQLiteDatabase { - pub fn new(path: &Path, ctrlc: Option>) -> Self { + pub fn new(path: &Path, signals: Signals) -> Self { Self { path: PathBuf::from(path), - ctrlc, + signals, } } - pub fn try_from_path( - path: &Path, - span: Span, - ctrlc: Option>, - ) -> Result { + pub fn try_from_path(path: &Path, span: Span, signals: Signals) -> Result { let mut file = File::open(path).map_err(|e| ShellError::ReadingFile { msg: e.to_string(), span, @@ -56,7 +51,7 @@ impl SQLiteDatabase { }) .and_then(|_| { if buf == SQLITE_MAGIC_BYTES { - Ok(SQLiteDatabase::new(path, ctrlc)) + Ok(SQLiteDatabase::new(path, signals)) } else { Err(ShellError::ReadingFile { msg: "Not a SQLite file".into(), @@ -72,7 +67,7 @@ impl SQLiteDatabase { Value::Custom { val, .. } => match val.as_any().downcast_ref::() { Some(db) => Ok(Self { path: db.path.clone(), - ctrlc: db.ctrlc.clone(), + signals: db.signals.clone(), }), None => Err(ShellError::CantConvert { to_type: "database".into(), @@ -107,16 +102,8 @@ impl SQLiteDatabase { call_span: Span, ) -> Result { let conn = open_sqlite_db(&self.path, call_span)?; - - let stream = run_sql_query(conn, sql, params, self.ctrlc.clone()).map_err(|e| { - ShellError::GenericError { - error: "Failed to query SQLite database".into(), - msg: e.to_string(), - span: Some(sql.span), - help: None, - inner: vec![], - } - })?; + let stream = run_sql_query(conn, sql, params, &self.signals) + .map_err(|e| e.into_shell_error(sql.span, "Failed to query SQLite database"))?; Ok(stream) } @@ -352,12 +339,7 @@ impl SQLiteDatabase { impl CustomValue for SQLiteDatabase { fn clone_value(&self, span: Span) -> Value { - let cloned = SQLiteDatabase { - path: self.path.clone(), - ctrlc: self.ctrlc.clone(), - }; - - Value::custom(Box::new(cloned), span) + Value::custom(Box::new(self.clone()), span) } fn type_name(&self) -> String { @@ -366,13 +348,8 @@ impl CustomValue for SQLiteDatabase { fn to_base_value(&self, span: Span) -> Result { let db = open_sqlite_db(&self.path, span)?; - read_entire_sqlite_db(db, span, self.ctrlc.clone()).map_err(|e| ShellError::GenericError { - error: "Failed to read from SQLite database".into(), - msg: e.to_string(), - span: Some(span), - help: None, - inner: vec![], - }) + read_entire_sqlite_db(db, span, &self.signals) + .map_err(|e| e.into_shell_error(span, "Failed to read from SQLite database")) } fn as_any(&self) -> &dyn std::any::Any { @@ -396,20 +373,12 @@ impl CustomValue for SQLiteDatabase { fn follow_path_string( &self, _self_span: Span, - _column_name: String, + column_name: String, path_span: Span, ) -> Result { let db = open_sqlite_db(&self.path, path_span)?; - - read_single_table(db, _column_name, path_span, self.ctrlc.clone()).map_err(|e| { - ShellError::GenericError { - error: "Failed to read from SQLite database".into(), - msg: e.to_string(), - span: Some(path_span), - help: None, - inner: vec![], - } - }) + read_single_table(db, column_name, path_span, &self.signals) + .map_err(|e| e.into_shell_error(path_span, "Failed to read from SQLite database")) } fn typetag_name(&self) -> &'static str { @@ -426,12 +395,12 @@ pub fn open_sqlite_db(path: &Path, call_span: Span) -> Result, params: NuSqlParams, - ctrlc: Option>, -) -> Result { + signals: &Signals, +) -> Result { let stmt = conn.prepare(&sql.item)?; - - prepared_statement_to_nu_list(stmt, params, sql.span, ctrlc) + prepared_statement_to_nu_list(stmt, params, sql.span, signals) } // This is taken from to text local_into_string but tweaks it a bit so that certain formatting does not happen @@ -534,23 +502,56 @@ pub fn nu_value_to_params(value: Value) -> Result { } } +#[derive(Debug)] +enum SqliteOrShellError { + SqliteError(SqliteError), + ShellError(ShellError), +} + +impl From for SqliteOrShellError { + fn from(error: SqliteError) -> Self { + Self::SqliteError(error) + } +} + +impl From for SqliteOrShellError { + fn from(error: ShellError) -> Self { + Self::ShellError(error) + } +} + +impl SqliteOrShellError { + fn into_shell_error(self, span: Span, msg: &str) -> ShellError { + match self { + Self::SqliteError(err) => ShellError::GenericError { + error: msg.into(), + msg: err.to_string(), + span: Some(span), + help: None, + inner: Vec::new(), + }, + Self::ShellError(err) => err, + } + } +} + fn read_single_table( conn: Connection, table_name: String, call_span: Span, - ctrlc: Option>, -) -> Result { + signals: &Signals, +) -> Result { // TODO: Should use params here? let stmt = conn.prepare(&format!("SELECT * FROM [{table_name}]"))?; - prepared_statement_to_nu_list(stmt, NuSqlParams::default(), call_span, ctrlc) + prepared_statement_to_nu_list(stmt, NuSqlParams::default(), call_span, signals) } fn prepared_statement_to_nu_list( mut stmt: Statement, params: NuSqlParams, call_span: Span, - ctrlc: Option>, -) -> Result { + signals: &Signals, +) -> Result { let column_names = stmt .column_names() .into_iter() @@ -576,11 +577,7 @@ fn prepared_statement_to_nu_list( let mut row_values = vec![]; for row_result in row_results { - if nu_utils::ctrl_c::was_pressed(&ctrlc) { - // return whatever we have so far, let the caller decide whether to use it - return Ok(Value::list(row_values, call_span)); - } - + signals.check(call_span)?; if let Ok(row_value) = row_result { row_values.push(row_value); } @@ -606,11 +603,7 @@ fn prepared_statement_to_nu_list( let mut row_values = vec![]; for row_result in row_results { - if nu_utils::ctrl_c::was_pressed(&ctrlc) { - // return whatever we have so far, let the caller decide whether to use it - return Ok(Value::list(row_values, call_span)); - } - + signals.check(call_span)?; if let Ok(row_value) = row_result { row_values.push(row_value); } @@ -626,8 +619,8 @@ fn prepared_statement_to_nu_list( fn read_entire_sqlite_db( conn: Connection, call_span: Span, - ctrlc: Option>, -) -> Result { + signals: &Signals, +) -> Result { let mut tables = Record::new(); let mut get_table_names = @@ -638,12 +631,8 @@ fn read_entire_sqlite_db( let table_name: String = row?; // TODO: Should use params here? let table_stmt = conn.prepare(&format!("select * from [{table_name}]"))?; - let rows = prepared_statement_to_nu_list( - table_stmt, - NuSqlParams::default(), - call_span, - ctrlc.clone(), - )?; + let rows = + prepared_statement_to_nu_list(table_stmt, NuSqlParams::default(), call_span, signals)?; tables.push(table_name, rows); } @@ -710,7 +699,7 @@ mod test { #[test] fn can_read_empty_db() { let db = open_connection_in_memory().unwrap(); - let converted_db = read_entire_sqlite_db(db, Span::test_data(), None).unwrap(); + let converted_db = read_entire_sqlite_db(db, Span::test_data(), &Signals::empty()).unwrap(); let expected = Value::test_record(Record::new()); @@ -730,7 +719,7 @@ mod test { [], ) .unwrap(); - let converted_db = read_entire_sqlite_db(db, Span::test_data(), None).unwrap(); + let converted_db = read_entire_sqlite_db(db, Span::test_data(), &Signals::empty()).unwrap(); let expected = Value::test_record(record! { "person" => Value::test_list(vec![]), @@ -759,7 +748,7 @@ mod test { db.execute("INSERT INTO item (id, name) VALUES (456, 'foo bar')", []) .unwrap(); - let converted_db = read_entire_sqlite_db(db, span, None).unwrap(); + let converted_db = read_entire_sqlite_db(db, span, &Signals::empty()).unwrap(); let expected = Value::test_record(record! { "item" => Value::test_list( diff --git a/crates/nu-command/src/date/humanize.rs b/crates/nu-command/src/date/humanize.rs index 2815571520..d8542a540c 100644 --- a/crates/nu-command/src/date/humanize.rs +++ b/crates/nu-command/src/date/humanize.rs @@ -50,7 +50,7 @@ impl Command for SubCommand { if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map(move |value| helper(value, head), engine_state.ctrlc.clone()) + input.map(move |value| helper(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/date/list_timezone.rs b/crates/nu-command/src/date/list_timezone.rs index 6f9267947d..56f7fe5376 100644 --- a/crates/nu-command/src/date/list_timezone.rs +++ b/crates/nu-command/src/date/list_timezone.rs @@ -40,7 +40,7 @@ impl Command for SubCommand { head, ) }) - .into_pipeline_data(head, engine_state.ctrlc.clone())) + .into_pipeline_data(head, engine_state.signals().clone())) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/date/to_record.rs b/crates/nu-command/src/date/to_record.rs index f9c0ceff1b..c0b09c040b 100644 --- a/crates/nu-command/src/date/to_record.rs +++ b/crates/nu-command/src/date/to_record.rs @@ -40,7 +40,7 @@ impl Command for SubCommand { if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map(move |value| helper(value, head), engine_state.ctrlc.clone()) + input.map(move |value| helper(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/date/to_table.rs b/crates/nu-command/src/date/to_table.rs index 36c3f4a94a..7ce8bc171b 100644 --- a/crates/nu-command/src/date/to_table.rs +++ b/crates/nu-command/src/date/to_table.rs @@ -40,7 +40,7 @@ impl Command for SubCommand { if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map(move |value| helper(value, head), engine_state.ctrlc.clone()) + input.map(move |value| helper(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/date/to_timezone.rs b/crates/nu-command/src/date/to_timezone.rs index 5f41d287ae..3d08d7271b 100644 --- a/crates/nu-command/src/date/to_timezone.rs +++ b/crates/nu-command/src/date/to_timezone.rs @@ -55,7 +55,7 @@ impl Command for SubCommand { } input.map( move |value| helper(value, head, &timezone), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/debug/debug_.rs b/crates/nu-command/src/debug/debug_.rs index c766081410..80a9067e49 100644 --- a/crates/nu-command/src/debug/debug_.rs +++ b/crates/nu-command/src/debug/debug_.rs @@ -33,7 +33,7 @@ impl Command for Debug { input: PipelineData, ) -> Result { let head = call.head; - let config = engine_state.get_config().clone(); + let config = stack.get_config(engine_state); let raw = call.has_flag(engine_state, stack, "raw")?; // Should PipelineData::Empty result in an error here? @@ -46,7 +46,7 @@ impl Command for Debug { Value::string(x.to_expanded_string(", ", &config), head) } }, - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/debug/explain.rs b/crates/nu-command/src/debug/explain.rs index b451d6916a..710e37935a 100644 --- a/crates/nu-command/src/debug/explain.rs +++ b/crates/nu-command/src/debug/explain.rs @@ -1,6 +1,6 @@ use nu_engine::{command_prelude::*, get_eval_expression}; use nu_protocol::{ - ast::{Argument, Block, Expr, Expression}, + ast::{self, Argument, Block, Expr, Expression}, engine::Closure, }; @@ -106,7 +106,7 @@ pub fn get_pipeline_elements( fn get_arguments( engine_state: &EngineState, stack: &mut Stack, - call: &Call, + call: &ast::Call, eval_expression_fn: fn(&EngineState, &mut Stack, &Expression) -> Result, ) -> Vec { let mut arg_value = vec![]; diff --git a/crates/nu-command/src/debug/metadata.rs b/crates/nu-command/src/debug/metadata.rs index 135047a3d9..245c150cea 100644 --- a/crates/nu-command/src/debug/metadata.rs +++ b/crates/nu-command/src/debug/metadata.rs @@ -28,6 +28,10 @@ impl Command for Metadata { .category(Category::Debug) } + fn requires_ast_for_arguments(&self) -> bool { + true + } + fn run( &self, engine_state: &EngineState, @@ -35,7 +39,7 @@ impl Command for Metadata { call: &Call, input: PipelineData, ) -> Result { - let arg = call.positional_nth(0); + let arg = call.positional_nth(stack, 0); let head = call.head; match arg { @@ -80,16 +84,23 @@ impl Command for Metadata { match x { PipelineMetadata { data_source: DataSource::Ls, + .. } => record.push("source", Value::string("ls", head)), PipelineMetadata { data_source: DataSource::HtmlThemes, + .. } => record.push("source", Value::string("into html --list", head)), PipelineMetadata { data_source: DataSource::FilePath(path), + .. } => record.push( "source", Value::string(path.to_string_lossy().to_string(), head), ), + _ => {} + } + if let Some(ref content_type) = x.content_type { + record.push("content_type", Value::string(content_type, head)); } } @@ -133,16 +144,23 @@ fn build_metadata_record(arg: &Value, metadata: Option<&PipelineMetadata>, head: match x { PipelineMetadata { data_source: DataSource::Ls, + .. } => record.push("source", Value::string("ls", head)), PipelineMetadata { data_source: DataSource::HtmlThemes, + .. } => record.push("source", Value::string("into html --list", head)), PipelineMetadata { data_source: DataSource::FilePath(path), + .. } => record.push( "source", Value::string(path.to_string_lossy().to_string(), head), ), + _ => {} + } + if let Some(ref content_type) = x.content_type { + record.push("content_type", Value::string(content_type, head)); } } diff --git a/crates/nu-command/src/debug/metadata_set.rs b/crates/nu-command/src/debug/metadata_set.rs index 6608827fda..96f50cfdba 100644 --- a/crates/nu-command/src/debug/metadata_set.rs +++ b/crates/nu-command/src/debug/metadata_set.rs @@ -1,5 +1,5 @@ use nu_engine::command_prelude::*; -use nu_protocol::{DataSource, PipelineMetadata}; +use nu_protocol::DataSource; #[derive(Clone)] pub struct MetadataSet; @@ -27,6 +27,12 @@ impl Command for MetadataSet { "Assign the DataSource::FilePath metadata to the input", Some('f'), ) + .named( + "content-type", + SyntaxShape::String, + "Assign content type metadata to the input", + Some('c'), + ) .allow_variants_without_examples(true) .category(Category::Debug) } @@ -41,33 +47,26 @@ impl Command for MetadataSet { let head = call.head; let ds_fp: Option = call.get_flag(engine_state, stack, "datasource-filepath")?; let ds_ls = call.has_flag(engine_state, stack, "datasource-ls")?; + let content_type: Option = call.get_flag(engine_state, stack, "content-type")?; + let signals = engine_state.signals().clone(); + let metadata = input + .metadata() + .clone() + .unwrap_or_default() + .with_content_type(content_type); match (ds_fp, ds_ls) { - (Some(path), false) => { - let metadata = PipelineMetadata { - data_source: DataSource::FilePath(path.into()), - }; - Ok(input.into_pipeline_data_with_metadata( - head, - engine_state.ctrlc.clone(), - metadata, - )) - } - (None, true) => { - let metadata = PipelineMetadata { - data_source: DataSource::Ls, - }; - Ok(input.into_pipeline_data_with_metadata( - head, - engine_state.ctrlc.clone(), - metadata, - )) - } - _ => Err(ShellError::IncorrectValue { - msg: "Expected either --datasource-ls(-l) or --datasource-filepath(-f)".to_string(), - val_span: head, - call_span: head, - }), + (Some(path), false) => Ok(input.into_pipeline_data_with_metadata( + head, + signals, + metadata.with_data_source(DataSource::FilePath(path.into())), + )), + (None, true) => Ok(input.into_pipeline_data_with_metadata( + head, + signals, + metadata.with_data_source(DataSource::Ls), + )), + _ => Ok(input.into_pipeline_data_with_metadata(head, signals, metadata)), } } @@ -83,18 +82,23 @@ impl Command for MetadataSet { example: "'crates' | metadata set --datasource-filepath $'(pwd)/crates' | metadata", result: None, }, + Example { + description: "Set the metadata of a file path", + example: "'crates' | metadata set --content-type text/plain | metadata", + result: Some(Value::record(record!("content_type" => Value::string("text/plain", Span::test_data())), Span::test_data())), + }, ] } } #[cfg(test)] mod test { + use crate::{test_examples_with_commands, Metadata}; + use super::*; #[test] fn test_examples() { - use crate::test_examples; - - test_examples(MetadataSet {}) + test_examples_with_commands(MetadataSet {}, &[&Metadata {}]) } } diff --git a/crates/nu-command/src/debug/mod.rs b/crates/nu-command/src/debug/mod.rs index f19ddab916..ec18c2be87 100644 --- a/crates/nu-command/src/debug/mod.rs +++ b/crates/nu-command/src/debug/mod.rs @@ -10,6 +10,7 @@ mod profile; mod timeit; mod view; mod view_files; +mod view_ir; mod view_source; mod view_span; @@ -25,5 +26,6 @@ pub use profile::DebugProfile; pub use timeit::TimeIt; pub use view::View; pub use view_files::ViewFiles; +pub use view_ir::ViewIr; pub use view_source::ViewSource; pub use view_span::ViewSpan; diff --git a/crates/nu-command/src/debug/profile.rs b/crates/nu-command/src/debug/profile.rs index 0cef979094..e8254563df 100644 --- a/crates/nu-command/src/debug/profile.rs +++ b/crates/nu-command/src/debug/profile.rs @@ -1,5 +1,8 @@ use nu_engine::{command_prelude::*, ClosureEvalOnce}; -use nu_protocol::{debugger::Profiler, engine::Closure}; +use nu_protocol::{ + debugger::{Profiler, ProfilerOptions}, + engine::Closure, +}; #[derive(Clone)] pub struct DebugProfile; @@ -28,6 +31,7 @@ impl Command for DebugProfile { Some('v'), ) .switch("expr", "Collect expression types", Some('x')) + .switch("instructions", "Collect IR instructions", Some('i')) .switch("lines", "Collect line numbers", Some('l')) .named( "max-depth", @@ -91,19 +95,23 @@ confusing the id/parent_id hierarchy. The --expr flag is helpful for investigati let collect_expanded_source = call.has_flag(engine_state, stack, "expanded-source")?; let collect_values = call.has_flag(engine_state, stack, "values")?; let collect_exprs = call.has_flag(engine_state, stack, "expr")?; + let collect_instructions = call.has_flag(engine_state, stack, "instructions")?; let collect_lines = call.has_flag(engine_state, stack, "lines")?; let max_depth = call .get_flag(engine_state, stack, "max-depth")? .unwrap_or(2); let profiler = Profiler::new( - max_depth, - collect_spans, - true, - collect_expanded_source, - collect_values, - collect_exprs, - collect_lines, + ProfilerOptions { + max_depth, + collect_spans, + collect_source: true, + collect_expanded_source, + collect_values, + collect_exprs, + collect_instructions, + collect_lines, + }, call.span(), ); diff --git a/crates/nu-command/src/debug/timeit.rs b/crates/nu-command/src/debug/timeit.rs index a445679b81..7a48644a6d 100644 --- a/crates/nu-command/src/debug/timeit.rs +++ b/crates/nu-command/src/debug/timeit.rs @@ -32,6 +32,10 @@ impl Command for TimeIt { vec!["timing", "timer", "benchmark", "measure"] } + fn requires_ast_for_arguments(&self) -> bool { + true + } + fn run( &self, engine_state: &EngineState, @@ -39,13 +43,14 @@ impl Command for TimeIt { call: &Call, input: PipelineData, ) -> Result { - let command_to_run = call.positional_nth(0); + // reset outdest, so the command can write to stdout and stderr. + let stack = &mut stack.push_redirection(None, None); + + let command_to_run = call.positional_nth(stack, 0); // Get the start time after all other computation has been done. let start_time = Instant::now(); - // reset outdest, so the command can write to stdout and stderr. - let stack = &mut stack.push_redirection(None, None); if let Some(command_to_run) = command_to_run { if let Some(block_id) = command_to_run.as_block() { let eval_block = get_eval_block(engine_state); @@ -53,7 +58,8 @@ impl Command for TimeIt { eval_block(engine_state, stack, block, input)? } else { let eval_expression_with_input = get_eval_expression_with_input(engine_state); - eval_expression_with_input(engine_state, stack, command_to_run, input)?.0 + let expression = &command_to_run.clone(); + eval_expression_with_input(engine_state, stack, expression, input)?.0 } } else { PipelineData::empty() diff --git a/crates/nu-command/src/debug/view_ir.rs b/crates/nu-command/src/debug/view_ir.rs new file mode 100644 index 0000000000..72136079fd --- /dev/null +++ b/crates/nu-command/src/debug/view_ir.rs @@ -0,0 +1,171 @@ +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct ViewIr; + +impl Command for ViewIr { + fn name(&self) -> &str { + "view ir" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required( + "target", + SyntaxShape::Any, + "The name or block to view compiled code for.", + ) + .switch( + "json", + "Dump the raw block data as JSON (unstable).", + Some('j'), + ) + .switch( + "decl-id", + "Integer is a declaration ID rather than a block ID.", + Some('d'), + ) + .input_output_type(Type::Nothing, Type::String) + .category(Category::Debug) + } + + fn usage(&self) -> &str { + "View the compiled IR code for a block of code." + } + + fn extra_usage(&self) -> &str { + " +The target can be a closure, the name of a custom command, or an internal block +ID. Closure literals within IR dumps often reference the block by ID (e.g. +`closure(3231)`), so this provides an easy way to read the IR of any embedded +closures. + +The --decl-id option is provided to use a declaration ID instead, which can be +found on `call` instructions. This is sometimes better than using the name, as +the declaration may not be in scope. +" + .trim() + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let target: Value = call.req(engine_state, stack, 0)?; + let json = call.has_flag(engine_state, stack, "json")?; + let is_decl_id = call.has_flag(engine_state, stack, "decl-id")?; + + let block_id = match target { + Value::Closure { ref val, .. } => val.block_id, + // Decl by name + Value::String { ref val, .. } => { + if let Some(decl_id) = engine_state.find_decl(val.as_bytes(), &[]) { + let decl = engine_state.get_decl(decl_id); + decl.block_id().ok_or_else(|| ShellError::GenericError { + error: format!("Can't view IR for `{val}`"), + msg: "not a custom command".into(), + span: Some(target.span()), + help: Some("internal commands don't have Nushell source code".into()), + inner: vec![], + })? + } else { + return Err(ShellError::GenericError { + error: format!("Can't view IR for `{val}`"), + msg: "can't find a command with this name".into(), + span: Some(target.span()), + help: None, + inner: vec![], + }); + } + } + // Decl by ID - IR dump always shows name of decl, but sometimes it isn't in scope + Value::Int { val, .. } if is_decl_id => { + let decl_id = val + .try_into() + .ok() + .filter(|id| *id < engine_state.num_decls()) + .ok_or_else(|| ShellError::IncorrectValue { + msg: "not a valid decl id".into(), + val_span: target.span(), + call_span: call.head, + })?; + let decl = engine_state.get_decl(decl_id); + decl.block_id().ok_or_else(|| ShellError::GenericError { + error: format!("Can't view IR for `{}`", decl.name()), + msg: "not a custom command".into(), + span: Some(target.span()), + help: Some("internal commands don't have Nushell source code".into()), + inner: vec![], + })? + } + // Block by ID - often shows up in IR + Value::Int { val, .. } => val.try_into().map_err(|_| ShellError::IncorrectValue { + msg: "not a valid block id".into(), + val_span: target.span(), + call_span: call.head, + })?, + // Pass through errors + Value::Error { error, .. } => return Err(*error), + _ => { + return Err(ShellError::TypeMismatch { + err_message: "expected closure, string, or int".into(), + span: call.head, + }) + } + }; + + let Some(block) = engine_state.try_get_block(block_id) else { + return Err(ShellError::GenericError { + error: format!("Unknown block ID: {block_id}"), + msg: "ensure the block ID is correct and try again".into(), + span: Some(target.span()), + help: None, + inner: vec![], + }); + }; + + let ir_block = block + .ir_block + .as_ref() + .ok_or_else(|| ShellError::GenericError { + error: "Can't view IR for this block".into(), + msg: "block is missing compiled representation".into(), + span: block.span, + help: Some("the IrBlock is probably missing due to a compilation error".into()), + inner: vec![], + })?; + + let formatted = if json { + let formatted_instructions = ir_block + .instructions + .iter() + .map(|instruction| { + instruction + .display(engine_state, &ir_block.data) + .to_string() + }) + .collect::>(); + + serde_json::to_string_pretty(&serde_json::json!({ + "block_id": block_id, + "span": block.span, + "ir_block": ir_block, + "formatted_instructions": formatted_instructions, + })) + .map_err(|err| ShellError::GenericError { + error: "JSON serialization failed".into(), + msg: err.to_string(), + span: Some(call.head), + help: None, + inner: vec![], + })? + } else { + format!("{}", ir_block.display(engine_state)) + }; + + Ok(Value::string(formatted, call.head).into_pipeline_data()) + } +} diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index a23f9c4ef4..4adcfe9214 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -31,6 +31,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { All, Any, Append, + Chunks, Columns, Compact, Default, @@ -127,7 +128,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { SysTemp, SysUsers, UName, - + Which, }; // Help @@ -154,6 +155,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { TimeIt, View, ViewFiles, + ViewIr, ViewSource, ViewSpan, }; @@ -172,9 +174,6 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { ))] bind_command! { Ps }; - #[cfg(feature = "which-support")] - bind_command! { Which }; - // Strings bind_command! { Char, @@ -192,6 +191,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { Str, StrCapitalize, StrContains, + StrDeunicode, StrDistance, StrDowncase, StrEndswith, @@ -291,7 +291,6 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { ToText, ToToml, ToTsv, - Touch, Upsert, Where, ToXml, diff --git a/crates/nu-command/src/env/config/config_env.rs b/crates/nu-command/src/env/config/config_env.rs index 32bd7bba90..777b6f3aac 100644 --- a/crates/nu-command/src/env/config/config_env.rs +++ b/crates/nu-command/src/env/config/config_env.rs @@ -60,12 +60,13 @@ impl Command for ConfigEnv { let (editor_name, editor_args) = get_editor(engine_state, stack, call.head)?; let paths = nu_engine::env::path_str(engine_state, stack, call.head)?; let cwd = engine_state.cwd(Some(stack))?; - let editor_executable = - crate::which(&editor_name, &paths, &cwd).ok_or(ShellError::ExternalCommand { + let editor_executable = crate::which(&editor_name, &paths, cwd.as_ref()).ok_or( + ShellError::ExternalCommand { label: format!("`{editor_name}` not found"), help: "Failed to find the editor executable".into(), span: call.head, - })?; + }, + )?; let Some(env_path) = engine_state.get_config_path("env-path") else { return Err(ShellError::GenericError { diff --git a/crates/nu-command/src/env/config/config_nu.rs b/crates/nu-command/src/env/config/config_nu.rs index 08695ca5bb..80f0bbc012 100644 --- a/crates/nu-command/src/env/config/config_nu.rs +++ b/crates/nu-command/src/env/config/config_nu.rs @@ -64,12 +64,13 @@ impl Command for ConfigNu { let (editor_name, editor_args) = get_editor(engine_state, stack, call.head)?; let paths = nu_engine::env::path_str(engine_state, stack, call.head)?; let cwd = engine_state.cwd(Some(stack))?; - let editor_executable = - crate::which(&editor_name, &paths, &cwd).ok_or(ShellError::ExternalCommand { + let editor_executable = crate::which(&editor_name, &paths, cwd.as_ref()).ok_or( + ShellError::ExternalCommand { label: format!("`{editor_name}` not found"), help: "Failed to find the editor executable".into(), span: call.head, - })?; + }, + )?; let Some(config_path) = engine_state.get_config_path("config-path") else { return Err(ShellError::GenericError { diff --git a/crates/nu-command/src/env/export_env.rs b/crates/nu-command/src/env/export_env.rs index 20605a9bb5..7b583c9959 100644 --- a/crates/nu-command/src/env/export_env.rs +++ b/crates/nu-command/src/env/export_env.rs @@ -1,4 +1,5 @@ use nu_engine::{command_prelude::*, get_eval_block, redirect_env}; +use nu_protocol::engine::CommandType; #[derive(Clone)] pub struct ExportEnv; @@ -23,6 +24,19 @@ impl Command for ExportEnv { "Run a block and preserve its environment in a current scope." } + fn extra_usage(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + + fn requires_ast_for_arguments(&self) -> bool { + true + } + fn run( &self, engine_state: &EngineState, @@ -31,7 +45,7 @@ impl Command for ExportEnv { input: PipelineData, ) -> Result { let block_id = call - .positional_nth(0) + .positional_nth(caller_stack, 0) .expect("checked through parser") .as_block() .expect("internal error: missing block"); diff --git a/crates/nu-command/src/env/source_env.rs b/crates/nu-command/src/env/source_env.rs index 71c1e6dc3f..1813a92f1f 100644 --- a/crates/nu-command/src/env/source_env.rs +++ b/crates/nu-command/src/env/source_env.rs @@ -2,6 +2,7 @@ use nu_engine::{ command_prelude::*, find_in_dirs_env, get_dirs_var_from_call, get_eval_block_with_early_return, redirect_env, }; +use nu_protocol::engine::CommandType; use std::path::PathBuf; /// Source a file for environment variables. @@ -28,6 +29,15 @@ impl Command for SourceEnv { "Source the environment from a source file into the current environment." } + fn extra_usage(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + fn run( &self, engine_state: &EngineState, @@ -46,7 +56,7 @@ impl Command for SourceEnv { &source_filename.item, engine_state, caller_stack, - get_dirs_var_from_call(call), + get_dirs_var_from_call(caller_stack, call), )? { PathBuf::from(&path) } else { diff --git a/crates/nu-command/src/example_test.rs b/crates/nu-command/src/example_test.rs index 476113aa2d..4879cab0a9 100644 --- a/crates/nu-command/src/example_test.rs +++ b/crates/nu-command/src/example_test.rs @@ -31,7 +31,7 @@ mod test_examples { check_example_evaluates_to_expected_output, check_example_input_and_output_types_match_command_signature, }; - use nu_cmd_lang::{Break, Echo, If, Let, Mut}; + use nu_cmd_lang::{Break, Describe, Echo, If, Let, Mut}; use nu_protocol::{ engine::{Command, EngineState, StateWorkingSet}, Type, @@ -81,6 +81,7 @@ mod test_examples { working_set.add_decl(Box::new(Break)); working_set.add_decl(Box::new(Date)); working_set.add_decl(Box::new(Default)); + working_set.add_decl(Box::new(Describe)); working_set.add_decl(Box::new(Each)); working_set.add_decl(Box::new(Echo)); working_set.add_decl(Box::new(Enumerate)); diff --git a/crates/nu-command/src/filesystem/cd.rs b/crates/nu-command/src/filesystem/cd.rs index 4e09d900d2..23faf6f67b 100644 --- a/crates/nu-command/src/filesystem/cd.rs +++ b/crates/nu-command/src/filesystem/cd.rs @@ -1,5 +1,6 @@ use nu_cmd_base::util::get_init_cwd; use nu_engine::command_prelude::*; +use nu_path::AbsolutePathBuf; use nu_utils::filesystem::{have_permission, PermissionResult}; #[derive(Clone)] @@ -43,7 +44,10 @@ impl Command for Cd { // If getting PWD failed, default to the initial directory. This way, the // user can use `cd` to recover PWD to a good state. - let cwd = engine_state.cwd(Some(stack)).unwrap_or(get_init_cwd()); + let cwd = engine_state + .cwd(Some(stack)) + .map(AbsolutePathBuf::into_std_path_buf) + .unwrap_or(get_init_cwd()); let path_val = { if let Some(path) = path_val { diff --git a/crates/nu-command/src/filesystem/du.rs b/crates/nu-command/src/filesystem/du.rs index c48a1f7ac1..d34892ace9 100644 --- a/crates/nu-command/src/filesystem/du.rs +++ b/crates/nu-command/src/filesystem/du.rs @@ -3,10 +3,9 @@ use crate::{DirBuilder, DirInfo, FileInfo}; #[allow(deprecated)] use nu_engine::{command_prelude::*, current_dir}; use nu_glob::Pattern; -use nu_protocol::NuGlob; +use nu_protocol::{NuGlob, Signals}; use serde::Deserialize; use std::path::Path; -use std::sync::{atomic::AtomicBool, Arc}; #[derive(Clone)] pub struct Du; @@ -103,7 +102,7 @@ impl Command for Du { let current_dir = current_dir(engine_state, stack)?; let paths = get_rest_for_glob_pattern(engine_state, stack, call, 0)?; - let paths = if call.rest_iter(0).count() == 0 { + let paths = if !call.has_positional_args(stack, 0) { None } else { Some(paths) @@ -120,8 +119,8 @@ impl Command for Du { min_size, }; Ok( - du_for_one_pattern(args, ¤t_dir, tag, engine_state.ctrlc.clone())? - .into_pipeline_data(tag, engine_state.ctrlc.clone()), + du_for_one_pattern(args, ¤t_dir, tag, engine_state.signals())? + .into_pipeline_data(tag, engine_state.signals().clone()), ) } Some(paths) => { @@ -139,7 +138,7 @@ impl Command for Du { args, ¤t_dir, tag, - engine_state.ctrlc.clone(), + engine_state.signals(), )?) } @@ -147,7 +146,7 @@ impl Command for Du { Ok(result_iters .into_iter() .flatten() - .into_pipeline_data(tag, engine_state.ctrlc.clone())) + .into_pipeline_data(tag, engine_state.signals().clone())) } } } @@ -164,8 +163,8 @@ impl Command for Du { fn du_for_one_pattern( args: DuArgs, current_dir: &Path, - call_span: Span, - ctrl_c: Option>, + span: Span, + signals: &Signals, ) -> Result + Send, ShellError> { let exclude = args.exclude.map_or(Ok(None), move |x| { Pattern::new(x.item.as_ref()) @@ -178,7 +177,7 @@ fn du_for_one_pattern( let include_files = args.all; let mut paths = match args.path { - Some(p) => nu_engine::glob_from(&p, current_dir, call_span, None), + Some(p) => nu_engine::glob_from(&p, current_dir, span, None), // The * pattern should never fail. None => nu_engine::glob_from( &Spanned { @@ -186,7 +185,7 @@ fn du_for_one_pattern( span: Span::unknown(), }, current_dir, - call_span, + span, None, ), } @@ -205,7 +204,7 @@ fn du_for_one_pattern( let min_size = args.min_size.map(|f| f.item as u64); let params = DirBuilder { - tag: call_span, + tag: span, min: min_size, deref, exclude, @@ -217,13 +216,13 @@ fn du_for_one_pattern( match p { Ok(a) => { if a.is_dir() { - output.push(DirInfo::new(a, ¶ms, max_depth, ctrl_c.clone()).into()); - } else if let Ok(v) = FileInfo::new(a, deref, call_span) { + output.push(DirInfo::new(a, ¶ms, max_depth, span, signals)?.into()); + } else if let Ok(v) = FileInfo::new(a, deref, span) { output.push(v.into()); } } Err(e) => { - output.push(Value::error(e, call_span)); + output.push(Value::error(e, span)); } } } diff --git a/crates/nu-command/src/filesystem/glob.rs b/crates/nu-command/src/filesystem/glob.rs index b10e8893a0..212c6ceb92 100644 --- a/crates/nu-command/src/filesystem/glob.rs +++ b/crates/nu-command/src/filesystem/glob.rs @@ -1,5 +1,5 @@ use nu_engine::command_prelude::*; -use std::sync::{atomic::AtomicBool, Arc}; +use nu_protocol::Signals; use wax::{Glob as WaxGlob, WalkBehavior, WalkEntry}; #[derive(Clone)] @@ -125,7 +125,6 @@ impl Command for Glob { call: &Call, _input: PipelineData, ) -> Result { - let ctrlc = engine_state.ctrlc.clone(); let span = call.head; let glob_pattern: Spanned = call.req(engine_state, stack, 0)?; let depth = call.get_flag(engine_state, stack, "depth")?; @@ -216,7 +215,14 @@ impl Command for Glob { inner: vec![], })? .flatten(); - glob_to_value(ctrlc, glob_results, no_dirs, no_files, no_symlinks, span) + glob_to_value( + engine_state.signals(), + glob_results, + no_dirs, + no_files, + no_symlinks, + span, + ) } else { let glob_results = glob .walk_with_behavior( @@ -227,12 +233,19 @@ impl Command for Glob { }, ) .flatten(); - glob_to_value(ctrlc, glob_results, no_dirs, no_files, no_symlinks, span) + glob_to_value( + engine_state.signals(), + glob_results, + no_dirs, + no_files, + no_symlinks, + span, + ) }?; Ok(result .into_iter() - .into_pipeline_data(span, engine_state.ctrlc.clone())) + .into_pipeline_data(span, engine_state.signals().clone())) } } @@ -252,7 +265,7 @@ fn convert_patterns(columns: &[Value]) -> Result, ShellError> { } fn glob_to_value<'a>( - ctrlc: Option>, + signals: &Signals, glob_results: impl Iterator>, no_dirs: bool, no_files: bool, @@ -261,10 +274,7 @@ fn glob_to_value<'a>( ) -> Result, ShellError> { let mut result: Vec = Vec::new(); for entry in glob_results { - if nu_utils::ctrl_c::was_pressed(&ctrlc) { - result.clear(); - return Err(ShellError::InterruptedByUser { span: None }); - } + signals.check(span)?; let file_type = entry.file_type(); if !(no_dirs && file_type.is_dir() diff --git a/crates/nu-command/src/filesystem/ls.rs b/crates/nu-command/src/filesystem/ls.rs index a89cf04ed6..807e4f3409 100644 --- a/crates/nu-command/src/filesystem/ls.rs +++ b/crates/nu-command/src/filesystem/ls.rs @@ -6,14 +6,13 @@ use nu_engine::glob_from; use nu_engine::{command_prelude::*, env::current_dir}; use nu_glob::MatchOptions; use nu_path::expand_to_real_path; -use nu_protocol::{DataSource, NuGlob, PipelineMetadata}; +use nu_protocol::{DataSource, NuGlob, PipelineMetadata, Signals}; use pathdiff::diff_paths; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; use std::{ path::PathBuf, - sync::Arc, time::{SystemTime, UNIX_EPOCH}, }; @@ -93,7 +92,6 @@ impl Command for Ls { let du = call.has_flag(engine_state, stack, "du")?; let directory = call.has_flag(engine_state, stack, "directory")?; let use_mime_type = call.has_flag(engine_state, stack, "mime-type")?; - let ctrl_c = engine_state.ctrlc.clone(); let call_span = call.head; #[allow(deprecated)] let cwd = current_dir(engine_state, stack)?; @@ -110,18 +108,19 @@ impl Command for Ls { }; let pattern_arg = get_rest_for_glob_pattern(engine_state, stack, call, 0)?; - let input_pattern_arg = if call.rest_iter(0).count() == 0 { + let input_pattern_arg = if !call.has_positional_args(stack, 0) { None } else { Some(pattern_arg) }; match input_pattern_arg { - None => Ok(ls_for_one_pattern(None, args, ctrl_c.clone(), cwd)? + None => Ok(ls_for_one_pattern(None, args, engine_state.signals(), cwd)? .into_pipeline_data_with_metadata( call_span, - ctrl_c, + engine_state.signals().clone(), PipelineMetadata { data_source: DataSource::Ls, + content_type: None, }, )), Some(pattern) => { @@ -130,7 +129,7 @@ impl Command for Ls { result_iters.push(ls_for_one_pattern( Some(pat), args, - ctrl_c.clone(), + engine_state.signals(), cwd.clone(), )?) } @@ -142,9 +141,10 @@ impl Command for Ls { .flatten() .into_pipeline_data_with_metadata( call_span, - ctrl_c, + engine_state.signals().clone(), PipelineMetadata { data_source: DataSource::Ls, + content_type: None, }, )) } @@ -175,20 +175,32 @@ impl Command for Ls { }, Example { description: "List files and directories whose name do not contain 'bar'", - example: "ls -s | where name !~ bar", + example: "ls | where name !~ bar", result: None, }, Example { - description: "List all dirs in your home directory", + description: "List the full path of all dirs in your home directory", example: "ls -a ~ | where type == dir", result: None, }, Example { description: - "List all dirs in your home directory which have not been modified in 7 days", + "List only the names (not paths) of all dirs in your home directory which have not been modified in 7 days", example: "ls -as ~ | where type == dir and modified < ((date now) - 7day)", result: None, }, + Example { + description: + "Recursively list all files and subdirectories under the current directory using a glob pattern", + example: "ls -a **/*", + result: None, + }, + Example { + description: + "Recursively list *.rs and *.toml files using the glob command", + example: "ls ...(glob **/*.{rs,toml})", + result: None, + }, Example { description: "List given paths and show directories themselves", example: "['/path/to/directory' '/path/to/file'] | each {|| ls -D $in } | flatten", @@ -201,7 +213,7 @@ impl Command for Ls { fn ls_for_one_pattern( pattern_arg: Option>, args: Args, - ctrl_c: Option>, + signals: &Signals, cwd: PathBuf, ) -> Result + Send>, ShellError> { let Args { @@ -328,7 +340,7 @@ fn ls_for_one_pattern( let mut hidden_dirs = vec![]; - let one_ctrl_c = ctrl_c.clone(); + let signals = signals.clone(); Ok(Box::new(paths_peek.filter_map(move |x| match x { Ok(path) => { let metadata = match std::fs::symlink_metadata(&path) { @@ -398,7 +410,7 @@ fn ls_for_one_pattern( call_span, long, du, - one_ctrl_c.clone(), + &signals, use_mime_type, ); match entry { @@ -460,7 +472,6 @@ fn path_contains_hidden_folder(path: &Path, folders: &[PathBuf]) -> bool { #[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, display_name: &str, use_mime_type: bool) -> String { let ft = md.file_type(); @@ -509,7 +520,7 @@ pub(crate) fn dir_entry_dict( span: Span, long: bool, du: bool, - ctrl_c: Option>, + signals: &Signals, use_mime_type: bool, ) -> Result { #[cfg(windows)] @@ -604,7 +615,7 @@ pub(crate) fn dir_entry_dict( if md.is_dir() { if du { let params = DirBuilder::new(Span::new(0, 2), None, false, None, false); - let dir_size = DirInfo::new(filename, ¶ms, None, ctrl_c).get_size(); + let dir_size = DirInfo::new(filename, ¶ms, None, span, signals)?.get_size(); Value::filesize(dir_size as i64, span) } else { diff --git a/crates/nu-command/src/filesystem/open.rs b/crates/nu-command/src/filesystem/open.rs index 842eaa5f4c..0351d1d9b2 100644 --- a/crates/nu-command/src/filesystem/open.rs +++ b/crates/nu-command/src/filesystem/open.rs @@ -1,7 +1,7 @@ use super::util::get_rest_for_glob_pattern; #[allow(deprecated)] use nu_engine::{command_prelude::*, current_dir, get_eval_block}; -use nu_protocol::{ByteStream, DataSource, NuGlob, PipelineMetadata}; +use nu_protocol::{ast, ByteStream, DataSource, NuGlob, PipelineMetadata}; use std::path::Path; #[cfg(feature = "sqlite")] @@ -51,13 +51,12 @@ impl Command for Open { ) -> Result { let raw = call.has_flag(engine_state, stack, "raw")?; let call_span = call.head; - let ctrlc = engine_state.ctrlc.clone(); #[allow(deprecated)] let cwd = current_dir(engine_state, stack)?; let mut paths = get_rest_for_glob_pattern(engine_state, stack, call, 0)?; let eval_block = get_eval_block(engine_state); - if paths.is_empty() && call.rest_iter(0).next().is_none() { + if paths.is_empty() && !call.has_positional_args(stack, 0) { // try to use path from pipeline input if there were no positional or spread args let (filename, span) = match input { PipelineData::Value(val, ..) => { @@ -122,8 +121,12 @@ impl Command for Open { } else { #[cfg(feature = "sqlite")] if !raw { - let res = SQLiteDatabase::try_from_path(path, arg_span, ctrlc.clone()) - .map(|db| db.into_value(call.head).into_pipeline_data()); + let res = SQLiteDatabase::try_from_path( + path, + arg_span, + engine_state.signals().clone(), + ) + .map(|db| db.into_value(call.head).into_pipeline_data()); if res.is_ok() { return res; @@ -144,9 +147,10 @@ impl Command for Open { }; let stream = PipelineData::ByteStream( - ByteStream::file(file, call_span, ctrlc.clone()), + ByteStream::file(file, call_span, engine_state.signals().clone()), Some(PipelineMetadata { data_source: DataSource::FilePath(path.to_path_buf()), + content_type: None, }), ); @@ -176,7 +180,8 @@ impl Command for Open { let block = engine_state.get_block(block_id); eval_block(engine_state, stack, block, stream) } else { - decl.run(engine_state, stack, &Call::new(call_span), stream) + let call = ast::Call::new(call_span); + decl.run(engine_state, stack, &(&call).into(), stream) }; output.push(command_output.map_err(|inner| { ShellError::GenericError{ @@ -202,7 +207,7 @@ impl Command for Open { Ok(output .into_iter() .flatten() - .into_pipeline_data(call_span, ctrlc)) + .into_pipeline_data(call_span, engine_state.signals().clone())) } } diff --git a/crates/nu-command/src/filesystem/rm.rs b/crates/nu-command/src/filesystem/rm.rs index 9696ae0c2f..9eb4fabace 100644 --- a/crates/nu-command/src/filesystem/rm.rs +++ b/crates/nu-command/src/filesystem/rm.rs @@ -170,7 +170,7 @@ fn rm( } let span = call.head; - let rm_always_trash = engine_state.get_config().rm_always_trash; + let rm_always_trash = stack.get_config(engine_state).rm_always_trash; if !TRASH_SUPPORTED { if rm_always_trash { @@ -451,12 +451,7 @@ fn rm( }); for result in iter { - if nu_utils::ctrl_c::was_pressed(&engine_state.ctrlc) { - return Err(ShellError::InterruptedByUser { - span: Some(call.head), - }); - } - + engine_state.signals().check(call.head)?; match result { Ok(None) => {} Ok(Some(msg)) => eprintln!("{msg}"), diff --git a/crates/nu-command/src/filesystem/save.rs b/crates/nu-command/src/filesystem/save.rs index ab5257fb01..be5073ef20 100644 --- a/crates/nu-command/src/filesystem/save.rs +++ b/crates/nu-command/src/filesystem/save.rs @@ -4,16 +4,13 @@ use nu_engine::get_eval_block; use nu_engine::{command_prelude::*, current_dir}; use nu_path::expand_path_with; use nu_protocol::{ - ast::{Expr, Expression}, - byte_stream::copy_with_interrupt, - process::ChildPipe, - ByteStreamSource, DataSource, OutDest, PipelineMetadata, + ast, byte_stream::copy_with_signals, process::ChildPipe, ByteStreamSource, DataSource, OutDest, + PipelineMetadata, Signals, }; use std::{ fs::File, io::{self, BufRead, BufReader, Read, Write}, path::{Path, PathBuf}, - sync::{atomic::AtomicBool, Arc}, thread, }; @@ -70,24 +67,6 @@ impl Command for Save { let append = call.has_flag(engine_state, stack, "append")?; let force = call.has_flag(engine_state, stack, "force")?; let progress = call.has_flag(engine_state, stack, "progress")?; - let out_append = if let Some(Expression { - expr: Expr::Bool(out_append), - .. - }) = call.get_parser_info("out-append") - { - *out_append - } else { - false - }; - let err_append = if let Some(Expression { - expr: Expr::Bool(err_append), - .. - }) = call.get_parser_info("err-append") - { - *err_append - } else { - false - }; let span = call.head; #[allow(deprecated)] @@ -110,40 +89,33 @@ impl Command for Save { PipelineData::ByteStream(stream, metadata) => { check_saving_to_source_file(metadata.as_ref(), &path, stderr_path.as_ref())?; - let (file, stderr_file) = get_files( - &path, - stderr_path.as_ref(), - append, - out_append, - err_append, - force, - )?; + let (file, stderr_file) = get_files(&path, stderr_path.as_ref(), append, force)?; let size = stream.known_size(); - let ctrlc = engine_state.ctrlc.clone(); + let signals = engine_state.signals(); match stream.into_source() { ByteStreamSource::Read(read) => { - stream_to_file(read, size, ctrlc, file, span, progress)?; + stream_to_file(read, size, signals, file, span, progress)?; } ByteStreamSource::File(source) => { - stream_to_file(source, size, ctrlc, file, span, progress)?; + stream_to_file(source, size, signals, file, span, progress)?; } ByteStreamSource::Child(mut child) => { fn write_or_consume_stderr( stderr: ChildPipe, file: Option, span: Span, - ctrlc: Option>, + signals: &Signals, progress: bool, ) -> Result<(), ShellError> { if let Some(file) = file { match stderr { ChildPipe::Pipe(pipe) => { - stream_to_file(pipe, None, ctrlc, file, span, progress) + stream_to_file(pipe, None, signals, file, span, progress) } ChildPipe::Tee(tee) => { - stream_to_file(tee, None, ctrlc, file, span, progress) + stream_to_file(tee, None, signals, file, span, progress) } }? } else { @@ -163,14 +135,14 @@ impl Command for Save { // delegate a thread to redirect stderr to result. let handler = stderr .map(|stderr| { - let ctrlc = ctrlc.clone(); + let signals = signals.clone(); thread::Builder::new().name("stderr saver".into()).spawn( move || { write_or_consume_stderr( stderr, stderr_file, span, - ctrlc, + &signals, progress, ) }, @@ -181,10 +153,10 @@ impl Command for Save { let res = match stdout { ChildPipe::Pipe(pipe) => { - stream_to_file(pipe, None, ctrlc, file, span, progress) + stream_to_file(pipe, None, signals, file, span, progress) } ChildPipe::Tee(tee) => { - stream_to_file(tee, None, ctrlc, file, span, progress) + stream_to_file(tee, None, signals, file, span, progress) } }; if let Some(h) = handler { @@ -202,7 +174,7 @@ impl Command for Save { stderr, stderr_file, span, - ctrlc, + signals, progress, )?; } @@ -222,14 +194,7 @@ impl Command for Save { stderr_path.as_ref(), )?; - let (mut file, _) = get_files( - &path, - stderr_path.as_ref(), - append, - out_append, - err_append, - force, - )?; + let (mut file, _) = get_files(&path, stderr_path.as_ref(), append, force)?; for val in ls { file.write_all(&value_to_bytes(val)?) .map_err(|err| ShellError::IOError { @@ -259,14 +224,7 @@ impl Command for Save { input_to_bytes(input, Path::new(&path.item), raw, engine_state, stack, span)?; // Only open file after successful conversion - let (mut file, _) = get_files( - &path, - stderr_path.as_ref(), - append, - out_append, - err_append, - force, - )?; + let (mut file, _) = get_files(&path, stderr_path.as_ref(), append, force)?; file.write_all(&bytes).map_err(|err| ShellError::IOError { msg: err.to_string(), @@ -398,7 +356,8 @@ fn convert_to_extension( let eval_block = get_eval_block(engine_state); eval_block(engine_state, stack, block, input) } else { - decl.run(engine_state, stack, &Call::new(span), input) + let call = ast::Call::new(span); + decl.run(engine_state, stack, &(&call).into(), input) } } else { Ok(input) @@ -461,7 +420,7 @@ fn open_file(path: &Path, span: Span, append: bool) -> Result }; file.map_err(|e| ShellError::GenericError { - error: "Permission denied".into(), + error: format!("Problem with [{}], Permission denied", path.display()), msg: e.to_string(), span: Some(span), help: None, @@ -474,19 +433,17 @@ fn get_files( path: &Spanned, stderr_path: Option<&Spanned>, append: bool, - out_append: bool, - err_append: bool, force: bool, ) -> Result<(File, Option), ShellError> { // First check both paths - let (path, path_span) = prepare_path(path, append || out_append, force)?; + let (path, path_span) = prepare_path(path, append, force)?; let stderr_path_and_span = stderr_path .as_ref() - .map(|stderr_path| prepare_path(stderr_path, append || err_append, force)) + .map(|stderr_path| prepare_path(stderr_path, append, force)) .transpose()?; // Only if both files can be used open and possibly truncate them - let file = open_file(path, path_span, append || out_append)?; + let file = open_file(path, path_span, append)?; let stderr_file = stderr_path_and_span .map(|(stderr_path, stderr_path_span)| { @@ -499,7 +456,7 @@ fn get_files( inner: vec![], }) } else { - open_file(stderr_path, stderr_path_span, append || err_append) + open_file(stderr_path, stderr_path_span, append) } }) .transpose()?; @@ -510,7 +467,7 @@ fn get_files( fn stream_to_file( source: impl Read, known_size: Option, - ctrlc: Option>, + signals: &Signals, mut file: File, span: Span, progress: bool, @@ -526,9 +483,9 @@ fn stream_to_file( let mut reader = BufReader::new(source); let res = loop { - if nu_utils::ctrl_c::was_pressed(&ctrlc) { + if let Err(err) = signals.check(span) { bar.abandoned_msg("# Cancelled #".to_owned()); - return Ok(()); + return Err(err); } match reader.fill_buf() { @@ -555,7 +512,7 @@ fn stream_to_file( Ok(()) } } else { - copy_with_interrupt(source, file, span, ctrlc.as_deref())?; + copy_with_signals(source, file, span, signals)?; Ok(()) } } diff --git a/crates/nu-command/src/filesystem/util.rs b/crates/nu-command/src/filesystem/util.rs index 1b755875bd..de32d204a0 100644 --- a/crates/nu-command/src/filesystem/util.rs +++ b/crates/nu-command/src/filesystem/util.rs @@ -1,6 +1,6 @@ use dialoguer::Input; use nu_engine::{command_prelude::*, get_eval_expression}; -use nu_protocol::{ast::Expr, FromValue, NuGlob}; +use nu_protocol::{FromValue, NuGlob}; use std::{ error::Error, path::{Path, PathBuf}, @@ -92,42 +92,19 @@ pub fn is_older(src: &Path, dst: &Path) -> Option { /// Get rest arguments from given `call`, starts with `starting_pos`. /// -/// It's similar to `call.rest`, except that it always returns NuGlob. And if input argument has -/// Type::Glob, the NuGlob is unquoted, which means it's required to expand. +/// It's similar to `call.rest`, except that it always returns NuGlob. pub fn get_rest_for_glob_pattern( engine_state: &EngineState, stack: &mut Stack, call: &Call, starting_pos: usize, ) -> Result>, ShellError> { - let mut output = vec![]; let eval_expression = get_eval_expression(engine_state); - for result in call.rest_iter_flattened(starting_pos, |expr| { - let result = eval_expression(engine_state, stack, expr); - match result { - Err(e) => Err(e), - Ok(result) => { - let span = result.span(); - // convert from string to quoted string if expr is a variable - // or string interpolation - match result { - Value::String { val, .. } - if matches!( - &expr.expr, - Expr::FullCellPath(_) | Expr::StringInterpolation(_) - ) => - { - // should not expand if given input type is not glob. - Ok(Value::glob(val, expr.ty != Type::Glob, span)) - } - other => Ok(other), - } - } - } - })? { - output.push(FromValue::from_value(result)?); - } - - Ok(output) + call.rest_iter_flattened(engine_state, stack, eval_expression, starting_pos)? + .into_iter() + // This used to be much more complex, but I think `FromValue` should be able to handle the + // nuance here. + .map(FromValue::from_value) + .collect() } diff --git a/crates/nu-command/src/filesystem/watch.rs b/crates/nu-command/src/filesystem/watch.rs index fda542c8a8..c9817de250 100644 --- a/crates/nu-command/src/filesystem/watch.rs +++ b/crates/nu-command/src/filesystem/watch.rs @@ -143,7 +143,6 @@ impl Command for Watch { None => RecursiveMode::Recursive, }; - let ctrlc_ref = &engine_state.ctrlc.clone(); let (tx, rx) = channel(); let mut debouncer = match new_debouncer(debounce_duration, None, tx) { @@ -256,7 +255,7 @@ impl Command for Watch { } Err(RecvTimeoutError::Timeout) => {} } - if nu_utils::ctrl_c::was_pressed(ctrlc_ref) { + if engine_state.signals().interrupted() { break; } } diff --git a/crates/nu-command/src/filters/append.rs b/crates/nu-command/src/filters/append.rs index af5bd49283..2064bdf1e8 100644 --- a/crates/nu-command/src/filters/append.rs +++ b/crates/nu-command/src/filters/append.rs @@ -116,7 +116,7 @@ only unwrap the outer list, and leave the variable's contents untouched."# Ok(input .into_iter() .chain(other.into_pipeline_data()) - .into_pipeline_data_with_metadata(call.head, engine_state.ctrlc.clone(), metadata)) + .into_pipeline_data_with_metadata(call.head, engine_state.signals().clone(), metadata)) } } diff --git a/crates/nu-command/src/filters/chunks.rs b/crates/nu-command/src/filters/chunks.rs new file mode 100644 index 0000000000..11a59c2aa9 --- /dev/null +++ b/crates/nu-command/src/filters/chunks.rs @@ -0,0 +1,153 @@ +use nu_engine::command_prelude::*; +use nu_protocol::ListStream; + +#[derive(Clone)] +pub struct Chunks; + +impl Command for Chunks { + fn name(&self) -> &str { + "chunks" + } + + fn signature(&self) -> Signature { + Signature::build("chunks") + .input_output_types(vec![ + (Type::table(), Type::list(Type::table())), + (Type::list(Type::Any), Type::list(Type::list(Type::Any))), + ]) + .required("chunk_size", SyntaxShape::Int, "The size of each chunk.") + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Divide a list or table into chunks of `chunk_size`." + } + + fn extra_usage(&self) -> &str { + "This command will error if `chunk_size` is negative or zero." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["batch", "group"] + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "[1 2 3 4] | chunks 2", + description: "Chunk a list into pairs", + result: Some(Value::test_list(vec![ + Value::test_list(vec![Value::test_int(1), Value::test_int(2)]), + Value::test_list(vec![Value::test_int(3), Value::test_int(4)]), + ])), + }, + Example { + example: "[[foo bar]; [0 1] [2 3] [4 5] [6 7] [8 9]] | chunks 3", + description: "Chunk the rows of a table into triplets", + result: Some(Value::test_list(vec![ + Value::test_list(vec![ + Value::test_record(record! { + "foo" => Value::test_int(0), + "bar" => Value::test_int(1), + }), + Value::test_record(record! { + "foo" => Value::test_int(2), + "bar" => Value::test_int(3), + }), + Value::test_record(record! { + "foo" => Value::test_int(4), + "bar" => Value::test_int(5), + }), + ]), + Value::test_list(vec![ + Value::test_record(record! { + "foo" => Value::test_int(6), + "bar" => Value::test_int(7), + }), + Value::test_record(record! { + "foo" => Value::test_int(8), + "bar" => Value::test_int(9), + }), + ]), + ])), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + let chunk_size: Value = call.req(engine_state, stack, 0)?; + + let size = + usize::try_from(chunk_size.as_int()?).map_err(|_| ShellError::NeedsPositiveValue { + span: chunk_size.span(), + })?; + + if size == 0 { + return Err(ShellError::IncorrectValue { + msg: "`chunk_size` cannot be zero".into(), + val_span: chunk_size.span(), + call_span: head, + }); + } + + match input { + PipelineData::Value(Value::List { vals, .. }, metadata) => { + let chunks = ChunksIter::new(vals, size, head); + let stream = ListStream::new(chunks, head, engine_state.signals().clone()); + Ok(PipelineData::ListStream(stream, metadata)) + } + PipelineData::ListStream(stream, metadata) => { + let stream = stream.modify(|iter| ChunksIter::new(iter, size, head)); + Ok(PipelineData::ListStream(stream, metadata)) + } + input => Err(input.unsupported_input_error("list", head)), + } + } +} + +struct ChunksIter> { + iter: I, + size: usize, + span: Span, +} + +impl> ChunksIter { + fn new(iter: impl IntoIterator, size: usize, span: Span) -> Self { + Self { + iter: iter.into_iter(), + size, + span, + } + } +} + +impl> Iterator for ChunksIter { + type Item = Value; + + fn next(&mut self) -> Option { + let first = self.iter.next()?; + let mut chunk = Vec::with_capacity(self.size); // delay allocation to optimize for empty iter + chunk.push(first); + chunk.extend((&mut self.iter).take(self.size - 1)); + Some(Value::list(chunk, self.span)) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Chunks {}) + } +} diff --git a/crates/nu-command/src/filters/compact.rs b/crates/nu-command/src/filters/compact.rs index 9a4e968e9b..f43d738ed1 100644 --- a/crates/nu-command/src/filters/compact.rs +++ b/crates/nu-command/src/filters/compact.rs @@ -140,7 +140,7 @@ pub fn compact( _ => true, } }, - engine_state.ctrlc.clone(), + engine_state.signals(), ) .map(|m| m.set_metadata(metadata)) } diff --git a/crates/nu-command/src/filters/default.rs b/crates/nu-command/src/filters/default.rs index 3eaa8d342e..9b08ce6331 100644 --- a/crates/nu-command/src/filters/default.rs +++ b/crates/nu-command/src/filters/default.rs @@ -51,11 +51,11 @@ impl Command for Default { description: "Get the env value of `MY_ENV` with a default value 'abc' if not present", example: "$env | get --ignore-errors MY_ENV | default 'abc'", - result: None, // Some(Value::test_string("abc")), + result: Some(Value::test_string("abc")), }, Example { description: "Replace the `null` value in a list", - example: "[1, 2, null, 4] | default 3", + example: "[1, 2, null, 4] | each { default 3 }", result: Some(Value::list( vec![ Value::test_int(1), @@ -80,8 +80,6 @@ fn default( let value: Value = call.req(engine_state, stack, 0)?; let column: Option> = call.opt(engine_state, stack, 1)?; - let ctrlc = engine_state.ctrlc.clone(); - if let Some(column) = column { input .map( @@ -109,21 +107,13 @@ fn default( } _ => item, }, - ctrlc, + engine_state.signals(), ) .map(|x| x.set_metadata(metadata)) } else if input.is_nothing() { Ok(value.into_pipeline_data()) } else { - input - .map( - move |item| match item { - Value::Nothing { .. } => value.clone(), - x => x, - }, - ctrlc, - ) - .map(|x| x.set_metadata(metadata)) + Ok(input) } } diff --git a/crates/nu-command/src/filters/drop/column.rs b/crates/nu-command/src/filters/drop/column.rs index 94c0308ea8..f168e6c2ca 100644 --- a/crates/nu-command/src/filters/drop/column.rs +++ b/crates/nu-command/src/filters/drop/column.rs @@ -102,7 +102,11 @@ fn drop_cols( Err(e) => Value::error(e, head), } })) - .into_pipeline_data_with_metadata(head, engine_state.ctrlc.clone(), metadata)) + .into_pipeline_data_with_metadata( + head, + engine_state.signals().clone(), + metadata, + )) } else { Ok(PipelineData::Empty) } diff --git a/crates/nu-command/src/filters/drop/nth.rs b/crates/nu-command/src/filters/drop/nth.rs index a530de5aa1..a76c4f0d92 100644 --- a/crates/nu-command/src/filters/drop/nth.rs +++ b/crates/nu-command/src/filters/drop/nth.rs @@ -156,7 +156,7 @@ impl Command for DropNth { .take(start) .into_pipeline_data_with_metadata( head, - engine_state.ctrlc.clone(), + engine_state.signals().clone(), metadata, )) } @@ -177,7 +177,7 @@ impl Command for DropNth { rows, current: 0, } - .into_pipeline_data_with_metadata(head, engine_state.ctrlc.clone(), metadata)) + .into_pipeline_data_with_metadata(head, engine_state.signals().clone(), metadata)) } } diff --git a/crates/nu-command/src/filters/each.rs b/crates/nu-command/src/filters/each.rs index a074f63abb..fa1bf390b8 100644 --- a/crates/nu-command/src/filters/each.rs +++ b/crates/nu-command/src/filters/each.rs @@ -140,7 +140,7 @@ with 'transpose' first."# } } }) - .into_pipeline_data(head, engine_state.ctrlc.clone())) + .into_pipeline_data(head, engine_state.signals().clone())) } PipelineData::ByteStream(stream, ..) => { if let Some(chunks) = stream.chunks() { @@ -171,7 +171,7 @@ with 'transpose' first."# } } }) - .into_pipeline_data(head, engine_state.ctrlc.clone())) + .into_pipeline_data(head, engine_state.signals().clone())) } else { Ok(PipelineData::Empty) } @@ -185,7 +185,7 @@ with 'transpose' first."# .and_then(|x| { x.filter( move |x| if !keep_empty { !x.is_nothing() } else { true }, - engine_state.ctrlc.clone(), + engine_state.signals(), ) }) .map(|data| data.set_metadata(metadata)) diff --git a/crates/nu-command/src/filters/enumerate.rs b/crates/nu-command/src/filters/enumerate.rs index 1034780657..df0f4e2fca 100644 --- a/crates/nu-command/src/filters/enumerate.rs +++ b/crates/nu-command/src/filters/enumerate.rs @@ -52,7 +52,6 @@ impl Command for Enumerate { ) -> Result { let head = call.head; let metadata = input.metadata(); - let ctrlc = engine_state.ctrlc.clone(); Ok(input .into_iter() @@ -66,7 +65,7 @@ impl Command for Enumerate { head, ) }) - .into_pipeline_data_with_metadata(head, ctrlc, metadata)) + .into_pipeline_data_with_metadata(head, engine_state.signals().clone(), metadata)) } } diff --git a/crates/nu-command/src/filters/every.rs b/crates/nu-command/src/filters/every.rs index 1202b4f7c9..664be33bed 100644 --- a/crates/nu-command/src/filters/every.rs +++ b/crates/nu-command/src/filters/every.rs @@ -78,7 +78,7 @@ impl Command for Every { None } }) - .into_pipeline_data_with_metadata(call.head, engine_state.ctrlc.clone(), metadata)) + .into_pipeline_data_with_metadata(call.head, engine_state.signals().clone(), metadata)) } } diff --git a/crates/nu-command/src/filters/filter.rs b/crates/nu-command/src/filters/filter.rs index 1ba1508839..f2efaa3af3 100644 --- a/crates/nu-command/src/filters/filter.rs +++ b/crates/nu-command/src/filters/filter.rs @@ -72,7 +72,7 @@ a variable. On the other hand, the "row condition" syntax is not supported."# } } }) - .into_pipeline_data(head, engine_state.ctrlc.clone())) + .into_pipeline_data(head, engine_state.signals().clone())) } PipelineData::ByteStream(stream, ..) => { if let Some(chunks) = stream.chunks() { @@ -97,7 +97,7 @@ a variable. On the other hand, the "row condition" syntax is not supported."# } } }) - .into_pipeline_data(head, engine_state.ctrlc.clone())) + .into_pipeline_data(head, engine_state.signals().clone())) } else { Ok(PipelineData::Empty) } @@ -117,7 +117,7 @@ a variable. On the other hand, the "row condition" syntax is not supported."# Some(Value::error(err, span)) } } - .into_pipeline_data(head, engine_state.ctrlc.clone())) + .into_pipeline_data(head, engine_state.signals().clone())) } } .map(|data| data.set_metadata(metadata)) diff --git a/crates/nu-command/src/filters/find.rs b/crates/nu-command/src/filters/find.rs index dfdef66969..9ed0c28a3c 100644 --- a/crates/nu-command/src/filters/find.rs +++ b/crates/nu-command/src/filters/find.rs @@ -69,9 +69,9 @@ impl Command for Find { result: None, }, Example { - description: "Search and highlight text for a term in a string", - example: r#"'Cargo.toml' | find toml"#, - result: Some(Value::test_string("\u{1b}[37mCargo.\u{1b}[0m\u{1b}[41;37mtoml\u{1b}[0m\u{1b}[37m\u{1b}[0m".to_owned())), + description: "Search and highlight text for a term in a string. Note that regular search is case insensitive", + example: r#"'Cargo.toml' | find cargo"#, + result: Some(Value::test_string("\u{1b}[37m\u{1b}[0m\u{1b}[41;37mCargo\u{1b}[0m\u{1b}[37m.toml\u{1b}[0m".to_owned())), }, Example { description: "Search a number or a file size in a list of numbers", @@ -213,8 +213,7 @@ fn find_with_regex( input: PipelineData, ) -> Result { let span = call.head; - let ctrlc = engine_state.ctrlc.clone(); - let config = engine_state.get_config().clone(); + let config = stack.get_config(engine_state); let insensitive = call.has_flag(engine_state, stack, "ignore-case")?; let multiline = call.has_flag(engine_state, stack, "multiline")?; @@ -246,7 +245,7 @@ fn find_with_regex( Value::List { vals, .. } => values_match_find(vals, &re, &config, invert), _ => false, }, - ctrlc, + engine_state.signals(), ) } @@ -349,18 +348,16 @@ fn find_with_rest_and_highlight( input: PipelineData, ) -> Result { let span = call.head; - let ctrlc = engine_state.ctrlc.clone(); - let engine_state = engine_state.clone(); - let config = engine_state.get_config().clone(); - let filter_config = engine_state.get_config().clone(); - let invert = call.has_flag(&engine_state, stack, "invert")?; - let terms = call.rest::(&engine_state, stack, 0)?; + let config = stack.get_config(engine_state); + let filter_config = config.clone(); + let invert = call.has_flag(engine_state, stack, "invert")?; + let terms = call.rest::(engine_state, stack, 0)?; let lower_terms = terms .iter() .map(|v| Value::string(v.to_expanded_string("", &config).to_lowercase(), span)) .collect::>(); - let style_computer = StyleComputer::from_config(&engine_state, stack); + let style_computer = StyleComputer::from_config(engine_state, stack); // Currently, search results all use the same style. // Also note that this sample string is passed into user-written code (the closure that may or may not be // defined for "string"). @@ -369,7 +366,7 @@ fn find_with_rest_and_highlight( style_computer.compute("search_result", &Value::string("search result", span)); let cols_to_search_in_map: Vec<_> = call - .get_flag(&engine_state, stack, "columns")? + .get_flag(engine_state, stack, "columns")? .unwrap_or_default(); let cols_to_search_in_filter = cols_to_search_in_map.clone(); @@ -401,7 +398,7 @@ fn find_with_rest_and_highlight( _ => x, } }, - ctrlc.clone(), + engine_state.signals(), )? .filter( move |value| { @@ -414,7 +411,7 @@ fn find_with_rest_and_highlight( invert, ) }, - ctrlc, + engine_state.signals(), ), PipelineData::ListStream(stream, metadata) => { let stream = stream.modify(|iter| { @@ -457,9 +454,10 @@ fn find_with_rest_and_highlight( let mut output: Vec = vec![]; for line in lines { - let line = line?.to_lowercase(); + let line = line?; + let lower_val = line.to_lowercase(); for term in &terms { - if line.contains(term) { + if lower_val.contains(term) { output.push(Value::string( highlight_search_string( &line, diff --git a/crates/nu-command/src/filters/first.rs b/crates/nu-command/src/filters/first.rs index 97c5159b97..8f2e8db1b9 100644 --- a/crates/nu-command/src/filters/first.rs +++ b/crates/nu-command/src/filters/first.rs @@ -1,4 +1,5 @@ use nu_engine::command_prelude::*; +use nu_protocol::Signals; use std::io::Read; #[derive(Clone)] @@ -133,8 +134,7 @@ fn first_helper( } } Value::Range { val, .. } => { - let ctrlc = engine_state.ctrlc.clone(); - let mut iter = val.into_range_iter(span, ctrlc.clone()); + let mut iter = val.into_range_iter(span, Signals::empty()); if return_single_element { if let Some(v) = iter.next() { Ok(v.into_pipeline_data()) @@ -142,9 +142,11 @@ fn first_helper( Err(ShellError::AccessEmptyContent { span: head }) } } else { - Ok(iter - .take(rows) - .into_pipeline_data_with_metadata(span, ctrlc, metadata)) + Ok(iter.take(rows).into_pipeline_data_with_metadata( + span, + engine_state.signals().clone(), + metadata, + )) } } // Propagate errors by explicitly matching them before the final case. @@ -189,7 +191,7 @@ fn first_helper( ByteStream::read( reader.take(rows as u64), head, - None, + Signals::empty(), ByteStreamType::Binary, ), metadata, diff --git a/crates/nu-command/src/filters/flatten.rs b/crates/nu-command/src/filters/flatten.rs index ec86677af4..b4faec9589 100644 --- a/crates/nu-command/src/filters/flatten.rs +++ b/crates/nu-command/src/filters/flatten.rs @@ -127,7 +127,7 @@ fn flatten( input .flat_map( move |item| flat_value(&columns, item, flatten_all), - engine_state.ctrlc.clone(), + engine_state.signals(), ) .map(|x| x.set_metadata(metadata)) } diff --git a/crates/nu-command/src/filters/get.rs b/crates/nu-command/src/filters/get.rs index 07f0ea9440..9f7d76277d 100644 --- a/crates/nu-command/src/filters/get.rs +++ b/crates/nu-command/src/filters/get.rs @@ -62,7 +62,6 @@ If multiple cell paths are given, this will produce a list of values."# let mut rest: Vec = call.rest(engine_state, stack, 1)?; let ignore_errors = call.has_flag(engine_state, stack, "ignore-errors")?; let sensitive = call.has_flag(engine_state, stack, "sensitive")?; - let ctrlc = engine_state.ctrlc.clone(); let metadata = input.metadata(); if ignore_errors { @@ -89,7 +88,9 @@ If multiple cell paths are given, this will produce a list of values."# output.push(val?); } - Ok(output.into_iter().into_pipeline_data(span, ctrlc)) + Ok(output + .into_iter() + .into_pipeline_data(span, engine_state.signals().clone())) } .map(|x| x.set_metadata(metadata)) } diff --git a/crates/nu-command/src/filters/group.rs b/crates/nu-command/src/filters/group.rs index 821f35f34e..60c720e325 100644 --- a/crates/nu-command/src/filters/group.rs +++ b/crates/nu-command/src/filters/group.rs @@ -1,5 +1,5 @@ use nu_engine::command_prelude::*; -use nu_protocol::ValueIterator; +use nu_protocol::{report_warning_new, ParseWarning, ValueIterator}; #[derive(Clone)] pub struct Group; @@ -54,11 +54,19 @@ impl Command for Group { input: PipelineData, ) -> Result { let head = call.head; - let group_size: Spanned = call.req(engine_state, stack, 0)?; - let ctrlc = engine_state.ctrlc.clone(); - let metadata = input.metadata(); - //FIXME: add in support for external redirection when engine-q supports it generally + report_warning_new( + engine_state, + &ParseWarning::DeprecatedWarning { + old_command: "group".into(), + new_suggestion: "the new `chunks` command".into(), + span: head, + url: "`help chunks`".into(), + }, + ); + + let group_size: Spanned = call.req(engine_state, stack, 0)?; + let metadata = input.metadata(); let each_group_iterator = EachGroupIterator { group_size: group_size.item, @@ -66,7 +74,11 @@ impl Command for Group { span: head, }; - Ok(each_group_iterator.into_pipeline_data_with_metadata(head, ctrlc, metadata)) + Ok(each_group_iterator.into_pipeline_data_with_metadata( + head, + engine_state.signals().clone(), + metadata, + )) } } diff --git a/crates/nu-command/src/filters/insert.rs b/crates/nu-command/src/filters/insert.rs index 5f1380b2ac..3b47c4e6a9 100644 --- a/crates/nu-command/src/filters/insert.rs +++ b/crates/nu-command/src/filters/insert.rs @@ -222,7 +222,11 @@ fn insert( Ok(pre_elems .into_iter() .chain(stream) - .into_pipeline_data_with_metadata(head, engine_state.ctrlc.clone(), metadata)) + .into_pipeline_data_with_metadata( + head, + engine_state.signals().clone(), + metadata, + )) } else if let Value::Closure { val, .. } = replacement { let mut closure = ClosureEval::new(engine_state, stack, *val); let stream = stream.map(move |mut value| { diff --git a/crates/nu-command/src/filters/interleave.rs b/crates/nu-command/src/filters/interleave.rs index 85a92741a1..9890fede1e 100644 --- a/crates/nu-command/src/filters/interleave.rs +++ b/crates/nu-command/src/filters/interleave.rs @@ -147,7 +147,7 @@ interleave // Now that threads are writing to the channel, we just return it as a stream Ok(rx .into_iter() - .into_pipeline_data(head, engine_state.ctrlc.clone())) + .into_pipeline_data(head, engine_state.signals().clone())) } } diff --git a/crates/nu-command/src/filters/items.rs b/crates/nu-command/src/filters/items.rs index ed30486bee..04a4c6d672 100644 --- a/crates/nu-command/src/filters/items.rs +++ b/crates/nu-command/src/filters/items.rs @@ -67,7 +67,7 @@ impl Command for Items { } } }) - .into_pipeline_data(head, engine_state.ctrlc.clone())) + .into_pipeline_data(head, engine_state.signals().clone())) } Value::Error { error, .. } => Err(*error), other => Err(ShellError::OnlySupportsThisInputType { diff --git a/crates/nu-command/src/filters/last.rs b/crates/nu-command/src/filters/last.rs index efb300db01..bb5a75fecd 100644 --- a/crates/nu-command/src/filters/last.rs +++ b/crates/nu-command/src/filters/last.rs @@ -99,14 +99,10 @@ impl Command for Last { let mut buf = VecDeque::new(); for row in iterator { - if nu_utils::ctrl_c::was_pressed(&engine_state.ctrlc) { - return Err(ShellError::InterruptedByUser { span: Some(head) }); - } - + engine_state.signals().check(head)?; if buf.len() == rows { buf.pop_front(); } - buf.push_back(row); } diff --git a/crates/nu-command/src/filters/lines.rs b/crates/nu-command/src/filters/lines.rs index 0b037dcaac..fc957e1c0e 100644 --- a/crates/nu-command/src/filters/lines.rs +++ b/crates/nu-command/src/filters/lines.rs @@ -26,7 +26,6 @@ impl Command for Lines { input: PipelineData, ) -> Result { let head = call.head; - let ctrlc = engine_state.ctrlc.clone(); let skip_empty = call.has_flag(engine_state, stack, "skip-empty")?; let span = input.span().unwrap_or(call.head); @@ -91,7 +90,7 @@ impl Command for Lines { Ok(line) => Value::string(line, head), Err(err) => Value::error(err, head), }) - .into_pipeline_data(head, ctrlc)) + .into_pipeline_data(head, engine_state.signals().clone())) } else { Ok(PipelineData::empty()) } diff --git a/crates/nu-command/src/filters/merge.rs b/crates/nu-command/src/filters/merge.rs index 8a3e613c56..191ed1706c 100644 --- a/crates/nu-command/src/filters/merge.rs +++ b/crates/nu-command/src/filters/merge.rs @@ -88,7 +88,6 @@ repeating this process with row 1, and so on."# let head = call.head; let merge_value: Value = call.req(engine_state, stack, 0)?; let metadata = input.metadata(); - let ctrlc = engine_state.ctrlc.clone(); match (&input, merge_value) { // table (list of records) @@ -110,7 +109,11 @@ repeating this process with row 1, and so on."# (Err(error), _) => Value::error(error, head), }); - Ok(res.into_pipeline_data_with_metadata(head, ctrlc, metadata)) + Ok(res.into_pipeline_data_with_metadata( + head, + engine_state.signals().clone(), + metadata, + )) } // record ( diff --git a/crates/nu-command/src/filters/mod.rs b/crates/nu-command/src/filters/mod.rs index f43bdb10b2..4ffb0fc6db 100644 --- a/crates/nu-command/src/filters/mod.rs +++ b/crates/nu-command/src/filters/mod.rs @@ -1,6 +1,7 @@ mod all; mod any; mod append; +mod chunks; mod columns; mod compact; mod default; @@ -58,6 +59,7 @@ mod zip; pub use all::All; pub use any::Any; pub use append::Append; +pub use chunks::Chunks; pub use columns::Columns; pub use compact::Compact; pub use default::Default; diff --git a/crates/nu-command/src/filters/move_.rs b/crates/nu-command/src/filters/move_.rs index d93368f291..6e826af050 100644 --- a/crates/nu-command/src/filters/move_.rs +++ b/crates/nu-command/src/filters/move_.rs @@ -144,7 +144,6 @@ impl Command for Move { }; let metadata = input.metadata(); - let ctrlc = engine_state.ctrlc.clone(); match input { PipelineData::Value(Value::List { .. }, ..) | PipelineData::ListStream { .. } => { @@ -158,7 +157,11 @@ impl Command for Move { Err(error) => Value::error(error, head), }); - Ok(res.into_pipeline_data_with_metadata(head, ctrlc, metadata)) + Ok(res.into_pipeline_data_with_metadata( + head, + engine_state.signals().clone(), + metadata, + )) } PipelineData::Value(Value::Record { val, .. }, ..) => { Ok(move_record_columns(&val, &columns, &before_or_after, head)? diff --git a/crates/nu-command/src/filters/par_each.rs b/crates/nu-command/src/filters/par_each.rs index af72895df6..958a7fbc76 100644 --- a/crates/nu-command/src/filters/par_each.rs +++ b/crates/nu-command/src/filters/par_each.rs @@ -1,6 +1,6 @@ use super::utils::chain_error_with_input; use nu_engine::{command_prelude::*, ClosureEvalOnce}; -use nu_protocol::engine::Closure; +use nu_protocol::{engine::Closure, Signals}; use rayon::prelude::*; #[derive(Clone)] @@ -158,12 +158,11 @@ impl Command for ParEach { }) .collect::>(); - apply_order(vec).into_pipeline_data(span, engine_state.ctrlc.clone()) + apply_order(vec).into_pipeline_data(span, engine_state.signals().clone()) })), Value::Range { val, .. } => Ok(create_pool(max_threads)?.install(|| { - let ctrlc = engine_state.ctrlc.clone(); let vec = val - .into_range_iter(span, ctrlc.clone()) + .into_range_iter(span, Signals::empty()) .enumerate() .par_bridge() .map(move |(index, value)| { @@ -184,7 +183,7 @@ impl Command for ParEach { }) .collect::>(); - apply_order(vec).into_pipeline_data(span, ctrlc) + apply_order(vec).into_pipeline_data(span, engine_state.signals().clone()) })), // This match allows non-iterables to be accepted, // which is currently considered undesirable (Nov 2022). @@ -212,7 +211,7 @@ impl Command for ParEach { }) .collect::>(); - apply_order(vec).into_pipeline_data(head, engine_state.ctrlc.clone()) + apply_order(vec).into_pipeline_data(head, engine_state.signals().clone()) })), PipelineData::ByteStream(stream, ..) => { if let Some(chunks) = stream.chunks() { @@ -236,14 +235,14 @@ impl Command for ParEach { }) .collect::>(); - apply_order(vec).into_pipeline_data(head, engine_state.ctrlc.clone()) + apply_order(vec).into_pipeline_data(head, engine_state.signals().clone()) })) } else { Ok(PipelineData::empty()) } } } - .and_then(|x| x.filter(|v| !v.is_nothing(), engine_state.ctrlc.clone())) + .and_then(|x| x.filter(|v| !v.is_nothing(), engine_state.signals())) .map(|data| data.set_metadata(metadata)) } } diff --git a/crates/nu-command/src/filters/prepend.rs b/crates/nu-command/src/filters/prepend.rs index f017420595..eeae0ef656 100644 --- a/crates/nu-command/src/filters/prepend.rs +++ b/crates/nu-command/src/filters/prepend.rs @@ -117,7 +117,7 @@ only unwrap the outer list, and leave the variable's contents untouched."# .into_pipeline_data() .into_iter() .chain(input) - .into_pipeline_data_with_metadata(call.head, engine_state.ctrlc.clone(), metadata)) + .into_pipeline_data_with_metadata(call.head, engine_state.signals().clone(), metadata)) } } diff --git a/crates/nu-command/src/filters/range.rs b/crates/nu-command/src/filters/range.rs index c89a0b11f2..0d4e703bb1 100644 --- a/crates/nu-command/src/filters/range.rs +++ b/crates/nu-command/src/filters/range.rs @@ -106,7 +106,7 @@ impl Command for Range { Ok(PipelineData::Value(Value::nothing(head), None)) } else { let iter = v.into_iter().skip(from).take(to - from + 1); - Ok(iter.into_pipeline_data(head, engine_state.ctrlc.clone())) + Ok(iter.into_pipeline_data(head, engine_state.signals().clone())) } } else { let from = start as usize; @@ -116,7 +116,7 @@ impl Command for Range { Ok(PipelineData::Value(Value::nothing(head), None)) } else { let iter = input.into_iter().skip(from).take(to - from + 1); - Ok(iter.into_pipeline_data(head, engine_state.ctrlc.clone())) + Ok(iter.into_pipeline_data(head, engine_state.signals().clone())) } } .map(|x| x.set_metadata(metadata)) diff --git a/crates/nu-command/src/filters/reduce.rs b/crates/nu-command/src/filters/reduce.rs index fc808ca9af..87fbe3b23e 100644 --- a/crates/nu-command/src/filters/reduce.rs +++ b/crates/nu-command/src/filters/reduce.rs @@ -107,10 +107,7 @@ impl Command for Reduce { let mut closure = ClosureEval::new(engine_state, stack, closure); for value in iter { - if nu_utils::ctrl_c::was_pressed(&engine_state.ctrlc) { - break; - } - + engine_state.signals().check(head)?; acc = closure .add_arg(value) .add_arg(acc) diff --git a/crates/nu-command/src/filters/rename.rs b/crates/nu-command/src/filters/rename.rs index b803bd8567..1e7ce34028 100644 --- a/crates/nu-command/src/filters/rename.rs +++ b/crates/nu-command/src/filters/rename.rs @@ -221,7 +221,7 @@ fn rename( ), } }, - engine_state.ctrlc.clone(), + engine_state.signals(), ) .map(|data| data.set_metadata(metadata)) } diff --git a/crates/nu-command/src/filters/reverse.rs b/crates/nu-command/src/filters/reverse.rs index f40e5f3be4..18891637ab 100644 --- a/crates/nu-command/src/filters/reverse.rs +++ b/crates/nu-command/src/filters/reverse.rs @@ -63,7 +63,7 @@ impl Command for Reverse { let metadata = input.metadata(); let values = input.into_iter_strict(head)?.collect::>(); let iter = values.into_iter().rev(); - Ok(iter.into_pipeline_data_with_metadata(head, engine_state.ctrlc.clone(), metadata)) + Ok(iter.into_pipeline_data_with_metadata(head, engine_state.signals().clone(), metadata)) } } diff --git a/crates/nu-command/src/filters/select.rs b/crates/nu-command/src/filters/select.rs index 3514ac6be7..2fe4da3c36 100644 --- a/crates/nu-command/src/filters/select.rs +++ b/crates/nu-command/src/filters/select.rs @@ -215,7 +215,11 @@ fn select( rows: unique_rows.into_iter().peekable(), current: 0, } - .into_pipeline_data_with_metadata(call_span, engine_state.ctrlc.clone(), metadata) + .into_pipeline_data_with_metadata( + call_span, + engine_state.signals().clone(), + metadata, + ) } else { input }; @@ -236,7 +240,7 @@ fn select( //FIXME: improve implementation to not clone match input_val.clone().follow_cell_path(&path.members, false) { Ok(fetcher) => { - record.push(path.to_string().replace('.', "_"), fetcher); + record.push(path.to_string(), fetcher); if !columns_with_value.contains(&path) { columns_with_value.push(path); } @@ -255,7 +259,7 @@ fn select( Ok(output.into_iter().into_pipeline_data_with_metadata( call_span, - engine_state.ctrlc.clone(), + engine_state.signals().clone(), metadata, )) } @@ -267,7 +271,7 @@ fn select( // FIXME: remove clone match v.clone().follow_cell_path(&cell_path.members, false) { Ok(result) => { - record.push(cell_path.to_string().replace('.', "_"), result); + record.push(cell_path.to_string(), result); } Err(e) => return Err(e), } @@ -291,7 +295,7 @@ fn select( //FIXME: improve implementation to not clone match x.clone().follow_cell_path(&path.members, false) { Ok(value) => { - record.push(path.to_string().replace('.', "_"), value); + record.push(path.to_string(), value); } Err(e) => return Err(e), } @@ -304,7 +308,7 @@ fn select( Ok(values.into_pipeline_data_with_metadata( call_span, - engine_state.ctrlc.clone(), + engine_state.signals().clone(), metadata, )) } diff --git a/crates/nu-command/src/filters/shuffle.rs b/crates/nu-command/src/filters/shuffle.rs index 9e023b86c6..ec4bf8c454 100644 --- a/crates/nu-command/src/filters/shuffle.rs +++ b/crates/nu-command/src/filters/shuffle.rs @@ -33,7 +33,11 @@ impl Command for Shuffle { let mut values = input.into_iter_strict(call.head)?.collect::>(); values.shuffle(&mut thread_rng()); let iter = values.into_iter(); - Ok(iter.into_pipeline_data_with_metadata(call.head, engine_state.ctrlc.clone(), metadata)) + Ok(iter.into_pipeline_data_with_metadata( + call.head, + engine_state.signals().clone(), + metadata, + )) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/filters/skip/skip_.rs b/crates/nu-command/src/filters/skip/skip_.rs index d293a743da..b64f438858 100644 --- a/crates/nu-command/src/filters/skip/skip_.rs +++ b/crates/nu-command/src/filters/skip/skip_.rs @@ -1,4 +1,5 @@ use nu_engine::command_prelude::*; +use nu_protocol::Signals; use std::io::{self, Read}; #[derive(Clone)] @@ -90,8 +91,6 @@ impl Command for Skip { } None => 1, }; - - let ctrlc = engine_state.ctrlc.clone(); let input_span = input.span().unwrap_or(call.head); match input { PipelineData::ByteStream(stream, metadata) => { @@ -102,7 +101,12 @@ impl Command for Skip { io::copy(&mut (&mut reader).take(n as u64), &mut io::sink()) .err_span(span)?; Ok(PipelineData::ByteStream( - ByteStream::read(reader, call.head, None, ByteStreamType::Binary), + ByteStream::read( + reader, + call.head, + Signals::empty(), + ByteStreamType::Binary, + ), metadata, )) } else { @@ -124,7 +128,11 @@ impl Command for Skip { _ => Ok(input .into_iter_strict(call.head)? .skip(n) - .into_pipeline_data_with_metadata(input_span, ctrlc, metadata)), + .into_pipeline_data_with_metadata( + input_span, + engine_state.signals().clone(), + metadata, + )), } } } diff --git a/crates/nu-command/src/filters/skip/skip_until.rs b/crates/nu-command/src/filters/skip/skip_until.rs index bb36785e00..72cae739af 100644 --- a/crates/nu-command/src/filters/skip/skip_until.rs +++ b/crates/nu-command/src/filters/skip/skip_until.rs @@ -89,7 +89,7 @@ impl Command for SkipUntil { .map(|cond| cond.is_false()) .unwrap_or(false) }) - .into_pipeline_data_with_metadata(head, engine_state.ctrlc.clone(), metadata)) + .into_pipeline_data_with_metadata(head, engine_state.signals().clone(), metadata)) } } diff --git a/crates/nu-command/src/filters/skip/skip_while.rs b/crates/nu-command/src/filters/skip/skip_while.rs index 2747ea6f97..ea9c12bf6a 100644 --- a/crates/nu-command/src/filters/skip/skip_while.rs +++ b/crates/nu-command/src/filters/skip/skip_while.rs @@ -94,7 +94,7 @@ impl Command for SkipWhile { .map(|cond| cond.is_true()) .unwrap_or(false) }) - .into_pipeline_data_with_metadata(head, engine_state.ctrlc.clone(), metadata)) + .into_pipeline_data_with_metadata(head, engine_state.signals().clone(), metadata)) } } diff --git a/crates/nu-command/src/filters/sort.rs b/crates/nu-command/src/filters/sort.rs index 965b997355..179235302d 100644 --- a/crates/nu-command/src/filters/sort.rs +++ b/crates/nu-command/src/filters/sort.rs @@ -173,7 +173,7 @@ impl Command for Sort { let iter = vec.into_iter(); Ok(iter.into_pipeline_data_with_metadata( head, - engine_state.ctrlc.clone(), + engine_state.signals().clone(), metadata, )) } diff --git a/crates/nu-command/src/filters/sort_by.rs b/crates/nu-command/src/filters/sort_by.rs index f3f715bc9d..e832255a26 100644 --- a/crates/nu-command/src/filters/sort_by.rs +++ b/crates/nu-command/src/filters/sort_by.rs @@ -100,7 +100,7 @@ impl Command for SortBy { } let iter = vec.into_iter(); - Ok(iter.into_pipeline_data_with_metadata(head, engine_state.ctrlc.clone(), metadata)) + Ok(iter.into_pipeline_data_with_metadata(head, engine_state.signals().clone(), metadata)) } } diff --git a/crates/nu-command/src/filters/split_by.rs b/crates/nu-command/src/filters/split_by.rs index 0d3bf1cd30..419119fe0c 100644 --- a/crates/nu-command/src/filters/split_by.rs +++ b/crates/nu-command/src/filters/split_by.rs @@ -130,7 +130,7 @@ pub fn split( Some(group_key) => Ok(group_key.coerce_string()?), None => Err(ShellError::CantFindColumn { col_name: column_name.item.to_string(), - span: column_name.span, + span: Some(column_name.span), src_span: row.span(), }), } diff --git a/crates/nu-command/src/filters/take/take_.rs b/crates/nu-command/src/filters/take/take_.rs index 7ebe22f914..1876244bdd 100644 --- a/crates/nu-command/src/filters/take/take_.rs +++ b/crates/nu-command/src/filters/take/take_.rs @@ -1,4 +1,5 @@ use nu_engine::command_prelude::*; +use nu_protocol::Signals; use std::io::Read; #[derive(Clone)] @@ -46,7 +47,6 @@ impl Command for Take { let head = call.head; let rows_desired: usize = call.req(engine_state, stack, 0)?; - let ctrlc = engine_state.ctrlc.clone(); let metadata = input.metadata(); match input { @@ -56,15 +56,23 @@ impl Command for Take { Value::List { vals, .. } => Ok(vals .into_iter() .take(rows_desired) - .into_pipeline_data_with_metadata(head, ctrlc, metadata)), + .into_pipeline_data_with_metadata( + head, + engine_state.signals().clone(), + metadata, + )), Value::Binary { val, .. } => { let slice: Vec = val.into_iter().take(rows_desired).collect(); Ok(PipelineData::Value(Value::binary(slice, span), metadata)) } Value::Range { val, .. } => Ok(val - .into_range_iter(span, ctrlc.clone()) + .into_range_iter(span, Signals::empty()) .take(rows_desired) - .into_pipeline_data_with_metadata(head, ctrlc, metadata)), + .into_pipeline_data_with_metadata( + head, + engine_state.signals().clone(), + metadata, + )), // Propagate errors by explicitly matching them before the final case. Value::Error { error, .. } => Err(*error), other => Err(ShellError::OnlySupportsThisInputType { @@ -87,7 +95,7 @@ impl Command for Take { ByteStream::read( reader.take(rows_desired as u64), head, - None, + Signals::empty(), ByteStreamType::Binary, ), metadata, diff --git a/crates/nu-command/src/filters/take/take_until.rs b/crates/nu-command/src/filters/take/take_until.rs index 0df2407cb1..c8544d364a 100644 --- a/crates/nu-command/src/filters/take/take_until.rs +++ b/crates/nu-command/src/filters/take/take_until.rs @@ -11,13 +11,10 @@ impl Command for TakeUntil { fn signature(&self) -> Signature { Signature::build(self.name()) - .input_output_types(vec![ - (Type::table(), Type::table()), - ( - Type::List(Box::new(Type::Any)), - Type::List(Box::new(Type::Any)), - ), - ]) + .input_output_types(vec![( + Type::List(Box::new(Type::Any)), + Type::List(Box::new(Type::Any)), + )]) .required( "predicate", SyntaxShape::Closure(Some(vec![SyntaxShape::Any, SyntaxShape::Int])), @@ -85,7 +82,7 @@ impl Command for TakeUntil { .map(|cond| cond.is_false()) .unwrap_or(false) }) - .into_pipeline_data_with_metadata(head, engine_state.ctrlc.clone(), metadata)) + .into_pipeline_data_with_metadata(head, engine_state.signals().clone(), metadata)) } } diff --git a/crates/nu-command/src/filters/take/take_while.rs b/crates/nu-command/src/filters/take/take_while.rs index 7c282ac38a..b8045080ea 100644 --- a/crates/nu-command/src/filters/take/take_while.rs +++ b/crates/nu-command/src/filters/take/take_while.rs @@ -85,7 +85,7 @@ impl Command for TakeWhile { .map(|cond| cond.is_true()) .unwrap_or(false) }) - .into_pipeline_data_with_metadata(head, engine_state.ctrlc.clone(), metadata)) + .into_pipeline_data_with_metadata(head, engine_state.signals().clone(), metadata)) } } diff --git a/crates/nu-command/src/filters/tee.rs b/crates/nu-command/src/filters/tee.rs index 2251f6646a..663b2f19af 100644 --- a/crates/nu-command/src/filters/tee.rs +++ b/crates/nu-command/src/filters/tee.rs @@ -1,15 +1,11 @@ use nu_engine::{command_prelude::*, get_eval_block_with_early_return}; use nu_protocol::{ - byte_stream::copy_with_interrupt, engine::Closure, process::ChildPipe, ByteStream, - ByteStreamSource, OutDest, PipelineMetadata, + byte_stream::copy_with_signals, engine::Closure, process::ChildPipe, ByteStream, + ByteStreamSource, OutDest, PipelineMetadata, Signals, }; use std::{ io::{self, Read, Write}, - sync::{ - atomic::AtomicBool, - mpsc::{self, Sender}, - Arc, - }, + sync::mpsc::{self, Sender}, thread::{self, JoinHandle}, }; @@ -103,12 +99,11 @@ use it in your pipeline."# if let PipelineData::ByteStream(stream, metadata) = input { let span = stream.span(); - let ctrlc = engine_state.ctrlc.clone(); let type_ = stream.type_(); let info = StreamInfo { span, - ctrlc: ctrlc.clone(), + signals: engine_state.signals().clone(), type_, metadata: metadata.clone(), }; @@ -123,7 +118,7 @@ use it in your pipeline."# let tee = IoTee::new(read, tee_thread); Ok(PipelineData::ByteStream( - ByteStream::read(tee, span, ctrlc, type_), + ByteStream::read(tee, span, engine_state.signals().clone(), type_), metadata, )) } @@ -136,7 +131,7 @@ use it in your pipeline."# let tee = IoTee::new(file, tee_thread); Ok(PipelineData::ByteStream( - ByteStream::read(tee, span, ctrlc, type_), + ByteStream::read(tee, span, engine_state.signals().clone(), type_), metadata, )) } @@ -234,19 +229,19 @@ use it in your pipeline."# } let span = input.span().unwrap_or(head); - let ctrlc = engine_state.ctrlc.clone(); let metadata = input.metadata(); let metadata_clone = metadata.clone(); + let signals = engine_state.signals().clone(); Ok(tee(input.into_iter(), move |rx| { - let input = rx.into_pipeline_data_with_metadata(span, ctrlc, metadata_clone); + let input = rx.into_pipeline_data_with_metadata(span, signals, metadata_clone); eval_block(input) }) .err_span(call.head)? .map(move |result| result.unwrap_or_else(|err| Value::error(err, closure_span))) .into_pipeline_data_with_metadata( span, - engine_state.ctrlc.clone(), + engine_state.signals().clone(), metadata, )) } @@ -386,8 +381,13 @@ fn spawn_tee( let thread = thread::Builder::new() .name("tee".into()) .spawn(move || { - // We don't use ctrlc here because we assume it already has it on the other side - let stream = ByteStream::from_iter(receiver.into_iter(), info.span, None, info.type_); + // We use Signals::empty() here because we assume there already is a Signals on the other side + let stream = ByteStream::from_iter( + receiver.into_iter(), + info.span, + Signals::empty(), + info.type_, + ); eval_block(PipelineData::ByteStream(stream, info.metadata)) }) .err_span(info.span)?; @@ -398,13 +398,13 @@ fn spawn_tee( #[derive(Clone)] struct StreamInfo { span: Span, - ctrlc: Option>, + signals: Signals, type_: ByteStreamType, metadata: Option, } fn copy(src: impl Read, dest: impl Write, info: &StreamInfo) -> Result<(), ShellError> { - copy_with_interrupt(src, dest, info.span, info.ctrlc.as_deref())?; + copy_with_signals(src, dest, info.span, &info.signals)?; Ok(()) } @@ -421,11 +421,11 @@ fn copy_on_thread( info: &StreamInfo, ) -> Result>, ShellError> { let span = info.span; - let ctrlc = info.ctrlc.clone(); + let signals = info.signals.clone(); thread::Builder::new() .name("stderr copier".into()) .spawn(move || { - copy_with_interrupt(src, dest, span, ctrlc.as_deref())?; + copy_with_signals(src, dest, span, &signals)?; Ok(()) }) .map_err(|e| e.into_spanned(span).into()) diff --git a/crates/nu-command/src/filters/transpose.rs b/crates/nu-command/src/filters/transpose.rs index 49caa56d18..4a14c1aea2 100644 --- a/crates/nu-command/src/filters/transpose.rs +++ b/crates/nu-command/src/filters/transpose.rs @@ -149,31 +149,30 @@ pub fn transpose( if !args.rest.is_empty() && args.header_row { return Err(ShellError::IncompatibleParametersSingle { msg: "Can not provide header names and use `--header-row`".into(), - span: call.get_named_arg("header-row").expect("has flag").span, + span: call.get_flag_span(stack, "header-row").expect("has flag"), }); } if !args.header_row && args.keep_all { return Err(ShellError::IncompatibleParametersSingle { msg: "Can only be used with `--header-row`(`-r`)".into(), - span: call.get_named_arg("keep-all").expect("has flag").span, + span: call.get_flag_span(stack, "keep-all").expect("has flag"), }); } if !args.header_row && args.keep_last { return Err(ShellError::IncompatibleParametersSingle { msg: "Can only be used with `--header-row`(`-r`)".into(), - span: call.get_named_arg("keep-last").expect("has flag").span, + span: call.get_flag_span(stack, "keep-last").expect("has flag"), }); } if args.keep_all && args.keep_last { return Err(ShellError::IncompatibleParameters { left_message: "can't use `--keep-last` at the same time".into(), - left_span: call.get_named_arg("keep-last").expect("has flag").span, + left_span: call.get_flag_span(stack, "keep-last").expect("has flag"), right_message: "because of `--keep-all`".into(), - right_span: call.get_named_arg("keep-all").expect("has flag").span, + right_span: call.get_flag_span(stack, "keep-all").expect("has flag"), }); } - let ctrlc = engine_state.ctrlc.clone(); let metadata = input.metadata(); let input: Vec<_> = input.into_iter().collect(); @@ -284,7 +283,11 @@ pub fn transpose( metadata, )) } else { - Ok(result_data.into_pipeline_data_with_metadata(name, ctrlc, metadata)) + Ok(result_data.into_pipeline_data_with_metadata( + name, + engine_state.signals().clone(), + metadata, + )) } } diff --git a/crates/nu-command/src/filters/uniq.rs b/crates/nu-command/src/filters/uniq.rs index 7d71d868f0..99abb4e152 100644 --- a/crates/nu-command/src/filters/uniq.rs +++ b/crates/nu-command/src/filters/uniq.rs @@ -241,18 +241,18 @@ pub fn uniq( item_mapper: Box ValueCounter>, metadata: Option, ) -> Result { - let ctrlc = engine_state.ctrlc.clone(); let head = call.head; let flag_show_count = call.has_flag(engine_state, stack, "count")?; let flag_show_repeated = call.has_flag(engine_state, stack, "repeated")?; let flag_ignore_case = call.has_flag(engine_state, stack, "ignore-case")?; let flag_only_uniques = call.has_flag(engine_state, stack, "unique")?; + let signals = engine_state.signals().clone(); let uniq_values = input .into_iter() .enumerate() .map_while(|(index, item)| { - if nu_utils::ctrl_c::was_pressed(&ctrlc) { + if signals.interrupted() { return None; } Some(item_mapper(ItemMapperState { diff --git a/crates/nu-command/src/filters/uniq_by.rs b/crates/nu-command/src/filters/uniq_by.rs index 7bbeb0afe2..e1a502b06b 100644 --- a/crates/nu-command/src/filters/uniq_by.rs +++ b/crates/nu-command/src/filters/uniq_by.rs @@ -123,7 +123,7 @@ fn validate(vec: &[Value], columns: &[String], span: Span) -> Result<(), ShellEr if let Some(nonexistent) = nonexistent_column(columns, record.columns()) { return Err(ShellError::CantFindColumn { col_name: nonexistent, - span, + span: Some(span), src_span: val_span, }); } diff --git a/crates/nu-command/src/filters/update.rs b/crates/nu-command/src/filters/update.rs index e724ae77ad..d5ef0825ec 100644 --- a/crates/nu-command/src/filters/update.rs +++ b/crates/nu-command/src/filters/update.rs @@ -187,7 +187,11 @@ fn update( Ok(pre_elems .into_iter() .chain(stream) - .into_pipeline_data_with_metadata(head, engine_state.ctrlc.clone(), metadata)) + .into_pipeline_data_with_metadata( + head, + engine_state.signals().clone(), + metadata, + )) } else if let Value::Closure { val, .. } = replacement { let mut closure = ClosureEval::new(engine_state, stack, *val); let stream = stream.map(move |mut value| { diff --git a/crates/nu-command/src/filters/upsert.rs b/crates/nu-command/src/filters/upsert.rs index e3678972fb..af2e6c74b1 100644 --- a/crates/nu-command/src/filters/upsert.rs +++ b/crates/nu-command/src/filters/upsert.rs @@ -247,7 +247,11 @@ fn upsert( Ok(pre_elems .into_iter() .chain(stream) - .into_pipeline_data_with_metadata(head, engine_state.ctrlc.clone(), metadata)) + .into_pipeline_data_with_metadata( + head, + engine_state.signals().clone(), + metadata, + )) } else if let Value::Closure { val, .. } = replacement { let mut closure = ClosureEval::new(engine_state, stack, *val); let stream = stream.map(move |mut value| { diff --git a/crates/nu-command/src/filters/utils.rs b/crates/nu-command/src/filters/utils.rs index 8d9b1300f6..4c67667e8e 100644 --- a/crates/nu-command/src/filters/utils.rs +++ b/crates/nu-command/src/filters/utils.rs @@ -1,7 +1,6 @@ use nu_engine::{CallExt, ClosureEval}; use nu_protocol::{ - ast::Call, - engine::{Closure, EngineState, Stack}, + engine::{Call, Closure, EngineState, Stack}, IntoPipelineData, PipelineData, ShellError, Span, Value, }; @@ -32,10 +31,7 @@ pub fn boolean_fold( let mut closure = ClosureEval::new(engine_state, stack, closure); for value in input { - if nu_utils::ctrl_c::was_pressed(&engine_state.ctrlc) { - break; - } - + engine_state.signals().check(head)?; let pred = closure.run_with_value(value)?.into_value(head)?.is_true(); if pred == accumulator { diff --git a/crates/nu-command/src/filters/values.rs b/crates/nu-command/src/filters/values.rs index f6ff8cda2e..9a08f1168c 100644 --- a/crates/nu-command/src/filters/values.rs +++ b/crates/nu-command/src/filters/values.rs @@ -134,7 +134,7 @@ fn values( head: Span, input: PipelineData, ) -> Result { - let ctrlc = engine_state.ctrlc.clone(); + let signals = engine_state.signals().clone(); let metadata = input.metadata(); match input { PipelineData::Empty => Ok(PipelineData::Empty), @@ -144,7 +144,7 @@ fn values( Value::List { vals, .. } => match get_values(&vals, head, span) { Ok(cols) => Ok(cols .into_iter() - .into_pipeline_data_with_metadata(head, ctrlc, metadata)), + .into_pipeline_data_with_metadata(head, signals, metadata)), Err(err) => Err(err), }, Value::Custom { val, .. } => { @@ -152,7 +152,7 @@ fn values( match get_values(&[input_as_base_value], head, span) { Ok(cols) => Ok(cols .into_iter() - .into_pipeline_data_with_metadata(head, ctrlc, metadata)), + .into_pipeline_data_with_metadata(head, signals, metadata)), Err(err) => Err(err), } } @@ -160,7 +160,7 @@ fn values( .values() .cloned() .collect::>() - .into_pipeline_data_with_metadata(head, ctrlc, metadata)), + .into_pipeline_data_with_metadata(head, signals, metadata)), // Propagate errors Value::Error { error, .. } => Err(*error), other => Err(ShellError::OnlySupportsThisInputType { @@ -176,7 +176,7 @@ fn values( match get_values(&vals, head, head) { Ok(cols) => Ok(cols .into_iter() - .into_pipeline_data_with_metadata(head, ctrlc, metadata)), + .into_pipeline_data_with_metadata(head, signals, metadata)), Err(err) => Err(err), } } diff --git a/crates/nu-command/src/filters/where_.rs b/crates/nu-command/src/filters/where_.rs index fe73de354f..0a8c8c9ef3 100644 --- a/crates/nu-command/src/filters/where_.rs +++ b/crates/nu-command/src/filters/where_.rs @@ -1,5 +1,5 @@ use nu_engine::{command_prelude::*, ClosureEval}; -use nu_protocol::engine::Closure; +use nu_protocol::engine::{Closure, CommandType}; #[derive(Clone)] pub struct Where; @@ -19,6 +19,10 @@ tables, known as "row conditions". On the other hand, reading the condition from not supported."# } + fn command_type(&self) -> CommandType { + CommandType::Keyword + } + fn signature(&self) -> nu_protocol::Signature { Signature::build("where") .input_output_types(vec![ @@ -66,7 +70,7 @@ not supported."# Err(err) => Some(Value::error(err, head)), } }) - .into_pipeline_data_with_metadata(head, engine_state.ctrlc.clone(), metadata)) + .into_pipeline_data_with_metadata(head, engine_state.signals().clone(), metadata)) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/filters/window.rs b/crates/nu-command/src/filters/window.rs index 5b386b9f84..3c470f478d 100644 --- a/crates/nu-command/src/filters/window.rs +++ b/crates/nu-command/src/filters/window.rs @@ -113,7 +113,6 @@ impl Command for Window { ) -> Result { let head = call.head; let group_size: Spanned = call.req(engine_state, stack, 0)?; - let ctrlc = engine_state.ctrlc.clone(); let metadata = input.metadata(); let stride: Option = call.get_flag(engine_state, stack, "stride")?; let remainder = call.has_flag(engine_state, stack, "remainder")?; @@ -131,7 +130,11 @@ impl Command for Window { remainder, }; - Ok(each_group_iterator.into_pipeline_data_with_metadata(head, ctrlc, metadata)) + Ok(each_group_iterator.into_pipeline_data_with_metadata( + head, + engine_state.signals().clone(), + metadata, + )) } } diff --git a/crates/nu-command/src/filters/wrap.rs b/crates/nu-command/src/filters/wrap.rs index 52a0fb22c3..dfe1c11d03 100644 --- a/crates/nu-command/src/filters/wrap.rs +++ b/crates/nu-command/src/filters/wrap.rs @@ -42,7 +42,7 @@ impl Command for Wrap { | PipelineData::ListStream { .. } => Ok(input .into_iter() .map(move |x| Value::record(record! { name.clone() => x }, span)) - .into_pipeline_data_with_metadata(span, engine_state.ctrlc.clone(), metadata)), + .into_pipeline_data_with_metadata(span, engine_state.signals().clone(), metadata)), PipelineData::ByteStream(stream, ..) => Ok(Value::record( record! { name => stream.into_value()? }, span, diff --git a/crates/nu-command/src/filters/zip.rs b/crates/nu-command/src/filters/zip.rs index 9d81451ed4..59e2b4ac98 100644 --- a/crates/nu-command/src/filters/zip.rs +++ b/crates/nu-command/src/filters/zip.rs @@ -112,7 +112,7 @@ impl Command for Zip { .into_iter() .zip(other) .map(move |(x, y)| Value::list(vec![x, y], head)) - .into_pipeline_data_with_metadata(head, engine_state.ctrlc.clone(), metadata)) + .into_pipeline_data_with_metadata(head, engine_state.signals().clone(), metadata)) } } diff --git a/crates/nu-command/src/formats/from/delimited.rs b/crates/nu-command/src/formats/from/delimited.rs index 853f3bd83e..0fea7e082b 100644 --- a/crates/nu-command/src/formats/from/delimited.rs +++ b/crates/nu-command/src/formats/from/delimited.rs @@ -1,5 +1,5 @@ use csv::{ReaderBuilder, Trim}; -use nu_protocol::{ByteStream, ListStream, PipelineData, ShellError, Span, Value}; +use nu_protocol::{ByteStream, ListStream, PipelineData, ShellError, Signals, Span, Value}; fn from_csv_error(err: csv::Error, span: Span) -> ShellError { ShellError::DelimiterError { @@ -25,7 +25,7 @@ fn from_delimited_stream( let input_reader = if let Some(stream) = input.reader() { stream } else { - return Ok(ListStream::new(std::iter::empty(), span, None)); + return Ok(ListStream::new(std::iter::empty(), span, Signals::empty())); }; let mut reader = ReaderBuilder::new() @@ -39,7 +39,7 @@ fn from_delimited_stream( .from_reader(input_reader); let headers = if noheaders { - (1..=reader + (0..reader .headers() .map_err(|err| from_csv_error(err, span))? .len()) @@ -83,7 +83,7 @@ fn from_delimited_stream( Value::record(columns.zip(values).collect(), span) }); - Ok(ListStream::new(iter, span, None)) + Ok(ListStream::new(iter, span, Signals::empty())) } pub(super) struct DelimitedReaderConfig { @@ -106,7 +106,7 @@ pub(super) fn from_delimited_data( PipelineData::Empty => Ok(PipelineData::Empty), PipelineData::Value(value, metadata) => { let string = value.into_string()?; - let byte_stream = ByteStream::read_string(string, name, None); + let byte_stream = ByteStream::read_string(string, name, Signals::empty()); Ok(PipelineData::ListStream( from_delimited_stream(config, byte_stream, name)?, metadata, diff --git a/crates/nu-command/src/formats/from/json.rs b/crates/nu-command/src/formats/from/json.rs index e8b5d58e9e..6252afff4c 100644 --- a/crates/nu-command/src/formats/from/json.rs +++ b/crates/nu-command/src/formats/from/json.rs @@ -1,10 +1,7 @@ -use std::{ - io::{BufRead, Cursor}, - sync::{atomic::AtomicBool, Arc}, -}; +use std::io::{BufRead, Cursor}; use nu_engine::command_prelude::*; -use nu_protocol::ListStream; +use nu_protocol::{ListStream, PipelineMetadata, Signals}; #[derive(Clone)] pub struct FromJson; @@ -80,8 +77,13 @@ impl Command for FromJson { match input { PipelineData::Value(Value::String { val, .. }, metadata) => { Ok(PipelineData::ListStream( - read_json_lines(Cursor::new(val), span, strict, engine_state.ctrlc.clone()), - metadata, + read_json_lines( + Cursor::new(val), + span, + strict, + engine_state.signals().clone(), + ), + update_metadata(metadata), )) } PipelineData::ByteStream(stream, metadata) @@ -89,8 +91,8 @@ impl Command for FromJson { { if let Some(reader) = stream.reader() { Ok(PipelineData::ListStream( - read_json_lines(reader, span, strict, None), - metadata, + read_json_lines(reader, span, strict, Signals::empty()), + update_metadata(metadata), )) } else { Ok(PipelineData::Empty) @@ -113,10 +115,10 @@ impl Command for FromJson { if strict { Ok(convert_string_to_value_strict(&string_input, span)? - .into_pipeline_data_with_metadata(metadata)) + .into_pipeline_data_with_metadata(update_metadata(metadata))) } else { Ok(convert_string_to_value(&string_input, span)? - .into_pipeline_data_with_metadata(metadata)) + .into_pipeline_data_with_metadata(update_metadata(metadata))) } } } @@ -127,7 +129,7 @@ fn read_json_lines( input: impl BufRead + Send + 'static, span: Span, strict: bool, - interrupt: Option>, + signals: Signals, ) -> ListStream { let iter = input .lines() @@ -142,7 +144,7 @@ fn read_json_lines( }) .map(move |result| result.unwrap_or_else(|err| Value::error(err, span))); - ListStream::new(iter, span, interrupt) + ListStream::new(iter, span, signals) } fn convert_nujson_to_value(value: nu_json::Value, span: Span) -> Value { @@ -263,6 +265,14 @@ fn convert_string_to_value_strict(string_input: &str, span: Span) -> Result) -> Option { + metadata + .map(|md| md.with_content_type(Some("application/json".into()))) + .or_else(|| { + Some(PipelineMetadata::default().with_content_type(Some("application/json".into()))) + }) +} + #[cfg(test)] mod test { use super::*; diff --git a/crates/nu-command/src/formats/from/msgpack.rs b/crates/nu-command/src/formats/from/msgpack.rs index 4d8ea5e320..d3d0d710f1 100644 --- a/crates/nu-command/src/formats/from/msgpack.rs +++ b/crates/nu-command/src/formats/from/msgpack.rs @@ -5,12 +5,12 @@ use std::{ error::Error, io::{self, Cursor, ErrorKind}, string::FromUtf8Error, - sync::{atomic::AtomicBool, Arc}, }; use byteorder::{BigEndian, ReadBytesExt}; use chrono::{TimeZone, Utc}; use nu_engine::command_prelude::*; +use nu_protocol::Signals; use rmp::decode::{self as mp, ValueReadError}; /// Max recursion depth @@ -111,7 +111,7 @@ MessagePack: https://msgpack.org/ let opts = Opts { span: call.head, objects, - ctrlc: engine_state.ctrlc.clone(), + signals: engine_state.signals().clone(), }; match input { // Deserialize from a byte buffer @@ -227,7 +227,7 @@ impl From for ShellError { pub(crate) struct Opts { pub span: Span, pub objects: bool, - pub ctrlc: Option>, + pub signals: Signals, } /// Read single or multiple values into PipelineData @@ -238,7 +238,7 @@ pub(crate) fn read_msgpack( let Opts { span, objects, - ctrlc, + signals, } = opts; if objects { // Make an iterator that reads multiple values from the reader @@ -262,7 +262,7 @@ pub(crate) fn read_msgpack( None } }) - .into_pipeline_data(span, ctrlc)) + .into_pipeline_data(span, signals)) } else { // Read a single value and then make sure it's EOF let result = read_value(&mut input, span, 0)?; diff --git a/crates/nu-command/src/formats/from/msgpackz.rs b/crates/nu-command/src/formats/from/msgpackz.rs index 7960f3f97a..c98b72cdd6 100644 --- a/crates/nu-command/src/formats/from/msgpackz.rs +++ b/crates/nu-command/src/formats/from/msgpackz.rs @@ -41,7 +41,7 @@ impl Command for FromMsgpackz { let opts = Opts { span, objects, - ctrlc: engine_state.ctrlc.clone(), + signals: engine_state.signals().clone(), }; match input { // Deserialize from a byte buffer diff --git a/crates/nu-command/src/formats/from/ssv.rs b/crates/nu-command/src/formats/from/ssv.rs index 5efb2a6c3b..1d17dfc69d 100644 --- a/crates/nu-command/src/formats/from/ssv.rs +++ b/crates/nu-command/src/formats/from/ssv.rs @@ -52,12 +52,12 @@ impl Command for FromSsv { Value::test_list( vec![ Value::test_record(record! { - "column1" => Value::test_string("FOO"), - "column2" => Value::test_string("BAR"), + "column0" => Value::test_string("FOO"), + "column1" => Value::test_string("BAR"), }), Value::test_record(record! { - "column1" => Value::test_string("1"), - "column2" => Value::test_string("2"), + "column0" => Value::test_string("1"), + "column1" => Value::test_string("2"), }), ], ) @@ -170,7 +170,7 @@ fn parse_aligned_columns<'a>( let headers: Vec<(String, usize)> = indices .iter() .enumerate() - .map(|(i, position)| (format!("column{}", i + 1), *position)) + .map(|(i, position)| (format!("column{}", i), *position)) .collect(); construct(ls.iter().map(|s| s.to_owned()), headers) @@ -215,7 +215,7 @@ fn parse_separated_columns<'a>( 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) + let headers = (0..=num_columns) .map(|i| format!("column{i}")) .collect::>(); collect(headers, ls.into_iter(), separator) @@ -370,9 +370,9 @@ mod tests { 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")] + vec![owned("column0", "a"), owned("column1", "b")], + vec![owned("column0", "1"), owned("column1", "2")], + vec![owned("column0", "3"), owned("column1", "4")] ] ); } @@ -484,25 +484,25 @@ mod tests { result, vec![ vec![ - owned("column1", "a multi-word value"), - owned("column2", "b"), - owned("column3", ""), - owned("column4", "d"), - owned("column5", "") - ], - vec![ - owned("column1", "1"), + owned("column0", "a multi-word value"), + owned("column1", "b"), owned("column2", ""), - owned("column3", "3-3"), - owned("column4", "4"), - owned("column5", "") + owned("column3", "d"), + owned("column4", "") ], vec![ + owned("column0", "1"), + owned("column1", ""), + owned("column2", "3-3"), + owned("column3", "4"), + owned("column4", "") + ], + vec![ + owned("column0", ""), owned("column1", ""), owned("column2", ""), owned("column3", ""), - owned("column4", ""), - owned("column5", "last") + owned("column4", "last") ], ] ); diff --git a/crates/nu-command/src/formats/from/toml.rs b/crates/nu-command/src/formats/from/toml.rs index e1ddca3164..266911f50a 100644 --- a/crates/nu-command/src/formats/from/toml.rs +++ b/crates/nu-command/src/formats/from/toml.rs @@ -1,5 +1,5 @@ use nu_engine::command_prelude::*; -use std::str::FromStr; +use toml::value::{Datetime, Offset}; #[derive(Clone)] pub struct FromToml; @@ -56,6 +56,54 @@ b = [1, 2]' | from toml", } } +fn convert_toml_datetime_to_value(dt: &Datetime, span: Span) -> Value { + match &dt.clone() { + toml::value::Datetime { + date: Some(_), + time: _, + offset: _, + } => (), + _ => return Value::string(dt.to_string(), span), + } + + let date = match dt.date { + Some(date) => { + chrono::NaiveDate::from_ymd_opt(date.year.into(), date.month.into(), date.day.into()) + } + None => Some(chrono::NaiveDate::default()), + }; + + let time = match dt.time { + Some(time) => chrono::NaiveTime::from_hms_nano_opt( + time.hour.into(), + time.minute.into(), + time.second.into(), + time.nanosecond, + ), + None => Some(chrono::NaiveTime::default()), + }; + + let tz = match dt.offset { + Some(offset) => match offset { + Offset::Z => chrono::FixedOffset::east_opt(0), + Offset::Custom { minutes: min } => chrono::FixedOffset::east_opt(min as i32 * 60), + }, + None => chrono::FixedOffset::east_opt(0), + }; + + let datetime = match (date, time, tz) { + (Some(date), Some(time), Some(tz)) => chrono::NaiveDateTime::new(date, time) + .and_local_timezone(tz) + .earliest(), + _ => None, + }; + + match datetime { + Some(datetime) => Value::date(datetime, span), + None => Value::string(dt.to_string(), span), + } +} + fn convert_toml_to_value(value: &toml::Value, span: Span) -> Value { match value { toml::Value::Array(array) => { @@ -76,13 +124,7 @@ fn convert_toml_to_value(value: &toml::Value, span: Span) -> Value { span, ), toml::Value::String(s) => Value::string(s.clone(), span), - toml::Value::Datetime(d) => match chrono::DateTime::from_str(&d.to_string()) { - Ok(nushell_date) => Value::date(nushell_date, span), - // in the unlikely event that parsing goes wrong, this function still returns a valid - // nushell date (however the default one). This decision was made to make the output of - // this function uniform amongst all eventualities - Err(_) => Value::date(chrono::DateTime::default(), span), - }, + toml::Value::Datetime(dt) => convert_toml_datetime_to_value(dt, span), } } @@ -113,32 +155,6 @@ mod tests { test_examples(FromToml {}) } - #[test] - fn from_toml_creates_nushell_date() { - let toml_date = toml::Value::Datetime(Datetime { - date: Option::from(toml::value::Date { - year: 1980, - month: 10, - day: 12, - }), - time: Option::from(toml::value::Time { - hour: 10, - minute: 12, - second: 44, - nanosecond: 0, - }), - offset: Option::from(toml::value::Offset::Custom { minutes: 120 }), - }); - - let span = Span::test_data(); - let reference_date = Value::date(Default::default(), Span::test_data()); - - let result = convert_toml_to_value(&toml_date, span); - - //positive test (from toml returns a nushell date) - assert_eq!(result.get_type(), reference_date.get_type()); - } - #[test] fn from_toml_creates_correct_date() { let toml_date = toml::Value::Datetime(Datetime { @@ -206,4 +222,113 @@ mod tests { assert!(result.is_err()); } + + #[test] + fn convert_toml_datetime_to_value_date_time_offset() { + let toml_date = Datetime { + date: Option::from(toml::value::Date { + year: 2000, + month: 1, + day: 1, + }), + time: Option::from(toml::value::Time { + hour: 12, + minute: 12, + second: 12, + nanosecond: 0, + }), + offset: Option::from(toml::value::Offset::Custom { minutes: 120 }), + }; + + let span = Span::test_data(); + let reference_date = Value::date( + chrono::FixedOffset::east_opt(60 * 120) + .unwrap() + .with_ymd_and_hms(2000, 1, 1, 12, 12, 12) + .unwrap(), + span, + ); + + let result = convert_toml_datetime_to_value(&toml_date, span); + + assert_eq!(result, reference_date); + } + + #[test] + fn convert_toml_datetime_to_value_date_time() { + let toml_date = Datetime { + date: Option::from(toml::value::Date { + year: 2000, + month: 1, + day: 1, + }), + time: Option::from(toml::value::Time { + hour: 12, + minute: 12, + second: 12, + nanosecond: 0, + }), + offset: None, + }; + + let span = Span::test_data(); + let reference_date = Value::date( + chrono::FixedOffset::east_opt(0) + .unwrap() + .with_ymd_and_hms(2000, 1, 1, 12, 12, 12) + .unwrap(), + span, + ); + + let result = convert_toml_datetime_to_value(&toml_date, span); + + assert_eq!(result, reference_date); + } + + #[test] + fn convert_toml_datetime_to_value_date() { + let toml_date = Datetime { + date: Option::from(toml::value::Date { + year: 2000, + month: 1, + day: 1, + }), + time: None, + offset: None, + }; + + let span = Span::test_data(); + let reference_date = Value::date( + chrono::FixedOffset::east_opt(0) + .unwrap() + .with_ymd_and_hms(2000, 1, 1, 0, 0, 0) + .unwrap(), + span, + ); + + let result = convert_toml_datetime_to_value(&toml_date, span); + + assert_eq!(result, reference_date); + } + + #[test] + fn convert_toml_datetime_to_value_only_time() { + let toml_date = Datetime { + date: None, + time: Option::from(toml::value::Time { + hour: 12, + minute: 12, + second: 12, + nanosecond: 0, + }), + offset: None, + }; + + let span = Span::test_data(); + let reference_date = Value::string(toml_date.to_string(), span); + + let result = convert_toml_datetime_to_value(&toml_date, span); + + assert_eq!(result, reference_date); + } } diff --git a/crates/nu-command/src/formats/to/delimited.rs b/crates/nu-command/src/formats/to/delimited.rs index a7a2480a34..34bb06b8a2 100644 --- a/crates/nu-command/src/formats/to/delimited.rs +++ b/crates/nu-command/src/formats/to/delimited.rs @@ -1,7 +1,7 @@ use csv::WriterBuilder; use nu_cmd_base::formats::to::delimited::merge_descriptors; use nu_protocol::{ - ByteStream, ByteStreamType, Config, PipelineData, ShellError, Span, Spanned, Value, + ByteStream, ByteStreamType, Config, PipelineData, ShellError, Signals, Span, Spanned, Value, }; use std::{iter, sync::Arc}; @@ -128,37 +128,42 @@ pub fn to_delimited_data( // If we're configured to generate a header, we generate it first, then set this false let mut is_header = !noheaders; - let stream = ByteStream::from_fn(head, None, ByteStreamType::String, move |buffer| { - let mut wtr = WriterBuilder::new() - .delimiter(separator) - .from_writer(buffer); + let stream = ByteStream::from_fn( + head, + Signals::empty(), + ByteStreamType::String, + move |buffer| { + let mut wtr = WriterBuilder::new() + .delimiter(separator) + .from_writer(buffer); - if is_header { - // Unless we are configured not to write a header, we write the header row now, once, - // before everything else. - wtr.write_record(&columns) - .map_err(|err| make_csv_error(err, format_name, head))?; - is_header = false; - Ok(true) - } else if let Some(row) = iter.next() { - // Write each column of a normal row, in order - let record = row.into_record()?; - for column in &columns { - let field = record - .get(column) - .map(|v| to_string_tagged_value(v, &config, format_name)) - .unwrap_or(Ok(String::new()))?; - wtr.write_field(field) + if is_header { + // Unless we are configured not to write a header, we write the header row now, once, + // before everything else. + wtr.write_record(&columns) .map_err(|err| make_csv_error(err, format_name, head))?; + is_header = false; + Ok(true) + } else if let Some(row) = iter.next() { + // Write each column of a normal row, in order + let record = row.into_record()?; + for column in &columns { + let field = record + .get(column) + .map(|v| to_string_tagged_value(v, &config, format_name)) + .unwrap_or(Ok(String::new()))?; + wtr.write_field(field) + .map_err(|err| make_csv_error(err, format_name, head))?; + } + // End the row + wtr.write_record(iter::empty::()) + .map_err(|err| make_csv_error(err, format_name, head))?; + Ok(true) + } else { + Ok(false) } - // End the row - wtr.write_record(iter::empty::()) - .map_err(|err| make_csv_error(err, format_name, head))?; - Ok(true) - } else { - Ok(false) - } - }); + }, + ); Ok(PipelineData::ByteStream(stream, metadata)) } diff --git a/crates/nu-command/src/formats/to/json.rs b/crates/nu-command/src/formats/to/json.rs index c4c87f804f..5722387e2c 100644 --- a/crates/nu-command/src/formats/to/json.rs +++ b/crates/nu-command/src/formats/to/json.rs @@ -1,5 +1,5 @@ use nu_engine::command_prelude::*; -use nu_protocol::ast::PathMember; +use nu_protocol::{ast::PathMember, PipelineMetadata}; #[derive(Clone)] pub struct ToJson; @@ -61,7 +61,12 @@ impl Command for ToJson { match json_result { Ok(serde_json_string) => { - Ok(Value::string(serde_json_string, span).into_pipeline_data()) + let res = Value::string(serde_json_string, span); + let metadata = PipelineMetadata { + data_source: nu_protocol::DataSource::None, + content_type: Some("application/json".to_string()), + }; + Ok(PipelineData::Value(res, Some(metadata))) } _ => Ok(Value::error( ShellError::CantConvert { diff --git a/crates/nu-command/src/formats/to/md.rs b/crates/nu-command/src/formats/to/md.rs index e2acdd703e..737a328cd3 100644 --- a/crates/nu-command/src/formats/to/md.rs +++ b/crates/nu-command/src/formats/to/md.rs @@ -70,8 +70,8 @@ impl Command for ToMd { let head = call.head; let pretty = call.has_flag(engine_state, stack, "pretty")?; let per_element = call.has_flag(engine_state, stack, "per-element")?; - let config = engine_state.get_config(); - to_md(input, pretty, per_element, config, head) + let config = stack.get_config(engine_state); + to_md(input, pretty, per_element, &config, head) } } diff --git a/crates/nu-command/src/formats/to/msgpack.rs b/crates/nu-command/src/formats/to/msgpack.rs index bfeb428e3e..02515e26fe 100644 --- a/crates/nu-command/src/formats/to/msgpack.rs +++ b/crates/nu-command/src/formats/to/msgpack.rs @@ -5,7 +5,7 @@ use std::io; use byteorder::{BigEndian, WriteBytesExt}; use nu_engine::command_prelude::*; -use nu_protocol::{ast::PathMember, Spanned}; +use nu_protocol::{ast::PathMember, Signals, Spanned}; use rmp::encode as mp; /// Max recursion depth @@ -189,7 +189,7 @@ pub(crate) fn write_value( // Convert range to list write_value( out, - &Value::list(val.into_range_iter(span, None).collect(), span), + &Value::list(val.into_range_iter(span, Signals::empty()).collect(), span), depth, )?; } diff --git a/crates/nu-command/src/formats/to/text.rs b/crates/nu-command/src/formats/to/text.rs index fb240654f6..33ca19f8f2 100644 --- a/crates/nu-command/src/formats/to/text.rs +++ b/crates/nu-command/src/formats/to/text.rs @@ -1,6 +1,8 @@ use chrono_humanize::HumanTime; use nu_engine::command_prelude::*; -use nu_protocol::{format_duration, format_filesize_from_conf, ByteStream, Config}; +use nu_protocol::{ + format_duration, format_filesize_from_conf, ByteStream, Config, PipelineMetadata, +}; const LINE_ENDING: &str = if cfg!(target_os = "windows") { "\r\n" @@ -29,22 +31,26 @@ impl Command for ToText { fn run( &self, engine_state: &EngineState, - _stack: &mut Stack, + stack: &mut Stack, call: &Call, input: PipelineData, ) -> Result { let span = call.head; let input = input.try_expand_range()?; + let config = stack.get_config(engine_state); match input { - PipelineData::Empty => Ok(Value::string(String::new(), span).into_pipeline_data()), + PipelineData::Empty => Ok(Value::string(String::new(), span) + .into_pipeline_data_with_metadata(update_metadata(None))), PipelineData::Value(value, ..) => { - let str = local_into_string(value, LINE_ENDING, engine_state.get_config()); - Ok(Value::string(str, span).into_pipeline_data()) + let str = local_into_string(value, LINE_ENDING, &config); + Ok( + Value::string(str, span) + .into_pipeline_data_with_metadata(update_metadata(None)), + ) } PipelineData::ListStream(stream, meta) => { let span = stream.span(); - let config = engine_state.get_config().clone(); let iter = stream.into_inner().map(move |value| { let mut str = local_into_string(value, LINE_ENDING, &config); str.push_str(LINE_ENDING); @@ -54,13 +60,15 @@ impl Command for ToText { ByteStream::from_iter( iter, span, - engine_state.ctrlc.clone(), + engine_state.signals().clone(), ByteStreamType::String, ), - meta, + update_metadata(meta), )) } - PipelineData::ByteStream(stream, meta) => Ok(PipelineData::ByteStream(stream, meta)), + PipelineData::ByteStream(stream, meta) => { + Ok(PipelineData::ByteStream(stream, update_metadata(meta))) + } } } @@ -124,6 +132,14 @@ fn local_into_string(value: Value, separator: &str, config: &Config) -> String { } } +fn update_metadata(metadata: Option) -> Option { + metadata + .map(|md| md.with_content_type(Some("text/plain".to_string()))) + .or_else(|| { + Some(PipelineMetadata::default().with_content_type(Some("text/plain".to_string()))) + }) +} + #[cfg(test)] mod test { use super::*; diff --git a/crates/nu-command/src/generators/cal.rs b/crates/nu-command/src/generators/cal.rs index e1ecc771de..018d9370dd 100644 --- a/crates/nu-command/src/generators/cal.rs +++ b/crates/nu-command/src/generators/cal.rs @@ -1,6 +1,7 @@ use chrono::{Datelike, Local, NaiveDate}; use nu_color_config::StyleComputer; use nu_engine::command_prelude::*; +use nu_protocol::ast::{self, Expr, Expression}; use std::collections::VecDeque; @@ -14,6 +15,7 @@ struct Arguments { month_names: bool, full_year: Option>, week_start: Option>, + as_table: bool, } impl Command for Cal { @@ -26,6 +28,7 @@ impl Command for 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')) + .switch("as-table", "output as a table", Some('t')) .named( "full-year", SyntaxShape::Int, @@ -43,7 +46,10 @@ impl Command for Cal { "Display the month names instead of integers", None, ) - .input_output_types(vec![(Type::Nothing, Type::table())]) + .input_output_types(vec![ + (Type::Nothing, Type::table()), + (Type::Nothing, Type::String), + ]) .allow_variants_without_examples(true) // TODO: supply exhaustive examples .category(Category::Generators) } @@ -75,10 +81,15 @@ impl Command for Cal { result: None, }, Example { - description: "This month's calendar with the week starting on monday", + description: "This month's calendar with the week starting on Monday", example: "cal --week-start mo", result: None, }, + Example { + description: "How many 'Friday the Thirteenths' occurred in 2015?", + example: "cal --as-table --full-year 2015 | where fr == 13 | length", + result: None, + }, ] } } @@ -101,6 +112,7 @@ pub fn cal( quarter: call.has_flag(engine_state, stack, "quarter")?, full_year: call.get_flag(engine_state, stack, "full-year")?, week_start: call.get_flag(engine_state, stack, "week-start")?, + as_table: call.has_flag(engine_state, stack, "as-table")?, }; let style_computer = &StyleComputer::from_config(engine_state, stack); @@ -131,7 +143,32 @@ pub fn cal( style_computer, )?; - Ok(Value::list(calendar_vec_deque.into_iter().collect(), tag).into_pipeline_data()) + let mut table_no_index = ast::Call::new(Span::unknown()); + table_no_index.add_named(( + Spanned { + item: "index".to_string(), + span: Span::unknown(), + }, + None, + Some(Expression::new_unknown( + Expr::Bool(false), + Span::unknown(), + Type::Bool, + )), + )); + + let cal_table_output = + Value::list(calendar_vec_deque.into_iter().collect(), tag).into_pipeline_data(); + if !arguments.as_table { + crate::Table.run( + engine_state, + stack, + &(&table_no_index).into(), + cal_table_output, + ) + } else { + Ok(cal_table_output) + } } fn get_invalid_year_shell_error(head: Span) -> ShellError { diff --git a/crates/nu-command/src/generators/generate.rs b/crates/nu-command/src/generators/generate.rs index 3549667ff0..8d4f48d3fb 100644 --- a/crates/nu-command/src/generators/generate.rs +++ b/crates/nu-command/src/generators/generate.rs @@ -1,4 +1,3 @@ -use itertools::unfold; use nu_engine::{command_prelude::*, ClosureEval}; use nu_protocol::engine::Closure; @@ -12,13 +11,7 @@ impl Command for Generate { fn signature(&self) -> Signature { Signature::build("generate") - .input_output_types(vec![ - (Type::Nothing, Type::List(Box::new(Type::Any))), - ( - Type::List(Box::new(Type::Any)), - Type::List(Box::new(Type::Any)), - ), - ]) + .input_output_types(vec![(Type::Nothing, Type::List(Box::new(Type::Any)))]) .required("initial", SyntaxShape::Any, "Initial value.") .required( "closure", @@ -63,23 +56,10 @@ used as the next argument to the closure, otherwise generation stops. )), }, Example { - example: "generate [0, 1] {|fib| {out: $fib.0, next: [$fib.1, ($fib.0 + $fib.1)]} } | first 10", - description: "Generate a stream of fibonacci numbers", - result: Some(Value::list( - vec![ - Value::test_int(0), - Value::test_int(1), - Value::test_int(1), - Value::test_int(2), - Value::test_int(3), - Value::test_int(5), - Value::test_int(8), - Value::test_int(13), - Value::test_int(21), - Value::test_int(34), - ], - Span::test_data(), - )), + example: + "generate [0, 1] {|fib| {out: $fib.0, next: [$fib.1, ($fib.0 + $fib.1)]} }", + description: "Generate a continuous stream of Fibonacci numbers", + result: None, }, ] } @@ -100,7 +80,8 @@ used as the next argument to the closure, otherwise generation stops. // A type of Option is used to represent state. Invocation // will stop on None. Using Option allows functions to output // one final value before stopping. - let iter = unfold(Some(initial), move |state| { + let mut state = Some(initial); + let iter = std::iter::from_fn(move || { let arg = state.take()?; let (output, next_input) = match closure.run_with_value(arg) { @@ -179,13 +160,13 @@ used as the next argument to the closure, otherwise generation stops. // We use `state` to control when to stop, not `output`. By wrapping // it in a `Some`, we allow the generator to output `None` as a valid output // value. - *state = next_input; + state = next_input; Some(output) }); Ok(iter .flatten() - .into_pipeline_data(call.head, engine_state.ctrlc.clone())) + .into_pipeline_data(call.head, engine_state.signals().clone())) } } diff --git a/crates/nu-command/src/generators/seq.rs b/crates/nu-command/src/generators/seq.rs index 359d479ca0..3636d89d4c 100644 --- a/crates/nu-command/src/generators/seq.rs +++ b/crates/nu-command/src/generators/seq.rs @@ -129,7 +129,7 @@ pub fn run_seq( span, }, span, - engine_state.ctrlc.clone(), + engine_state.signals().clone(), ) } else { ListStream::new( @@ -141,7 +141,7 @@ pub fn run_seq( span, }, span, - engine_state.ctrlc.clone(), + engine_state.signals().clone(), ) }; diff --git a/crates/nu-command/src/hash/generic_digest.rs b/crates/nu-command/src/hash/generic_digest.rs index ab15ccae7a..f098f8a0cb 100644 --- a/crates/nu-command/src/hash/generic_digest.rs +++ b/crates/nu-command/src/hash/generic_digest.rs @@ -96,7 +96,7 @@ where } } else { let args = Arguments { binary, cell_paths }; - operate(action::, args, input, head, engine_state.ctrlc.clone()) + operate(action::, args, input, head, engine_state.signals()) } } } diff --git a/crates/nu-command/src/help/help_aliases.rs b/crates/nu-command/src/help/help_aliases.rs index da03fa6398..41af9da0b3 100644 --- a/crates/nu-command/src/help/help_aliases.rs +++ b/crates/nu-command/src/help/help_aliases.rs @@ -143,7 +143,7 @@ pub fn help_aliases( long_desc.push_str("\n\n"); long_desc.push_str(&format!("{G}Expansion{RESET}:\n {alias_expansion}")); - let config = engine_state.get_config(); + let config = stack.get_config(engine_state); if !config.use_ansi_coloring { long_desc = nu_utils::strip_ansi_string_likely(long_desc); } diff --git a/crates/nu-command/src/help/help_modules.rs b/crates/nu-command/src/help/help_modules.rs index 5b39133a6d..c2c00cf400 100644 --- a/crates/nu-command/src/help/help_modules.rs +++ b/crates/nu-command/src/help/help_modules.rs @@ -230,7 +230,7 @@ pub fn help_modules( )); } - let config = engine_state.get_config(); + let config = stack.get_config(engine_state); if !config.use_ansi_coloring { long_desc = nu_utils::strip_ansi_string_likely(long_desc); } diff --git a/crates/nu-command/src/help/help_operators.rs b/crates/nu-command/src/help/help_operators.rs index a7beee3dd5..12803fad8e 100644 --- a/crates/nu-command/src/help/help_operators.rs +++ b/crates/nu-command/src/help/help_operators.rs @@ -1,4 +1,5 @@ use nu_engine::command_prelude::*; +use nu_protocol::ast::{Assignment, Bits, Boolean, Comparison, Math, Operator}; #[derive(Clone)] pub struct HelpOperators; @@ -27,282 +28,148 @@ impl Command for HelpOperators { _input: PipelineData, ) -> Result { let head = call.head; - let op_info = generate_operator_info(); - let mut recs = vec![]; - - for op in op_info { - recs.push(Value::record( + let mut operators = [ + Operator::Assignment(Assignment::Assign), + Operator::Assignment(Assignment::PlusAssign), + Operator::Assignment(Assignment::AppendAssign), + Operator::Assignment(Assignment::MinusAssign), + Operator::Assignment(Assignment::MultiplyAssign), + Operator::Assignment(Assignment::DivideAssign), + Operator::Comparison(Comparison::Equal), + Operator::Comparison(Comparison::NotEqual), + Operator::Comparison(Comparison::LessThan), + Operator::Comparison(Comparison::GreaterThan), + Operator::Comparison(Comparison::LessThanOrEqual), + Operator::Comparison(Comparison::GreaterThanOrEqual), + Operator::Comparison(Comparison::RegexMatch), + Operator::Comparison(Comparison::NotRegexMatch), + Operator::Comparison(Comparison::In), + Operator::Comparison(Comparison::NotIn), + Operator::Comparison(Comparison::StartsWith), + Operator::Comparison(Comparison::EndsWith), + Operator::Math(Math::Plus), + Operator::Math(Math::Append), + Operator::Math(Math::Minus), + Operator::Math(Math::Multiply), + Operator::Math(Math::Divide), + Operator::Math(Math::Modulo), + Operator::Math(Math::FloorDivision), + Operator::Math(Math::Pow), + Operator::Bits(Bits::BitOr), + Operator::Bits(Bits::BitXor), + Operator::Bits(Bits::BitAnd), + Operator::Bits(Bits::ShiftLeft), + Operator::Bits(Bits::ShiftRight), + Operator::Boolean(Boolean::And), + Operator::Boolean(Boolean::Or), + Operator::Boolean(Boolean::Xor), + ] + .into_iter() + .map(|op| { + Value::record( record! { - "type" => Value::string(op.op_type, head), - "operator" => Value::string(op.operator, head), - "name" => Value::string(op.name, head), - "description" => Value::string(op.description, head), - "precedence" => Value::int(op.precedence, head), + "type" => Value::string(op_type(&op), head), + "operator" => Value::string(op.to_string(), head), + "name" => Value::string(name(&op), head), + "description" => Value::string(description(&op), head), + "precedence" => Value::int(op.precedence().into(), head), }, head, - )); - } + ) + }) + .collect::>(); - Ok(Value::list(recs, head).into_pipeline_data()) + operators.push(Value::record( + record! { + "type" => Value::string("Boolean", head), + "operator" => Value::string("not", head), + "name" => Value::string("Not", head), + "description" => Value::string("Negates a value or expression.", head), + "precedence" => Value::int(0, head), + }, + head, + )); + + Ok(Value::list(operators, head).into_pipeline_data()) } } -struct OperatorInfo { - op_type: String, - operator: String, - name: String, - description: String, - precedence: i64, +fn op_type(operator: &Operator) -> &'static str { + match operator { + Operator::Comparison(_) => "Comparison", + Operator::Math(_) => "Math", + Operator::Boolean(_) => "Boolean", + Operator::Bits(_) => "Bitwise", + Operator::Assignment(_) => "Assignment", + } } -fn generate_operator_info() -> Vec { - vec![ - OperatorInfo { - op_type: "Assignment".into(), - operator: "=".into(), - name: "Assign".into(), - description: "Assigns a value to a variable.".into(), - precedence: 10, - }, - OperatorInfo { - op_type: "Assignment".into(), - operator: "+=".into(), - name: "PlusAssign".into(), - description: "Adds a value to a variable.".into(), - precedence: 10, - }, - OperatorInfo { - op_type: "Assignment".into(), - operator: "++=".into(), - name: "AppendAssign".into(), - description: "Appends a list or a value to a variable.".into(), - precedence: 10, - }, - OperatorInfo { - op_type: "Assignment".into(), - operator: "-=".into(), - name: "MinusAssign".into(), - description: "Subtracts a value from a variable.".into(), - precedence: 10, - }, - OperatorInfo { - op_type: "Assignment".into(), - operator: "*=".into(), - name: "MultiplyAssign".into(), - description: "Multiplies a variable by a value.".into(), - precedence: 10, - }, - OperatorInfo { - op_type: "Assignment".into(), - operator: "/=".into(), - name: "DivideAssign".into(), - description: "Divides a variable by a value.".into(), - precedence: 10, - }, - OperatorInfo { - op_type: "Comparison".into(), - operator: "==".into(), - name: "Equal".into(), - description: "Checks if two values are equal.".into(), - precedence: 80, - }, - OperatorInfo { - op_type: "Comparison".into(), - operator: "!=".into(), - name: "NotEqual".into(), - description: "Checks if two values are not equal.".into(), - precedence: 80, - }, - OperatorInfo { - op_type: "Comparison".into(), - operator: "<".into(), - name: "LessThan".into(), - description: "Checks if a value is less than another.".into(), - precedence: 80, - }, - OperatorInfo { - op_type: "Comparison".into(), - operator: "<=".into(), - name: "LessThanOrEqual".into(), - description: "Checks if a value is less than or equal to another.".into(), - precedence: 80, - }, - OperatorInfo { - op_type: "Comparison".into(), - operator: ">".into(), - name: "GreaterThan".into(), - description: "Checks if a value is greater than another.".into(), - precedence: 80, - }, - OperatorInfo { - op_type: "Comparison".into(), - operator: ">=".into(), - name: "GreaterThanOrEqual".into(), - description: "Checks if a value is greater than or equal to another.".into(), - precedence: 80, - }, - OperatorInfo { - op_type: "Comparison".into(), - operator: "=~".into(), - name: "RegexMatch".into(), - description: "Checks if a value matches a regular expression.".into(), - precedence: 80, - }, - OperatorInfo { - op_type: "Comparison".into(), - operator: "!~".into(), - name: "NotRegexMatch".into(), - description: "Checks if a value does not match a regular expression.".into(), - precedence: 80, - }, - OperatorInfo { - op_type: "Comparison".into(), - operator: "in".into(), - name: "In".into(), - description: "Checks if a value is in a list or string.".into(), - precedence: 80, - }, - OperatorInfo { - op_type: "Comparison".into(), - operator: "not-in".into(), - name: "NotIn".into(), - description: "Checks if a value is not in a list or string.".into(), - precedence: 80, - }, - OperatorInfo { - op_type: "Comparison".into(), - operator: "starts-with".into(), - name: "StartsWith".into(), - description: "Checks if a string starts with another.".into(), - precedence: 80, - }, - OperatorInfo { - op_type: "Comparison".into(), - operator: "ends-with".into(), - name: "EndsWith".into(), - description: "Checks if a string ends with another.".into(), - precedence: 80, - }, - OperatorInfo { - op_type: "Comparison".into(), - operator: "not".into(), - name: "UnaryNot".into(), - description: "Negates a value or expression.".into(), - precedence: 0, - }, - OperatorInfo { - op_type: "Math".into(), - operator: "+".into(), - name: "Plus".into(), - description: "Adds two values.".into(), - precedence: 90, - }, - OperatorInfo { - op_type: "Math".into(), - operator: "++".into(), - name: "Append".into(), - description: "Appends two lists or a list and a value.".into(), - precedence: 80, - }, - OperatorInfo { - op_type: "Math".into(), - operator: "-".into(), - name: "Minus".into(), - description: "Subtracts two values.".into(), - precedence: 90, - }, - OperatorInfo { - op_type: "Math".into(), - operator: "*".into(), - name: "Multiply".into(), - description: "Multiplies two values.".into(), - precedence: 95, - }, - OperatorInfo { - op_type: "Math".into(), - operator: "/".into(), - name: "Divide".into(), - description: "Divides two values.".into(), - precedence: 95, - }, - OperatorInfo { - op_type: "Math".into(), - operator: "//".into(), - name: "FloorDivision".into(), - description: "Divides two values and floors the result.".into(), - precedence: 95, - }, - OperatorInfo { - op_type: "Math".into(), - operator: "mod".into(), - name: "Modulo".into(), - description: "Divides two values and returns the remainder.".into(), - precedence: 95, - }, - OperatorInfo { - op_type: "Math".into(), - operator: "**".into(), - name: "Pow ".into(), - description: "Raises one value to the power of another.".into(), - precedence: 100, - }, - OperatorInfo { - op_type: "Bitwise".into(), - operator: "bit-or".into(), - name: "BitOr".into(), - description: "Performs a bitwise OR on two values.".into(), - precedence: 60, - }, - OperatorInfo { - op_type: "Bitwise".into(), - operator: "bit-xor".into(), - name: "BitXor".into(), - description: "Performs a bitwise XOR on two values.".into(), - precedence: 70, - }, - OperatorInfo { - op_type: "Bitwise".into(), - operator: "bit-and".into(), - name: "BitAnd".into(), - description: "Performs a bitwise AND on two values.".into(), - precedence: 75, - }, - OperatorInfo { - op_type: "Bitwise".into(), - operator: "bit-shl".into(), - name: "ShiftLeft".into(), - description: "Shifts a value left by another.".into(), - precedence: 85, - }, - OperatorInfo { - op_type: "Bitwise".into(), - operator: "bit-shr".into(), - name: "ShiftRight".into(), - description: "Shifts a value right by another.".into(), - precedence: 85, - }, - OperatorInfo { - op_type: "Boolean".into(), - operator: "and".into(), - name: "And".into(), - description: "Checks if two values are true.".into(), - precedence: 50, - }, - OperatorInfo { - op_type: "Boolean".into(), - operator: "or".into(), - name: "Or".into(), - description: "Checks if either value is true.".into(), - precedence: 40, - }, - OperatorInfo { - op_type: "Boolean".into(), - operator: "xor".into(), - name: "Xor".into(), - description: "Checks if one value is true and the other is false.".into(), - precedence: 45, - }, - ] +fn name(operator: &Operator) -> String { + match operator { + Operator::Comparison(op) => format!("{op:?}"), + Operator::Math(op) => format!("{op:?}"), + Operator::Boolean(op) => format!("{op:?}"), + Operator::Bits(op) => format!("{op:?}"), + Operator::Assignment(op) => format!("{op:?}"), + } +} + +fn description(operator: &Operator) -> &'static str { + // NOTE: if you have to add an operator here, also add it to the operator list above. + match operator { + Operator::Comparison(Comparison::Equal) => "Checks if two values are equal.", + Operator::Comparison(Comparison::NotEqual) => "Checks if two values are not equal.", + Operator::Comparison(Comparison::LessThan) => "Checks if a value is less than another.", + Operator::Comparison(Comparison::GreaterThan) => { + "Checks if a value is greater than another." + } + Operator::Comparison(Comparison::LessThanOrEqual) => { + "Checks if a value is less than or equal to another." + } + Operator::Comparison(Comparison::GreaterThanOrEqual) => { + "Checks if a value is greater than or equal to another." + } + Operator::Comparison(Comparison::RegexMatch) => { + "Checks if a value matches a regular expression." + } + Operator::Comparison(Comparison::NotRegexMatch) => { + "Checks if a value does not match a regular expression." + } + Operator::Comparison(Comparison::In) => { + "Checks if a value is in a list, is part of a string, or is a key in a record." + } + Operator::Comparison(Comparison::NotIn) => { + "Checks if a value is not in a list, is not part of a string, or is not a key in a record." + } + Operator::Comparison(Comparison::StartsWith) => "Checks if a string starts with another.", + Operator::Comparison(Comparison::EndsWith) => "Checks if a string ends with another.", + Operator::Math(Math::Plus) => "Adds two values.", + Operator::Math(Math::Append) => { + "Appends two lists, a list and a value, two strings, or two binary values." + } + Operator::Math(Math::Minus) => "Subtracts two values.", + Operator::Math(Math::Multiply) => "Multiplies two values.", + Operator::Math(Math::Divide) => "Divides two values.", + Operator::Math(Math::Modulo) => "Divides two values and returns the remainder.", + Operator::Math(Math::FloorDivision) => "Divides two values and floors the result.", + Operator::Math(Math::Pow) => "Raises one value to the power of another.", + Operator::Boolean(Boolean::And) => "Checks if both values are true.", + Operator::Boolean(Boolean::Or) => "Checks if either value is true.", + Operator::Boolean(Boolean::Xor) => "Checks if one value is true and the other is false.", + Operator::Bits(Bits::BitOr) => "Performs a bitwise OR on two values.", + Operator::Bits(Bits::BitXor) => "Performs a bitwise XOR on two values.", + Operator::Bits(Bits::BitAnd) => "Performs a bitwise AND on two values.", + Operator::Bits(Bits::ShiftLeft) => "Bitwise shifts a value left by another.", + Operator::Bits(Bits::ShiftRight) => "Bitwise shifts a value right by another.", + Operator::Assignment(Assignment::Assign) => "Assigns a value to a variable.", + Operator::Assignment(Assignment::PlusAssign) => "Adds a value to a variable.", + Operator::Assignment(Assignment::AppendAssign) => { + "Appends a list, a value, a string, or a binary value to a variable." + } + Operator::Assignment(Assignment::MinusAssign) => "Subtracts a value from a variable.", + Operator::Assignment(Assignment::MultiplyAssign) => "Multiplies a variable by a value.", + Operator::Assignment(Assignment::DivideAssign) => "Divides a variable by a value.", + } } #[cfg(test)] diff --git a/crates/nu-command/src/lib.rs b/crates/nu-command/src/lib.rs index a854fe3507..55c4974325 100644 --- a/crates/nu-command/src/lib.rs +++ b/crates/nu-command/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] mod bytes; mod charting; mod conversions; diff --git a/crates/nu-command/src/math/abs.rs b/crates/nu-command/src/math/abs.rs index 8f653142fa..b16097ba8d 100644 --- a/crates/nu-command/src/math/abs.rs +++ b/crates/nu-command/src/math/abs.rs @@ -42,10 +42,7 @@ impl Command for SubCommand { input: PipelineData, ) -> Result { let head = call.head; - input.map( - move |value| abs_helper(value, head), - engine_state.ctrlc.clone(), - ) + input.map(move |value| abs_helper(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/math/ceil.rs b/crates/nu-command/src/math/ceil.rs index 0b0f6d1696..0886ff0bc5 100644 --- a/crates/nu-command/src/math/ceil.rs +++ b/crates/nu-command/src/math/ceil.rs @@ -41,10 +41,7 @@ impl Command for SubCommand { if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map( - move |value| operate(value, head), - engine_state.ctrlc.clone(), - ) + input.map(move |value| operate(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/math/floor.rs b/crates/nu-command/src/math/floor.rs index e85e3ca674..c2b34c2cf9 100644 --- a/crates/nu-command/src/math/floor.rs +++ b/crates/nu-command/src/math/floor.rs @@ -41,10 +41,7 @@ impl Command for SubCommand { if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map( - move |value| operate(value, head), - engine_state.ctrlc.clone(), - ) + input.map(move |value| operate(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/math/log.rs b/crates/nu-command/src/math/log.rs index 90fad17daf..7a7e4bd27b 100644 --- a/crates/nu-command/src/math/log.rs +++ b/crates/nu-command/src/math/log.rs @@ -59,7 +59,7 @@ impl Command for SubCommand { let base = base.item; input.map( move |value| operate(value, head, base), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/math/round.rs b/crates/nu-command/src/math/round.rs index 7693c9a316..eafe669cf1 100644 --- a/crates/nu-command/src/math/round.rs +++ b/crates/nu-command/src/math/round.rs @@ -50,7 +50,7 @@ impl Command for SubCommand { } input.map( move |value| operate(value, head, precision_param), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/math/sqrt.rs b/crates/nu-command/src/math/sqrt.rs index c9c9765912..a3ab5fbbd8 100644 --- a/crates/nu-command/src/math/sqrt.rs +++ b/crates/nu-command/src/math/sqrt.rs @@ -41,10 +41,7 @@ impl Command for SubCommand { if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map( - move |value| operate(value, head), - engine_state.ctrlc.clone(), - ) + input.map(move |value| operate(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/math/utils.rs b/crates/nu-command/src/math/utils.rs index 9d2c15e15f..62f96ea073 100644 --- a/crates/nu-command/src/math/utils.rs +++ b/crates/nu-command/src/math/utils.rs @@ -1,6 +1,6 @@ use core::slice; use indexmap::IndexMap; -use nu_protocol::{ast::Call, IntoPipelineData, PipelineData, ShellError, Span, Value}; +use nu_protocol::{engine::Call, IntoPipelineData, PipelineData, ShellError, Signals, Span, Value}; pub fn run_with_function( call: &Call, @@ -92,7 +92,7 @@ pub fn calculate( } PipelineData::Value(Value::Range { val, .. }, ..) => { let new_vals: Result, ShellError> = val - .into_range_iter(span, None) + .into_range_iter(span, Signals::empty()) .map(|val| mf(&[val], span, name)) .collect(); diff --git a/crates/nu-command/src/network/http/client.rs b/crates/nu-command/src/network/http/client.rs index 8317fb50bc..deeb768eea 100644 --- a/crates/nu-command/src/network/http/client.rs +++ b/crates/nu-command/src/network/http/client.rs @@ -5,16 +5,12 @@ use base64::{ Engine, }; use nu_engine::command_prelude::*; -use nu_protocol::ByteStream; +use nu_protocol::{ByteStream, Signals}; use std::{ collections::HashMap, path::PathBuf, str::FromStr, - sync::{ - atomic::AtomicBool, - mpsc::{self, RecvTimeoutError}, - Arc, - }, + sync::mpsc::{self, RecvTimeoutError}, time::Duration, }; use ureq::{Error, ErrorKind, Request, Response}; @@ -129,7 +125,7 @@ pub fn response_to_buffer( let reader = response.into_reader(); PipelineData::ByteStream( - ByteStream::read(reader, span, engine_state.ctrlc.clone(), response_type) + ByteStream::read(reader, span, engine_state.signals().clone(), response_type) .with_known_size(buffer_size), None, ) @@ -180,88 +176,131 @@ impl From for ShellErrorOrRequestError { } } +#[derive(Debug)] +pub enum HttpBody { + Value(Value), + ByteStream(ByteStream), + None, +} + +// remove once all commands have been migrated pub fn send_request( request: Request, - body: Option, + http_body: HttpBody, content_type: Option, - ctrl_c: Option>, + span: Span, + signals: &Signals, ) -> Result { let request_url = request.url().to_string(); - if body.is_none() { - return send_cancellable_request(&request_url, Box::new(|| request.call()), ctrl_c); - } - let body = body.expect("Should never be none."); - let body_type = match content_type { - Some(it) if it == "application/json" => BodyType::Json, - Some(it) if it == "application/x-www-form-urlencoded" => BodyType::Form, - _ => BodyType::Unknown, - }; - match body { - Value::Binary { val, .. } => send_cancellable_request( - &request_url, - Box::new(move || request.send_bytes(&val)), - ctrl_c, - ), - Value::String { .. } if body_type == BodyType::Json => { - let data = value_to_json_value(&body)?; - send_cancellable_request(&request_url, Box::new(|| request.send_json(data)), ctrl_c) + match http_body { + HttpBody::None => { + send_cancellable_request(&request_url, Box::new(|| request.call()), span, signals) } - Value::String { val, .. } => send_cancellable_request( - &request_url, - Box::new(move || request.send_string(&val)), - ctrl_c, - ), - Value::Record { .. } if body_type == BodyType::Json => { - let data = value_to_json_value(&body)?; - send_cancellable_request(&request_url, Box::new(|| request.send_json(data)), ctrl_c) - } - Value::Record { val, .. } if body_type == BodyType::Form => { - let mut data: Vec<(String, String)> = Vec::with_capacity(val.len()); - - for (col, val) in val.into_owned() { - data.push((col, val.coerce_into_string()?)) - } - - let request_fn = move || { - // coerce `data` into a shape that send_form() is happy with - let data = data - .iter() - .map(|(a, b)| (a.as_str(), b.as_str())) - .collect::>(); - request.send_form(&data) + HttpBody::ByteStream(byte_stream) => { + let req = if let Some(content_type) = content_type { + request.set("Content-Type", &content_type) + } else { + request }; - send_cancellable_request(&request_url, Box::new(request_fn), ctrl_c) + + send_cancellable_request_bytes(&request_url, req, byte_stream, span, signals) } - Value::List { vals, .. } if body_type == BodyType::Form => { - if vals.len() % 2 != 0 { - return Err(ShellErrorOrRequestError::ShellError(ShellError::IOError { + HttpBody::Value(body) => { + let (body_type, req) = match content_type { + Some(it) if it == "application/json" => (BodyType::Json, request), + Some(it) if it == "application/x-www-form-urlencoded" => (BodyType::Form, request), + Some(it) => { + let r = request.clone().set("Content-Type", &it); + (BodyType::Unknown, r) + } + _ => (BodyType::Unknown, request), + }; + + match body { + Value::Binary { val, .. } => send_cancellable_request( + &request_url, + Box::new(move || req.send_bytes(&val)), + span, + signals, + ), + Value::String { .. } if body_type == BodyType::Json => { + let data = value_to_json_value(&body)?; + send_cancellable_request( + &request_url, + Box::new(|| req.send_json(data)), + span, + signals, + ) + } + Value::String { val, .. } => send_cancellable_request( + &request_url, + Box::new(move || req.send_string(&val)), + span, + signals, + ), + Value::Record { .. } if body_type == BodyType::Json => { + let data = value_to_json_value(&body)?; + send_cancellable_request( + &request_url, + Box::new(|| req.send_json(data)), + span, + signals, + ) + } + Value::Record { val, .. } if body_type == BodyType::Form => { + let mut data: Vec<(String, String)> = Vec::with_capacity(val.len()); + + for (col, val) in val.into_owned() { + data.push((col, val.coerce_into_string()?)) + } + + let request_fn = move || { + // coerce `data` into a shape that send_form() is happy with + let data = data + .iter() + .map(|(a, b)| (a.as_str(), b.as_str())) + .collect::>(); + req.send_form(&data) + }; + send_cancellable_request(&request_url, Box::new(request_fn), span, signals) + } + Value::List { vals, .. } if body_type == BodyType::Form => { + if vals.len() % 2 != 0 { + return Err(ShellErrorOrRequestError::ShellError(ShellError::IOError { + msg: "unsupported body input".into(), + })); + } + + let data = vals + .chunks(2) + .map(|it| Ok((it[0].coerce_string()?, it[1].coerce_string()?))) + .collect::, ShellErrorOrRequestError>>()?; + + let request_fn = move || { + // coerce `data` into a shape that send_form() is happy with + let data = data + .iter() + .map(|(a, b)| (a.as_str(), b.as_str())) + .collect::>(); + req.send_form(&data) + }; + send_cancellable_request(&request_url, Box::new(request_fn), span, signals) + } + Value::List { .. } if body_type == BodyType::Json => { + let data = value_to_json_value(&body)?; + send_cancellable_request( + &request_url, + Box::new(|| req.send_json(data)), + span, + signals, + ) + } + _ => Err(ShellErrorOrRequestError::ShellError(ShellError::IOError { msg: "unsupported body input".into(), - })); + })), } - - let data = vals - .chunks(2) - .map(|it| Ok((it[0].coerce_string()?, it[1].coerce_string()?))) - .collect::, ShellErrorOrRequestError>>()?; - - let request_fn = move || { - // coerce `data` into a shape that send_form() is happy with - let data = data - .iter() - .map(|(a, b)| (a.as_str(), b.as_str())) - .collect::>(); - request.send_form(&data) - }; - send_cancellable_request(&request_url, Box::new(request_fn), ctrl_c) } - Value::List { .. } if body_type == BodyType::Json => { - let data = value_to_json_value(&body)?; - send_cancellable_request(&request_url, Box::new(|| request.send_json(data)), ctrl_c) - } - _ => Err(ShellErrorOrRequestError::ShellError(ShellError::IOError { - msg: "unsupported body input".into(), - })), } } @@ -270,7 +309,8 @@ pub fn send_request( fn send_cancellable_request( request_url: &str, request_fn: Box Result + Sync + Send>, - ctrl_c: Option>, + span: Span, + signals: &Signals, ) -> Result { let (tx, rx) = mpsc::channel::>(); @@ -285,12 +325,7 @@ fn send_cancellable_request( // ...and poll the channel for responses loop { - if nu_utils::ctrl_c::was_pressed(&ctrl_c) { - // Return early and give up on the background thread. The connection will either time out or be disconnected - return Err(ShellErrorOrRequestError::ShellError( - ShellError::InterruptedByUser { span: None }, - )); - } + signals.check(span)?; // 100ms wait time chosen arbitrarily match rx.recv_timeout(Duration::from_millis(100)) { @@ -305,6 +340,57 @@ fn send_cancellable_request( } } +// Helper method used to make blocking HTTP request calls cancellable with ctrl+c +// ureq functions can block for a long time (default 30s?) while attempting to make an HTTP connection +fn send_cancellable_request_bytes( + request_url: &str, + request: Request, + byte_stream: ByteStream, + span: Span, + signals: &Signals, +) -> Result { + let (tx, rx) = mpsc::channel::>(); + let request_url_string = request_url.to_string(); + + // Make the blocking request on a background thread... + std::thread::Builder::new() + .name("HTTP requester".to_string()) + .spawn(move || { + let ret = byte_stream + .reader() + .ok_or_else(|| { + ShellErrorOrRequestError::ShellError(ShellError::GenericError { + error: "Could not read byte stream".to_string(), + msg: "".into(), + span: None, + help: None, + inner: vec![], + }) + }) + .and_then(|reader| { + request.send(reader).map_err(|e| { + ShellErrorOrRequestError::RequestError(request_url_string, Box::new(e)) + }) + }); + + // may fail if the user has cancelled the operation + let _ = tx.send(ret); + }) + .map_err(ShellError::from)?; + + // ...and poll the channel for responses + loop { + signals.check(span)?; + + // 100ms wait time chosen arbitrarily + match rx.recv_timeout(Duration::from_millis(100)) { + Ok(result) => return result, + Err(RecvTimeoutError::Timeout) => continue, + Err(RecvTimeoutError::Disconnected) => panic!("http response channel disconnected"), + } + } +} + pub fn request_set_timeout( timeout: Option, mut request: Request, diff --git a/crates/nu-command/src/network/http/delete.rs b/crates/nu-command/src/network/http/delete.rs index 77cfa70fb8..f32c9d15bb 100644 --- a/crates/nu-command/src/network/http/delete.rs +++ b/crates/nu-command/src/network/http/delete.rs @@ -1,7 +1,7 @@ use crate::network::http::client::{ check_response_redirection, http_client, http_parse_redirect_mode, http_parse_url, request_add_authorization_header, request_add_custom_headers, request_handle_response, - request_set_timeout, send_request, RequestFlags, + request_set_timeout, send_request, HttpBody, RequestFlags, }; use nu_engine::command_prelude::*; @@ -15,7 +15,7 @@ impl Command for SubCommand { fn signature(&self) -> Signature { Signature::build("http delete") - .input_output_types(vec![(Type::Nothing, Type::Any)]) + .input_output_types(vec![(Type::Any, Type::Any)]) .allow_variants_without_examples(true) .required( "URL", @@ -132,6 +132,11 @@ impl Command for SubCommand { "http delete --content-type application/json --data { field: value } https://www.example.com", result: None, }, + Example { + description: "Perform an HTTP delete with JSON content from a pipeline to example.com", + example: "open foo.json | http delete https://www.example.com", + result: None, + }, ] } } @@ -139,7 +144,7 @@ impl Command for SubCommand { struct Arguments { url: Value, headers: Option, - data: Option, + data: HttpBody, content_type: Option, raw: bool, insecure: bool, @@ -155,13 +160,27 @@ fn run_delete( engine_state: &EngineState, stack: &mut Stack, call: &Call, - _input: PipelineData, + input: PipelineData, ) -> Result { + let (data, maybe_metadata) = call + .get_flag::(engine_state, stack, "data")? + .map(|v| (HttpBody::Value(v), None)) + .unwrap_or_else(|| match input { + PipelineData::Value(v, metadata) => (HttpBody::Value(v), metadata), + PipelineData::ByteStream(byte_stream, metadata) => { + (HttpBody::ByteStream(byte_stream), metadata) + } + _ => (HttpBody::None, None), + }); + let content_type = call + .get_flag(engine_state, stack, "content-type")? + .or_else(|| maybe_metadata.and_then(|m| m.content_type)); + let args = Arguments { url: call.req(engine_state, stack, 0)?, headers: call.get_flag(engine_state, stack, "headers")?, - data: call.get_flag(engine_state, stack, "data")?, - content_type: call.get_flag(engine_state, stack, "content-type")?, + data, + content_type, raw: call.has_flag(engine_state, stack, "raw")?, insecure: call.has_flag(engine_state, stack, "insecure")?, user: call.get_flag(engine_state, stack, "user")?, @@ -184,7 +203,6 @@ fn helper( args: Arguments, ) -> Result { let span = args.url.span(); - let ctrl_c = engine_state.ctrlc.clone(); let (requested_url, _) = http_parse_url(call, span, args.url)?; let redirect_mode = http_parse_redirect_mode(args.redirect)?; @@ -195,7 +213,13 @@ fn helper( request = request_add_authorization_header(args.user, args.password, request); request = request_add_custom_headers(args.headers, request)?; - let response = send_request(request.clone(), args.data, args.content_type, ctrl_c); + let response = send_request( + request.clone(), + args.data, + args.content_type, + call.head, + engine_state.signals(), + ); let request_flags = RequestFlags { raw: args.raw, diff --git a/crates/nu-command/src/network/http/get.rs b/crates/nu-command/src/network/http/get.rs index e86c44e0a1..3b702404f4 100644 --- a/crates/nu-command/src/network/http/get.rs +++ b/crates/nu-command/src/network/http/get.rs @@ -5,6 +5,8 @@ use crate::network::http::client::{ }; use nu_engine::command_prelude::*; +use super::client::HttpBody; + #[derive(Clone)] pub struct SubCommand; @@ -169,7 +171,6 @@ fn helper( args: Arguments, ) -> Result { let span = args.url.span(); - let ctrl_c = engine_state.ctrlc.clone(); let (requested_url, _) = http_parse_url(call, span, args.url)?; let redirect_mode = http_parse_redirect_mode(args.redirect)?; @@ -180,7 +181,13 @@ fn helper( request = request_add_authorization_header(args.user, args.password, request); request = request_add_custom_headers(args.headers, request)?; - let response = send_request(request.clone(), None, None, ctrl_c); + let response = send_request( + request.clone(), + HttpBody::None, + None, + call.head, + engine_state.signals(), + ); let request_flags = RequestFlags { raw: args.raw, diff --git a/crates/nu-command/src/network/http/head.rs b/crates/nu-command/src/network/http/head.rs index 875cdc8d8e..797fa138e3 100644 --- a/crates/nu-command/src/network/http/head.rs +++ b/crates/nu-command/src/network/http/head.rs @@ -1,11 +1,11 @@ +use super::client::HttpBody; use crate::network::http::client::{ check_response_redirection, http_client, http_parse_redirect_mode, http_parse_url, request_add_authorization_header, request_add_custom_headers, request_handle_response_headers, request_set_timeout, send_request, }; use nu_engine::command_prelude::*; - -use std::sync::{atomic::AtomicBool, Arc}; +use nu_protocol::Signals; #[derive(Clone)] pub struct SubCommand; @@ -131,9 +131,8 @@ fn run_head( timeout: call.get_flag(engine_state, stack, "max-time")?, redirect: call.get_flag(engine_state, stack, "redirect-mode")?, }; - let ctrl_c = engine_state.ctrlc.clone(); - helper(engine_state, stack, call, args, ctrl_c) + helper(engine_state, stack, call, args, engine_state.signals()) } // Helper function that actually goes to retrieve the resource from the url given @@ -143,7 +142,7 @@ fn helper( stack: &mut Stack, call: &Call, args: Arguments, - ctrlc: Option>, + signals: &Signals, ) -> Result { let span = args.url.span(); let (requested_url, _) = http_parse_url(call, span, args.url)?; @@ -156,7 +155,7 @@ fn helper( request = request_add_authorization_header(args.user, args.password, request); request = request_add_custom_headers(args.headers, request)?; - let response = send_request(request, None, None, ctrlc); + let response = send_request(request, HttpBody::None, None, call.head, signals); check_response_redirection(redirect_mode, span, &response)?; request_handle_response_headers(span, response) } diff --git a/crates/nu-command/src/network/http/options.rs b/crates/nu-command/src/network/http/options.rs index fe4b7dcb1a..cd9a1f79c1 100644 --- a/crates/nu-command/src/network/http/options.rs +++ b/crates/nu-command/src/network/http/options.rs @@ -4,6 +4,8 @@ use crate::network::http::client::{ }; use nu_engine::command_prelude::*; +use super::client::HttpBody; + #[derive(Clone)] pub struct SubCommand; @@ -149,7 +151,6 @@ fn helper( args: Arguments, ) -> Result { let span = args.url.span(); - let ctrl_c = engine_state.ctrlc.clone(); let (requested_url, _) = http_parse_url(call, span, args.url)?; let client = http_client(args.insecure, RedirectMode::Follow, engine_state, stack)?; @@ -159,7 +160,13 @@ fn helper( request = request_add_authorization_header(args.user, args.password, request); request = request_add_custom_headers(args.headers, request)?; - let response = send_request(request.clone(), None, None, ctrl_c); + let response = send_request( + request.clone(), + HttpBody::None, + None, + call.head, + engine_state.signals(), + ); // http options' response always showed in header, so we set full to true. // And `raw` is useless too because options method doesn't return body, here we set to true diff --git a/crates/nu-command/src/network/http/patch.rs b/crates/nu-command/src/network/http/patch.rs index ca302702ef..7f49781284 100644 --- a/crates/nu-command/src/network/http/patch.rs +++ b/crates/nu-command/src/network/http/patch.rs @@ -1,7 +1,7 @@ use crate::network::http::client::{ check_response_redirection, http_client, http_parse_redirect_mode, http_parse_url, request_add_authorization_header, request_add_custom_headers, request_handle_response, - request_set_timeout, send_request, RequestFlags, + request_set_timeout, send_request, HttpBody, RequestFlags, }; use nu_engine::command_prelude::*; @@ -15,10 +15,10 @@ impl Command for SubCommand { fn signature(&self) -> Signature { Signature::build("http patch") - .input_output_types(vec![(Type::Nothing, Type::Any)]) + .input_output_types(vec![(Type::Any, Type::Any)]) .allow_variants_without_examples(true) .required("URL", SyntaxShape::String, "The URL to post to.") - .required("data", SyntaxShape::Any, "The contents of the post body.") + .optional("data", SyntaxShape::Any, "The contents of the post body.") .named( "user", SyntaxShape::Any, @@ -124,6 +124,11 @@ impl Command for SubCommand { example: "http patch --content-type application/json https://www.example.com { field: value }", result: None, }, + Example { + description: "Patch JSON content from a pipeline to example.com", + example: "open foo.json | http patch https://www.example.com", + result: None, + }, ] } } @@ -131,7 +136,7 @@ impl Command for SubCommand { struct Arguments { url: Value, headers: Option, - data: Value, + data: HttpBody, content_type: Option, raw: bool, insecure: bool, @@ -147,13 +152,37 @@ fn run_patch( engine_state: &EngineState, stack: &mut Stack, call: &Call, - _input: PipelineData, + input: PipelineData, ) -> Result { + let (data, maybe_metadata) = call + .opt::(engine_state, stack, 1)? + .map(|v| (HttpBody::Value(v), None)) + .unwrap_or_else(|| match input { + PipelineData::Value(v, metadata) => (HttpBody::Value(v), metadata), + PipelineData::ByteStream(byte_stream, metadata) => { + (HttpBody::ByteStream(byte_stream), metadata) + } + _ => (HttpBody::None, None), + }); + let content_type = call + .get_flag(engine_state, stack, "content-type")? + .or_else(|| maybe_metadata.and_then(|m| m.content_type)); + + if let HttpBody::None = data { + return Err(ShellError::GenericError { + error: "Data must be provided either through pipeline or positional argument".into(), + msg: "".into(), + span: Some(call.head), + help: None, + inner: vec![], + }); + } + let args = Arguments { url: call.req(engine_state, stack, 0)?, headers: call.get_flag(engine_state, stack, "headers")?, - data: call.req(engine_state, stack, 1)?, - content_type: call.get_flag(engine_state, stack, "content-type")?, + data, + content_type, raw: call.has_flag(engine_state, stack, "raw")?, insecure: call.has_flag(engine_state, stack, "insecure")?, user: call.get_flag(engine_state, stack, "user")?, @@ -176,7 +205,6 @@ fn helper( args: Arguments, ) -> Result { let span = args.url.span(); - let ctrl_c = engine_state.ctrlc.clone(); let (requested_url, _) = http_parse_url(call, span, args.url)?; let redirect_mode = http_parse_redirect_mode(args.redirect)?; @@ -187,7 +215,13 @@ fn helper( request = request_add_authorization_header(args.user, args.password, request); request = request_add_custom_headers(args.headers, request)?; - let response = send_request(request.clone(), Some(args.data), args.content_type, ctrl_c); + let response = send_request( + request.clone(), + args.data, + args.content_type, + call.head, + engine_state.signals(), + ); let request_flags = RequestFlags { raw: args.raw, diff --git a/crates/nu-command/src/network/http/post.rs b/crates/nu-command/src/network/http/post.rs index 1f0d7f81b0..ea4ec093f3 100644 --- a/crates/nu-command/src/network/http/post.rs +++ b/crates/nu-command/src/network/http/post.rs @@ -1,7 +1,7 @@ use crate::network::http::client::{ check_response_redirection, http_client, http_parse_redirect_mode, http_parse_url, request_add_authorization_header, request_add_custom_headers, request_handle_response, - request_set_timeout, send_request, RequestFlags, + request_set_timeout, send_request, HttpBody, RequestFlags, }; use nu_engine::command_prelude::*; @@ -15,10 +15,10 @@ impl Command for SubCommand { fn signature(&self) -> Signature { Signature::build("http post") - .input_output_types(vec![(Type::Nothing, Type::Any)]) + .input_output_types(vec![(Type::Any, Type::Any)]) .allow_variants_without_examples(true) .required("URL", SyntaxShape::String, "The URL to post to.") - .required("data", SyntaxShape::Any, "The contents of the post body.") + .optional("data", SyntaxShape::Any, "The contents of the post body. Required unless part of a pipeline.") .named( "user", SyntaxShape::Any, @@ -122,6 +122,11 @@ impl Command for SubCommand { example: "http post --content-type application/json https://www.example.com { field: value }", result: None, }, + Example { + description: "Post JSON content from a pipeline to example.com", + example: "open foo.json | http post https://www.example.com", + result: None, + }, ] } } @@ -129,7 +134,7 @@ impl Command for SubCommand { struct Arguments { url: Value, headers: Option, - data: Value, + data: HttpBody, content_type: Option, raw: bool, insecure: bool, @@ -145,13 +150,37 @@ fn run_post( engine_state: &EngineState, stack: &mut Stack, call: &Call, - _input: PipelineData, + input: PipelineData, ) -> Result { + let (data, maybe_metadata) = call + .opt::(engine_state, stack, 1)? + .map(|v| (HttpBody::Value(v), None)) + .unwrap_or_else(|| match input { + PipelineData::Value(v, metadata) => (HttpBody::Value(v), metadata), + PipelineData::ByteStream(byte_stream, metadata) => { + (HttpBody::ByteStream(byte_stream), metadata) + } + _ => (HttpBody::None, None), + }); + let content_type = call + .get_flag(engine_state, stack, "content-type")? + .or_else(|| maybe_metadata.and_then(|m| m.content_type)); + + if let HttpBody::None = data { + return Err(ShellError::GenericError { + error: "Data must be provided either through pipeline or positional argument".into(), + msg: "".into(), + span: Some(call.head), + help: None, + inner: vec![], + }); + } + let args = Arguments { url: call.req(engine_state, stack, 0)?, headers: call.get_flag(engine_state, stack, "headers")?, - data: call.req(engine_state, stack, 1)?, - content_type: call.get_flag(engine_state, stack, "content-type")?, + data, + content_type, raw: call.has_flag(engine_state, stack, "raw")?, insecure: call.has_flag(engine_state, stack, "insecure")?, user: call.get_flag(engine_state, stack, "user")?, @@ -174,7 +203,6 @@ fn helper( args: Arguments, ) -> Result { let span = args.url.span(); - let ctrl_c = engine_state.ctrlc.clone(); let (requested_url, _) = http_parse_url(call, span, args.url)?; let redirect_mode = http_parse_redirect_mode(args.redirect)?; @@ -185,7 +213,13 @@ fn helper( request = request_add_authorization_header(args.user, args.password, request); request = request_add_custom_headers(args.headers, request)?; - let response = send_request(request.clone(), Some(args.data), args.content_type, ctrl_c); + let response = send_request( + request.clone(), + args.data, + args.content_type, + call.head, + engine_state.signals(), + ); let request_flags = RequestFlags { raw: args.raw, diff --git a/crates/nu-command/src/network/http/put.rs b/crates/nu-command/src/network/http/put.rs index 8abedd2b4c..e2118ea359 100644 --- a/crates/nu-command/src/network/http/put.rs +++ b/crates/nu-command/src/network/http/put.rs @@ -1,7 +1,7 @@ use crate::network::http::client::{ check_response_redirection, http_client, http_parse_redirect_mode, http_parse_url, request_add_authorization_header, request_add_custom_headers, request_handle_response, - request_set_timeout, send_request, RequestFlags, + request_set_timeout, send_request, HttpBody, RequestFlags, }; use nu_engine::command_prelude::*; @@ -15,10 +15,10 @@ impl Command for SubCommand { fn signature(&self) -> Signature { Signature::build("http put") - .input_output_types(vec![(Type::Nothing, Type::Any)]) + .input_output_types(vec![(Type::Any, Type::Any)]) .allow_variants_without_examples(true) .required("URL", SyntaxShape::String, "The URL to post to.") - .required("data", SyntaxShape::Any, "The contents of the post body.") + .optional("data", SyntaxShape::Any, "The contents of the post body. Required unless part of a pipeline.") .named( "user", SyntaxShape::Any, @@ -122,6 +122,11 @@ impl Command for SubCommand { example: "http put --content-type application/json https://www.example.com { field: value }", result: None, }, + Example { + description: "Put JSON content from a pipeline to example.com", + example: "open foo.json | http put https://www.example.com", + result: None, + }, ] } } @@ -129,7 +134,7 @@ impl Command for SubCommand { struct Arguments { url: Value, headers: Option, - data: Value, + data: HttpBody, content_type: Option, raw: bool, insecure: bool, @@ -145,13 +150,38 @@ fn run_put( engine_state: &EngineState, stack: &mut Stack, call: &Call, - _input: PipelineData, + input: PipelineData, ) -> Result { + let (data, maybe_metadata) = call + .opt::(engine_state, stack, 1)? + .map(|v| (HttpBody::Value(v), None)) + .unwrap_or_else(|| match input { + PipelineData::Value(v, metadata) => (HttpBody::Value(v), metadata), + PipelineData::ByteStream(byte_stream, metadata) => { + (HttpBody::ByteStream(byte_stream), metadata) + } + _ => (HttpBody::None, None), + }); + + if let HttpBody::None = data { + return Err(ShellError::GenericError { + error: "Data must be provided either through pipeline or positional argument".into(), + msg: "".into(), + span: Some(call.head), + help: None, + inner: vec![], + }); + } + + let content_type = call + .get_flag(engine_state, stack, "content-type")? + .or_else(|| maybe_metadata.and_then(|m| m.content_type)); + let args = Arguments { url: call.req(engine_state, stack, 0)?, headers: call.get_flag(engine_state, stack, "headers")?, - data: call.req(engine_state, stack, 1)?, - content_type: call.get_flag(engine_state, stack, "content-type")?, + data, + content_type, raw: call.has_flag(engine_state, stack, "raw")?, insecure: call.has_flag(engine_state, stack, "insecure")?, user: call.get_flag(engine_state, stack, "user")?, @@ -174,7 +204,6 @@ fn helper( args: Arguments, ) -> Result { let span = args.url.span(); - let ctrl_c = engine_state.ctrlc.clone(); let (requested_url, _) = http_parse_url(call, span, args.url)?; let redirect_mode = http_parse_redirect_mode(args.redirect)?; @@ -185,7 +214,13 @@ fn helper( request = request_add_authorization_header(args.user, args.password, request); request = request_add_custom_headers(args.headers, request)?; - let response = send_request(request.clone(), Some(args.data), args.content_type, ctrl_c); + let response = send_request( + request.clone(), + args.data, + args.content_type, + call.head, + engine_state.signals(), + ); let request_flags = RequestFlags { raw: args.raw, diff --git a/crates/nu-command/src/network/url/decode.rs b/crates/nu-command/src/network/url/decode.rs index 8789eb13ca..b98e50a56e 100644 --- a/crates/nu-command/src/network/url/decode.rs +++ b/crates/nu-command/src/network/url/decode.rs @@ -48,7 +48,7 @@ impl Command for SubCommand { ) -> Result { let cell_paths: Vec = call.rest(engine_state, stack, 0)?; let args = CellPathOnlyArgs::from(cell_paths); - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/network/url/encode.rs b/crates/nu-command/src/network/url/encode.rs index 845487963b..96e0289903 100644 --- a/crates/nu-command/src/network/url/encode.rs +++ b/crates/nu-command/src/network/url/encode.rs @@ -50,15 +50,9 @@ impl Command for SubCommand { let cell_paths: Vec = call.rest(engine_state, stack, 0)?; let args = CellPathOnlyArgs::from(cell_paths); if call.has_flag(engine_state, stack, "all")? { - operate( - action_all, - args, - input, - call.head, - engine_state.ctrlc.clone(), - ) + operate(action_all, args, input, call.head, engine_state.signals()) } else { - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } } diff --git a/crates/nu-command/src/network/url/parse.rs b/crates/nu-command/src/network/url/parse.rs index e71c8d472a..287c64c7b9 100644 --- a/crates/nu-command/src/network/url/parse.rs +++ b/crates/nu-command/src/network/url/parse.rs @@ -1,4 +1,5 @@ use nu_engine::command_prelude::*; +use nu_protocol::Config; use url::Url; #[derive(Clone)] @@ -38,11 +39,15 @@ impl Command for SubCommand { fn run( &self, engine_state: &EngineState, - _: &mut Stack, + stack: &mut Stack, call: &Call, input: PipelineData, ) -> Result { - parse(input.into_value(call.head)?, call.head, engine_state) + parse( + input.into_value(call.head)?, + call.head, + &stack.get_config(engine_state), + ) } fn examples(&self) -> Vec { @@ -68,12 +73,12 @@ impl Command for SubCommand { } } -fn get_url_string(value: &Value, engine_state: &EngineState) -> String { - value.to_expanded_string("", engine_state.get_config()) +fn get_url_string(value: &Value, config: &Config) -> String { + value.to_expanded_string("", config) } -fn parse(value: Value, head: Span, engine_state: &EngineState) -> Result { - let url_string = get_url_string(&value, engine_state); +fn parse(value: Value, head: Span, config: &Config) -> Result { + let url_string = get_url_string(&value, config); let result_url = Url::parse(url_string.as_str()); diff --git a/crates/nu-command/src/path/basename.rs b/crates/nu-command/src/path/basename.rs index 04f7cf380e..8b7082048e 100644 --- a/crates/nu-command/src/path/basename.rs +++ b/crates/nu-command/src/path/basename.rs @@ -61,7 +61,7 @@ impl Command for SubCommand { } input.map( move |value| super::operate(&get_basename, &args, value, head), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } @@ -82,7 +82,7 @@ impl Command for SubCommand { } input.map( move |value| super::operate(&get_basename, &args, value, head), - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/path/dirname.rs b/crates/nu-command/src/path/dirname.rs index 2eb864215e..218091ee34 100644 --- a/crates/nu-command/src/path/dirname.rs +++ b/crates/nu-command/src/path/dirname.rs @@ -69,7 +69,7 @@ impl Command for SubCommand { } input.map( move |value| super::operate(&get_dirname, &args, value, head), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } @@ -91,7 +91,7 @@ impl Command for SubCommand { } input.map( move |value| super::operate(&get_dirname, &args, value, head), - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/path/exists.rs b/crates/nu-command/src/path/exists.rs index a99f04113e..86b00c6024 100644 --- a/crates/nu-command/src/path/exists.rs +++ b/crates/nu-command/src/path/exists.rs @@ -65,7 +65,7 @@ If you need to distinguish dirs and files, please use `path type`."# } input.map( move |value| super::operate(&exists, &args, value, head), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } @@ -87,7 +87,7 @@ If you need to distinguish dirs and files, please use `path type`."# } input.map( move |value| super::operate(&exists, &args, value, head), - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/path/expand.rs b/crates/nu-command/src/path/expand.rs index 18497c6426..ac51978810 100644 --- a/crates/nu-command/src/path/expand.rs +++ b/crates/nu-command/src/path/expand.rs @@ -70,7 +70,7 @@ impl Command for SubCommand { } input.map( move |value| super::operate(&expand, &args, value, head), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } @@ -93,7 +93,7 @@ impl Command for SubCommand { } input.map( move |value| super::operate(&expand, &args, value, head), - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/path/parse.rs b/crates/nu-command/src/path/parse.rs index 039f1012ed..cec2f5c6ac 100644 --- a/crates/nu-command/src/path/parse.rs +++ b/crates/nu-command/src/path/parse.rs @@ -63,7 +63,7 @@ On Windows, an extra 'prefix' column is added."# } input.map( move |value| super::operate(&parse, &args, value, head), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } @@ -84,7 +84,7 @@ On Windows, an extra 'prefix' column is added."# } input.map( move |value| super::operate(&parse, &args, value, head), - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/path/relative_to.rs b/crates/nu-command/src/path/relative_to.rs index 6533f6fa79..35df385d74 100644 --- a/crates/nu-command/src/path/relative_to.rs +++ b/crates/nu-command/src/path/relative_to.rs @@ -67,7 +67,7 @@ path."# } input.map( move |value| super::operate(&relative_to, &args, value, head), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } @@ -88,7 +88,7 @@ path."# } input.map( move |value| super::operate(&relative_to, &args, value, head), - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/path/split.rs b/crates/nu-command/src/path/split.rs index ea8e7e1b1f..80d86fb998 100644 --- a/crates/nu-command/src/path/split.rs +++ b/crates/nu-command/src/path/split.rs @@ -51,7 +51,7 @@ impl Command for SubCommand { } input.map( move |value| super::operate(&split, &args, value, head), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } @@ -70,7 +70,7 @@ impl Command for SubCommand { } input.map( move |value| super::operate(&split, &args, value, head), - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/path/type.rs b/crates/nu-command/src/path/type.rs index 0897593109..010fd295ad 100644 --- a/crates/nu-command/src/path/type.rs +++ b/crates/nu-command/src/path/type.rs @@ -1,10 +1,11 @@ use super::PathSubcommandArguments; use nu_engine::command_prelude::*; +use nu_path::AbsolutePathBuf; use nu_protocol::engine::StateWorkingSet; -use std::path::{Path, PathBuf}; +use std::{io, path::Path}; struct Arguments { - pwd: PathBuf, + pwd: AbsolutePathBuf, } impl PathSubcommandArguments for Arguments {} @@ -36,7 +37,7 @@ impl Command for SubCommand { fn extra_usage(&self) -> &str { r#"This checks the file system to confirm the path's object type. -If nothing is found, an empty string will be returned."# +If the path does not exist, null will be returned."# } fn is_const(&self) -> bool { @@ -61,7 +62,7 @@ If nothing is found, an empty string will be returned."# } input.map( move |value| super::operate(&path_type, &args, value, head), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } @@ -82,7 +83,7 @@ If nothing is found, an empty string will be returned."# } input.map( move |value| super::operate(&path_type, &args, value, head), - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } @@ -104,9 +105,11 @@ If nothing is found, an empty string will be returned."# fn path_type(path: &Path, span: Span, args: &Arguments) -> Value { let path = nu_path::expand_path_with(path, &args.pwd, true); - let meta = path.symlink_metadata(); - let ty = meta.as_ref().map(get_file_type).unwrap_or(""); - Value::string(ty, span) + match path.symlink_metadata() { + Ok(metadata) => Value::string(get_file_type(&metadata), span), + Err(err) if err.kind() == io::ErrorKind::NotFound => Value::nothing(span), + Err(err) => Value::error(err.into_spanned(span).into(), span), + } } fn get_file_type(md: &std::fs::Metadata) -> &str { diff --git a/crates/nu-command/src/platform/ansi/ansi_.rs b/crates/nu-command/src/platform/ansi/ansi_.rs index 9e0b0bba66..3daed5e157 100644 --- a/crates/nu-command/src/platform/ansi/ansi_.rs +++ b/crates/nu-command/src/platform/ansi/ansi_.rs @@ -1,11 +1,8 @@ use nu_ansi_term::*; use nu_engine::command_prelude::*; -use nu_protocol::engine::StateWorkingSet; +use nu_protocol::{engine::StateWorkingSet, Signals}; use once_cell::sync::Lazy; -use std::{ - collections::HashMap, - sync::{atomic::AtomicBool, Arc}, -}; +use std::collections::HashMap; #[derive(Clone)] pub struct AnsiCommand; @@ -656,11 +653,14 @@ Operating system commands: let list: bool = call.has_flag(engine_state, stack, "list")?; let escape: bool = call.has_flag(engine_state, stack, "escape")?; let osc: bool = call.has_flag(engine_state, stack, "osc")?; - let use_ansi_coloring = engine_state.get_config().use_ansi_coloring; - let ctrlc = engine_state.ctrlc.clone(); + let use_ansi_coloring = stack.get_config(engine_state).use_ansi_coloring; if list { - return Ok(generate_ansi_code_list(ctrlc, call.head, use_ansi_coloring)); + return Ok(generate_ansi_code_list( + engine_state.signals().clone(), + call.head, + use_ansi_coloring, + )); } // The code can now be one of the ansi abbreviations like green_bold @@ -676,7 +676,7 @@ Operating system commands: } }; - let output = heavy_lifting(code, escape, osc, call)?; + let output = heavy_lifting(code, escape, osc, stack, call)?; Ok(Value::string(output, call.head).into_pipeline_data()) } @@ -691,10 +691,13 @@ Operating system commands: let escape: bool = call.has_flag_const(working_set, "escape")?; let osc: bool = call.has_flag_const(working_set, "osc")?; let use_ansi_coloring = working_set.get_config().use_ansi_coloring; - let ctrlc = working_set.permanent().ctrlc.clone(); if list { - return Ok(generate_ansi_code_list(ctrlc, call.head, use_ansi_coloring)); + return Ok(generate_ansi_code_list( + working_set.permanent().signals().clone(), + call.head, + use_ansi_coloring, + )); } // The code can now be one of the ansi abbreviations like green_bold @@ -710,26 +713,30 @@ Operating system commands: } }; - let output = heavy_lifting(code, escape, osc, call)?; + let output = heavy_lifting(code, escape, osc, &Stack::new(), call)?; Ok(Value::string(output, call.head).into_pipeline_data()) } } -fn heavy_lifting(code: Value, escape: bool, osc: bool, call: &Call) -> Result { +fn heavy_lifting( + code: Value, + escape: bool, + osc: bool, + stack: &Stack, + call: &Call, +) -> Result { let param_is_string = matches!(code, Value::String { .. }); if escape && osc { return Err(ShellError::IncompatibleParameters { left_message: "escape".into(), left_span: call - .get_named_arg("escape") - .expect("Unexpected missing argument") - .span, + .get_flag_span(stack, "escape") + .expect("Unexpected missing argument"), right_message: "osc".into(), right_span: call - .get_named_arg("osc") - .expect("Unexpected missing argument") - .span, + .get_flag_span(stack, "osc") + .expect("Unexpected missing argument"), }); } let code_string = if param_is_string { @@ -741,10 +748,7 @@ fn heavy_lifting(code: Value, escape: bool, osc: bool, call: &Call) -> Result = code_string.chars().collect(); if code_vec[0] == '\\' { - let span = match call.get_flag_expr("escape") { - Some(expr) => expr.span, - None => call.head, - }; + let span = call.get_flag_span(stack, "escape").unwrap_or(call.head); return Err(ShellError::TypeMismatch { err_message: "no need for escape characters".into(), @@ -827,7 +831,7 @@ pub fn str_to_ansi(s: &str) -> Option { } fn generate_ansi_code_list( - ctrlc: Option>, + signals: Signals, call_span: Span, use_ansi_coloring: bool, ) -> PipelineData { @@ -862,7 +866,7 @@ fn generate_ansi_code_list( Value::record(record, call_span) }) - .into_pipeline_data(call_span, ctrlc) + .into_pipeline_data(call_span, signals.clone()) } fn build_ansi_hashmap(v: &[AnsiCode]) -> HashMap<&str, &str> { diff --git a/crates/nu-command/src/platform/ansi/link.rs b/crates/nu-command/src/platform/ansi/link.rs index 68fc17977b..b45b365d3f 100644 --- a/crates/nu-command/src/platform/ansi/link.rs +++ b/crates/nu-command/src/platform/ansi/link.rs @@ -91,12 +91,12 @@ fn operate( if column_paths.is_empty() { input.map( move |v| process_value(&v, text.as_deref()), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } else { input.map( move |v| process_each_path(v, &column_paths, text.as_deref(), command_span), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } } diff --git a/crates/nu-command/src/platform/ansi/strip.rs b/crates/nu-command/src/platform/ansi/strip.rs index 35d410161c..4f7f8ea596 100644 --- a/crates/nu-command/src/platform/ansi/strip.rs +++ b/crates/nu-command/src/platform/ansi/strip.rs @@ -1,10 +1,12 @@ +use std::sync::Arc; + use nu_cmd_base::input_handler::{operate, CmdArgument}; use nu_engine::command_prelude::*; use nu_protocol::Config; pub struct Arguments { cell_paths: Option>, - config: Config, + config: Arc, } impl CmdArgument for Arguments { @@ -51,12 +53,9 @@ impl Command for SubCommand { ) -> Result { let cell_paths: Vec = call.rest(engine_state, stack, 1)?; let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths); - let config = engine_state.get_config(); - let args = Arguments { - cell_paths, - config: config.clone(), - }; - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + let config = stack.get_config(engine_state); + let args = Arguments { cell_paths, config }; + operate(action, args, input, call.head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/platform/dir_info.rs b/crates/nu-command/src/platform/dir_info.rs index e4a3039672..10ce4f5420 100644 --- a/crates/nu-command/src/platform/dir_info.rs +++ b/crates/nu-command/src/platform/dir_info.rs @@ -1,10 +1,7 @@ use filesize::file_real_size_fast; use nu_glob::Pattern; -use nu_protocol::{record, ShellError, Span, Value}; -use std::{ - path::PathBuf, - sync::{atomic::AtomicBool, Arc}, -}; +use nu_protocol::{record, ShellError, Signals, Span, Value}; +use std::path::PathBuf; #[derive(Debug, Clone)] pub struct DirBuilder { @@ -82,8 +79,9 @@ impl DirInfo { path: impl Into, params: &DirBuilder, depth: Option, - ctrl_c: Option>, - ) -> Self { + span: Span, + signals: &Signals, + ) -> Result { let path = path.into(); let mut s = Self { @@ -107,14 +105,12 @@ impl DirInfo { match std::fs::read_dir(&s.path) { Ok(d) => { for f in d { - if nu_utils::ctrl_c::was_pressed(&ctrl_c) { - break; - } + signals.check(span)?; 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()) + s = s.add_dir(i.path(), depth, params, span, signals)? } Ok(_t) => s = s.add_file(i.path(), params), Err(e) => s = s.add_error(e.into()), @@ -125,7 +121,7 @@ impl DirInfo { } Err(e) => s = s.add_error(e.into()), } - s + Ok(s) } fn add_dir( @@ -133,21 +129,22 @@ impl DirInfo { path: impl Into, mut depth: Option, params: &DirBuilder, - ctrl_c: Option>, - ) -> Self { + span: Span, + signals: &Signals, + ) -> Result { if let Some(current) = depth { if let Some(new) = current.checked_sub(1) { depth = Some(new); } else { - return self; + return Ok(self); } } - let d = DirInfo::new(path, params, depth, ctrl_c); + let d = DirInfo::new(path, params, depth, span, signals)?; self.size += d.size; self.blocks += d.blocks; self.dirs.push(d); - self + Ok(self) } fn add_file(mut self, f: impl Into, params: &DirBuilder) -> Self { diff --git a/crates/nu-command/src/platform/input/list.rs b/crates/nu-command/src/platform/input/list.rs index 44b21a8da4..37f16e75d7 100644 --- a/crates/nu-command/src/platform/input/list.rs +++ b/crates/nu-command/src/platform/input/list.rs @@ -79,6 +79,7 @@ impl Command for InputList { let fuzzy = call.has_flag(engine_state, stack, "fuzzy")?; let index = call.has_flag(engine_state, stack, "index")?; let display_path: Option = call.get_flag(engine_state, stack, "display")?; + let config = stack.get_config(engine_state); let options: Vec = match input { PipelineData::Value(Value::Range { .. }, ..) @@ -89,9 +90,9 @@ impl Command for InputList { let display_value = if let Some(ref cellpath) = display_path { val.clone() .follow_cell_path(&cellpath.members, false)? - .to_expanded_string(", ", engine_state.get_config()) + .to_expanded_string(", ", &config) } else { - val.to_expanded_string(", ", engine_state.get_config()) + val.to_expanded_string(", ", &config) }; Ok(Options { name: display_value, diff --git a/crates/nu-command/src/platform/is_terminal.rs b/crates/nu-command/src/platform/is_terminal.rs index c67329e839..2195f3ff8a 100644 --- a/crates/nu-command/src/platform/is_terminal.rs +++ b/crates/nu-command/src/platform/is_terminal.rs @@ -58,7 +58,7 @@ impl Command for IsTerminal { _ => { return Err(ShellError::IncompatibleParametersSingle { msg: "Only one stream may be checked".into(), - span: Span::merge_many(call.arguments.iter().map(|arg| arg.span())), + span: call.arguments_span(), }); } }; diff --git a/crates/nu-command/src/platform/kill.rs b/crates/nu-command/src/platform/kill.rs index 2e47ee8c78..1cf6f15f01 100644 --- a/crates/nu-command/src/platform/kill.rs +++ b/crates/nu-command/src/platform/kill.rs @@ -84,27 +84,26 @@ impl Command for Kill { { return Err(ShellError::IncompatibleParameters { left_message: "force".to_string(), - left_span: call - .get_named_arg("force") - .ok_or_else(|| ShellError::GenericError { + left_span: call.get_flag_span(stack, "force").ok_or_else(|| { + ShellError::GenericError { error: "Flag error".into(), msg: "flag force not found".into(), span: Some(call.head), help: None, inner: vec![], - })? - .span, + } + })?, right_message: "signal".to_string(), right_span: Span::merge( - call.get_named_arg("signal") - .ok_or_else(|| ShellError::GenericError { + call.get_flag_span(stack, "signal").ok_or_else(|| { + ShellError::GenericError { error: "Flag error".into(), msg: "flag signal not found".into(), span: Some(call.head), help: None, inner: vec![], - })? - .span, + } + })?, signal_span, ), }); diff --git a/crates/nu-command/src/platform/sleep.rs b/crates/nu-command/src/platform/sleep.rs index ddf429509c..4d5f6ec827 100644 --- a/crates/nu-command/src/platform/sleep.rs +++ b/crates/nu-command/src/platform/sleep.rs @@ -56,12 +56,7 @@ impl Command for Sleep { break; } thread::sleep(CTRL_C_CHECK_INTERVAL.min(time_until_deadline)); - // exit early if Ctrl+C was pressed - if nu_utils::ctrl_c::was_pressed(&engine_state.ctrlc) { - return Err(ShellError::InterruptedByUser { - span: Some(call.head), - }); - } + engine_state.signals().check(call.head)?; } Ok(Value::nothing(call.head).into_pipeline_data()) diff --git a/crates/nu-command/src/random/dice.rs b/crates/nu-command/src/random/dice.rs index 2fb659ad1e..5e3a1b98b6 100644 --- a/crates/nu-command/src/random/dice.rs +++ b/crates/nu-command/src/random/dice.rs @@ -78,7 +78,7 @@ fn dice( Value::int(thread_rng.gen_range(1..sides + 1) as i64, span) }); - Ok(ListStream::new(iter, span, engine_state.ctrlc.clone()).into()) + Ok(ListStream::new(iter, span, engine_state.signals().clone()).into()) } #[cfg(test)] diff --git a/crates/nu-command/src/sort_utils.rs b/crates/nu-command/src/sort_utils.rs index 50d8256353..01bb604eac 100644 --- a/crates/nu-command/src/sort_utils.rs +++ b/crates/nu-command/src/sort_utils.rs @@ -81,7 +81,7 @@ pub fn sort( if let Some(nonexistent) = nonexistent_column(&sort_columns, record.columns()) { return Err(ShellError::CantFindColumn { col_name: nonexistent, - span, + span: Some(span), src_span: val_span, }); } diff --git a/crates/nu-command/src/stor/create.rs b/crates/nu-command/src/stor/create.rs index 630718489b..1e1219889d 100644 --- a/crates/nu-command/src/stor/create.rs +++ b/crates/nu-command/src/stor/create.rs @@ -54,7 +54,10 @@ impl Command for StorCreate { let span = call.head; let table_name: Option = call.get_flag(engine_state, stack, "table-name")?; let columns: Option = call.get_flag(engine_state, stack, "columns")?; - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + engine_state.signals().clone(), + )); process(table_name, span, &db, columns)?; // dbg!(db.clone()); @@ -141,6 +144,8 @@ fn process( #[cfg(test)] mod test { + use nu_protocol::Signals; + use super::*; #[test] @@ -154,7 +159,10 @@ mod test { fn test_process_with_valid_parameters() { let table_name = Some("test_table".to_string()); let span = Span::unknown(); - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); let mut columns = Record::new(); columns.insert( "int_column".to_string(), @@ -170,7 +178,10 @@ mod test { fn test_process_with_missing_table_name() { let table_name = None; let span = Span::unknown(); - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); let mut columns = Record::new(); columns.insert( "int_column".to_string(), @@ -190,7 +201,10 @@ mod test { fn test_process_with_missing_columns() { let table_name = Some("test_table".to_string()); let span = Span::unknown(); - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); let result = process(table_name, span, &db, None); @@ -205,7 +219,10 @@ mod test { fn test_process_with_unsupported_column_data_type() { let table_name = Some("test_table".to_string()); let span = Span::unknown(); - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); let mut columns = Record::new(); let column_datatype = "bogus_data_type".to_string(); columns.insert( diff --git a/crates/nu-command/src/stor/delete.rs b/crates/nu-command/src/stor/delete.rs index 4de4874140..676e0490b0 100644 --- a/crates/nu-command/src/stor/delete.rs +++ b/crates/nu-command/src/stor/delete.rs @@ -1,5 +1,6 @@ use crate::database::{SQLiteDatabase, MEMORY_DB}; use nu_engine::command_prelude::*; +use nu_protocol::Signals; #[derive(Clone)] pub struct StorDelete; @@ -82,7 +83,10 @@ impl Command for StorDelete { } // Open the in-mem database - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); if let Some(new_table_name) = table_name_opt { if let Ok(conn) = db.open_connection() { diff --git a/crates/nu-command/src/stor/export.rs b/crates/nu-command/src/stor/export.rs index 95c5ee9f35..f8255eec49 100644 --- a/crates/nu-command/src/stor/export.rs +++ b/crates/nu-command/src/stor/export.rs @@ -1,5 +1,6 @@ use crate::database::{SQLiteDatabase, MEMORY_DB}; use nu_engine::command_prelude::*; +use nu_protocol::Signals; #[derive(Clone)] pub struct StorExport; @@ -58,7 +59,10 @@ impl Command for StorExport { }; // Open the in-mem database - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); if let Ok(conn) = db.open_connection() { // This uses vacuum. I'm not really sure if this is the best way to do this. diff --git a/crates/nu-command/src/stor/import.rs b/crates/nu-command/src/stor/import.rs index 682694e8bb..20b5b64f2d 100644 --- a/crates/nu-command/src/stor/import.rs +++ b/crates/nu-command/src/stor/import.rs @@ -1,5 +1,6 @@ use crate::database::{SQLiteDatabase, MEMORY_DB}; use nu_engine::command_prelude::*; +use nu_protocol::Signals; #[derive(Clone)] pub struct StorImport; @@ -58,7 +59,10 @@ impl Command for StorImport { }; // Open the in-mem database - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); if let Ok(mut conn) = db.open_connection() { db.restore_database_from_file(&mut conn, file_name) diff --git a/crates/nu-command/src/stor/insert.rs b/crates/nu-command/src/stor/insert.rs index 4b9677941b..0e5c9ec6af 100644 --- a/crates/nu-command/src/stor/insert.rs +++ b/crates/nu-command/src/stor/insert.rs @@ -1,5 +1,6 @@ use crate::database::{values_to_sql, SQLiteDatabase, MEMORY_DB}; use nu_engine::command_prelude::*; +use nu_protocol::Signals; use rusqlite::params_from_iter; #[derive(Clone)] @@ -64,8 +65,11 @@ impl Command for StorInsert { let span = call.head; let table_name: Option = call.get_flag(engine_state, stack, "table-name")?; let data_record: Option = call.get_flag(engine_state, stack, "data-record")?; - // let config = engine_state.get_config(); - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + // let config = stack.get_config(engine_state); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); // Check if the record is being passed as input or using the data record parameter let columns = handle(span, data_record, input)?; @@ -198,7 +202,10 @@ mod test { #[test] fn test_process_with_simple_parameters() { - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); let create_stmt = "CREATE TABLE test_process_with_simple_parameters ( int_column INTEGER, real_column REAL, @@ -237,7 +244,10 @@ mod test { #[test] fn test_process_string_with_space() { - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); let create_stmt = "CREATE TABLE test_process_string_with_space ( str_column VARCHAR(255) )"; @@ -262,7 +272,10 @@ mod test { #[test] fn test_no_errors_when_string_too_long() { - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); let create_stmt = "CREATE TABLE test_errors_when_string_too_long ( str_column VARCHAR(8) )"; @@ -287,7 +300,10 @@ mod test { #[test] fn test_no_errors_when_param_is_wrong_type() { - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); let create_stmt = "CREATE TABLE test_errors_when_param_is_wrong_type ( int_column INT )"; @@ -312,7 +328,10 @@ mod test { #[test] fn test_errors_when_column_doesnt_exist() { - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); let create_stmt = "CREATE TABLE test_errors_when_column_doesnt_exist ( int_column INT )"; @@ -337,7 +356,10 @@ mod test { #[test] fn test_errors_when_table_doesnt_exist() { - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); let table_name = Some("test_errors_when_table_doesnt_exist".to_string()); let span = Span::unknown(); diff --git a/crates/nu-command/src/stor/open.rs b/crates/nu-command/src/stor/open.rs index c7f6f9f746..ba0f17c2af 100644 --- a/crates/nu-command/src/stor/open.rs +++ b/crates/nu-command/src/stor/open.rs @@ -1,5 +1,6 @@ use crate::database::{SQLiteDatabase, MEMORY_DB}; use nu_engine::command_prelude::*; +use nu_protocol::Signals; #[derive(Clone)] pub struct StorOpen; @@ -54,7 +55,10 @@ impl Command for StorOpen { // It returns the output of `select * from my_table_name` // Just create an empty database with MEMORY_DB and nothing else - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); // dbg!(db.clone()); Ok(db.into_value(call.head).into_pipeline_data()) diff --git a/crates/nu-command/src/stor/reset.rs b/crates/nu-command/src/stor/reset.rs index d4489fb702..ba9e2a9681 100644 --- a/crates/nu-command/src/stor/reset.rs +++ b/crates/nu-command/src/stor/reset.rs @@ -1,5 +1,6 @@ use crate::database::{SQLiteDatabase, MEMORY_DB}; use nu_engine::command_prelude::*; +use nu_protocol::Signals; #[derive(Clone)] pub struct StorReset; @@ -42,7 +43,10 @@ impl Command for StorReset { let span = call.head; // Open the in-mem database - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); if let Ok(conn) = db.open_connection() { db.drop_all_tables(&conn) diff --git a/crates/nu-command/src/stor/update.rs b/crates/nu-command/src/stor/update.rs index d731207a3f..18cf6f9f0f 100644 --- a/crates/nu-command/src/stor/update.rs +++ b/crates/nu-command/src/stor/update.rs @@ -1,5 +1,6 @@ use crate::database::{SQLiteDatabase, MEMORY_DB}; use nu_engine::command_prelude::*; +use nu_protocol::Signals; #[derive(Clone)] pub struct StorUpdate; @@ -79,7 +80,10 @@ impl Command for StorUpdate { call.get_flag(engine_state, stack, "where-clause")?; // Open the in-mem database - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); // Check if the record is being passed as input or using the update record parameter let columns = handle(span, update_record, input)?; diff --git a/crates/nu-command/src/strings/char_.rs b/crates/nu-command/src/strings/char_.rs index dd7b5cf39b..7737210254 100644 --- a/crates/nu-command/src/strings/char_.rs +++ b/crates/nu-command/src/strings/char_.rs @@ -1,8 +1,8 @@ use indexmap::{indexmap, IndexMap}; use nu_engine::command_prelude::*; +use nu_protocol::Signals; use once_cell::sync::Lazy; -use std::sync::{atomic::AtomicBool, Arc}; // Character used to separate directories in a Path Environment variable on windows is ";" #[cfg(target_family = "windows")] @@ -19,6 +19,9 @@ static CHAR_MAP: Lazy> = Lazy::new(|| { // These are some regular characters that either can't be used or // it's just easier to use them like this. + "nul" => '\x00'.to_string(), // nul character, 0x00 + "null_byte" => '\x00'.to_string(), // nul character, 0x00 + "zero_byte" => '\x00'.to_string(), // nul character, 0x00 // This are the "normal" characters section "newline" => '\n'.to_string(), "enter" => '\n'.to_string(), @@ -226,11 +229,13 @@ impl Command for Char { let list = call.has_flag_const(working_set, "list")?; let integer = call.has_flag_const(working_set, "integer")?; let unicode = call.has_flag_const(working_set, "unicode")?; - let ctrlc = working_set.permanent().ctrlc.clone(); // handle -l flag if list { - return Ok(generate_character_list(ctrlc, call.head)); + return Ok(generate_character_list( + working_set.permanent().signals().clone(), + call.head, + )); } // handle -i flag @@ -261,11 +266,13 @@ impl Command for Char { let list = call.has_flag(engine_state, stack, "list")?; let integer = call.has_flag(engine_state, stack, "integer")?; let unicode = call.has_flag(engine_state, stack, "unicode")?; - let ctrlc = engine_state.ctrlc.clone(); // handle -l flag if list { - return Ok(generate_character_list(ctrlc, call_span)); + return Ok(generate_character_list( + engine_state.signals().clone(), + call_span, + )); } // handle -i flag @@ -286,7 +293,7 @@ impl Command for Char { } } -fn generate_character_list(ctrlc: Option>, call_span: Span) -> PipelineData { +fn generate_character_list(signals: Signals, call_span: Span) -> PipelineData { CHAR_MAP .iter() .map(move |(name, s)| { @@ -305,7 +312,7 @@ fn generate_character_list(ctrlc: Option>, call_span: Span) -> P Value::record(record, call_span) }) - .into_pipeline_data(call_span, ctrlc) + .into_pipeline_data(call_span, signals) } fn handle_integer_flag( diff --git a/crates/nu-command/src/strings/detect_columns.rs b/crates/nu-command/src/strings/detect_columns.rs index 62a2d8bbcc..37a7cfe303 100644 --- a/crates/nu-command/src/strings/detect_columns.rs +++ b/crates/nu-command/src/strings/detect_columns.rs @@ -1,7 +1,7 @@ use itertools::Itertools; use nu_engine::command_prelude::*; -use nu_protocol::Range; -use std::{io::Cursor, iter::Peekable, str::CharIndices}; +use nu_protocol::{Config, Range}; +use std::{io::Cursor, iter::Peekable, str::CharIndices, sync::Arc}; type Input<'t> = Peekable>; @@ -110,11 +110,13 @@ none 8150224 4 8150220 1% /mnt/c' | detect columns --gue let num_rows_to_skip: Option = call.get_flag(engine_state, stack, "skip")?; let noheader = call.has_flag(engine_state, stack, "no-headers")?; let range: Option = call.get_flag(engine_state, stack, "combine-columns")?; + let config = stack.get_config(engine_state); let args = Arguments { noheader, num_rows_to_skip, range, + config, }; if call.has_flag(engine_state, stack, "guess")? { @@ -133,11 +135,13 @@ none 8150224 4 8150220 1% /mnt/c' | detect columns --gue let num_rows_to_skip: Option = call.get_flag_const(working_set, "skip")?; let noheader = call.has_flag_const(working_set, "no-headers")?; let range: Option = call.get_flag_const(working_set, "combine-columns")?; + let config = working_set.get_config().clone(); let args = Arguments { noheader, num_rows_to_skip, range, + config, }; if call.has_flag_const(working_set, "guess")? { @@ -152,6 +156,7 @@ struct Arguments { num_rows_to_skip: Option, noheader: bool, range: Option, + config: Arc, } fn guess_width( @@ -163,7 +168,7 @@ fn guess_width( use super::guess_width::GuessWidth; let input_span = input.span().unwrap_or(call.head); - let mut input = input.collect_string("", engine_state.get_config())?; + let mut input = input.collect_string("", &args.config)?; if let Some(rows) = args.num_rows_to_skip { input = input.lines().skip(rows).map(|x| x.to_string()).join("\n"); } @@ -199,7 +204,7 @@ fn guess_width( Err(e) => Value::error(e, input_span), } }) - .into_pipeline_data(input_span, engine_state.ctrlc.clone())) + .into_pipeline_data(input_span, engine_state.signals().clone())) } else { let length = result[0].len(); let columns: Vec = (0..length).map(|n| format!("column{n}")).collect(); @@ -224,7 +229,7 @@ fn guess_width( Err(e) => Value::error(e, input_span), } }) - .into_pipeline_data(input_span, engine_state.ctrlc.clone())) + .into_pipeline_data(input_span, engine_state.signals().clone())) } } @@ -235,9 +240,7 @@ fn detect_columns( args: Arguments, ) -> Result { let name_span = call.head; - let ctrlc = engine_state.ctrlc.clone(); - let config = engine_state.get_config(); - let input = input.collect_string("", config)?; + let input = input.collect_string("", &args.config)?; let input: Vec<_> = input .lines() @@ -316,7 +319,7 @@ fn detect_columns( None => Value::record(record, name_span), } }) - .into_pipeline_data(call.head, ctrlc)) + .into_pipeline_data(call.head, engine_state.signals().clone())) } else { Ok(PipelineData::empty()) } diff --git a/crates/nu-command/src/strings/encode_decode/base64.rs b/crates/nu-command/src/strings/encode_decode/base64.rs index 8050193212..dd9289a141 100644 --- a/crates/nu-command/src/strings/encode_decode/base64.rs +++ b/crates/nu-command/src/strings/encode_decode/base64.rs @@ -8,8 +8,8 @@ use base64::{ }; use nu_cmd_base::input_handler::{operate as general_operate, CmdArgument}; use nu_protocol::{ - ast::{Call, CellPath}, - engine::EngineState, + ast::CellPath, + engine::{Call, EngineState}, PipelineData, ShellError, Span, Spanned, Value, }; @@ -75,7 +75,7 @@ pub fn operate( cell_paths, }; - general_operate(action, args, input, call.head, engine_state.ctrlc.clone()) + general_operate(action, args, input, call.head, engine_state.signals()) } fn action( diff --git a/crates/nu-command/src/strings/format/date.rs b/crates/nu-command/src/strings/format/date.rs index 1aaf1fb851..dd005b9216 100644 --- a/crates/nu-command/src/strings/format/date.rs +++ b/crates/nu-command/src/strings/format/date.rs @@ -127,7 +127,7 @@ fn run( Some(format) => format_helper(value, format.item.as_str(), format.span, head), None => format_helper_rfc2822(value, head), }, - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/strings/format/duration.rs b/crates/nu-command/src/strings/format/duration.rs index 281542f49a..fbbe192048 100644 --- a/crates/nu-command/src/strings/format/duration.rs +++ b/crates/nu-command/src/strings/format/duration.rs @@ -81,7 +81,7 @@ impl Command for FormatDuration { arg, input, call.head, - engine_state.ctrlc.clone(), + engine_state.signals(), ) } @@ -108,7 +108,7 @@ impl Command for FormatDuration { arg, input, call.head, - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/strings/format/filesize.rs b/crates/nu-command/src/strings/format/filesize.rs index ebd43d90b1..63865ac2ac 100644 --- a/crates/nu-command/src/strings/format/filesize.rs +++ b/crates/nu-command/src/strings/format/filesize.rs @@ -76,7 +76,7 @@ impl Command for FormatFilesize { arg, input, call.head, - engine_state.ctrlc.clone(), + engine_state.signals(), ) } @@ -101,7 +101,7 @@ impl Command for FormatFilesize { arg, input, call.head, - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/strings/guess_width.rs b/crates/nu-command/src/strings/guess_width.rs index 59cfbcb2cf..8c758516bd 100644 --- a/crates/nu-command/src/strings/guess_width.rs +++ b/crates/nu-command/src/strings/guess_width.rs @@ -72,7 +72,9 @@ impl GuessWidth { let mut rows = Vec::new(); while let Ok(columns) = self.read() { - rows.push(columns); + if !columns.is_empty() { + rows.push(columns); + } } rows } @@ -175,34 +177,47 @@ fn separator_position(lr: &[char], p: usize, pos: &[usize], n: usize) -> usize { fn split(line: &str, pos: &[usize], trim_space: bool) -> Vec { let mut n = 0; - let mut start = 0; + let mut start_char = 0; let mut columns = Vec::with_capacity(pos.len() + 1); - let lr: Vec = line.chars().collect(); + let (line_char_boundaries, line_chars): (Vec, Vec) = line.char_indices().unzip(); let mut w = 0; - for p in 0..lr.len() { + if line_chars.is_empty() || line_chars.iter().all(|&c| c.is_whitespace()) { + // current line is completely empty, or only filled with whitespace + return Vec::new(); + } else if !pos.is_empty() + && line_chars.iter().all(|&c| !c.is_whitespace()) + && pos[0] < UnicodeWidthStr::width(line) + { + // we have more than 1 column in the input, but the current line has no whitespace, + // and it is longer than the first detected column separation position + // this indicates some kind of decoration line. let's skip it + return Vec::new(); + } + + for p in 0..line_char_boundaries.len() { if pos.is_empty() || n > pos.len() - 1 { - start = p; + start_char = p; break; } if pos[n] <= w { - let end = separator_position(&lr, p, pos, n); - if start > end { + let end_char = separator_position(&line_chars, p, pos, n); + if start_char > end_char { break; } - let col = &line[start..end]; + let col = &line[line_char_boundaries[start_char]..line_char_boundaries[end_char]]; let col = if trim_space { col.trim() } else { col }; columns.push(col.to_string()); n += 1; - start = end; + start_char = end_char; } - w += UnicodeWidthStr::width(lr[p].to_string().as_str()); + w += UnicodeWidthStr::width(line_chars[p].to_string().as_str()); } // add last part. - let col = &line[start..]; + let col = &line[line_char_boundaries[start_char]..]; let col = if trim_space { col.trim() } else { col }; columns.push(col.to_string()); columns @@ -423,6 +438,162 @@ D: 104792064 17042676 87749388 17% /d"; assert_eq!(got, want); } + #[test] + fn test_guess_width_multibyte() { + let input = "A… B\nC… D"; + let r = Box::new(std::io::BufReader::new(input.as_bytes())) as Box; + let reader = std::io::BufReader::new(r); + + let mut guess_width = GuessWidth { + reader, + pos: Vec::new(), + pre_lines: Vec::new(), + pre_count: 0, + limit_split: 0, + }; + + let want = vec![vec!["A…", "B"], vec!["C…", "D"]]; + let got = guess_width.read_all(); + assert_eq!(got, want); + } + + #[test] + fn test_guess_width_combining_diacritical_marks() { + let input = "Name Surname +Ștefan Țincu "; + + let r = Box::new(std::io::BufReader::new(input.as_bytes())) as Box; + let reader = std::io::BufReader::new(r); + + let mut guess_width = GuessWidth { + reader, + pos: Vec::new(), + pre_lines: Vec::new(), + pre_count: 0, + limit_split: 0, + }; + + let want = vec![vec!["Name", "Surname"], vec!["Ștefan", "Țincu"]]; + let got = guess_width.read_all(); + assert_eq!(got, want); + } + + #[test] + fn test_guess_width_single_column() { + let input = "A + +B + +C"; + + let r = Box::new(std::io::BufReader::new(input.as_bytes())) as Box; + let reader = std::io::BufReader::new(r); + + let mut guess_width = GuessWidth { + reader, + pos: Vec::new(), + pre_lines: Vec::new(), + pre_count: 0, + limit_split: 0, + }; + + let want = vec![vec!["A"], vec!["B"], vec!["C"]]; + let got = guess_width.read_all(); + assert_eq!(got, want); + } + + #[test] + fn test_guess_width_row_without_whitespace() { + let input = "A B C D +------- +E F G H"; + + let r = Box::new(std::io::BufReader::new(input.as_bytes())) as Box; + let reader = std::io::BufReader::new(r); + + let mut guess_width = GuessWidth { + reader, + pos: Vec::new(), + pre_lines: Vec::new(), + pre_count: 0, + limit_split: 0, + }; + + let want = vec![vec!["A", "B", "C", "D"], vec!["E", "F", "G", "H"]]; + let got = guess_width.read_all(); + assert_eq!(got, want); + } + + #[test] + fn test_guess_width_row_with_single_column() { + let input = "A B C D +E +F G H I"; + + let r = Box::new(std::io::BufReader::new(input.as_bytes())) as Box; + let reader = std::io::BufReader::new(r); + + let mut guess_width = GuessWidth { + reader, + pos: Vec::new(), + pre_lines: Vec::new(), + pre_count: 0, + limit_split: 0, + }; + + let want = vec![ + vec!["A", "B", "C", "D"], + vec!["E"], + vec!["F", "G", "H", "I"], + ]; + let got = guess_width.read_all(); + assert_eq!(got, want); + } + + #[test] + fn test_guess_width_empty_row() { + let input = "A B C D + +E F G H"; + + let r = Box::new(std::io::BufReader::new(input.as_bytes())) as Box; + let reader = std::io::BufReader::new(r); + + let mut guess_width = GuessWidth { + reader, + pos: Vec::new(), + pre_lines: Vec::new(), + pre_count: 0, + limit_split: 0, + }; + + let want = vec![vec!["A", "B", "C", "D"], vec!["E", "F", "G", "H"]]; + let got = guess_width.read_all(); + assert_eq!(got, want); + } + + #[test] + fn test_guess_width_row_with_only_whitespace() { + let input = "A B C D + +E F G H"; + + let r = Box::new(std::io::BufReader::new(input.as_bytes())) as Box; + let reader = std::io::BufReader::new(r); + + let mut guess_width = GuessWidth { + reader, + pos: Vec::new(), + pre_lines: Vec::new(), + pre_count: 0, + limit_split: 0, + }; + + let want = vec![vec!["A", "B", "C", "D"], vec!["E", "F", "G", "H"]]; + let got = guess_width.read_all(); + assert_eq!(got, want); + } + #[test] fn test_to_table() { let lines = vec![ diff --git a/crates/nu-command/src/strings/mod.rs b/crates/nu-command/src/strings/mod.rs index d1ebf540e5..8b5af2dec4 100644 --- a/crates/nu-command/src/strings/mod.rs +++ b/crates/nu-command/src/strings/mod.rs @@ -17,8 +17,7 @@ pub use str_::*; use nu_engine::CallExt; use nu_protocol::{ - ast::Call, - engine::{EngineState, Stack, StateWorkingSet}, + engine::{Call, EngineState, Stack, StateWorkingSet}, ShellError, }; diff --git a/crates/nu-command/src/strings/parse.rs b/crates/nu-command/src/strings/parse.rs index 4318b3da8b..86b27aecbc 100644 --- a/crates/nu-command/src/strings/parse.rs +++ b/crates/nu-command/src/strings/parse.rs @@ -1,10 +1,7 @@ use fancy_regex::{Captures, Regex}; use nu_engine::command_prelude::*; -use nu_protocol::{engine::StateWorkingSet, ListStream}; -use std::{ - collections::VecDeque, - sync::{atomic::AtomicBool, Arc}, -}; +use nu_protocol::{engine::StateWorkingSet, ListStream, Signals}; +use std::collections::VecDeque; #[derive(Clone)] pub struct Parse; @@ -15,13 +12,17 @@ impl Command for Parse { } fn usage(&self) -> &str { - "Parse columns from string data using a simple pattern." + "Parse columns from string data using a simple pattern or a supplied regular expression." } fn search_terms(&self) -> Vec<&str> { vec!["pattern", "match", "regex", "str extract"] } + fn extra_usage(&self) -> &str { + "The parse command always uses regular expressions even when you use a simple pattern. If a simple pattern is supplied, parse will transform that pattern into a regular expression." + } + fn signature(&self) -> nu_protocol::Signature { Signature::build("parse") .required("pattern", SyntaxShape::String, "The pattern to match.") @@ -35,21 +36,24 @@ impl Command for Parse { } fn examples(&self) -> Vec { - let result = Value::test_list(vec![Value::test_record(record! { - "foo" => Value::test_string("hi"), - "bar" => Value::test_string("there"), - })]); - vec![ Example { description: "Parse a string into two named columns", example: "\"hi there\" | parse \"{foo} {bar}\"", - result: Some(result.clone()), + result: Some(Value::test_list( + vec![Value::test_record(record! { + "foo" => Value::test_string("hi"), + "bar" => Value::test_string("there"), + })])), }, Example { - description: "Parse a string using regex pattern", - example: "\"hi there\" | parse --regex '(?P\\w+) (?P\\w+)'", - result: Some(result), + description: "This is how the first example is interpreted in the source code", + example: "\"hi there\" | parse --regex '(?s)\\A(?P.*?) (?P.*?)\\z'", + result: Some(Value::test_list( + vec![Value::test_record(record! { + "foo" => Value::test_string("hi"), + "bar" => Value::test_string("there"), + })])), }, Example { description: "Parse a string using fancy-regex named capture group pattern", @@ -163,8 +167,6 @@ fn operate( }) .collect::>(); - let ctrlc = engine_state.ctrlc.clone(); - match input { PipelineData::Empty => Ok(PipelineData::Empty), PipelineData::Value(value, ..) => match value { @@ -192,10 +194,10 @@ fn operate( columns, iter, span: head, - ctrlc, + signals: engine_state.signals().clone(), }; - Ok(ListStream::new(iter, head, None).into()) + Ok(ListStream::new(iter, head, Signals::empty()).into()) } value => Err(ShellError::PipelineMismatch { exp_input_type: "string".into(), @@ -220,7 +222,7 @@ fn operate( columns, iter, span: head, - ctrlc, + signals: engine_state.signals().clone(), } }) .into()), @@ -232,10 +234,10 @@ fn operate( columns, iter: lines, span: head, - ctrlc, + signals: engine_state.signals().clone(), }; - Ok(ListStream::new(iter, head, None).into()) + Ok(ListStream::new(iter, head, Signals::empty()).into()) } else { Ok(PipelineData::Empty) } @@ -302,7 +304,7 @@ struct ParseIter>> { columns: Vec, iter: I, span: Span, - ctrlc: Option>, + signals: Signals, } impl>> ParseIter { @@ -320,7 +322,7 @@ impl>> Iterator for ParseIter { fn next(&mut self) -> Option { loop { - if nu_utils::ctrl_c::was_pressed(&self.ctrlc) { + if self.signals.interrupted() { return None; } diff --git a/crates/nu-command/src/strings/split/chars.rs b/crates/nu-command/src/strings/split/chars.rs index 08e73b9830..370df262ea 100644 --- a/crates/nu-command/src/strings/split/chars.rs +++ b/crates/nu-command/src/strings/split/chars.rs @@ -124,7 +124,7 @@ fn split_chars( let span = call.head; input.map( move |x| split_chars_helper(&x, span, graphemes), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/strings/split/column.rs b/crates/nu-command/src/strings/split/column.rs index 02eff7845f..540cfabe54 100644 --- a/crates/nu-command/src/strings/split/column.rs +++ b/crates/nu-command/src/strings/split/column.rs @@ -170,7 +170,7 @@ fn split_column( input.flat_map( move |x| split_column_helper(&x, ®ex, &args.rest, args.collapse_empty, name_span), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/strings/split/list.rs b/crates/nu-command/src/strings/split/list.rs index d52bb5401a..eb874841a2 100644 --- a/crates/nu-command/src/strings/split/list.rs +++ b/crates/nu-command/src/strings/split/list.rs @@ -215,9 +215,7 @@ fn split_list( let matcher = Matcher::new(has_regex, separator)?; for val in input { - if nu_utils::ctrl_c::was_pressed(&engine_state.ctrlc) { - break; - } + engine_state.signals().check(call.head)?; if matcher.compare(&val)? { if !temp_list.is_empty() { diff --git a/crates/nu-command/src/strings/split/row.rs b/crates/nu-command/src/strings/split/row.rs index 8bc0003cb6..1f427a06e0 100644 --- a/crates/nu-command/src/strings/split/row.rs +++ b/crates/nu-command/src/strings/split/row.rs @@ -170,7 +170,7 @@ fn split_row( })?; input.flat_map( move |x| split_row_helper(&x, ®ex, args.max_split, name_span), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/strings/split/words.rs b/crates/nu-command/src/strings/split/words.rs index 0dd5dc9383..6cb5562a70 100644 --- a/crates/nu-command/src/strings/split/words.rs +++ b/crates/nu-command/src/strings/split/words.rs @@ -177,7 +177,7 @@ fn split_words( input.map( move |x| split_words_helper(&x, args.word_length, span, args.graphemes), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/strings/str_/case/capitalize.rs b/crates/nu-command/src/strings/str_/case/capitalize.rs index 20f334976c..862ca127c2 100644 --- a/crates/nu-command/src/strings/str_/case/capitalize.rs +++ b/crates/nu-command/src/strings/str_/case/capitalize.rs @@ -108,7 +108,7 @@ fn operate( ret } }, - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/strings/str_/case/downcase.rs b/crates/nu-command/src/strings/str_/case/downcase.rs index 316050d501..0493a663aa 100644 --- a/crates/nu-command/src/strings/str_/case/downcase.rs +++ b/crates/nu-command/src/strings/str_/case/downcase.rs @@ -116,7 +116,7 @@ fn operate( ret } }, - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/strings/str_/case/mod.rs b/crates/nu-command/src/strings/str_/case/mod.rs index 32b50c4bc6..3390ff097e 100644 --- a/crates/nu-command/src/strings/str_/case/mod.rs +++ b/crates/nu-command/src/strings/str_/case/mod.rs @@ -38,7 +38,7 @@ where case_operation, cell_paths, }; - general_operate(action, args, input, call.head, engine_state.ctrlc.clone()) + general_operate(action, args, input, call.head, engine_state.signals()) } fn action(input: &Value, args: &Arguments, head: Span) -> Value diff --git a/crates/nu-command/src/strings/str_/case/upcase.rs b/crates/nu-command/src/strings/str_/case/upcase.rs index 9e55f25f64..557239ec91 100644 --- a/crates/nu-command/src/strings/str_/case/upcase.rs +++ b/crates/nu-command/src/strings/str_/case/upcase.rs @@ -93,7 +93,7 @@ fn operate( ret } }, - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/strings/str_/contains.rs b/crates/nu-command/src/strings/str_/contains.rs index 63b9366b09..27d8a8e313 100644 --- a/crates/nu-command/src/strings/str_/contains.rs +++ b/crates/nu-command/src/strings/str_/contains.rs @@ -10,7 +10,6 @@ struct Arguments { substring: String, cell_paths: Option>, case_insensitive: bool, - not_contain: bool, } impl CmdArgument for Arguments { @@ -40,7 +39,6 @@ impl Command for SubCommand { "For a data structure input, check strings at the given cell paths, and replace with result.", ) .switch("ignore-case", "search is case insensitive", Some('i')) - .switch("not", "DEPRECATED OPTION: does not contain", Some('n')) .category(Category::Strings) } @@ -63,29 +61,14 @@ impl Command for SubCommand { call: &Call, input: PipelineData, ) -> Result { - if call.has_flag(engine_state, stack, "not")? { - nu_protocol::report_error_new( - engine_state, - &ShellError::GenericError { - error: "Deprecated option".into(), - msg: "`str contains --not {string}` is deprecated and will be removed in 0.95." - .into(), - span: Some(call.head), - help: Some("Please use the `not` operator instead.".into()), - inner: vec![], - }, - ); - } - let cell_paths: Vec = call.rest(engine_state, stack, 1)?; let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths); let args = Arguments { substring: call.req::(engine_state, stack, 0)?, cell_paths, case_insensitive: call.has_flag(engine_state, stack, "ignore-case")?, - not_contain: call.has_flag(engine_state, stack, "not")?, }; - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn run_const( @@ -114,14 +97,13 @@ impl Command for SubCommand { substring: call.req_const::(working_set, 0)?, cell_paths, case_insensitive: call.has_flag_const(working_set, "ignore-case")?, - not_contain: call.has_flag_const(working_set, "not")?, }; operate( action, args, input, call.head, - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } @@ -183,7 +165,6 @@ fn action( input: &Value, Arguments { case_insensitive, - not_contain, substring, .. }: &Arguments, @@ -191,23 +172,11 @@ fn action( ) -> Value { match input { Value::String { val, .. } => Value::bool( - match case_insensitive { - true => { - if *not_contain { - !val.to_folded_case() - .contains(substring.to_folded_case().as_str()) - } else { - val.to_folded_case() - .contains(substring.to_folded_case().as_str()) - } - } - false => { - if *not_contain { - !val.contains(substring) - } else { - val.contains(substring) - } - } + if *case_insensitive { + val.to_folded_case() + .contains(substring.to_folded_case().as_str()) + } else { + val.contains(substring) }, head, ), diff --git a/crates/nu-command/src/strings/str_/deunicode.rs b/crates/nu-command/src/strings/str_/deunicode.rs new file mode 100644 index 0000000000..ef732571f9 --- /dev/null +++ b/crates/nu-command/src/strings/str_/deunicode.rs @@ -0,0 +1,98 @@ +use deunicode::deunicode; +use nu_cmd_base::input_handler::{operate, CellPathOnlyArgs}; +use nu_engine::command_prelude::*; +use nu_protocol::engine::StateWorkingSet; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "str deunicode" + } + + fn signature(&self) -> Signature { + Signature::build("str deunicode") + .input_output_types(vec![(Type::String, Type::String)]) + .category(Category::Strings) + } + + fn usage(&self) -> &str { + "Convert Unicode string to pure ASCII." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["convert", "ascii"] + } + + fn is_const(&self) -> bool { + true + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let cell_paths: Vec = call.rest(engine_state, stack, 0)?; + let args = CellPathOnlyArgs::from(cell_paths); + + operate(action, args, input, call.head, engine_state.signals()) + } + + fn run_const( + &self, + working_set: &StateWorkingSet, + call: &Call, + input: PipelineData, + ) -> Result { + let cell_paths: Vec = call.rest_const(working_set, 0)?; + let args = CellPathOnlyArgs::from(cell_paths); + + operate( + action, + args, + input, + call.head, + working_set.permanent().signals(), + ) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "deunicode a string", + example: "'A…B' | str deunicode", + result: Some(Value::test_string("A...B")), + }] + } +} + +fn action(input: &Value, _args: &CellPathOnlyArgs, head: Span) -> Value { + match input { + Value::String { val, .. } => Value::string(deunicode(val), head), + Value::Error { .. } => input.clone(), + _ => Value::error( + ShellError::OnlySupportsThisInputType { + exp_input_type: "string".into(), + wrong_type: input.get_type().to_string(), + dst_span: head, + src_span: input.span(), + }, + 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_/distance.rs b/crates/nu-command/src/strings/str_/distance.rs index bd666e53f5..9b88f8e3e0 100644 --- a/crates/nu-command/src/strings/str_/distance.rs +++ b/crates/nu-command/src/strings/str_/distance.rs @@ -67,7 +67,7 @@ impl Command for SubCommand { compare_string, cell_paths, }; - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn run_const( @@ -88,7 +88,7 @@ impl Command for SubCommand { args, input, call.head, - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/strings/str_/ends_with.rs b/crates/nu-command/src/strings/str_/ends_with.rs index 40f643ea8e..d4841b74e0 100644 --- a/crates/nu-command/src/strings/str_/ends_with.rs +++ b/crates/nu-command/src/strings/str_/ends_with.rs @@ -68,7 +68,7 @@ impl Command for SubCommand { cell_paths, case_insensitive: call.has_flag(engine_state, stack, "ignore-case")?, }; - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn run_const( @@ -89,7 +89,7 @@ impl Command for SubCommand { args, input, call.head, - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/strings/str_/expand.rs b/crates/nu-command/src/strings/str_/expand.rs index 70fb51ec4c..b9759ef6a1 100644 --- a/crates/nu-command/src/strings/str_/expand.rs +++ b/crates/nu-command/src/strings/str_/expand.rs @@ -233,7 +233,7 @@ fn run( ), } }, - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/strings/str_/index_of.rs b/crates/nu-command/src/strings/str_/index_of.rs index 5b40f80d3d..a6b06ef9b7 100644 --- a/crates/nu-command/src/strings/str_/index_of.rs +++ b/crates/nu-command/src/strings/str_/index_of.rs @@ -93,7 +93,7 @@ impl Command for SubCommand { cell_paths, graphemes: grapheme_flags(engine_state, stack, call)?, }; - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn run_const( @@ -117,7 +117,7 @@ impl Command for SubCommand { args, input, call.head, - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/strings/str_/join.rs b/crates/nu-command/src/strings/str_/join.rs index 1d36f216d0..d297ea4cbc 100644 --- a/crates/nu-command/src/strings/str_/join.rs +++ b/crates/nu-command/src/strings/str_/join.rs @@ -1,4 +1,5 @@ use nu_engine::command_prelude::*; +use nu_protocol::Signals; use std::io::Write; @@ -88,30 +89,35 @@ fn run( let mut iter = input.into_iter(); let mut first = true; - let output = ByteStream::from_fn(span, None, ByteStreamType::String, move |buffer| { - // Write each input to the buffer - if let Some(value) = iter.next() { - // Write the separator if this is not the first - if first { - first = false; - } else if let Some(separator) = &separator { - write!(buffer, "{}", separator)?; - } - - match value { - Value::Error { error, .. } => { - return Err(*error); + let output = ByteStream::from_fn( + span, + Signals::empty(), + ByteStreamType::String, + move |buffer| { + // Write each input to the buffer + if let Some(value) = iter.next() { + // Write the separator if this is not the first + if first { + first = false; + } else if let Some(separator) = &separator { + write!(buffer, "{}", separator)?; } - // Hmm, not sure what we actually want. - // `to_expanded_string` formats dates as human readable which feels funny. - Value::Date { val, .. } => write!(buffer, "{val:?}")?, - value => write!(buffer, "{}", value.to_expanded_string("\n", &config))?, + + match value { + Value::Error { error, .. } => { + return Err(*error); + } + // Hmm, not sure what we actually want. + // `to_expanded_string` formats dates as human readable which feels funny. + Value::Date { val, .. } => write!(buffer, "{val:?}")?, + value => write!(buffer, "{}", value.to_expanded_string("\n", &config))?, + } + Ok(true) + } else { + Ok(false) } - Ok(true) - } else { - Ok(false) - } - }); + }, + ); Ok(PipelineData::ByteStream(output, metadata)) } diff --git a/crates/nu-command/src/strings/str_/length.rs b/crates/nu-command/src/strings/str_/length.rs index f456fe9467..ab0ba8db49 100644 --- a/crates/nu-command/src/strings/str_/length.rs +++ b/crates/nu-command/src/strings/str_/length.rs @@ -130,7 +130,7 @@ fn run( cell_paths: (!cell_paths.is_empty()).then_some(cell_paths), graphemes, }; - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn action(input: &Value, arg: &Arguments, head: Span) -> Value { diff --git a/crates/nu-command/src/strings/str_/mod.rs b/crates/nu-command/src/strings/str_/mod.rs index fb34ace833..b9290ada2c 100644 --- a/crates/nu-command/src/strings/str_/mod.rs +++ b/crates/nu-command/src/strings/str_/mod.rs @@ -1,5 +1,6 @@ mod case; mod contains; +mod deunicode; mod distance; mod ends_with; mod expand; @@ -15,6 +16,7 @@ mod trim; pub use case::*; pub use contains::SubCommand as StrContains; +pub use deunicode::SubCommand as StrDeunicode; pub use distance::SubCommand as StrDistance; pub use ends_with::SubCommand as StrEndswith; pub use expand::SubCommand as StrExpand; diff --git a/crates/nu-command/src/strings/str_/replace.rs b/crates/nu-command/src/strings/str_/replace.rs index 58e2574681..342dd0693e 100644 --- a/crates/nu-command/src/strings/str_/replace.rs +++ b/crates/nu-command/src/strings/str_/replace.rs @@ -102,7 +102,7 @@ impl Command for SubCommand { no_regex, multiline, }; - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn run_const( @@ -134,7 +134,7 @@ impl Command for SubCommand { args, input, call.head, - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/strings/str_/reverse.rs b/crates/nu-command/src/strings/str_/reverse.rs index cc3772db7c..9a339f7bcc 100644 --- a/crates/nu-command/src/strings/str_/reverse.rs +++ b/crates/nu-command/src/strings/str_/reverse.rs @@ -50,7 +50,7 @@ impl Command for SubCommand { ) -> Result { let cell_paths: Vec = call.rest(engine_state, stack, 0)?; let args = CellPathOnlyArgs::from(cell_paths); - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn run_const( @@ -66,7 +66,7 @@ impl Command for SubCommand { args, input, call.head, - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/strings/str_/starts_with.rs b/crates/nu-command/src/strings/str_/starts_with.rs index aec12f6f77..bac451466c 100644 --- a/crates/nu-command/src/strings/str_/starts_with.rs +++ b/crates/nu-command/src/strings/str_/starts_with.rs @@ -70,7 +70,7 @@ impl Command for SubCommand { cell_paths, case_insensitive: call.has_flag(engine_state, stack, "ignore-case")?, }; - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn run_const( @@ -92,7 +92,7 @@ impl Command for SubCommand { args, input, call.head, - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/strings/str_/stats.rs b/crates/nu-command/src/strings/str_/stats.rs index 28c65afe2b..eb091ae4f6 100644 --- a/crates/nu-command/src/strings/str_/stats.rs +++ b/crates/nu-command/src/strings/str_/stats.rs @@ -122,7 +122,7 @@ fn stats( ), } }, - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/strings/str_/substring.rs b/crates/nu-command/src/strings/str_/substring.rs index 99ba8ede1e..10464580c7 100644 --- a/crates/nu-command/src/strings/str_/substring.rs +++ b/crates/nu-command/src/strings/str_/substring.rs @@ -5,7 +5,6 @@ use nu_cmd_base::{ }; use nu_engine::command_prelude::*; use nu_protocol::{engine::StateWorkingSet, Range}; -use std::cmp::Ordering; use unicode_segmentation::UnicodeSegmentation; #[derive(Clone)] @@ -104,7 +103,7 @@ impl Command for SubCommand { cell_paths, graphemes: grapheme_flags(engine_state, stack, call)?, }; - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn run_const( @@ -134,7 +133,7 @@ impl Command for SubCommand { args, input, call.head, - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } @@ -151,6 +150,11 @@ impl Command for SubCommand { example: " '🇯🇵ほげ ふが ぴよ' | str substring --grapheme-clusters 4..5", result: Some(Value::test_string("ふが")), }, + Example { + description: "sub string by negative index", + example: " 'good nushell' | str substring 5..-2", + result: Some(Value::test_string("nushel")), + }, ] } } @@ -167,56 +171,46 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value { options.0 }; let end: isize = if options.1 < 0 { - std::cmp::max(len + options.1, 0) + options.1 + len } else { options.1 }; - if start < len && end >= 0 { - match start.cmp(&end) { - Ordering::Equal => Value::string("", head), - Ordering::Greater => Value::error( - ShellError::TypeMismatch { - err_message: "End must be greater than or equal to Start".to_string(), - span: head, - }, - head, - ), - Ordering::Less => Value::string( - { - if end == isize::MAX { - if args.graphemes { - s.graphemes(true) - .skip(start as usize) - .collect::>() - .join("") - } else { - String::from_utf8_lossy( - &s.bytes().skip(start as usize).collect::>(), - ) - .to_string() - } - } else if args.graphemes { + if start > end { + Value::string("", head) + } else { + Value::string( + { + if end == isize::MAX { + if args.graphemes { s.graphemes(true) .skip(start as usize) - .take((end - start) as usize) .collect::>() .join("") } else { String::from_utf8_lossy( - &s.bytes() - .skip(start as usize) - .take((end - start) as usize) - .collect::>(), + &s.bytes().skip(start as usize).collect::>(), ) .to_string() } - }, - head, - ), - } - } else { - Value::string("", head) + } else if args.graphemes { + s.graphemes(true) + .skip(start as usize) + .take((end - start + 1) as usize) + .collect::>() + .join("") + } else { + String::from_utf8_lossy( + &s.bytes() + .skip(start as usize) + .take((end - start + 1) as usize) + .collect::>(), + ) + .to_string() + } + }, + head, + ) } } // Propagate errors by explicitly matching them before the final case. @@ -243,6 +237,7 @@ mod tests { test_examples(SubCommand {}) } + #[derive(Debug)] struct Expectation<'a> { options: (isize, isize), expected: &'a str, @@ -266,18 +261,19 @@ mod tests { let word = Value::test_string("andres"); let cases = vec![ - expectation("a", (0, 1)), - expectation("an", (0, 2)), - expectation("and", (0, 3)), - expectation("andr", (0, 4)), - expectation("andre", (0, 5)), + expectation("a", (0, 0)), + expectation("an", (0, 1)), + expectation("and", (0, 2)), + expectation("andr", (0, 3)), + expectation("andre", (0, 4)), + expectation("andres", (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)), + expectation("a", (0, -6)), + expectation("an", (0, -5)), + expectation("and", (0, -4)), + expectation("andr", (0, -3)), + expectation("andre", (0, -2)), + expectation("andres", (0, -1)), // str substring [ -4 , _ ] // str substring -4 , expectation("dres", (-4, isize::MAX)), @@ -292,6 +288,7 @@ mod tests { ]; for expectation in &cases { + println!("{:?}", expectation); let expected = expectation.expected; let actual = action( &word, diff --git a/crates/nu-command/src/strings/str_/trim/trim_.rs b/crates/nu-command/src/strings/str_/trim/trim_.rs index 52c4017cda..76d886a010 100644 --- a/crates/nu-command/src/strings/str_/trim/trim_.rs +++ b/crates/nu-command/src/strings/str_/trim/trim_.rs @@ -190,7 +190,7 @@ fn run( cell_paths, mode, }; - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } #[derive(Debug, Copy, Clone)] diff --git a/crates/nu-command/src/system/exec.rs b/crates/nu-command/src/system/exec.rs index 35ab9428be..018017cfb2 100644 --- a/crates/nu-command/src/system/exec.rs +++ b/crates/nu-command/src/system/exec.rs @@ -39,7 +39,7 @@ On Windows based systems, Nushell will wait for the command to finish and then e let name: Spanned = call.req(engine_state, stack, 0)?; let executable = { let paths = nu_engine::env::path_str(engine_state, stack, call.head)?; - let Some(executable) = crate::which(&name.item, &paths, &cwd) else { + let Some(executable) = crate::which(&name.item, &paths, cwd.as_ref()) else { return Err(crate::command_not_found( &name.item, call.head, diff --git a/crates/nu-command/src/system/nu_check.rs b/crates/nu-command/src/system/nu_check.rs index f9e0879c00..c0c29858d5 100644 --- a/crates/nu-command/src/system/nu_check.rs +++ b/crates/nu-command/src/system/nu_check.rs @@ -1,4 +1,4 @@ -use nu_engine::{command_prelude::*, env::get_config, find_in_dirs_env, get_dirs_var_from_call}; +use nu_engine::{command_prelude::*, find_in_dirs_env, get_dirs_var_from_call}; use nu_parser::{parse, parse_module_block, parse_module_file_or_dir, unescape_unquote_string}; use nu_protocol::engine::{FileStack, StateWorkingSet}; use std::path::Path; @@ -59,7 +59,7 @@ impl Command for NuCheck { } } PipelineData::ListStream(stream, ..) => { - let config = get_config(engine_state, stack); + let config = stack.get_config(engine_state); let list_stream = stream.into_string("\n", &config); let contents = Vec::from(list_stream); @@ -87,7 +87,7 @@ impl Command for NuCheck { &path_str.item, engine_state, stack, - get_dirs_var_from_call(call), + get_dirs_var_from_call(stack, call), ) { Ok(path) => { if let Some(path) = path { diff --git a/crates/nu-command/src/system/ps.rs b/crates/nu-command/src/system/ps.rs index c64549a44d..859575abbd 100644 --- a/crates/nu-command/src/system/ps.rs +++ b/crates/nu-command/src/system/ps.rs @@ -182,5 +182,5 @@ fn run_ps( Ok(output .into_iter() - .into_pipeline_data(span, engine_state.ctrlc.clone())) + .into_pipeline_data(span, engine_state.signals().clone())) } diff --git a/crates/nu-command/src/system/registry_query.rs b/crates/nu-command/src/system/registry_query.rs index 1e2a328356..9b68059ffa 100644 --- a/crates/nu-command/src/system/registry_query.rs +++ b/crates/nu-command/src/system/registry_query.rs @@ -106,7 +106,7 @@ fn registry_query( *registry_key_span, )) } - Ok(reg_values.into_pipeline_data(call_span, engine_state.ctrlc.clone())) + Ok(reg_values.into_pipeline_data(call_span, engine_state.signals().clone())) } else { match registry_value { Some(value) => { diff --git a/crates/nu-command/src/system/run_external.rs b/crates/nu-command/src/system/run_external.rs index bc6152e7c8..0b9f1950ea 100644 --- a/crates/nu-command/src/system/run_external.rs +++ b/crates/nu-command/src/system/run_external.rs @@ -1,20 +1,17 @@ use nu_cmd_base::hook::eval_hook; use nu_engine::{command_prelude::*, env_to_strings, get_eval_expression}; -use nu_protocol::{ - ast::{Expr, Expression}, - did_you_mean, - process::ChildProcess, - ByteStream, NuGlob, OutDest, -}; +use nu_path::{dots::expand_ndots, expand_tilde}; +use nu_protocol::{did_you_mean, process::ChildProcess, ByteStream, NuGlob, OutDest, Signals}; use nu_system::ForegroundChild; use nu_utils::IgnoreCaseExt; use pathdiff::diff_paths; use std::{ borrow::Cow, + ffi::{OsStr, OsString}, io::Write, path::{Path, PathBuf}, process::Stdio, - sync::{atomic::AtomicBool, Arc}, + sync::Arc, thread, }; @@ -33,8 +30,16 @@ impl Command for External { fn signature(&self) -> nu_protocol::Signature { Signature::build(self.name()) .input_output_types(vec![(Type::Any, Type::Any)]) - .required("command", SyntaxShape::String, "External command to run.") - .rest("args", SyntaxShape::Any, "Arguments for external command.") + .required( + "command", + SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::String]), + "External command to run.", + ) + .rest( + "args", + SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::Any]), + "Arguments for external command.", + ) .category(Category::System) } @@ -47,42 +52,33 @@ impl Command for External { ) -> Result { let cwd = engine_state.cwd(Some(stack))?; - // Evaluate the command name in the same way the arguments are evaluated. Since this isn't - // a spread, it should return a one-element vec. - let name_expr = call - .positional_nth(0) - .ok_or_else(|| ShellError::MissingParameter { - param_name: "command".into(), - span: call.head, - })?; - let name = eval_argument(engine_state, stack, name_expr, false)? - .pop() - .expect("eval_argument returned zero-element vec") - .into_spanned(name_expr.span); + let name: Value = call.req(engine_state, stack, 0)?; + + let name_str: Cow = match &name { + Value::Glob { val, .. } => Cow::Borrowed(val), + Value::String { val, .. } => Cow::Borrowed(val), + _ => Cow::Owned(name.clone().coerce_into_string()?), + }; + + let expanded_name = match &name { + // Expand tilde and ndots on the name if it's a bare string / glob (#13000) + Value::Glob { no_expand, .. } if !*no_expand => { + expand_ndots_safe(expand_tilde(&*name_str)) + } + _ => Path::new(&*name_str).to_owned(), + }; // Find the absolute path to the executable. On Windows, set the // executable to "cmd.exe" if it's is a CMD internal command. If the // command is not found, display a helpful error message. - let executable = if cfg!(windows) && is_cmd_internal_command(&name.item) { + let executable = if cfg!(windows) && is_cmd_internal_command(&name_str) { PathBuf::from("cmd.exe") } else { - // Expand tilde on the name if it's a bare string (#13000) - let expanded_name = if is_bare_string(name_expr) { - expand_tilde(&name.item) - } else { - name.item.clone() - }; - // Determine the PATH to be used and then use `which` to find it - though this has no // effect if it's an absolute path already let paths = nu_engine::env::path_str(engine_state, stack, call.head)?; - let Some(executable) = which(&expanded_name, &paths, &cwd) else { - return Err(command_not_found( - &name.item, - call.head, - engine_state, - stack, - )); + let Some(executable) = which(expanded_name, &paths, cwd.as_ref()) else { + return Err(command_not_found(&name_str, call.head, engine_state, stack)); }; executable }; @@ -101,15 +97,15 @@ impl Command for External { // Configure args. let args = eval_arguments_from_call(engine_state, stack, call)?; #[cfg(windows)] - if is_cmd_internal_command(&name.item) { + if is_cmd_internal_command(&name_str) { use std::os::windows::process::CommandExt; // The /D flag disables execution of AutoRun commands from registry. // The /C flag followed by a command name instructs CMD to execute // that command and quit. - command.args(["/D", "/C", &name.item]); + command.args(["/D", "/C", &name_str]); for arg in &args { - command.raw_arg(escape_cmd_argument(arg)?.as_ref()); + command.raw_arg(escape_cmd_argument(arg)?); } } else { command.args(args.into_iter().map(|s| s.item)); @@ -217,110 +213,44 @@ impl Command for External { } } -/// Removes surrounding quotes from a string. Doesn't remove quotes from raw -/// strings. Returns the original string if it doesn't have matching quotes. -fn remove_quotes(s: &str) -> Cow<'_, str> { - let quoted_by_double_quotes = s.len() >= 2 && s.starts_with('"') && s.ends_with('"'); - let quoted_by_single_quotes = s.len() >= 2 && s.starts_with('\'') && s.ends_with('\''); - let quoted_by_backticks = s.len() >= 2 && s.starts_with('`') && s.ends_with('`'); - if quoted_by_double_quotes { - Cow::Owned(s[1..s.len() - 1].to_string().replace(r#"\""#, "\"")) - } else if quoted_by_single_quotes || quoted_by_backticks { - Cow::Borrowed(&s[1..s.len() - 1]) - } else { - Cow::Borrowed(s) - } -} - /// Evaluate all arguments from a call, performing expansions when necessary. pub fn eval_arguments_from_call( engine_state: &EngineState, stack: &mut Stack, call: &Call, -) -> Result>, ShellError> { - let ctrlc = &engine_state.ctrlc; +) -> Result>, ShellError> { let cwd = engine_state.cwd(Some(stack))?; - let mut args: Vec> = vec![]; - for (expr, spread) in call.rest_iter(1) { - if is_bare_string(expr) { - // If `expr` is a bare string, perform tilde-expansion, - // glob-expansion, and inner-quotes-removal, in that order. - for arg in eval_argument(engine_state, stack, expr, spread)? { - let tilde_expanded = expand_tilde(&arg); - for glob_expanded in expand_glob(&tilde_expanded, &cwd, expr.span, ctrlc)? { - let inner_quotes_removed = remove_inner_quotes(&glob_expanded); - args.push(inner_quotes_removed.into_owned().into_spanned(expr.span)); - } - } - } else { - for arg in eval_argument(engine_state, stack, expr, spread)? { - args.push(arg.into_spanned(expr.span)); - } + let eval_expression = get_eval_expression(engine_state); + let call_args = call.rest_iter_flattened(engine_state, stack, eval_expression, 1)?; + let mut args: Vec> = Vec::with_capacity(call_args.len()); + + for arg in call_args { + let span = arg.span(); + match arg { + // Expand globs passed to run-external + Value::Glob { val, no_expand, .. } if !no_expand => args.extend( + expand_glob(&val, cwd.as_std_path(), span, engine_state.signals())? + .into_iter() + .map(|s| s.into_spanned(span)), + ), + other => args + .push(OsString::from(coerce_into_string(engine_state, other)?).into_spanned(span)), } } Ok(args) } -/// Evaluates an expression, coercing the values to strings. -/// -/// Note: The parser currently has a special hack that retains surrounding -/// quotes for string literals in `Expression`, so that we can decide whether -/// the expression is considered a bare string. The hack doesn't affect string -/// literals within lists or records. This function will remove the quotes -/// before evaluating the expression. -fn eval_argument( - engine_state: &EngineState, - stack: &mut Stack, - expr: &Expression, - spread: bool, -) -> Result, ShellError> { - // Remove quotes from string literals. - let mut expr = expr.clone(); - if let Expr::String(s) = &expr.expr { - expr.expr = Expr::String(remove_quotes(s).into()); +/// Custom `coerce_into_string()`, including globs, since those are often args to `run-external` +/// as well +fn coerce_into_string(engine_state: &EngineState, val: Value) -> Result { + match val { + Value::List { .. } => Err(ShellError::CannotPassListToExternal { + arg: String::from_utf8_lossy(engine_state.get_span_contents(val.span())).into_owned(), + span: val.span(), + }), + Value::Glob { val, .. } => Ok(val), + _ => val.coerce_into_string(), } - - let eval = get_eval_expression(engine_state); - match eval(engine_state, stack, &expr)? { - Value::List { vals, .. } => { - if spread { - vals.into_iter() - .map(|val| val.coerce_into_string()) - .collect() - } else { - Err(ShellError::CannotPassListToExternal { - arg: String::from_utf8_lossy(engine_state.get_span_contents(expr.span)).into(), - span: expr.span, - }) - } - } - value => { - if spread { - Err(ShellError::CannotSpreadAsList { span: expr.span }) - } else { - Ok(vec![value.coerce_into_string()?]) - } - } - } -} - -/// Returns whether an expression is considered a bare string. -/// -/// Bare strings are defined as string literals that are either unquoted or -/// quoted by backticks. Raw strings or string interpolations don't count. -fn is_bare_string(expr: &Expression) -> bool { - let Expr::String(s) = &expr.expr else { - return false; - }; - let quoted_by_double_quotes = s.len() >= 2 && s.starts_with('"') && s.ends_with('"'); - let quoted_by_single_quotes = s.len() >= 2 && s.starts_with('\'') && s.ends_with('\''); - !quoted_by_double_quotes && !quoted_by_single_quotes -} - -/// Performs tilde expansion on `arg`. Returns the original string if `arg` -/// doesn't start with tilde. -fn expand_tilde(arg: &str) -> String { - nu_path::expand_tilde(arg).to_string_lossy().to_string() } /// Performs glob expansion on `arg`. If the expansion found no matches or the pattern @@ -332,28 +262,28 @@ fn expand_glob( arg: &str, cwd: &Path, span: Span, - interrupt: &Option>, -) -> Result, ShellError> { + signals: &Signals, +) -> Result, ShellError> { const GLOB_CHARS: &[char] = &['*', '?', '[']; - // Don't expand something that doesn't include the GLOB_CHARS + // For an argument that doesn't include the GLOB_CHARS, just do the `expand_tilde` + // and `expand_ndots` expansion if !arg.contains(GLOB_CHARS) { - return Ok(vec![arg.into()]); + let path = expand_ndots_safe(expand_tilde(arg)); + return Ok(vec![path.into()]); } // We must use `nu_engine::glob_from` here, in order to ensure we get paths from the correct // dir let glob = NuGlob::Expand(arg.to_owned()).into_spanned(span); if let Ok((prefix, matches)) = nu_engine::glob_from(&glob, cwd, span, None) { - let mut result = vec![]; + let mut result: Vec = vec![]; for m in matches { - if nu_utils::ctrl_c::was_pressed(interrupt) { - return Err(ShellError::InterruptedByUser { span: Some(span) }); - } + signals.check(span)?; if let Ok(arg) = m { let arg = resolve_globbed_path_to_cwd_relative(arg, prefix.as_ref(), cwd); - result.push(arg.to_string_lossy().to_string()); + result.push(arg.into()); } else { result.push(arg.into()); } @@ -392,23 +322,6 @@ fn resolve_globbed_path_to_cwd_relative( } } -/// Transforms `--option="value"` into `--option=value`. `value` can be quoted -/// with double quotes, single quotes, or backticks. Only removes the outermost -/// pair of quotes after the equal sign. -fn remove_inner_quotes(arg: &str) -> Cow<'_, str> { - // Split `arg` on the first `=`. - let Some((option, value)) = arg.split_once('=') else { - return Cow::Borrowed(arg); - }; - // Check that `option` doesn't contain quotes. - if option.contains('"') || option.contains('\'') || option.contains('`') { - return Cow::Borrowed(arg); - } - // Remove the outermost pair of quotes from `value`. - let value = remove_quotes(value); - Cow::Owned(format!("{option}={value}")) -} - /// Write `PipelineData` into `writer`. If `PipelineData` is not binary, it is /// first rendered using the `table` command. /// @@ -453,7 +366,7 @@ pub fn command_not_found( stack: &mut Stack, ) -> ShellError { // Run the `command_not_found` hook if there is one. - if let Some(hook) = &engine_state.config.hooks.command_not_found { + if let Some(hook) = &stack.get_config(engine_state).hooks.command_not_found { let mut stack = stack.start_capture(); // Set a special environment variable to avoid infinite loops when the // `command_not_found` hook triggers itself. @@ -577,7 +490,7 @@ pub fn command_not_found( /// Note: the `which.rs` crate always uses PATHEXT from the environment. As /// such, changing PATHEXT within Nushell doesn't work without updating the /// actual environment of the Nushell process. -pub fn which(name: &str, paths: &str, cwd: &Path) -> Option { +pub fn which(name: impl AsRef, paths: &str, cwd: &Path) -> Option { #[cfg(windows)] let paths = format!("{};{}", cwd.display(), paths); which::which_in(name, Some(paths), cwd).ok() @@ -593,17 +506,18 @@ fn is_cmd_internal_command(name: &str) -> bool { } /// Returns true if a string contains CMD special characters. -#[cfg(windows)] -fn has_cmd_special_character(s: &str) -> bool { - const SPECIAL_CHARS: &[char] = &['<', '>', '&', '|', '^']; - SPECIAL_CHARS.iter().any(|c| s.contains(*c)) +fn has_cmd_special_character(s: impl AsRef<[u8]>) -> bool { + s.as_ref() + .iter() + .any(|b| matches!(b, b'<' | b'>' | b'&' | b'|' | b'^')) } /// Escape an argument for CMD internal commands. The result can be safely passed to `raw_arg()`. -#[cfg(windows)] -fn escape_cmd_argument(arg: &Spanned) -> Result, ShellError> { +#[cfg_attr(not(windows), allow(dead_code))] +fn escape_cmd_argument(arg: &Spanned) -> Result, ShellError> { let Spanned { item: arg, span } = arg; - if arg.contains(['\r', '\n', '%']) { + let bytes = arg.as_encoded_bytes(); + if bytes.iter().any(|b| matches!(b, b'\r' | b'\n' | b'%')) { // \r and \n trunacte the rest of the arguments and % can expand environment variables Err(ShellError::ExternalCommand { label: @@ -612,12 +526,12 @@ fn escape_cmd_argument(arg: &Spanned) -> Result, ShellError help: "some characters currently cannot be securely escaped".into(), span: *span, }) - } else if arg.contains('"') { + } else if bytes.contains(&b'"') { // If `arg` is already quoted by double quotes, confirm there's no // embedded double quotes, then leave it as is. - if arg.chars().filter(|c| *c == '"').count() == 2 - && arg.starts_with('"') - && arg.ends_with('"') + if bytes.iter().filter(|b| **b == b'"').count() == 2 + && bytes.starts_with(b"\"") + && bytes.ends_with(b"\"") { Ok(Cow::Borrowed(arg)) } else { @@ -628,139 +542,76 @@ fn escape_cmd_argument(arg: &Spanned) -> Result, ShellError span: *span, }) } - } else if arg.contains(' ') || has_cmd_special_character(arg) { + } else if bytes.contains(&b' ') || has_cmd_special_character(bytes) { // If `arg` contains space or special characters, quote the entire argument by double quotes. - Ok(Cow::Owned(format!("\"{arg}\""))) + let mut new_str = OsString::new(); + new_str.push("\""); + new_str.push(arg); + new_str.push("\""); + Ok(Cow::Owned(new_str)) } else { // FIXME?: what if `arg.is_empty()`? Ok(Cow::Borrowed(arg)) } } +/// Expand ndots, but only if it looks like it probably contains them, because there is some lossy +/// path normalization that happens. +fn expand_ndots_safe(path: impl AsRef) -> PathBuf { + let string = path.as_ref().to_string_lossy(); + + // Use ndots if it contains at least `...`, since that's the minimum trigger point, and don't + // use it if it contains ://, because that looks like a URL scheme and the path normalization + // will mess with that. + if string.contains("...") && !string.contains("://") { + expand_ndots(path) + } else { + path.as_ref().to_owned() + } +} + #[cfg(test)] mod test { use super::*; - use nu_protocol::ast::ListItem; use nu_test_support::{fs::Stub, playground::Playground}; - #[test] - fn test_remove_quotes() { - assert_eq!(remove_quotes(r#""#), r#""#); - assert_eq!(remove_quotes(r#"'"#), r#"'"#); - assert_eq!(remove_quotes(r#"''"#), r#""#); - assert_eq!(remove_quotes(r#""foo""#), r#"foo"#); - assert_eq!(remove_quotes(r#"`foo '"' bar`"#), r#"foo '"' bar"#); - assert_eq!(remove_quotes(r#"'foo' bar"#), r#"'foo' bar"#); - assert_eq!(remove_quotes(r#"r#'foo'#"#), r#"r#'foo'#"#); - assert_eq!(remove_quotes(r#""foo\" bar""#), r#"foo" bar"#); - } - - #[test] - fn test_eval_argument() { - fn expression(expr: Expr) -> Expression { - Expression::new_unknown(expr, Span::unknown(), Type::Any) - } - - fn eval(expr: Expr, spread: bool) -> Result, ShellError> { - let engine_state = EngineState::new(); - let mut stack = Stack::new(); - eval_argument(&engine_state, &mut stack, &expression(expr), spread) - } - - let actual = eval(Expr::String("".into()), false).unwrap(); - let expected = &[""]; - assert_eq!(actual, expected); - - let actual = eval(Expr::String("'foo'".into()), false).unwrap(); - let expected = &["foo"]; - assert_eq!(actual, expected); - - let actual = eval(Expr::RawString("'foo'".into()), false).unwrap(); - let expected = &["'foo'"]; - assert_eq!(actual, expected); - - let actual = eval(Expr::List(vec![]), true).unwrap(); - let expected: &[&str] = &[]; - assert_eq!(actual, expected); - - let actual = eval( - Expr::List(vec![ - ListItem::Item(expression(Expr::String("'foo'".into()))), - ListItem::Item(expression(Expr::String("bar".into()))), - ]), - true, - ) - .unwrap(); - let expected = &["'foo'", "bar"]; - assert_eq!(actual, expected); - - eval(Expr::String("".into()), true).unwrap_err(); - eval(Expr::List(vec![]), false).unwrap_err(); - } - #[test] fn test_expand_glob() { Playground::setup("test_expand_glob", |dirs, play| { play.with_files(&[Stub::EmptyFile("a.txt"), Stub::EmptyFile("b.txt")]); - let cwd = dirs.test(); + let cwd = dirs.test().as_std_path(); - let actual = expand_glob("*.txt", cwd, Span::unknown(), &None).unwrap(); + let actual = expand_glob("*.txt", cwd, Span::unknown(), &Signals::empty()).unwrap(); let expected = &["a.txt", "b.txt"]; assert_eq!(actual, expected); - let actual = expand_glob("./*.txt", cwd, Span::unknown(), &None).unwrap(); + let actual = expand_glob("./*.txt", cwd, Span::unknown(), &Signals::empty()).unwrap(); assert_eq!(actual, expected); - let actual = expand_glob("'*.txt'", cwd, Span::unknown(), &None).unwrap(); + let actual = expand_glob("'*.txt'", cwd, Span::unknown(), &Signals::empty()).unwrap(); let expected = &["'*.txt'"]; assert_eq!(actual, expected); - let actual = expand_glob(".", cwd, Span::unknown(), &None).unwrap(); + let actual = expand_glob(".", cwd, Span::unknown(), &Signals::empty()).unwrap(); let expected = &["."]; assert_eq!(actual, expected); - let actual = expand_glob("./a.txt", cwd, Span::unknown(), &None).unwrap(); + let actual = expand_glob("./a.txt", cwd, Span::unknown(), &Signals::empty()).unwrap(); let expected = &["./a.txt"]; assert_eq!(actual, expected); - let actual = expand_glob("[*.txt", cwd, Span::unknown(), &None).unwrap(); + let actual = expand_glob("[*.txt", cwd, Span::unknown(), &Signals::empty()).unwrap(); let expected = &["[*.txt"]; assert_eq!(actual, expected); + + let actual = expand_glob("~/foo.txt", cwd, Span::unknown(), &Signals::empty()).unwrap(); + let home = dirs::home_dir().expect("failed to get home dir"); + let expected: Vec = vec![home.join("foo.txt").into()]; + assert_eq!(actual, expected); }) } - #[test] - fn test_remove_inner_quotes() { - let actual = remove_inner_quotes(r#"--option=value"#); - let expected = r#"--option=value"#; - assert_eq!(actual, expected); - - let actual = remove_inner_quotes(r#"--option="value""#); - let expected = r#"--option=value"#; - assert_eq!(actual, expected); - - let actual = remove_inner_quotes(r#"--option='value'"#); - let expected = r#"--option=value"#; - assert_eq!(actual, expected); - - let actual = remove_inner_quotes(r#"--option "value""#); - let expected = r#"--option "value""#; - assert_eq!(actual, expected); - - let actual = remove_inner_quotes(r#"-option="value""#); - let expected = r#"-option=value"#; - assert_eq!(actual, expected); - - let actual = remove_inner_quotes(r#"option="value""#); - let expected = r#"option=value"#; - assert_eq!(actual, expected); - - let actual = remove_inner_quotes(r#"option="v\"value""#); - let expected = r#"option=v"value"#; - assert_eq!(actual, expected); - } - #[test] fn test_write_pipeline_data() { let engine_state = EngineState::new(); @@ -786,7 +637,7 @@ mod test { ByteStream::read( b"foo".as_slice(), Span::unknown(), - None, + Signals::empty(), ByteStreamType::Unknown, ), None, diff --git a/crates/nu-command/src/system/sys/cpu.rs b/crates/nu-command/src/system/sys/cpu.rs index b2f6d6afa6..d24197bf70 100644 --- a/crates/nu-command/src/system/sys/cpu.rs +++ b/crates/nu-command/src/system/sys/cpu.rs @@ -1,4 +1,6 @@ +use super::trim_cstyle_null; use nu_engine::command_prelude::*; +use sysinfo::{CpuRefreshKind, System, MINIMUM_CPU_UPDATE_INTERVAL}; #[derive(Clone)] pub struct SysCpu; @@ -26,7 +28,7 @@ impl Command for SysCpu { call: &Call, _input: PipelineData, ) -> Result { - Ok(super::cpu(call.head).into_pipeline_data()) + Ok(cpu(call.head).into_pipeline_data()) } fn examples(&self) -> Vec { @@ -37,3 +39,42 @@ impl Command for SysCpu { }] } } + +fn cpu(span: Span) -> Value { + let mut sys = System::new(); + sys.refresh_cpu_specifics(CpuRefreshKind::everything()); + // We must refresh the CPU twice a while apart to get valid usage data. + // In theory we could just sleep MINIMUM_CPU_UPDATE_INTERVAL, but I've noticed that + // that gives poor results (error of ~5%). Decided to wait 2x that long, somewhat arbitrarily + std::thread::sleep(MINIMUM_CPU_UPDATE_INTERVAL * 2); + sys.refresh_cpu_specifics(CpuRefreshKind::new().with_cpu_usage()); + + let cpus = sys + .cpus() + .iter() + .map(|cpu| { + // sysinfo CPU usage numbers are not very precise unless you wait a long time between refreshes. + // Round to 1DP (chosen somewhat arbitrarily) so people aren't misled by high-precision floats. + let rounded_usage = (cpu.cpu_usage() * 10.0).round() / 10.0; + + let load_avg = System::load_average(); + let load_avg = format!( + "{:.2}, {:.2}, {:.2}", + load_avg.one, load_avg.five, load_avg.fifteen + ); + + let record = record! { + "name" => Value::string(trim_cstyle_null(cpu.name()), span), + "brand" => Value::string(trim_cstyle_null(cpu.brand()), span), + "freq" => Value::int(cpu.frequency() as i64, span), + "cpu_usage" => Value::float(rounded_usage.into(), span), + "load_average" => Value::string(load_avg, span), + "vendor_id" => Value::string(trim_cstyle_null(cpu.vendor_id()), span), + }; + + Value::record(record, span) + }) + .collect(); + + Value::list(cpus, span) +} diff --git a/crates/nu-command/src/system/sys/disks.rs b/crates/nu-command/src/system/sys/disks.rs index 66991c9087..0d9ce09db3 100644 --- a/crates/nu-command/src/system/sys/disks.rs +++ b/crates/nu-command/src/system/sys/disks.rs @@ -1,4 +1,6 @@ +use super::trim_cstyle_null; use nu_engine::command_prelude::*; +use sysinfo::Disks; #[derive(Clone)] pub struct SysDisks; @@ -26,7 +28,7 @@ impl Command for SysDisks { call: &Call, _input: PipelineData, ) -> Result { - Ok(super::disks(call.head).into_pipeline_data()) + Ok(disks(call.head).into_pipeline_data()) } fn examples(&self) -> Vec { @@ -37,3 +39,27 @@ impl Command for SysDisks { }] } } + +fn disks(span: Span) -> Value { + let disks = Disks::new_with_refreshed_list() + .iter() + .map(|disk| { + let device = trim_cstyle_null(disk.name().to_string_lossy()); + let typ = trim_cstyle_null(disk.file_system().to_string_lossy()); + + let record = record! { + "device" => Value::string(device, span), + "type" => Value::string(typ, span), + "mount" => Value::string(disk.mount_point().to_string_lossy(), span), + "total" => Value::filesize(disk.total_space() as i64, span), + "free" => Value::filesize(disk.available_space() as i64, span), + "removable" => Value::bool(disk.is_removable(), span), + "kind" => Value::string(disk.kind().to_string(), span), + }; + + Value::record(record, span) + }) + .collect(); + + Value::list(disks, span) +} diff --git a/crates/nu-command/src/system/sys/host.rs b/crates/nu-command/src/system/sys/host.rs index 969f59ef99..64dd43424d 100644 --- a/crates/nu-command/src/system/sys/host.rs +++ b/crates/nu-command/src/system/sys/host.rs @@ -1,4 +1,7 @@ +use super::trim_cstyle_null; +use chrono::{DateTime, FixedOffset, Local}; use nu_engine::command_prelude::*; +use sysinfo::System; #[derive(Clone)] pub struct SysHost; @@ -26,8 +29,7 @@ impl Command for SysHost { call: &Call, _input: PipelineData, ) -> Result { - let host = super::host(call.head); - Ok(Value::record(host, call.head).into_pipeline_data()) + Ok(host(call.head).into_pipeline_data()) } fn examples(&self) -> Vec { @@ -38,3 +40,53 @@ impl Command for SysHost { }] } } + +fn host(span: Span) -> Value { + let mut record = Record::new(); + + if let Some(name) = System::name() { + record.push("name", Value::string(trim_cstyle_null(name), span)); + } + if let Some(version) = System::os_version() { + record.push("os_version", Value::string(trim_cstyle_null(version), span)); + } + if let Some(long_version) = System::long_os_version() { + record.push( + "long_os_version", + Value::string(trim_cstyle_null(long_version), span), + ); + } + if let Some(version) = System::kernel_version() { + record.push( + "kernel_version", + Value::string(trim_cstyle_null(version), span), + ); + } + if let Some(hostname) = System::host_name() { + record.push("hostname", Value::string(trim_cstyle_null(hostname), span)); + } + + let uptime = System::uptime() + .saturating_mul(1_000_000_000) + .try_into() + .unwrap_or(i64::MAX); + + record.push("uptime", Value::duration(uptime, span)); + + let boot_time = boot_time() + .map(|time| Value::date(time, span)) + .unwrap_or(Value::nothing(span)); + + record.push("boot_time", boot_time); + + Value::record(record, span) +} + +fn boot_time() -> Option> { + // Broken systems can apparently return really high values. + // See: https://github.com/nushell/nushell/issues/10155 + // First, try to convert u64 to i64, and then try to create a `DateTime`. + let secs = System::boot_time().try_into().ok()?; + let time = DateTime::from_timestamp(secs, 0)?; + Some(time.with_timezone(&Local).fixed_offset()) +} diff --git a/crates/nu-command/src/system/sys/mem.rs b/crates/nu-command/src/system/sys/mem.rs index e5dc36e1b7..89527807d7 100644 --- a/crates/nu-command/src/system/sys/mem.rs +++ b/crates/nu-command/src/system/sys/mem.rs @@ -1,4 +1,5 @@ use nu_engine::command_prelude::*; +use sysinfo::System; #[derive(Clone)] pub struct SysMem; @@ -26,7 +27,7 @@ impl Command for SysMem { call: &Call, _input: PipelineData, ) -> Result { - Ok(super::mem(call.head).into_pipeline_data()) + Ok(mem(call.head).into_pipeline_data()) } fn examples(&self) -> Vec { @@ -37,3 +38,20 @@ impl Command for SysMem { }] } } + +fn mem(span: Span) -> Value { + let mut sys = System::new(); + sys.refresh_memory(); + + let record = record! { + "total" => Value::filesize(sys.total_memory() as i64, span), + "free" => Value::filesize(sys.free_memory() as i64, span), + "used" => Value::filesize(sys.used_memory() as i64, span), + "available" => Value::filesize(sys.available_memory() as i64, span), + "swap total" => Value::filesize(sys.total_swap() as i64, span), + "swap free" => Value::filesize(sys.free_swap() as i64, span), + "swap used" => Value::filesize(sys.used_swap() as i64, span), + }; + + Value::record(record, span) +} diff --git a/crates/nu-command/src/system/sys/mod.rs b/crates/nu-command/src/system/sys/mod.rs index fcb40fd37d..02d271537e 100644 --- a/crates/nu-command/src/system/sys/mod.rs +++ b/crates/nu-command/src/system/sys/mod.rs @@ -16,202 +16,6 @@ pub use sys_::Sys; pub use temp::SysTemp; pub use users::SysUsers; -use chrono::{DateTime, FixedOffset, Local}; -use nu_protocol::{record, Record, Span, Value}; -use sysinfo::{ - Components, CpuRefreshKind, Disks, Networks, System, Users, MINIMUM_CPU_UPDATE_INTERVAL, -}; - -pub fn trim_cstyle_null(s: impl AsRef) -> String { +fn trim_cstyle_null(s: impl AsRef) -> String { s.as_ref().trim_matches('\0').into() } - -pub fn disks(span: Span) -> Value { - let disks = Disks::new_with_refreshed_list() - .iter() - .map(|disk| { - let device = trim_cstyle_null(disk.name().to_string_lossy()); - let typ = trim_cstyle_null(disk.file_system().to_string_lossy()); - - let record = record! { - "device" => Value::string(device, span), - "type" => Value::string(typ, span), - "mount" => Value::string(disk.mount_point().to_string_lossy(), span), - "total" => Value::filesize(disk.total_space() as i64, span), - "free" => Value::filesize(disk.available_space() as i64, span), - "removable" => Value::bool(disk.is_removable(), span), - "kind" => Value::string(disk.kind().to_string(), span), - }; - - Value::record(record, span) - }) - .collect(); - - Value::list(disks, span) -} - -pub fn net(span: Span) -> Value { - let networks = Networks::new_with_refreshed_list() - .iter() - .map(|(iface, data)| { - let record = record! { - "name" => Value::string(trim_cstyle_null(iface), span), - "sent" => Value::filesize(data.total_transmitted() as i64, span), - "recv" => Value::filesize(data.total_received() as i64, span), - }; - - Value::record(record, span) - }) - .collect(); - - Value::list(networks, span) -} - -pub fn cpu(span: Span) -> Value { - let mut sys = System::new(); - sys.refresh_cpu_specifics(CpuRefreshKind::everything()); - // We must refresh the CPU twice a while apart to get valid usage data. - // In theory we could just sleep MINIMUM_CPU_UPDATE_INTERVAL, but I've noticed that - // that gives poor results (error of ~5%). Decided to wait 2x that long, somewhat arbitrarily - std::thread::sleep(MINIMUM_CPU_UPDATE_INTERVAL * 2); - sys.refresh_cpu_specifics(CpuRefreshKind::new().with_cpu_usage()); - - let cpus = sys - .cpus() - .iter() - .map(|cpu| { - // sysinfo CPU usage numbers are not very precise unless you wait a long time between refreshes. - // Round to 1DP (chosen somewhat arbitrarily) so people aren't misled by high-precision floats. - let rounded_usage = (cpu.cpu_usage() * 10.0).round() / 10.0; - - let load_avg = System::load_average(); - let load_avg = format!( - "{:.2}, {:.2}, {:.2}", - load_avg.one, load_avg.five, load_avg.fifteen - ); - - let record = record! { - "name" => Value::string(trim_cstyle_null(cpu.name()), span), - "brand" => Value::string(trim_cstyle_null(cpu.brand()), span), - "freq" => Value::int(cpu.frequency() as i64, span), - "cpu_usage" => Value::float(rounded_usage.into(), span), - "load_average" => Value::string(load_avg, span), - "vendor_id" => Value::string(trim_cstyle_null(cpu.vendor_id()), span), - }; - - Value::record(record, span) - }) - .collect(); - - Value::list(cpus, span) -} - -pub fn mem(span: Span) -> Value { - let mut sys = System::new(); - sys.refresh_memory(); - - let record = record! { - "total" => Value::filesize(sys.total_memory() as i64, span), - "free" => Value::filesize(sys.free_memory() as i64, span), - "used" => Value::filesize(sys.used_memory() as i64, span), - "available" => Value::filesize(sys.available_memory() as i64, span), - "swap total" => Value::filesize(sys.total_swap() as i64, span), - "swap free" => Value::filesize(sys.free_swap() as i64, span), - "swap used" => Value::filesize(sys.used_swap() as i64, span), - }; - - Value::record(record, span) -} - -pub fn users(span: Span) -> Value { - let users = Users::new_with_refreshed_list() - .iter() - .map(|user| { - let groups = user - .groups() - .iter() - .map(|group| Value::string(trim_cstyle_null(group.name()), span)) - .collect(); - - let record = record! { - "name" => Value::string(trim_cstyle_null(user.name()), span), - "groups" => Value::list(groups, span), - }; - - Value::record(record, span) - }) - .collect(); - - Value::list(users, span) -} - -pub fn host(span: Span) -> Record { - let mut record = Record::new(); - - if let Some(name) = System::name() { - record.push("name", Value::string(trim_cstyle_null(name), span)); - } - if let Some(version) = System::os_version() { - record.push("os_version", Value::string(trim_cstyle_null(version), span)); - } - if let Some(long_version) = System::long_os_version() { - record.push( - "long_os_version", - Value::string(trim_cstyle_null(long_version), span), - ); - } - if let Some(version) = System::kernel_version() { - record.push( - "kernel_version", - Value::string(trim_cstyle_null(version), span), - ); - } - if let Some(hostname) = System::host_name() { - record.push("hostname", Value::string(trim_cstyle_null(hostname), span)); - } - - let uptime = System::uptime() - .saturating_mul(1_000_000_000) - .try_into() - .unwrap_or(i64::MAX); - - record.push("uptime", Value::duration(uptime, span)); - - let boot_time = boot_time() - .map(|time| Value::date(time, span)) - .unwrap_or(Value::nothing(span)); - - record.push("boot_time", boot_time); - - record -} - -fn boot_time() -> Option> { - // Broken systems can apparently return really high values. - // See: https://github.com/nushell/nushell/issues/10155 - // First, try to convert u64 to i64, and then try to create a `DateTime`. - let secs = System::boot_time().try_into().ok()?; - let time = DateTime::from_timestamp(secs, 0)?; - Some(time.with_timezone(&Local).fixed_offset()) -} - -pub fn temp(span: Span) -> Value { - let components = Components::new_with_refreshed_list() - .iter() - .map(|component| { - let mut record = record! { - "unit" => Value::string(component.label(), span), - "temp" => Value::float(component.temperature().into(), span), - "high" => Value::float(component.max().into(), span), - }; - - if let Some(critical) = component.critical() { - record.push("critical", Value::float(critical.into(), span)); - } - - Value::record(record, span) - }) - .collect(); - - Value::list(components, span) -} diff --git a/crates/nu-command/src/system/sys/net.rs b/crates/nu-command/src/system/sys/net.rs index 98dca1bc2f..ef1c595800 100644 --- a/crates/nu-command/src/system/sys/net.rs +++ b/crates/nu-command/src/system/sys/net.rs @@ -1,4 +1,6 @@ +use super::trim_cstyle_null; use nu_engine::command_prelude::*; +use sysinfo::Networks; #[derive(Clone)] pub struct SysNet; @@ -26,7 +28,7 @@ impl Command for SysNet { call: &Call, _input: PipelineData, ) -> Result { - Ok(super::net(call.head).into_pipeline_data()) + Ok(net(call.head).into_pipeline_data()) } fn examples(&self) -> Vec { @@ -37,3 +39,20 @@ impl Command for SysNet { }] } } + +fn net(span: Span) -> Value { + let networks = Networks::new_with_refreshed_list() + .iter() + .map(|(iface, data)| { + let record = record! { + "name" => Value::string(trim_cstyle_null(iface), span), + "sent" => Value::filesize(data.total_transmitted() as i64, span), + "recv" => Value::filesize(data.total_received() as i64, span), + }; + + Value::record(record, span) + }) + .collect(); + + Value::list(networks, span) +} diff --git a/crates/nu-command/src/system/sys/sys_.rs b/crates/nu-command/src/system/sys/sys_.rs index 2886836be9..5369064f50 100644 --- a/crates/nu-command/src/system/sys/sys_.rs +++ b/crates/nu-command/src/system/sys/sys_.rs @@ -1,4 +1,4 @@ -use nu_engine::command_prelude::*; +use nu_engine::{command_prelude::*, get_full_help}; #[derive(Clone)] pub struct Sys; @@ -20,41 +20,17 @@ impl Command for Sys { } fn extra_usage(&self) -> &str { - "Note that this command may take a noticeable amount of time to run. To reduce the time taken, you can use the various `sys` sub commands to get the subset of information you are interested in." + "You must use one of the following subcommands. Using this command as-is will only produce this help message." } fn run( &self, engine_state: &EngineState, - _stack: &mut Stack, + stack: &mut Stack, call: &Call, _input: PipelineData, ) -> Result { - nu_protocol::report_error_new( - engine_state, - &ShellError::GenericError { - error: "Deprecated command".into(), - msg: "the `sys` command is deprecated, please use the new subcommands (`sys host`, `sys mem`, etc.)." - .into(), - span: Some(call.head), - help: None, - inner: vec![], - }, - ); - - let head = call.head; - - let mut host = super::host(head); - host.push("sessions", super::users(head)); - let record = record! { - "host" => Value::record(host, head), - "cpu" => super::cpu(head), - "disks" => super::disks(head), - "mem" => super::mem(head), - "temp" => super::temp(head), - "net" => super::net(head), - }; - Ok(Value::record(record, head).into_pipeline_data()) + Ok(Value::string(get_full_help(self, engine_state, stack), call.head).into_pipeline_data()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/system/sys/temp.rs b/crates/nu-command/src/system/sys/temp.rs index eaf1ed05db..088471f0fd 100644 --- a/crates/nu-command/src/system/sys/temp.rs +++ b/crates/nu-command/src/system/sys/temp.rs @@ -1,4 +1,5 @@ use nu_engine::command_prelude::*; +use sysinfo::Components; #[derive(Clone)] pub struct SysTemp; @@ -30,7 +31,7 @@ impl Command for SysTemp { call: &Call, _input: PipelineData, ) -> Result { - Ok(super::temp(call.head).into_pipeline_data()) + Ok(temp(call.head).into_pipeline_data()) } fn examples(&self) -> Vec { @@ -41,3 +42,24 @@ impl Command for SysTemp { }] } } + +fn temp(span: Span) -> Value { + let components = Components::new_with_refreshed_list() + .iter() + .map(|component| { + let mut record = record! { + "unit" => Value::string(component.label(), span), + "temp" => Value::float(component.temperature().into(), span), + "high" => Value::float(component.max().into(), span), + }; + + if let Some(critical) = component.critical() { + record.push("critical", Value::float(critical.into(), span)); + } + + Value::record(record, span) + }) + .collect(); + + Value::list(components, span) +} diff --git a/crates/nu-command/src/system/sys/users.rs b/crates/nu-command/src/system/sys/users.rs index 9aab2b9b7b..04d9b9db91 100644 --- a/crates/nu-command/src/system/sys/users.rs +++ b/crates/nu-command/src/system/sys/users.rs @@ -1,4 +1,6 @@ +use super::trim_cstyle_null; use nu_engine::command_prelude::*; +use sysinfo::Users; #[derive(Clone)] pub struct SysUsers; @@ -11,7 +13,7 @@ impl Command for SysUsers { fn signature(&self) -> Signature { Signature::build("sys users") .category(Category::System) - .input_output_types(vec![(Type::Nothing, Type::record())]) + .input_output_types(vec![(Type::Nothing, Type::table())]) } fn usage(&self) -> &str { @@ -25,7 +27,7 @@ impl Command for SysUsers { call: &Call, _input: PipelineData, ) -> Result { - Ok(super::users(call.head).into_pipeline_data()) + Ok(users(call.head).into_pipeline_data()) } fn examples(&self) -> Vec { @@ -36,3 +38,25 @@ impl Command for SysUsers { }] } } + +fn users(span: Span) -> Value { + let users = Users::new_with_refreshed_list() + .iter() + .map(|user| { + let groups = user + .groups() + .iter() + .map(|group| Value::string(trim_cstyle_null(group.name()), span)) + .collect(); + + let record = record! { + "name" => Value::string(trim_cstyle_null(user.name()), span), + "groups" => Value::list(groups, span), + }; + + Value::record(record, span) + }) + .collect(); + + Value::list(users, span) +} diff --git a/crates/nu-command/src/system/uname.rs b/crates/nu-command/src/system/uname.rs index e267fcaeb2..0bcb749f02 100644 --- a/crates/nu-command/src/system/uname.rs +++ b/crates/nu-command/src/system/uname.rs @@ -1,10 +1,5 @@ -use nu_protocol::record; -use nu_protocol::Value; -use nu_protocol::{ - ast::Call, - engine::{Command, EngineState, Stack}, - Category, Example, PipelineData, ShellError, Signature, Type, -}; +use nu_engine::command_prelude::*; +use nu_protocol::{record, Value}; #[derive(Clone)] pub struct UName; diff --git a/crates/nu-command/src/system/which_.rs b/crates/nu-command/src/system/which_.rs index 81b6057864..fd5b0beb18 100644 --- a/crates/nu-command/src/system/which_.rs +++ b/crates/nu-command/src/system/which_.rs @@ -92,7 +92,6 @@ fn get_entries_in_nu( all_entries } -#[cfg(feature = "which-support")] fn get_first_entry_in_path( item: &str, span: Span, @@ -104,17 +103,6 @@ fn get_first_entry_in_path( .ok() } -#[cfg(not(feature = "which-support"))] -fn get_first_entry_in_path( - _item: &str, - _span: Span, - _cwd: impl AsRef, - _paths: impl AsRef, -) -> Option { - None -} - -#[cfg(feature = "which-support")] fn get_all_entries_in_path( item: &str, span: Span, @@ -129,16 +117,6 @@ fn get_all_entries_in_path( .unwrap_or_default() } -#[cfg(not(feature = "which-support"))] -fn get_all_entries_in_path( - _item: &str, - _span: Span, - _cwd: impl AsRef, - _paths: impl AsRef, -) -> Vec { - vec![] -} - #[derive(Debug)] struct WhichArgs { applications: Vec>, @@ -210,7 +188,6 @@ fn which( applications: call.rest(engine_state, stack, 0)?, all: call.has_flag(engine_state, stack, "all")?, }; - let ctrlc = engine_state.ctrlc.clone(); if which_args.applications.is_empty() { return Err(ShellError::MissingParameter { @@ -236,7 +213,9 @@ fn which( output.extend(values); } - Ok(output.into_iter().into_pipeline_data(head, ctrlc)) + Ok(output + .into_iter() + .into_pipeline_data(head, engine_state.signals().clone())) } #[cfg(test)] diff --git a/crates/nu-command/src/viewers/griddle.rs b/crates/nu-command/src/viewers/griddle.rs index 8b39bf5649..c3e003be39 100644 --- a/crates/nu-command/src/viewers/griddle.rs +++ b/crates/nu-command/src/viewers/griddle.rs @@ -61,7 +61,7 @@ prints out the list properly."# let width_param: Option = call.get_flag(engine_state, stack, "width")?; let color_param: bool = call.has_flag(engine_state, stack, "color")?; let separator_param: Option = call.get_flag(engine_state, stack, "separator")?; - let config = engine_state.get_config(); + let config = &stack.get_config(engine_state); let env_str = match stack.get_env_var(engine_state, "LS_COLORS") { Some(v) => Some(env_to_string("LS_COLORS", &v, engine_state, stack)?), None => None, @@ -83,7 +83,7 @@ prints out the list properly."# separator_param, env_str, use_grid_icons, - &cwd, + cwd.as_ref(), )?) } else { Ok(PipelineData::empty()) @@ -101,7 +101,7 @@ prints out the list properly."# separator_param, env_str, use_grid_icons, - &cwd, + cwd.as_ref(), )?) } else { // dbg!(data); @@ -124,7 +124,7 @@ prints out the list properly."# separator_param, env_str, use_grid_icons, - &cwd, + cwd.as_ref(), )?) } x => { diff --git a/crates/nu-command/src/viewers/table.rs b/crates/nu-command/src/viewers/table.rs index 2fe9319821..e5fdf681f7 100644 --- a/crates/nu-command/src/viewers/table.rs +++ b/crates/nu-command/src/viewers/table.rs @@ -4,10 +4,10 @@ use lscolors::{LsColors, Style}; use nu_color_config::{color_from_hex, StyleComputer, TextStyle}; -use nu_engine::{command_prelude::*, env::get_config, env_to_string}; +use nu_engine::{command_prelude::*, env_to_string}; use nu_pretty_hex::HexConfig; use nu_protocol::{ - ByteStream, Config, DataSource, ListStream, PipelineMetadata, TableMode, ValueIterator, + ByteStream, Config, DataSource, ListStream, PipelineMetadata, Signals, TableMode, ValueIterator, }; use nu_table::{ common::create_nu_table_config, CollapsedTable, ExpandedTable, JustTable, NuTable, NuTableCell, @@ -19,7 +19,6 @@ use std::{ io::{IsTerminal, Read}, path::PathBuf, str::FromStr, - sync::{atomic::AtomicBool, Arc}, time::Instant, }; use terminal_size::{Height, Width}; @@ -170,7 +169,10 @@ impl Command for Table { }), Value::test_record(record! { "a" => Value::test_int(3), - "b" => Value::test_int(4), + "b" => Value::test_list(vec![ + Value::test_int(4), + Value::test_int(4), + ]) }), ])), }, @@ -184,7 +186,10 @@ impl Command for Table { }), Value::test_record(record! { "a" => Value::test_int(3), - "b" => Value::test_int(4), + "b" => Value::test_list(vec![ + Value::test_int(4), + Value::test_int(4), + ]) }), ])), }, @@ -253,7 +258,7 @@ fn parse_table_config( let flatten_separator: Option = call.get_flag(state, stack, "flatten-separator")?; let abbrivation: Option = call .get_flag(state, stack, "abbreviated")? - .or_else(|| get_config(state, stack).table_abbreviation_threshold); + .or_else(|| stack.get_config(state).table_abbreviation_threshold); let table_view = match (expand, collapse) { (false, false) => TableView::General, (_, true) => TableView::Collapsed, @@ -264,7 +269,7 @@ fn parse_table_config( }, }; let theme = - get_theme_flag(call, state, stack)?.unwrap_or_else(|| get_config(state, stack).table_mode); + get_theme_flag(call, state, stack)?.unwrap_or_else(|| stack.get_config(state).table_mode); let index = get_index_flag(call, state, stack)?; let term_width = get_width_param(width_param); @@ -339,7 +344,7 @@ fn get_theme_flag( struct CmdInput<'a> { engine_state: &'a EngineState, stack: &'a mut Stack, - call: &'a Call, + call: &'a Call<'a>, data: PipelineData, } @@ -347,7 +352,7 @@ impl<'a> CmdInput<'a> { fn new( engine_state: &'a EngineState, stack: &'a mut Stack, - call: &'a Call, + call: &'a Call<'a>, data: PipelineData, ) -> Self { Self { @@ -371,8 +376,8 @@ fn handle_table_command( ), PipelineData::ByteStream(..) => Ok(input.data), PipelineData::Value(Value::Binary { val, .. }, ..) => { - let ctrlc = input.engine_state.ctrlc.clone(); - let stream = ByteStream::read_binary(val, input.call.head, ctrlc); + let signals = input.engine_state.signals().clone(); + let stream = ByteStream::read_binary(val, input.call.head, signals); Ok(PipelineData::ByteStream( pretty_hex_stream(stream, input.call.head), None, @@ -380,8 +385,8 @@ fn handle_table_command( } // None of these two receive a StyleComputer because handle_row_stream() can produce it by itself using engine_state and stack. PipelineData::Value(Value::List { vals, .. }, metadata) => { - let ctrlc = input.engine_state.ctrlc.clone(); - let stream = ListStream::new(vals.into_iter(), span, ctrlc); + let signals = input.engine_state.signals().clone(); + let stream = ListStream::new(vals.into_iter(), span, signals); input.data = PipelineData::Empty; handle_row_stream(input, cfg, stream, metadata) @@ -404,8 +409,9 @@ fn handle_table_command( Table.run(input.engine_state, input.stack, input.call, base_pipeline) } PipelineData::Value(Value::Range { val, .. }, metadata) => { - let ctrlc = input.engine_state.ctrlc.clone(); - let stream = ListStream::new(val.into_range_iter(span, ctrlc), span, None); + let signals = input.engine_state.signals().clone(); + let stream = + ListStream::new(val.into_range_iter(span, Signals::empty()), span, signals); input.data = PipelineData::Empty; handle_row_stream(input, cfg, stream, metadata) } @@ -431,50 +437,55 @@ fn pretty_hex_stream(stream: ByteStream, span: Span) -> ByteStream { reader } else { // No stream to read from - return ByteStream::read_string("".into(), span, None); + return ByteStream::read_string("".into(), span, Signals::empty()); }; - ByteStream::from_fn(span, None, ByteStreamType::String, move |buffer| { - // Turn the buffer into a String we can write to - let mut write_buf = std::mem::take(buffer); - write_buf.clear(); - // SAFETY: we just truncated it empty - let mut write_buf = unsafe { String::from_utf8_unchecked(write_buf) }; + ByteStream::from_fn( + span, + Signals::empty(), + ByteStreamType::String, + move |buffer| { + // Turn the buffer into a String we can write to + let mut write_buf = std::mem::take(buffer); + write_buf.clear(); + // SAFETY: we just truncated it empty + let mut write_buf = unsafe { String::from_utf8_unchecked(write_buf) }; - // Write the title at the beginning - if cfg.title { - nu_pretty_hex::write_title(&mut write_buf, cfg, true).expect("format error"); - cfg.title = false; - - // Put the write_buf back into buffer - *buffer = write_buf.into_bytes(); - - Ok(true) - } else { - // Read up to `cfg.width` bytes - read_buf.clear(); - (&mut reader) - .take(cfg.width as u64) - .read_to_end(&mut read_buf) - .err_span(span)?; - - if !read_buf.is_empty() { - nu_pretty_hex::hex_write(&mut write_buf, &read_buf, cfg, Some(true)) - .expect("format error"); - write_buf.push('\n'); - - // Advance the address offset for next time - cfg.address_offset += read_buf.len(); + // Write the title at the beginning + if cfg.title { + nu_pretty_hex::write_title(&mut write_buf, cfg, true).expect("format error"); + cfg.title = false; // Put the write_buf back into buffer *buffer = write_buf.into_bytes(); Ok(true) } else { - Ok(false) + // Read up to `cfg.width` bytes + read_buf.clear(); + (&mut reader) + .take(cfg.width as u64) + .read_to_end(&mut read_buf) + .err_span(span)?; + + if !read_buf.is_empty() { + nu_pretty_hex::hex_write(&mut write_buf, &read_buf, cfg, Some(true)) + .expect("format error"); + write_buf.push('\n'); + + // Advance the address offset for next time + cfg.address_offset += read_buf.len(); + + // Put the write_buf back into buffer + *buffer = write_buf.into_bytes(); + + Ok(true) + } else { + Ok(false) + } } - } - }) + }, + ) } fn handle_record( @@ -482,11 +493,13 @@ fn handle_record( cfg: TableConfig, mut record: Record, ) -> Result { - let config = get_config(input.engine_state, input.stack); + let config = { + let state = input.engine_state; + let stack: &Stack = input.stack; + stack.get_config(state) + }; let span = input.data.span().unwrap_or(input.call.head); let styles = &StyleComputer::from_config(input.engine_state, input.stack); - let ctrlc = input.engine_state.ctrlc.clone(); - let ctrlc1 = ctrlc.clone(); if record.is_empty() { let value = @@ -511,7 +524,7 @@ fn handle_record( let opts = TableOpts::new( &config, styles, - ctrlc, + input.engine_state.signals(), span, cfg.term_width, indent, @@ -523,7 +536,7 @@ fn handle_record( let result = match result { Some(output) => maybe_strip_color(output, &config), - None => report_unsuccessful_output(ctrlc1, cfg.term_width), + None => report_unsuccessful_output(input.engine_state.signals(), cfg.term_width), }; let val = Value::string(result, span); @@ -531,8 +544,8 @@ fn handle_record( Ok(val.into_pipeline_data()) } -fn report_unsuccessful_output(ctrlc1: Option>, term_width: usize) -> String { - if nu_utils::ctrl_c::was_pressed(&ctrlc1) { +fn report_unsuccessful_output(signals: &Signals, term_width: usize) -> String { + if signals.interrupted() { "".into() } else { // assume this failed because the table was too wide @@ -593,14 +606,17 @@ fn handle_row_stream( stream: ListStream, metadata: Option, ) -> Result { - let ctrlc = input.engine_state.ctrlc.clone(); - let stream = match metadata.as_ref() { // First, `ls` sources: Some(PipelineMetadata { data_source: DataSource::Ls, + .. }) => { - let config = get_config(input.engine_state, input.stack); + let config = { + let state = input.engine_state; + let stack: &Stack = input.stack; + stack.get_config(state) + }; let ls_colors_env_str = match input.stack.get_env_var(input.engine_state, "LS_COLORS") { Some(v) => Some(env_to_string( "LS_COLORS", @@ -630,6 +646,7 @@ fn handle_row_stream( // Next, `to html -l` sources: Some(PipelineMetadata { data_source: DataSource::HtmlThemes, + .. }) => { stream.map(|mut value| { if let Value::Record { val: record, .. } = &mut value { @@ -672,11 +689,14 @@ fn handle_row_stream( // for the values it outputs. Because engine_state is passed in, config doesn't need to. input.engine_state.clone(), input.stack.clone(), - ctrlc.clone(), cfg, ); - let stream = - ByteStream::from_result_iter(paginator, input.call.head, None, ByteStreamType::String); + let stream = ByteStream::from_result_iter( + paginator, + input.call.head, + Signals::empty(), + ByteStreamType::String, + ); Ok(PipelineData::ByteStream(stream, None)) } @@ -709,7 +729,6 @@ struct PagingTableCreator { stream: ValueIterator, engine_state: EngineState, stack: Stack, - ctrlc: Option>, elements_displayed: usize, reached_end: bool, cfg: TableConfig, @@ -722,7 +741,6 @@ impl PagingTableCreator { stream: ListStream, engine_state: EngineState, stack: Stack, - ctrlc: Option>, cfg: TableConfig, ) -> Self { PagingTableCreator { @@ -730,7 +748,6 @@ impl PagingTableCreator { stream: stream.into_inner(), engine_state, stack, - ctrlc, cfg, elements_displayed: 0, reached_end: false, @@ -749,7 +766,11 @@ impl PagingTableCreator { return Ok(None); } - let cfg = get_config(&self.engine_state, &self.stack); + let cfg = { + let state = &self.engine_state; + let stack = &self.stack; + stack.get_config(state) + }; let style_comp = StyleComputer::from_config(&self.engine_state, &self.stack); let opts = self.create_table_opts(&cfg, &style_comp); let view = TableView::Expanded { @@ -766,7 +787,11 @@ impl PagingTableCreator { return Ok(None); } - let cfg = get_config(&self.engine_state, &self.stack); + let cfg = { + let state = &self.engine_state; + let stack = &self.stack; + stack.get_config(state) + }; let style_comp = StyleComputer::from_config(&self.engine_state, &self.stack); let opts = self.create_table_opts(&cfg, &style_comp); @@ -774,7 +799,11 @@ impl PagingTableCreator { } fn build_general(&mut self, batch: Vec) -> StringResult { - let cfg = get_config(&self.engine_state, &self.stack); + let cfg = { + let state = &self.engine_state; + let stack = &self.stack; + stack.get_config(state) + }; let style_comp = StyleComputer::from_config(&self.engine_state, &self.stack); let opts = self.create_table_opts(&cfg, &style_comp); @@ -782,14 +811,14 @@ impl PagingTableCreator { } fn create_table_opts<'a>( - &self, + &'a self, cfg: &'a Config, style_comp: &'a StyleComputer<'a>, ) -> TableOpts<'a> { TableOpts::new( cfg, style_comp, - self.ctrlc.clone(), + self.engine_state.signals(), self.head, self.cfg.term_width, (cfg.table_indent.left, cfg.table_indent.right), @@ -822,12 +851,15 @@ impl Iterator for PagingTableCreator { match self.cfg.abbreviation { Some(abbr) => { (batch, _, end) = - stream_collect_abbriviated(&mut self.stream, abbr, self.ctrlc.clone()); + stream_collect_abbriviated(&mut self.stream, abbr, self.engine_state.signals()); } None => { // Pull from stream until time runs out or we have enough items - (batch, end) = - stream_collect(&mut self.stream, STREAM_PAGE_SIZE, self.ctrlc.clone()); + (batch, end) = stream_collect( + &mut self.stream, + STREAM_PAGE_SIZE, + self.engine_state.signals(), + ); } } @@ -860,15 +892,24 @@ impl Iterator for PagingTableCreator { self.row_offset += batch_size; - let config = get_config(&self.engine_state, &self.stack); - convert_table_to_output(table, &config, &self.ctrlc, self.cfg.term_width) + let config = { + let state = &self.engine_state; + let stack = &self.stack; + stack.get_config(state) + }; + convert_table_to_output( + table, + &config, + self.engine_state.signals(), + self.cfg.term_width, + ) } } fn stream_collect( stream: impl Iterator, size: usize, - ctrlc: Option>, + signals: &Signals, ) -> (Vec, bool) { let start_time = Instant::now(); let mut end = true; @@ -888,7 +929,7 @@ fn stream_collect( break; } - if nu_utils::ctrl_c::was_pressed(&ctrlc) { + if signals.interrupted() { break; } } @@ -899,7 +940,7 @@ fn stream_collect( fn stream_collect_abbriviated( stream: impl Iterator, size: usize, - ctrlc: Option>, + signals: &Signals, ) -> (Vec, usize, bool) { let mut end = true; let mut read = 0; @@ -922,7 +963,7 @@ fn stream_collect_abbriviated( tail.push_back(item); } - if nu_utils::ctrl_c::was_pressed(&ctrlc) { + if signals.interrupted() { end = false; break; } @@ -1032,7 +1073,7 @@ fn create_empty_placeholder( engine_state: &EngineState, stack: &Stack, ) -> String { - let config = get_config(engine_state, stack); + let config = stack.get_config(engine_state); if !config.table_show_empty { return String::new(); } @@ -1054,7 +1095,7 @@ fn create_empty_placeholder( fn convert_table_to_output( table: Result, ShellError>, config: &Config, - ctrlc: &Option>, + signals: &Signals, term_width: usize, ) -> Option, ShellError>> { match table { @@ -1067,7 +1108,7 @@ fn convert_table_to_output( Some(Ok(bytes)) } Ok(None) => { - let msg = if nu_utils::ctrl_c::was_pressed(ctrlc) { + let msg = if signals.interrupted() { String::from("") } else { // assume this failed because the table was too wide diff --git a/crates/nu-command/tests/commands/cal.rs b/crates/nu-command/tests/commands/cal.rs index 651ab8d3cd..65d6306845 100644 --- a/crates/nu-command/tests/commands/cal.rs +++ b/crates/nu-command/tests/commands/cal.rs @@ -1,8 +1,9 @@ use nu_test_support::{nu, pipeline}; +// Tests against table/structured data #[test] fn cal_full_year() { - let actual = nu!("cal -y --full-year 2010 | first | to json -r"); + let actual = nu!("cal -t -y --full-year 2010 | first | to json -r"); let first_week_2010_json = r#"{"year":2010,"su":null,"mo":null,"tu":null,"we":null,"th":null,"fr":1,"sa":2}"#; @@ -14,7 +15,7 @@ fn cal_full_year() { fn cal_february_2020_leap_year() { let actual = nu!(pipeline( r#" - cal -ym --full-year 2020 --month-names | where month == "february" | to json -r + cal --as-table -ym --full-year 2020 --month-names | where month == "february" | to json -r "# )); @@ -27,7 +28,7 @@ fn cal_february_2020_leap_year() { fn cal_fr_the_thirteenths_in_2015() { let actual = nu!(pipeline( r#" - cal --full-year 2015 | default 0 fr | where fr == 13 | length + cal --as-table --full-year 2015 | default 0 fr | where fr == 13 | length "# )); @@ -38,7 +39,7 @@ fn cal_fr_the_thirteenths_in_2015() { fn cal_rows_in_2020() { let actual = nu!(pipeline( r#" - cal --full-year 2020 | length + cal --as-table --full-year 2020 | length "# )); @@ -49,7 +50,7 @@ fn cal_rows_in_2020() { fn cal_week_day_start_mo() { let actual = nu!(pipeline( r#" - cal --full-year 2020 -m --month-names --week-start mo | where month == january | to json -r + cal --as-table --full-year 2020 -m --month-names --week-start mo | where month == january | to json -r "# )); @@ -62,9 +63,43 @@ fn cal_week_day_start_mo() { fn cal_sees_pipeline_year() { let actual = nu!(pipeline( r#" - cal --full-year 1020 | get mo | first 4 | to json -r + cal --as-table --full-year 1020 | get mo | first 4 | to json -r "# )); assert_eq!(actual.out, "[null,3,10,17]"); } + +// Tests against default string output +#[test] +fn cal_is_string() { + let actual = nu!(pipeline( + r#" + cal | describe + "# + )); + + assert_eq!(actual.out, "string (stream)"); +} + +#[test] +fn cal_year_num_lines() { + let actual = nu!(pipeline( + r#" + cal --full-year 2024 | lines | length + "# + )); + + assert_eq!(actual.out, "68"); +} + +#[test] +fn cal_week_start_string() { + let actual = nu!(pipeline( + r#" + cal --week-start fr | lines | get 1 | split row '│' | get 2 | ansi strip | str trim + "# + )); + + assert_eq!(actual.out, "sa"); +} diff --git a/crates/nu-command/tests/commands/cd.rs b/crates/nu-command/tests/commands/cd.rs index 87af52aa4d..f58638cc35 100644 --- a/crates/nu-command/tests/commands/cd.rs +++ b/crates/nu-command/tests/commands/cd.rs @@ -151,7 +151,7 @@ fn filesystem_change_to_home_directory() { " ); - assert_eq!(Some(PathBuf::from(actual.out)), dirs_next::home_dir()); + assert_eq!(Some(PathBuf::from(actual.out)), dirs::home_dir()); }) } diff --git a/crates/nu-command/tests/commands/chunks.rs b/crates/nu-command/tests/commands/chunks.rs new file mode 100644 index 0000000000..eb0c580f15 --- /dev/null +++ b/crates/nu-command/tests/commands/chunks.rs @@ -0,0 +1,43 @@ +use nu_test_support::nu; + +#[test] +fn chunk_size_negative() { + let actual = nu!("[0 1 2] | chunks -1"); + assert!(actual.err.contains("positive")); +} + +#[test] +fn chunk_size_zero() { + let actual = nu!("[0 1 2] | chunks 0"); + assert!(actual.err.contains("zero")); +} + +#[test] +fn chunk_size_not_int() { + let actual = nu!("[0 1 2] | chunks (if true { 1sec })"); + assert!(actual.err.contains("can't convert")); +} + +#[test] +fn empty() { + let actual = nu!("[] | chunks 2 | is-empty"); + assert_eq!(actual.out, "true"); +} + +#[test] +fn list_stream() { + let actual = nu!("([0 1 2] | every 1 | chunks 2) == ([0 1 2] | chunks 2)"); + assert_eq!(actual.out, "true"); +} + +#[test] +fn table_stream() { + let actual = nu!("([[foo bar]; [0 1] [2 3] [4 5]] | every 1 | chunks 2) == ([[foo bar]; [0 1] [2 3] [4 5]] | chunks 2)"); + assert_eq!(actual.out, "true"); +} + +#[test] +fn no_empty_chunks() { + let actual = nu!("([0 1 2 3 4 5] | chunks 3 | length) == 2"); + assert_eq!(actual.out, "true"); +} diff --git a/crates/nu-command/tests/commands/conversions/into/binary.rs b/crates/nu-command/tests/commands/conversions/into/binary.rs new file mode 100644 index 0000000000..c8fd24df77 --- /dev/null +++ b/crates/nu-command/tests/commands/conversions/into/binary.rs @@ -0,0 +1,13 @@ +use nu_test_support::nu; + +#[test] +fn sets_stream_from_internal_command_as_binary() { + let result = nu!("seq 1 10 | to text | into binary | describe"); + assert_eq!("binary (stream)", result.out); +} + +#[test] +fn sets_stream_from_external_command_as_binary() { + let result = nu!("^nu --testbin cococo | into binary | describe"); + assert_eq!("binary (stream)", result.out); +} diff --git a/crates/nu-command/tests/commands/conversions/into/mod.rs b/crates/nu-command/tests/commands/conversions/into/mod.rs index a7a829445a..ad10199b5c 100644 --- a/crates/nu-command/tests/commands/conversions/into/mod.rs +++ b/crates/nu-command/tests/commands/conversions/into/mod.rs @@ -1 +1,2 @@ +mod binary; mod int; diff --git a/crates/nu-command/tests/commands/database/into_sqlite.rs b/crates/nu-command/tests/commands/database/into_sqlite.rs index 595c88dc38..faa45040b3 100644 --- a/crates/nu-command/tests/commands/database/into_sqlite.rs +++ b/crates/nu-command/tests/commands/database/into_sqlite.rs @@ -465,7 +465,7 @@ fn make_sqlite_db(dirs: &Dirs, nu_table: &str) -> PathBuf { ); assert!(nucmd.status.success()); - testdb_path + testdb_path.into() } fn insert_test_rows(dirs: &Dirs, nu_table: &str, sql_query: Option<&str>, expected: Vec) { diff --git a/crates/nu-command/tests/commands/default.rs b/crates/nu-command/tests/commands/default.rs index 4fd41d7ec0..6ecd14960a 100644 --- a/crates/nu-command/tests/commands/default.rs +++ b/crates/nu-command/tests/commands/default.rs @@ -32,3 +32,15 @@ fn default_after_empty_filter() { assert_eq!(actual.out, "d"); } + +#[test] +fn keeps_nulls_in_lists() { + let actual = nu!(r#"[null, 2, 3] | default [] | to json -r"#); + assert_eq!(actual.out, "[null,2,3]"); +} + +#[test] +fn replaces_null() { + let actual = nu!(r#"null | default 1"#); + assert_eq!(actual.out, "1"); +} diff --git a/crates/nu-command/tests/commands/detect_columns.rs b/crates/nu-command/tests/commands/detect_columns.rs index 5bc057fa18..296d039b02 100644 --- a/crates/nu-command/tests/commands/detect_columns.rs +++ b/crates/nu-command/tests/commands/detect_columns.rs @@ -31,12 +31,12 @@ fn detect_columns_with_legacy_and_flag_c() { ( "$\"c1 c2 c3 c4 c5(char nl)a b c d e\"", "[[c1,c3,c4,c5]; ['a b',c,d,e]]", - "0..0", + "0..1", ), ( "$\"c1 c2 c3 c4 c5(char nl)a b c d e\"", "[[c1,c2,c3,c4]; [a,b,c,'d e']]", - "(-2)..(-2)", + "(-2)..(-1)", ), ( "$\"c1 c2 c3 c4 c5(char nl)a b c d e\"", @@ -72,10 +72,10 @@ drwxr-xr-x 2 root root 4.0K Mar 20 08:28 =(char nl) drwxr-xr-x 4 root root 4.0K Mar 20 08:18 ~(char nl) -rw-r--r-- 1 root root 3.0K Mar 20 07:23 ~asdf(char nl)\""; let expected = "[ -['column0', 'column1', 'column2', 'column3', 'column4', 'column5', 'column8']; -['drwxr-xr-x', '2', 'root', 'root', '4.0K', 'Mar 20 08:28', '='], -['drwxr-xr-x', '4', 'root', 'root', '4.0K', 'Mar 20 08:18', '~'], -['-rw-r--r--', '1', 'root', 'root', '3.0K', 'Mar 20 07:23', '~asdf'] +['column0', 'column1', 'column2', 'column3', 'column4', 'column5', 'column7', 'column8']; +['drwxr-xr-x', '2', 'root', 'root', '4.0K', 'Mar 20', '08:28', '='], +['drwxr-xr-x', '4', 'root', 'root', '4.0K', 'Mar 20', '08:18', '~'], +['-rw-r--r--', '1', 'root', 'root', '3.0K', 'Mar 20', '07:23', '~asdf'] ]"; let range = "5..6"; let cmd = format!( diff --git a/crates/nu-command/tests/commands/error_make.rs b/crates/nu-command/tests/commands/error_make.rs index 0c6908d2aa..f5714c88ac 100644 --- a/crates/nu-command/tests/commands/error_make.rs +++ b/crates/nu-command/tests/commands/error_make.rs @@ -4,8 +4,9 @@ use nu_test_support::nu; fn error_label_works() { let actual = nu!("error make {msg:foo label:{text:unseen}}"); - assert!(actual.err.contains("unseen")); - assert!(actual.err.contains("╰──")); + assert!(actual + .err + .contains("label at line 1, columns 1 to 10: unseen")); } #[test] diff --git a/crates/nu-command/tests/commands/find.rs b/crates/nu-command/tests/commands/find.rs index ed811f2a57..89bf2d9f39 100644 --- a/crates/nu-command/tests/commands/find.rs +++ b/crates/nu-command/tests/commands/find.rs @@ -17,6 +17,16 @@ fn find_with_list_search_with_char() { assert_eq!(actual.out, "[\"\\u001b[37m\\u001b[0m\\u001b[41;37ml\\u001b[0m\\u001b[37marry\\u001b[0m\",\"\\u001b[37mcur\\u001b[0m\\u001b[41;37ml\\u001b[0m\\u001b[37my\\u001b[0m\"]"); } +#[test] +fn find_with_bytestream_search_with_char() { + let actual = + nu!("\"ABC\" | save foo.txt; let res = open foo.txt | find abc; rm foo.txt; $res | get 0"); + assert_eq!( + actual.out, + "\u{1b}[37m\u{1b}[0m\u{1b}[41;37mABC\u{1b}[0m\u{1b}[37m\u{1b}[0m" + ) +} + #[test] fn find_with_list_search_with_number() { let actual = nu!("[1 2 3 4 5] | find 3 | get 0"); diff --git a/crates/nu-command/tests/commands/into_filesize.rs b/crates/nu-command/tests/commands/into_filesize.rs index d8ba88021c..6a72c76b5d 100644 --- a/crates/nu-command/tests/commands/into_filesize.rs +++ b/crates/nu-command/tests/commands/into_filesize.rs @@ -76,6 +76,13 @@ fn into_filesize_wrong_negative_str_filesize() { assert!(actual.err.contains("can't convert string to filesize")); } +#[test] +fn into_filesize_large_negative_str_filesize() { + let actual = nu!("'-10000PiB' | into filesize"); + + assert!(actual.err.contains("can't convert string to filesize")); +} + #[test] fn into_filesize_negative_str() { let actual = nu!("'-1' | into filesize"); @@ -104,6 +111,13 @@ fn into_filesize_wrong_positive_str_filesize() { assert!(actual.err.contains("can't convert string to filesize")); } +#[test] +fn into_filesize_large_positive_str_filesize() { + let actual = nu!("'+10000PiB' | into filesize"); + + assert!(actual.err.contains("can't convert string to filesize")); +} + #[test] fn into_filesize_positive_str() { let actual = nu!("'+1' | into filesize"); diff --git a/crates/nu-command/tests/commands/length.rs b/crates/nu-command/tests/commands/length.rs index 63318e65db..b67ac452e8 100644 --- a/crates/nu-command/tests/commands/length.rs +++ b/crates/nu-command/tests/commands/length.rs @@ -2,7 +2,7 @@ use nu_test_support::nu; #[test] fn length_columns_in_cal_table() { - let actual = nu!("cal | columns | length"); + let actual = nu!("cal --as-table | columns | length"); assert_eq!(actual.out, "7"); } diff --git a/crates/nu-command/tests/commands/mod.rs b/crates/nu-command/tests/commands/mod.rs index 922e804405..4c9f8e86f8 100644 --- a/crates/nu-command/tests/commands/mod.rs +++ b/crates/nu-command/tests/commands/mod.rs @@ -7,6 +7,7 @@ mod break_; mod bytes; mod cal; mod cd; +mod chunks; mod compact; mod complete; mod config_env_default; @@ -125,7 +126,6 @@ mod upsert; mod url; mod use_; mod where_; -#[cfg(feature = "which-support")] mod which; mod while_; mod with_env; diff --git a/crates/nu-command/tests/commands/network/http/delete.rs b/crates/nu-command/tests/commands/network/http/delete.rs index bc71ed3945..c284c2ccfd 100644 --- a/crates/nu-command/tests/commands/network/http/delete.rs +++ b/crates/nu-command/tests/commands/network/http/delete.rs @@ -20,6 +20,25 @@ fn http_delete_is_success() { assert!(actual.out.is_empty()) } +#[test] +fn http_delete_is_success_pipeline() { + let mut server = Server::new(); + + let _mock = server.mock("DELETE", "/").create(); + + let actual = nu!(pipeline( + format!( + r#" + "foo" | http delete {url} + "#, + url = server.url() + ) + .as_str() + )); + + assert!(actual.out.is_empty()) +} + #[test] fn http_delete_failed_due_to_server_error() { let mut server = Server::new(); diff --git a/crates/nu-command/tests/commands/network/http/patch.rs b/crates/nu-command/tests/commands/network/http/patch.rs index 4196b304c6..dc3a755baa 100644 --- a/crates/nu-command/tests/commands/network/http/patch.rs +++ b/crates/nu-command/tests/commands/network/http/patch.rs @@ -20,6 +20,25 @@ fn http_patch_is_success() { assert!(actual.out.is_empty()) } +#[test] +fn http_patch_is_success_pipeline() { + let mut server = Server::new(); + + let _mock = server.mock("PATCH", "/").match_body("foo").create(); + + let actual = nu!(pipeline( + format!( + r#" + "foo" | http patch {url} + "#, + url = server.url() + ) + .as_str() + )); + + assert!(actual.out.is_empty()) +} + #[test] fn http_patch_failed_due_to_server_error() { let mut server = Server::new(); @@ -55,7 +74,9 @@ fn http_patch_failed_due_to_missing_body() { .as_str() )); - assert!(actual.err.contains("Usage: http patch")) + assert!(actual + .err + .contains("Data must be provided either through pipeline or positional argument")) } #[test] diff --git a/crates/nu-command/tests/commands/network/http/post.rs b/crates/nu-command/tests/commands/network/http/post.rs index bd13542482..02aa8df23f 100644 --- a/crates/nu-command/tests/commands/network/http/post.rs +++ b/crates/nu-command/tests/commands/network/http/post.rs @@ -19,6 +19,24 @@ fn http_post_is_success() { assert!(actual.out.is_empty()) } +#[test] +fn http_post_is_success_pipeline() { + let mut server = Server::new(); + + let _mock = server.mock("POST", "/").match_body("foo").create(); + + let actual = nu!(pipeline( + format!( + r#" + "foo" | http post {url} + "#, + url = server.url() + ) + .as_str() + )); + + assert!(actual.out.is_empty()) +} #[test] fn http_post_failed_due_to_server_error() { @@ -55,7 +73,9 @@ fn http_post_failed_due_to_missing_body() { .as_str() )); - assert!(actual.err.contains("Usage: http post")) + assert!(actual + .err + .contains("Data must be provided either through pipeline or positional argument")) } #[test] diff --git a/crates/nu-command/tests/commands/network/http/put.rs b/crates/nu-command/tests/commands/network/http/put.rs index 002b1f8632..43251cd21a 100644 --- a/crates/nu-command/tests/commands/network/http/put.rs +++ b/crates/nu-command/tests/commands/network/http/put.rs @@ -20,6 +20,25 @@ fn http_put_is_success() { assert!(actual.out.is_empty()) } +#[test] +fn http_put_is_success_pipeline() { + let mut server = Server::new(); + + let _mock = server.mock("PUT", "/").match_body("foo").create(); + + let actual = nu!(pipeline( + format!( + r#" + "foo" | http put {url} + "#, + url = server.url() + ) + .as_str() + )); + + assert!(actual.out.is_empty()) +} + #[test] fn http_put_failed_due_to_server_error() { let mut server = Server::new(); @@ -55,7 +74,9 @@ fn http_put_failed_due_to_missing_body() { .as_str() )); - assert!(actual.err.contains("Usage: http put")) + assert!(actual + .err + .contains("Data must be provided either through pipeline or positional argument")) } #[test] diff --git a/crates/nu-command/tests/commands/redirection.rs b/crates/nu-command/tests/commands/redirection.rs index 1eb57077b9..02f85a90f8 100644 --- a/crates/nu-command/tests/commands/redirection.rs +++ b/crates/nu-command/tests/commands/redirection.rs @@ -439,3 +439,37 @@ fn no_duplicate_redirection() { ); }); } + +#[rstest::rstest] +#[case("let", "out>")] +#[case("let", "err>")] +#[case("let", "out+err>")] +#[case("mut", "out>")] +#[case("mut", "err>")] +#[case("mut", "out+err>")] +fn file_redirection_in_let_and_mut(#[case] keyword: &str, #[case] redir: &str) { + Playground::setup("file redirection in let and mut", |dirs, _| { + let actual = nu!( + cwd: dirs.test(), + format!("$env.BAZ = 'foo'; {keyword} v = nu --testbin echo_env_mixed out-err BAZ BAZ {redir} result.txt") + ); + assert!(actual.status.success()); + assert!(file_contents(dirs.test().join("result.txt")).contains("foo")); + }) +} + +#[rstest::rstest] +#[case("let", "err>|", "foo3")] +#[case("let", "out+err>|", "7")] +#[case("mut", "err>|", "foo3")] +#[case("mut", "out+err>|", "7")] +fn pipe_redirection_in_let_and_mut( + #[case] keyword: &str, + #[case] redir: &str, + #[case] output: &str, +) { + let actual = nu!( + format!("$env.BAZ = 'foo'; {keyword} v = nu --testbin echo_env_mixed out-err BAZ BAZ {redir} str length; $v") + ); + assert_eq!(actual.out, output); +} diff --git a/crates/nu-command/tests/commands/rm.rs b/crates/nu-command/tests/commands/rm.rs index b3a273ea86..e6cbf88cda 100644 --- a/crates/nu-command/tests/commands/rm.rs +++ b/crates/nu-command/tests/commands/rm.rs @@ -1,3 +1,4 @@ +use nu_path::AbsolutePath; use nu_test_support::fs::{files_exist_at, Stub::EmptyFile}; use nu_test_support::nu; use nu_test_support::playground::Playground; @@ -405,10 +406,10 @@ fn removes_file_after_cd() { } struct Cleanup<'a> { - dir_to_clean: &'a Path, + dir_to_clean: &'a AbsolutePath, } -fn set_dir_read_only(directory: &Path, read_only: bool) { +fn set_dir_read_only(directory: &AbsolutePath, read_only: bool) { let mut permissions = fs::metadata(directory).unwrap().permissions(); permissions.set_readonly(read_only); fs::set_permissions(directory, permissions).expect("failed to set directory permissions"); diff --git a/crates/nu-command/tests/commands/run_external.rs b/crates/nu-command/tests/commands/run_external.rs index 14428edcbe..17667c9bb3 100644 --- a/crates/nu-command/tests/commands/run_external.rs +++ b/crates/nu-command/tests/commands/run_external.rs @@ -1,4 +1,3 @@ -#[cfg(not(windows))] use nu_test_support::fs::Stub::EmptyFile; use nu_test_support::playground::Playground; use nu_test_support::{nu, pipeline}; @@ -17,7 +16,6 @@ fn better_empty_redirection() { assert!(!actual.out.contains('2')); } -#[cfg(not(windows))] #[test] fn explicit_glob() { Playground::setup("external with explicit glob", |dirs, sandbox| { @@ -30,15 +28,15 @@ fn explicit_glob() { let actual = nu!( cwd: dirs.test(), pipeline( r#" - ^ls | glob '*.txt' | length + ^nu --testbin cococo ('*.txt' | into glob) "# )); - assert_eq!(actual.out, "2"); + assert!(actual.out.contains("D&D_volume_1.txt")); + assert!(actual.out.contains("D&D_volume_2.txt")); }) } -#[cfg(not(windows))] #[test] fn bare_word_expand_path_glob() { Playground::setup("bare word should do the expansion", |dirs, sandbox| { @@ -51,7 +49,7 @@ fn bare_word_expand_path_glob() { let actual = nu!( cwd: dirs.test(), pipeline( " - ^ls *.txt + ^nu --testbin cococo *.txt " )); @@ -60,7 +58,6 @@ fn bare_word_expand_path_glob() { }) } -#[cfg(not(windows))] #[test] fn backtick_expand_path_glob() { Playground::setup("backtick should do the expansion", |dirs, sandbox| { @@ -73,7 +70,7 @@ fn backtick_expand_path_glob() { let actual = nu!( cwd: dirs.test(), pipeline( r#" - ^ls `*.txt` + ^nu --testbin cococo `*.txt` "# )); @@ -82,7 +79,6 @@ fn backtick_expand_path_glob() { }) } -#[cfg(not(windows))] #[test] fn single_quote_does_not_expand_path_glob() { Playground::setup("single quote do not run the expansion", |dirs, sandbox| { @@ -95,15 +91,14 @@ fn single_quote_does_not_expand_path_glob() { let actual = nu!( cwd: dirs.test(), pipeline( r#" - ^ls '*.txt' + ^nu --testbin cococo '*.txt' "# )); - assert!(actual.err.contains("No such file or directory")); + assert_eq!(actual.out, "*.txt"); }) } -#[cfg(not(windows))] #[test] fn double_quote_does_not_expand_path_glob() { Playground::setup("double quote do not run the expansion", |dirs, sandbox| { @@ -116,22 +111,21 @@ fn double_quote_does_not_expand_path_glob() { let actual = nu!( cwd: dirs.test(), pipeline( r#" - ^ls "*.txt" + ^nu --testbin cococo "*.txt" "# )); - assert!(actual.err.contains("No such file or directory")); + assert_eq!(actual.out, "*.txt"); }) } -#[cfg(not(windows))] #[test] fn failed_command_with_semicolon_will_not_execute_following_cmds() { Playground::setup("external failed command with semicolon", |dirs, _| { let actual = nu!( cwd: dirs.test(), pipeline( " - ^ls *.abc; echo done + nu --testbin fail; echo done " )); @@ -155,16 +149,51 @@ fn external_args_with_quoted() { #[cfg(not(windows))] #[test] -fn external_arg_with_long_flag_value_quoted() { - Playground::setup("external failed command with semicolon", |dirs, _| { +fn external_arg_with_option_like_embedded_quotes() { + // TODO: would be nice to make this work with cococo, but arg parsing interferes + Playground::setup( + "external arg with option like embedded quotes", + |dirs, _| { + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ^echo --foo='bar' -foo='bar' + "# + )); + + assert_eq!(actual.out, "--foo=bar -foo=bar"); + }, + ) +} + +#[test] +fn external_arg_with_non_option_like_embedded_quotes() { + Playground::setup( + "external arg with non option like embedded quotes", + |dirs, _| { + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ^nu --testbin cococo foo='bar' 'foo'=bar + "# + )); + + assert_eq!(actual.out, "foo=bar foo=bar"); + }, + ) +} + +#[test] +fn external_arg_with_string_interpolation() { + Playground::setup("external arg with string interpolation", |dirs, _| { let actual = nu!( cwd: dirs.test(), pipeline( r#" - ^echo --foo='bar' + ^nu --testbin cococo foo=(2 + 2) $"foo=(2 + 2)" foo=$"(2 + 2)" "# )); - assert_eq!(actual.out, "--foo=bar"); + assert_eq!(actual.out, "foo=4 foo=4 foo=4"); }) } @@ -200,6 +229,99 @@ fn external_command_escape_args() { }) } +#[test] +fn external_command_ndots_args() { + let actual = nu!(r#" + nu --testbin cococo foo/. foo/.. foo/... foo/./bar foo/../bar foo/.../bar ./bar ../bar .../bar + "#); + + assert_eq!( + actual.out, + if cfg!(windows) { + // Windows is a bit weird right now, where if ndots has to fix something it's going to + // change everything to backslashes too. Would be good to fix that + r"foo/. foo/.. foo\..\.. foo/./bar foo/../bar foo\..\..\bar ./bar ../bar ..\..\bar" + } else { + r"foo/. foo/.. foo/../.. foo/./bar foo/../bar foo/../../bar ./bar ../bar ../../bar" + } + ); +} + +#[test] +fn external_command_url_args() { + // If ndots is not handled correctly, we can lose the double forward slashes that are needed + // here + let actual = nu!(r#" + nu --testbin cococo http://example.com http://example.com/.../foo //foo + "#); + + assert_eq!( + actual.out, + "http://example.com http://example.com/.../foo //foo" + ); +} + +#[test] +#[cfg_attr( + not(target_os = "linux"), + ignore = "only runs on Linux, where controlling the HOME var is reliable" +)] +fn external_command_expand_tilde() { + Playground::setup("external command expand tilde", |dirs, _| { + // Make a copy of the nu executable that we can use + let mut src = std::fs::File::open(nu_test_support::fs::binaries().join("nu")) + .expect("failed to open nu"); + let mut dst = std::fs::File::create_new(dirs.test().join("test_nu")) + .expect("failed to create test_nu file"); + std::io::copy(&mut src, &mut dst).expect("failed to copy data for nu binary"); + + // Make test_nu have the same permissions so that it's executable + dst.set_permissions( + src.metadata() + .expect("failed to get nu metadata") + .permissions(), + ) + .expect("failed to set permissions on test_nu"); + + // Close the files + drop(dst); + drop(src); + + let actual = nu!( + envs: vec![ + ("HOME".to_string(), dirs.test().to_string_lossy().into_owned()), + ], + r#" + ^~/test_nu --testbin cococo hello + "# + ); + assert_eq!(actual.out, "hello"); + }) +} + +#[test] +fn external_arg_expand_tilde() { + Playground::setup("external arg expand tilde", |dirs, _| { + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ^nu --testbin cococo ~/foo ~/(2 + 2) + "# + )); + + let home = dirs::home_dir().expect("failed to find home dir"); + + assert_eq!( + actual.out, + format!( + "{} {}", + home.join("foo").display(), + home.join("4").display() + ) + ); + }) +} + #[test] fn external_command_not_expand_tilde_with_quotes() { Playground::setup( @@ -231,21 +353,6 @@ fn external_command_receives_raw_binary_data() { }) } -#[cfg(windows)] -#[test] -fn failed_command_with_semicolon_will_not_execute_following_cmds_windows() { - Playground::setup("external failed command with semicolon", |dirs, _| { - let actual = nu!( - cwd: dirs.test(), pipeline( - " - ^cargo asdf; echo done - " - )); - - assert!(!actual.out.contains("done")); - }) -} - #[cfg(windows)] #[test] fn can_run_batch_files() { diff --git a/crates/nu-command/tests/commands/select.rs b/crates/nu-command/tests/commands/select.rs index 741386998a..004aa16d6b 100644 --- a/crates/nu-command/tests/commands/select.rs +++ b/crates/nu-command/tests/commands/select.rs @@ -50,7 +50,7 @@ fn complex_nested_columns() { r#" {sample} | select nu."0xATYKARNU" nu.committers.name nu.releases.version - | get nu_releases_version + | get "nu.releases.version" | where $it > "0.8" | get 0 "# diff --git a/crates/nu-command/tests/commands/source_env.rs b/crates/nu-command/tests/commands/source_env.rs index 9dd0320a45..f9cc3fbc1c 100644 --- a/crates/nu-command/tests/commands/source_env.rs +++ b/crates/nu-command/tests/commands/source_env.rs @@ -1,4 +1,3 @@ -use nu_test_support::fs::AbsolutePath; use nu_test_support::fs::Stub::{FileWithContent, FileWithContentToBeTrimmed}; use nu_test_support::nu; use nu_test_support::pipeline; @@ -8,17 +7,18 @@ use nu_test_support::playground::Playground; #[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")); + let file = dirs.test().join("config.toml"); + let library_path = dirs.test().join("lib"); - nu.with_config(&file); + nu.with_config(file); nu.with_files(&[FileWithContent( "config.toml", &format!( r#" - lib_dirs = ["{library_path}"] + lib_dirs = ["{}"] skip_welcome_message = true - "# + "#, + library_path.as_os_str().to_str().unwrap(), ), )]); diff --git a/crates/nu-command/tests/commands/str_/mod.rs b/crates/nu-command/tests/commands/str_/mod.rs index 43f59b3e10..2aeabfd04a 100644 --- a/crates/nu-command/tests/commands/str_/mod.rs +++ b/crates/nu-command/tests/commands/str_/mod.rs @@ -255,7 +255,7 @@ fn substrings_the_input() { } #[test] -fn substring_errors_if_start_index_is_greater_than_end_index() { +fn substring_empty_if_start_index_is_greater_than_end_index() { Playground::setup("str_test_9", |dirs, sandbox| { sandbox.with_files(&[FileWithContent( "sample.toml", @@ -270,12 +270,10 @@ fn substring_errors_if_start_index_is_greater_than_end_index() { r#" open sample.toml | str substring 6..4 fortune.teller.phone + | get fortune.teller.phone "# )); - - assert!(actual - .err - .contains("End must be greater than or equal to Start")) + assert_eq!(actual.out, "") }) } @@ -375,6 +373,21 @@ fn substrings_the_input_and_treats_end_index_as_length_if_blank_end_index_given( }) } +#[test] +fn substring_by_negative_index() { + Playground::setup("str_test_13", |dirs, _| { + let actual = nu!( + cwd: dirs.test(), "'apples' | str substring 0..-1", + ); + assert_eq!(actual.out, "apples"); + + let actual = nu!( + cwd: dirs.test(), "'apples' | str substring 0..<-1", + ); + assert_eq!(actual.out, "apple"); + }) +} + #[test] fn str_reverse() { let actual = nu!(r#" diff --git a/crates/nu-command/tests/commands/table.rs b/crates/nu-command/tests/commands/table.rs index 9c4f34ebfd..272369a1c9 100644 --- a/crates/nu-command/tests/commands/table.rs +++ b/crates/nu-command/tests/commands/table.rs @@ -2889,3 +2889,21 @@ fn table_list() { let actual = nu!("table --list --theme basic"); assert_eq!(actual.out, "╭────┬────────────────╮│ 0 │ basic ││ 1 │ compact ││ 2 │ compact_double ││ 3 │ default ││ 4 │ heavy ││ 5 │ light ││ 6 │ none ││ 7 │ reinforced ││ 8 │ rounded ││ 9 │ thin ││ 10 │ with_love ││ 11 │ psql ││ 12 │ markdown ││ 13 │ dots ││ 14 │ restructured ││ 15 │ ascii_rounded ││ 16 │ basic_compact │╰────┴────────────────╯"); } + +#[test] +fn table_kv_header_on_separator_trim_algorithm() { + let actual = nu!("$env.config.table.header_on_separator = true; {key1: '111111111111111111111111111111111111111111111111111111111111'} | table --width=60 --theme basic"); + assert_eq!(actual.out, "+------+---------------------------------------------------+| key1 | 1111111111111111111111111111111111111111111111111 || | 11111111111 |+------+---------------------------------------------------+"); +} + +#[test] +fn table_general_header_on_separator_trim_algorithm() { + let actual = nu!("$env.config.table.header_on_separator = true; [[a b]; ['11111111111111111111111111111111111111' 2] ] | table --width=20 --theme basic"); + assert_eq!(actual.out, "+-#-+----a-----+-b-+| 0 | 11111111 | 2 || | 11111111 | || | 11111111 | || | 11111111 | || | 111111 | |+---+----------+---+"); +} + +#[test] +fn table_general_header_on_separator_issue1() { + let actual = nu!("$env.config.table.header_on_separator = true; [['Llll oo Bbbbbbbb' 'Bbbbbbbb Aaaa' Nnnnnn Ggggg 'Xxxxx Llllllll #' Bbb 'Pppp Ccccc' 'Rrrrrrrr Dddd' Rrrrrr 'Rrrrrr Ccccc II' 'Rrrrrr Ccccc Ppppppp II' 'Pppppp Dddddddd Tttt' 'Pppppp Dddddddd Dddd' 'Rrrrrrrrr Trrrrrr' 'Pppppp Ppppp Dddd' 'Ppppp Dddd' Hhhh]; [RRRRRRR FFFFFFFF UUUU VV 202407160001 BBB 1 '7/16/2024' '' AAA-1111 AAA-1111-11 '7 YEARS' 2555 'RRRRRRRR DDDD' '7/16/2031' '7/16/2031' NN]] | table --width=87 --theme basic"); + assert_eq!(actual.out, "+-#-+-Llll oo Bbbbbbbb-+-Bbbbbbbb Aaaa-+-Nnnnnn-+-Ggggg-+-Xxxxx Llllllll #-+-...-+| 0 | RRRRRRR | FFFFFFFF | UUUU | VV | 202407160001 | ... |+---+------------------+---------------+--------+-------+------------------+-----+"); +} diff --git a/crates/nu-command/tests/commands/ucp.rs b/crates/nu-command/tests/commands/ucp.rs index 453786ad4e..2c2b30f252 100644 --- a/crates/nu-command/tests/commands/ucp.rs +++ b/crates/nu-command/tests/commands/ucp.rs @@ -1,6 +1,6 @@ use nu_test_support::fs::file_contents; use nu_test_support::fs::{ - files_exist_at, AbsoluteFile, + files_exist_at, Stub::{EmptyFile, FileWithContent, FileWithPermission}, }; use nu_test_support::nu; @@ -55,7 +55,7 @@ fn copies_the_file_inside_directory_if_path_to_copy_is_directory() { fn copies_the_file_inside_directory_if_path_to_copy_is_directory_impl(progress: bool) { Playground::setup("ucp_test_2", |dirs, _| { - let expected_file = AbsoluteFile::new(dirs.test().join("sample.ini")); + let expected_file = dirs.test().join("sample.ini"); let progress_flag = if progress { "-p" } else { "" }; // Get the hash of the file content to check integrity after copy. @@ -64,13 +64,13 @@ fn copies_the_file_inside_directory_if_path_to_copy_is_directory_impl(progress: cwd: dirs.formats(), "cp {} ../formats/sample.ini {}", progress_flag, - expected_file.dir() + expected_file.parent().unwrap().as_os_str().to_str().unwrap(), ); assert!(dirs.test().join("sample.ini").exists()); // Check the integrity of the file. - let after_cp_hash = get_file_hash(expected_file); + let after_cp_hash = get_file_hash(expected_file.display()); assert_eq!(first_hash, after_cp_hash); }) } @@ -906,7 +906,7 @@ fn test_cp_debug_default() { #[cfg(any(target_os = "linux", target_os = "freebsd"))] if !actual .out - .contains("copy offload: unknown, reflink: unsupported, sparse detection: no") + .contains("copy offload: yes, reflink: unsupported, sparse detection: no") { panic!("{}", format!("Failure: stdout was \n{}", actual.out)); } diff --git a/crates/nu-command/tests/commands/use_.rs b/crates/nu-command/tests/commands/use_.rs index 63b1f8391a..2895e1635f 100644 --- a/crates/nu-command/tests/commands/use_.rs +++ b/crates/nu-command/tests/commands/use_.rs @@ -1,4 +1,3 @@ -use nu_test_support::fs::AbsolutePath; use nu_test_support::fs::Stub::{FileWithContent, FileWithContentToBeTrimmed}; use nu_test_support::nu; use nu_test_support::pipeline; @@ -7,10 +6,10 @@ use nu_test_support::playground::Playground; #[test] fn use_module_file_within_block() { Playground::setup("use_test_1", |dirs, nu| { - let file = AbsolutePath::new(dirs.test().join("spam.nu")); + let file = dirs.test().join("spam.nu"); nu.with_files(&[FileWithContent( - &file.to_string(), + file.as_os_str().to_str().unwrap(), r#" export def foo [] { echo "hello world" @@ -37,10 +36,10 @@ fn use_module_file_within_block() { #[test] fn use_keeps_doc_comments() { Playground::setup("use_doc_comments", |dirs, nu| { - let file = AbsolutePath::new(dirs.test().join("spam.nu")); + let file = dirs.test().join("spam.nu"); nu.with_files(&[FileWithContent( - &file.to_string(), + file.as_os_str().to_str().unwrap(), r#" # this is my foo command export def foo [ diff --git a/crates/nu-command/tests/format_conversions/csv.rs b/crates/nu-command/tests/format_conversions/csv.rs index a9be76d5c3..f10a84b672 100644 --- a/crates/nu-command/tests/format_conversions/csv.rs +++ b/crates/nu-command/tests/format_conversions/csv.rs @@ -284,7 +284,7 @@ fn from_csv_text_skipping_headers_to_table() { r#" open los_tres_amigos.txt | from csv --noheaders - | get column3 + | get column2 | length "# )); diff --git a/crates/nu-command/tests/format_conversions/ssv.rs b/crates/nu-command/tests/format_conversions/ssv.rs index a673b122fb..5a62f796f7 100644 --- a/crates/nu-command/tests/format_conversions/ssv.rs +++ b/crates/nu-command/tests/format_conversions/ssv.rs @@ -74,7 +74,7 @@ fn from_ssv_text_treating_first_line_as_data_with_flag() { open oc_get_svc.txt | from ssv --noheaders -a | first - | get column1 + | get column0 "# )); @@ -84,7 +84,7 @@ fn from_ssv_text_treating_first_line_as_data_with_flag() { open oc_get_svc.txt | from ssv --noheaders | first - | get column1 + | get column0 "# )); diff --git a/crates/nu-command/tests/format_conversions/tsv.rs b/crates/nu-command/tests/format_conversions/tsv.rs index be57c60242..31740ef8d9 100644 --- a/crates/nu-command/tests/format_conversions/tsv.rs +++ b/crates/nu-command/tests/format_conversions/tsv.rs @@ -207,7 +207,7 @@ fn from_tsv_text_skipping_headers_to_table() { r#" open los_tres_amigos.txt | from tsv --noheaders - | get column3 + | get column2 | length "# )); diff --git a/crates/nu-derive-value/Cargo.toml b/crates/nu-derive-value/Cargo.toml new file mode 100644 index 0000000000..50ca5d2407 --- /dev/null +++ b/crates/nu-derive-value/Cargo.toml @@ -0,0 +1,21 @@ +[package] +authors = ["The Nushell Project Developers"] +description = "Macros implementation of #[derive(FromValue, IntoValue)]" +edition = "2021" +license = "MIT" +name = "nu-derive-value" +repository = "https://github.com/nushell/nushell/tree/main/crates/nu-derive-value" +version = "0.95.1" + +[lib] +proc-macro = true +# we can only use exposed macros in doctests really, +# so we cannot test anything useful in a doctest +doctest = false + +[dependencies] +proc-macro2 = { workspace = true } +syn = { workspace = true } +quote = { workspace = true } +proc-macro-error = { workspace = true } +convert_case = { workspace = true } \ No newline at end of file diff --git a/crates/nu-derive-value/LICENSE b/crates/nu-derive-value/LICENSE new file mode 100644 index 0000000000..ae174e8595 --- /dev/null +++ b/crates/nu-derive-value/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 - 2023 The Nushell Project 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-derive-value/src/attributes.rs b/crates/nu-derive-value/src/attributes.rs new file mode 100644 index 0000000000..6969b64287 --- /dev/null +++ b/crates/nu-derive-value/src/attributes.rs @@ -0,0 +1,116 @@ +use convert_case::Case; +use syn::{spanned::Spanned, Attribute, Fields, LitStr}; + +use crate::{error::DeriveError, HELPER_ATTRIBUTE}; + +#[derive(Debug)] +pub struct ContainerAttributes { + pub rename_all: Case, +} + +impl Default for ContainerAttributes { + fn default() -> Self { + Self { + rename_all: Case::Snake, + } + } +} + +impl ContainerAttributes { + pub fn parse_attrs<'a, M>( + iter: impl Iterator, + ) -> Result> { + let mut container_attrs = ContainerAttributes::default(); + for attr in filter(iter) { + // This is a container to allow returning derive errors inside the parse_nested_meta fn. + let mut err = Ok(()); + + attr.parse_nested_meta(|meta| { + let ident = meta.path.require_ident()?; + match ident.to_string().as_str() { + "rename_all" => { + // The matched case are all useful variants from `convert_case` with aliases + // that `serde` uses. + let case: LitStr = meta.value()?.parse()?; + let case = match case.value().as_str() { + "UPPER CASE" | "UPPER WITH SPACES CASE" => Case::Upper, + "lower case" | "lower with spaces case" => Case::Lower, + "Title Case" => Case::Title, + "camelCase" => Case::Camel, + "PascalCase" | "UpperCamelCase" => Case::Pascal, + "snake_case" => Case::Snake, + "UPPER_SNAKE_CASE" | "SCREAMING_SNAKE_CASE" => Case::UpperSnake, + "kebab-case" => Case::Kebab, + "COBOL-CASE" | "UPPER-KEBAB-CASE" | "SCREAMING-KEBAB-CASE" => { + Case::Cobol + } + "Train-Case" => Case::Train, + "flatcase" | "lowercase" => Case::Flat, + "UPPERFLATCASE" | "UPPERCASE" => Case::UpperFlat, + // Although very funny, we don't support `Case::{Toggle, Alternating}`, + // as we see no real benefit. + c => { + err = Err(DeriveError::InvalidAttributeValue { + value_span: case.span(), + value: Box::new(c.to_string()), + }); + return Ok(()); // We stored the err in `err`. + } + }; + container_attrs.rename_all = case; + } + ident => { + err = Err(DeriveError::UnexpectedAttribute { + meta_span: ident.span(), + }); + } + } + + Ok(()) + }) + .map_err(DeriveError::Syn)?; + + err?; // Shortcircuit here if `err` is holding some error. + } + + Ok(container_attrs) + } +} + +pub fn filter<'a>( + iter: impl Iterator, +) -> impl Iterator { + iter.filter(|attr| attr.path().is_ident(HELPER_ATTRIBUTE)) +} + +// The deny functions are built to easily deny the use of the helper attribute if used incorrectly. +// As the usage of it gets more complex, these functions might be discarded or replaced. + +/// Deny any attribute that uses the helper attribute. +pub fn deny(attrs: &[Attribute]) -> Result<(), DeriveError> { + match filter(attrs.iter()).next() { + Some(attr) => Err(DeriveError::InvalidAttributePosition { + attribute_span: attr.span(), + }), + None => Ok(()), + } +} + +/// Deny any attributes that uses the helper attribute on any field. +pub fn deny_fields(fields: &Fields) -> Result<(), DeriveError> { + match fields { + Fields::Named(fields) => { + for field in fields.named.iter() { + deny(&field.attrs)?; + } + } + Fields::Unnamed(fields) => { + for field in fields.unnamed.iter() { + deny(&field.attrs)?; + } + } + Fields::Unit => (), + } + + Ok(()) +} diff --git a/crates/nu-derive-value/src/error.rs b/crates/nu-derive-value/src/error.rs new file mode 100644 index 0000000000..b7fd3e2f91 --- /dev/null +++ b/crates/nu-derive-value/src/error.rs @@ -0,0 +1,82 @@ +use std::{any, fmt::Debug, marker::PhantomData}; + +use proc_macro2::Span; +use proc_macro_error::{Diagnostic, Level}; + +#[derive(Debug)] +pub enum DeriveError { + /// Marker variant, makes the `M` generic parameter valid. + _Marker(PhantomData), + + /// Parsing errors thrown by `syn`. + Syn(syn::parse::Error), + + /// `syn::DeriveInput` was a union, currently not supported + UnsupportedUnions, + + /// Only plain enums are supported right now. + UnsupportedEnums { fields_span: Span }, + + /// Found a `#[nu_value(x)]` attribute where `x` is unexpected. + UnexpectedAttribute { meta_span: Span }, + + /// Found a `#[nu_value(x)]` attribute at a invalid position. + InvalidAttributePosition { attribute_span: Span }, + + /// Found a valid `#[nu_value(x)]` attribute but the passed values is invalid. + InvalidAttributeValue { + value_span: Span, + value: Box, + }, +} + +impl From> for Diagnostic { + fn from(value: DeriveError) -> Self { + let derive_name = any::type_name::().split("::").last().expect("not empty"); + match value { + DeriveError::_Marker(_) => panic!("used marker variant"), + + DeriveError::Syn(e) => Diagnostic::spanned(e.span(), Level::Error, e.to_string()), + + DeriveError::UnsupportedUnions => Diagnostic::new( + Level::Error, + format!("`{derive_name}` cannot be derived from unions"), + ) + .help("consider refactoring to a struct".to_string()) + .note("if you really need a union, consider opening an issue on Github".to_string()), + + DeriveError::UnsupportedEnums { fields_span } => Diagnostic::spanned( + fields_span, + Level::Error, + format!("`{derive_name}` can only be derived from plain enums"), + ) + .help( + "consider refactoring your data type to a struct with a plain enum as a field" + .to_string(), + ) + .note("more complex enums could be implemented in the future".to_string()), + + DeriveError::InvalidAttributePosition { attribute_span } => Diagnostic::spanned( + attribute_span, + Level::Error, + "invalid attribute position".to_string(), + ) + .help(format!( + "check documentation for `{derive_name}` for valid placements" + )), + + DeriveError::UnexpectedAttribute { meta_span } => { + Diagnostic::spanned(meta_span, Level::Error, "unknown attribute".to_string()).help( + format!("check documentation for `{derive_name}` for valid attributes"), + ) + } + + DeriveError::InvalidAttributeValue { value_span, value } => { + Diagnostic::spanned(value_span, Level::Error, format!("invalid value {value:?}")) + .help(format!( + "check documentation for `{derive_name}` for valid attribute values" + )) + } + } + } +} diff --git a/crates/nu-derive-value/src/from.rs b/crates/nu-derive-value/src/from.rs new file mode 100644 index 0000000000..783a22920e --- /dev/null +++ b/crates/nu-derive-value/src/from.rs @@ -0,0 +1,567 @@ +use convert_case::Casing; +use proc_macro2::TokenStream as TokenStream2; +use quote::{quote, ToTokens}; +use syn::{ + spanned::Spanned, Attribute, Data, DataEnum, DataStruct, DeriveInput, Fields, Generics, Ident, + Type, +}; + +use crate::attributes::{self, ContainerAttributes}; + +#[derive(Debug)] +pub struct FromValue; +type DeriveError = super::error::DeriveError; + +/// Inner implementation of the `#[derive(FromValue)]` macro for structs and enums. +/// +/// Uses `proc_macro2::TokenStream` for better testing support, unlike `proc_macro::TokenStream`. +/// +/// This function directs the `FromValue` trait derivation to the correct implementation based on +/// the input type: +/// - For structs: [`derive_struct_from_value`] +/// - For enums: [`derive_enum_from_value`] +/// - Unions are not supported and will return an error. +pub fn derive_from_value(input: TokenStream2) -> Result { + let input: DeriveInput = syn::parse2(input).map_err(DeriveError::Syn)?; + match input.data { + Data::Struct(data_struct) => Ok(derive_struct_from_value( + input.ident, + data_struct, + input.generics, + input.attrs, + )?), + Data::Enum(data_enum) => Ok(derive_enum_from_value( + input.ident, + data_enum, + input.generics, + input.attrs, + )?), + Data::Union(_) => Err(DeriveError::UnsupportedUnions), + } +} + +/// Implements the `#[derive(FromValue)]` macro for structs. +/// +/// This function ensures that the helper attribute is not used anywhere, as it is not supported for +/// structs. +/// Other than this, this function provides the impl signature for `FromValue`. +/// The implementation for `FromValue::from_value` is handled by [`struct_from_value`] and the +/// `FromValue::expected_type` is handled by [`struct_expected_type`]. +fn derive_struct_from_value( + ident: Ident, + data: DataStruct, + generics: Generics, + attrs: Vec, +) -> Result { + attributes::deny(&attrs)?; + attributes::deny_fields(&data.fields)?; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let from_value_impl = struct_from_value(&data); + let expected_type_impl = struct_expected_type(&data.fields); + Ok(quote! { + #[automatically_derived] + impl #impl_generics nu_protocol::FromValue for #ident #ty_generics #where_clause { + #from_value_impl + #expected_type_impl + } + }) +} + +/// Implements `FromValue::from_value` for structs. +/// +/// This function constructs the `from_value` function for structs. +/// The implementation is straightforward as most of the heavy lifting is handled by +/// `parse_value_via_fields`, and this function only needs to construct the signature around it. +/// +/// For structs with named fields, this constructs a large return type where each field +/// contains the implementation for that specific field. +/// In structs with unnamed fields, a [`VecDeque`](std::collections::VecDeque) is used to load each +/// field one after another, and the result is used to construct the tuple. +/// For unit structs, this only checks if the input value is `Value::Nothing`. +/// +/// # Examples +/// +/// These examples show what the macro would generate. +/// +/// Struct with named fields: +/// ```rust +/// #[derive(IntoValue)] +/// struct Pet { +/// name: String, +/// age: u8, +/// favorite_toy: Option, +/// } +/// +/// impl nu_protocol::FromValue for Pet { +/// fn from_value( +/// v: nu_protocol::Value +/// ) -> std::result::Result { +/// let span = v.span(); +/// let mut record = v.into_record()?; +/// std::result::Result::Ok(Pet { +/// name: ::from_value( +/// record +/// .remove("name") +/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn { +/// col_name: std::string::ToString::to_string("name"), +/// span: std::option::Option::None, +/// src_span: span +/// })?, +/// )?, +/// age: ::from_value( +/// record +/// .remove("age") +/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn { +/// col_name: std::string::ToString::to_string("age"), +/// span: std::option::Option::None, +/// src_span: span +/// })?, +/// )?, +/// favorite_toy: record +/// .remove("favorite_toy") +/// .map(|v| <#ty as nu_protocol::FromValue>::from_value(v)) +/// .transpose()? +/// .flatten(), +/// }) +/// } +/// } +/// ``` +/// +/// Struct with unnamed fields: +/// ```rust +/// #[derive(IntoValue)] +/// struct Color(u8, u8, u8); +/// +/// impl nu_protocol::FromValue for Color { +/// fn from_value( +/// v: nu_protocol::Value +/// ) -> std::result::Result { +/// let span = v.span(); +/// let list = v.into_list()?; +/// let mut deque: std::collections::VecDeque<_> = std::convert::From::from(list); +/// std::result::Result::Ok(Self( +/// { +/// ::from_value( +/// deque +/// .pop_front() +/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn { +/// col_name: std::string::ToString::to_string(&0), +/// span: std::option::Option::None, +/// src_span: span +/// })?, +/// )? +/// }, +/// { +/// ::from_value( +/// deque +/// .pop_front() +/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn { +/// col_name: std::string::ToString::to_string(&1), +/// span: std::option::Option::None, +/// src_span: span +/// })?, +/// )? +/// }, +/// { +/// ::from_value( +/// deque +/// .pop_front() +/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn { +/// col_name: std::string::ToString::to_string(&2), +/// span: std::option::Option::None, +/// src_span: span +/// })?, +/// )? +/// } +/// )) +/// } +/// } +/// ``` +/// +/// Unit struct: +/// ```rust +/// #[derive(IntoValue)] +/// struct Unicorn; +/// +/// impl nu_protocol::FromValue for Unicorn { +/// fn from_value( +/// v: nu_protocol::Value +/// ) -> std::result::Result { +/// match v { +/// nu_protocol::Value::Nothing {..} => Ok(Self), +/// v => std::result::Result::Err(nu_protocol::ShellError::CantConvert { +/// to_type: std::string::ToString::to_string(&::expected_type()), +/// from_type: std::string::ToString::to_string(&v.get_type()), +/// span: v.span(), +/// help: std::option::Option::None +/// }) +/// } +/// } +/// } +/// ``` +fn struct_from_value(data: &DataStruct) -> TokenStream2 { + let body = parse_value_via_fields(&data.fields, quote!(Self)); + quote! { + fn from_value( + v: nu_protocol::Value + ) -> std::result::Result { + #body + } + } +} + +/// Implements `FromValue::expected_type` for structs. +/// +/// This function constructs the `expected_type` function for structs. +/// The type depends on the `fields`: named fields construct a record type with every key and type +/// laid out. +/// Unnamed fields construct a custom type with the name in the format like +/// `list[type0, type1, type2]`. +/// No fields expect the `Type::Nothing`. +/// +/// # Examples +/// +/// These examples show what the macro would generate. +/// +/// Struct with named fields: +/// ```rust +/// #[derive(IntoValue)] +/// struct Pet { +/// name: String, +/// age: u8, +/// favorite_toy: Option, +/// } +/// +/// impl nu_protocol::FromValue for Pet { +/// fn expected_type() -> nu_protocol::Type { +/// nu_protocol::Type::Record( +/// std::vec![ +/// ( +/// std::string::ToString::to_string("name"), +/// ::expected_type(), +/// ), +/// ( +/// std::string::ToString::to_string("age"), +/// ::expected_type(), +/// ), +/// ( +/// std::string::ToString::to_string("favorite_toy"), +/// as nu_protocol::FromValue>::expected_type(), +/// ) +/// ].into_boxed_slice() +/// ) +/// } +/// } +/// ``` +/// +/// Struct with unnamed fields: +/// ```rust +/// #[derive(IntoValue)] +/// struct Color(u8, u8, u8); +/// +/// impl nu_protocol::FromValue for Color { +/// fn expected_type() -> nu_protocol::Type { +/// nu_protocol::Type::Custom( +/// std::format!( +/// "[{}, {}, {}]", +/// ::expected_type(), +/// ::expected_type(), +/// ::expected_type() +/// ) +/// .into_boxed_str() +/// ) +/// } +/// } +/// ``` +/// +/// Unit struct: +/// ```rust +/// #[derive(IntoValue)] +/// struct Unicorn; +/// +/// impl nu_protocol::FromValue for Color { +/// fn expected_type() -> nu_protocol::Type { +/// nu_protocol::Type::Nothing +/// } +/// } +/// ``` +fn struct_expected_type(fields: &Fields) -> TokenStream2 { + let ty = match fields { + Fields::Named(fields) => { + let fields = fields.named.iter().map(|field| { + let ident = field.ident.as_ref().expect("named has idents"); + let ident_s = ident.to_string(); + let ty = &field.ty; + quote! {( + std::string::ToString::to_string(#ident_s), + <#ty as nu_protocol::FromValue>::expected_type(), + )} + }); + quote!(nu_protocol::Type::Record( + std::vec![#(#fields),*].into_boxed_slice() + )) + } + Fields::Unnamed(fields) => { + let mut iter = fields.unnamed.iter(); + let fields = fields.unnamed.iter().map(|field| { + let ty = &field.ty; + quote!(<#ty as nu_protocol::FromValue>::expected_type()) + }); + let mut template = String::new(); + template.push('['); + if iter.next().is_some() { + template.push_str("{}") + } + iter.for_each(|_| template.push_str(", {}")); + template.push(']'); + quote! { + nu_protocol::Type::Custom( + std::format!( + #template, + #(#fields),* + ) + .into_boxed_str() + ) + } + } + Fields::Unit => quote!(nu_protocol::Type::Nothing), + }; + + quote! { + fn expected_type() -> nu_protocol::Type { + #ty + } + } +} + +/// Implements the `#[derive(FromValue)]` macro for enums. +/// +/// This function constructs the implementation of the `FromValue` trait for enums. +/// It is designed to be on the same level as [`derive_struct_from_value`], even though this +/// function only provides the impl signature for `FromValue`. +/// The actual implementation for `FromValue::from_value` is handled by [`enum_from_value`]. +/// +/// Since variants are difficult to type with the current type system, this function uses the +/// default implementation for `expected_type`. +fn derive_enum_from_value( + ident: Ident, + data: DataEnum, + generics: Generics, + attrs: Vec, +) -> Result { + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let from_value_impl = enum_from_value(&data, &attrs)?; + Ok(quote! { + #[automatically_derived] + impl #impl_generics nu_protocol::FromValue for #ident #ty_generics #where_clause { + #from_value_impl + } + }) +} + +/// Implements `FromValue::from_value` for enums. +/// +/// This function constructs the `from_value` implementation for enums. +/// It only accepts enums with unit variants, as it is currently unclear how other types of enums +/// should be represented via a `Value`. +/// This function checks that every field is a unit variant and constructs a match statement over +/// all possible variants. +/// The input value is expected to be a `Value::String` containing the name of the variant formatted +/// as defined by the `#[nu_value(rename_all = "...")]` attribute. +/// If no attribute is given, [`convert_case::Case::Snake`] is expected. +/// +/// If no matching variant is found, `ShellError::CantConvert` is returned. +/// +/// This is how such a derived implementation looks: +/// ```rust +/// #[derive(IntoValue)] +/// enum Weather { +/// Sunny, +/// Cloudy, +/// Raining +/// } +/// +/// impl nu_protocol::IntoValue for Weather { +/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value { +/// let span = v.span(); +/// let ty = v.get_type(); +/// +/// let s = v.into_string()?; +/// match s.as_str() { +/// "sunny" => std::result::Ok(Self::Sunny), +/// "cloudy" => std::result::Ok(Self::Cloudy), +/// "raining" => std::result::Ok(Self::Raining), +/// _ => std::result::Result::Err(nu_protocol::ShellError::CantConvert { +/// to_type: std::string::ToString::to_string( +/// &::expected_type() +/// ), +/// from_type: std::string::ToString::to_string(&ty), +/// span: span,help: std::option::Option::None, +/// }), +/// } +/// } +/// } +/// ``` +fn enum_from_value(data: &DataEnum, attrs: &[Attribute]) -> Result { + let container_attrs = ContainerAttributes::parse_attrs(attrs.iter())?; + let arms: Vec = data + .variants + .iter() + .map(|variant| { + attributes::deny(&variant.attrs)?; + let ident = &variant.ident; + let ident_s = format!("{ident}") + .as_str() + .to_case(container_attrs.rename_all); + match &variant.fields { + Fields::Named(fields) => Err(DeriveError::UnsupportedEnums { + fields_span: fields.span(), + }), + Fields::Unnamed(fields) => Err(DeriveError::UnsupportedEnums { + fields_span: fields.span(), + }), + Fields::Unit => Ok(quote!(#ident_s => std::result::Result::Ok(Self::#ident))), + } + }) + .collect::>()?; + + Ok(quote! { + fn from_value( + v: nu_protocol::Value + ) -> std::result::Result { + let span = v.span(); + let ty = v.get_type(); + + let s = v.into_string()?; + match s.as_str() { + #(#arms,)* + _ => std::result::Result::Err(nu_protocol::ShellError::CantConvert { + to_type: std::string::ToString::to_string( + &::expected_type() + ), + from_type: std::string::ToString::to_string(&ty), + span: span, + help: std::option::Option::None, + }), + } + } + }) +} + +/// Parses a `Value` into self. +/// +/// This function handles the actual parsing of a `Value` into self. +/// It takes two parameters: `fields` and `self_ident`. +/// The `fields` parameter determines the expected type of `Value`: named fields expect a +/// `Value::Record`, unnamed fields expect a `Value::List`, and a unit expects `Value::Nothing`. +/// +/// For named fields, the `fields` parameter indicates which field in the record corresponds to +/// which struct field. +/// For both named and unnamed fields, it also helps cast the type into a `FromValue` type. +/// This approach maintains +/// [hygiene](https://doc.rust-lang.org/reference/macros-by-example.html#hygiene). +/// +/// The `self_ident` parameter is used to describe the identifier of the returned value. +/// For structs, `Self` is usually sufficient, but for enums, `Self::Variant` may be needed in the +/// future. +/// +/// This function is more complex than the equivalent for `IntoValue` due to error handling +/// requirements. +/// For missing fields, `ShellError::CantFindColumn` is used, and for unit structs, +/// `ShellError::CantConvert` is used. +/// The implementation avoids local variables for fields to prevent accidental shadowing, ensuring +/// that poorly named fields don't cause issues. +/// While this style is not typically recommended in handwritten Rust, it is acceptable for code +/// generation. +fn parse_value_via_fields(fields: &Fields, self_ident: impl ToTokens) -> TokenStream2 { + match fields { + Fields::Named(fields) => { + let fields = fields.named.iter().map(|field| { + let ident = field.ident.as_ref().expect("named has idents"); + let ident_s = ident.to_string(); + let ty = &field.ty; + match type_is_option(ty) { + true => quote! { + #ident: record + .remove(#ident_s) + .map(|v| <#ty as nu_protocol::FromValue>::from_value(v)) + .transpose()? + .flatten() + }, + + false => quote! { + #ident: <#ty as nu_protocol::FromValue>::from_value( + record + .remove(#ident_s) + .ok_or_else(|| nu_protocol::ShellError::CantFindColumn { + col_name: std::string::ToString::to_string(#ident_s), + span: std::option::Option::None, + src_span: span + })?, + )? + }, + } + }); + quote! { + let span = v.span(); + let mut record = v.into_record()?; + std::result::Result::Ok(#self_ident {#(#fields),*}) + } + } + Fields::Unnamed(fields) => { + let fields = fields.unnamed.iter().enumerate().map(|(i, field)| { + let ty = &field.ty; + quote! {{ + <#ty as nu_protocol::FromValue>::from_value( + deque + .pop_front() + .ok_or_else(|| nu_protocol::ShellError::CantFindColumn { + col_name: std::string::ToString::to_string(&#i), + span: std::option::Option::None, + src_span: span + })?, + )? + }} + }); + quote! { + let span = v.span(); + let list = v.into_list()?; + let mut deque: std::collections::VecDeque<_> = std::convert::From::from(list); + std::result::Result::Ok(#self_ident(#(#fields),*)) + } + } + Fields::Unit => quote! { + match v { + nu_protocol::Value::Nothing {..} => Ok(#self_ident), + v => std::result::Result::Err(nu_protocol::ShellError::CantConvert { + to_type: std::string::ToString::to_string(&::expected_type()), + from_type: std::string::ToString::to_string(&v.get_type()), + span: v.span(), + help: std::option::Option::None + }) + } + }, + } +} + +const FULLY_QUALIFIED_OPTION: &str = "std::option::Option"; +const PARTIALLY_QUALIFIED_OPTION: &str = "option::Option"; +const PRELUDE_OPTION: &str = "Option"; + +/// Check if the field type is an `Option`. +/// +/// This function checks if a given type is an `Option`. +/// We assume that an `Option` is [`std::option::Option`] because we can't see the whole code and +/// can't ask the compiler itself. +/// If the `Option` type isn't `std::option::Option`, the user will get a compile error due to a +/// type mismatch. +/// It's very unusual for people to override `Option`, so this should rarely be an issue. +/// +/// When [rust#63084](https://github.com/rust-lang/rust/issues/63084) is resolved, we can use +/// [`std::any::type_name`] for a static assertion check to get a more direct error messages. +fn type_is_option(ty: &Type) -> bool { + let s = ty.to_token_stream().to_string(); + s.starts_with(PRELUDE_OPTION) + || s.starts_with(PARTIALLY_QUALIFIED_OPTION) + || s.starts_with(FULLY_QUALIFIED_OPTION) +} diff --git a/crates/nu-derive-value/src/into.rs b/crates/nu-derive-value/src/into.rs new file mode 100644 index 0000000000..a7cec06378 --- /dev/null +++ b/crates/nu-derive-value/src/into.rs @@ -0,0 +1,266 @@ +use convert_case::Casing; +use proc_macro2::TokenStream as TokenStream2; +use quote::{quote, ToTokens}; +use syn::{ + spanned::Spanned, Attribute, Data, DataEnum, DataStruct, DeriveInput, Fields, Generics, Ident, + Index, +}; + +use crate::attributes::{self, ContainerAttributes}; + +#[derive(Debug)] +pub struct IntoValue; +type DeriveError = super::error::DeriveError; + +/// Inner implementation of the `#[derive(IntoValue)]` macro for structs and enums. +/// +/// Uses `proc_macro2::TokenStream` for better testing support, unlike `proc_macro::TokenStream`. +/// +/// This function directs the `IntoValue` trait derivation to the correct implementation based on +/// the input type: +/// - For structs: [`struct_into_value`] +/// - For enums: [`enum_into_value`] +/// - Unions are not supported and will return an error. +pub fn derive_into_value(input: TokenStream2) -> Result { + let input: DeriveInput = syn::parse2(input).map_err(DeriveError::Syn)?; + match input.data { + Data::Struct(data_struct) => Ok(struct_into_value( + input.ident, + data_struct, + input.generics, + input.attrs, + )?), + Data::Enum(data_enum) => Ok(enum_into_value( + input.ident, + data_enum, + input.generics, + input.attrs, + )?), + Data::Union(_) => Err(DeriveError::UnsupportedUnions), + } +} + +/// Implements the `#[derive(IntoValue)]` macro for structs. +/// +/// Automatically derives the `IntoValue` trait for any struct where each field implements +/// `IntoValue`. +/// For structs with named fields, the derived implementation creates a `Value::Record` using the +/// struct fields as keys. +/// Each field value is converted using the `IntoValue::into_value` method. +/// For structs with unnamed fields, this generates a `Value::List` with each field in the list. +/// For unit structs, this generates `Value::Nothing`, because there is no data. +/// +/// Note: The helper attribute `#[nu_value(...)]` is currently not allowed on structs. +/// +/// # Examples +/// +/// These examples show what the macro would generate. +/// +/// Struct with named fields: +/// ```rust +/// #[derive(IntoValue)] +/// struct Pet { +/// name: String, +/// age: u8, +/// favorite_toy: Option, +/// } +/// +/// impl nu_protocol::IntoValue for Pet { +/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value { +/// nu_protocol::Value::record(nu_protocol::record! { +/// "name" => nu_protocol::IntoValue::into_value(self.name, span), +/// "age" => nu_protocol::IntoValue::into_value(self.age, span), +/// "favorite_toy" => nu_protocol::IntoValue::into_value(self.favorite_toy, span), +/// }, span) +/// } +/// } +/// ``` +/// +/// Struct with unnamed fields: +/// ```rust +/// #[derive(IntoValue)] +/// struct Color(u8, u8, u8); +/// +/// impl nu_protocol::IntoValue for Color { +/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value { +/// nu_protocol::Value::list(vec![ +/// nu_protocol::IntoValue::into_value(self.0, span), +/// nu_protocol::IntoValue::into_value(self.1, span), +/// nu_protocol::IntoValue::into_value(self.2, span), +/// ], span) +/// } +/// } +/// ``` +/// +/// Unit struct: +/// ```rust +/// #[derive(IntoValue)] +/// struct Unicorn; +/// +/// impl nu_protocol::IntoValue for Unicorn { +/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value { +/// nu_protocol::Value::nothing(span) +/// } +/// } +/// ``` +fn struct_into_value( + ident: Ident, + data: DataStruct, + generics: Generics, + attrs: Vec, +) -> Result { + attributes::deny(&attrs)?; + attributes::deny_fields(&data.fields)?; + let record = match &data.fields { + Fields::Named(fields) => { + let accessor = fields + .named + .iter() + .map(|field| field.ident.as_ref().expect("named has idents")) + .map(|ident| quote!(self.#ident)); + fields_return_value(&data.fields, accessor) + } + Fields::Unnamed(fields) => { + let accessor = fields + .unnamed + .iter() + .enumerate() + .map(|(n, _)| Index::from(n)) + .map(|index| quote!(self.#index)); + fields_return_value(&data.fields, accessor) + } + Fields::Unit => quote!(nu_protocol::Value::nothing(span)), + }; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + Ok(quote! { + #[automatically_derived] + impl #impl_generics nu_protocol::IntoValue for #ident #ty_generics #where_clause { + fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value { + #record + } + } + }) +} + +/// Implements the `#[derive(IntoValue)]` macro for enums. +/// +/// This function implements the derive macro `IntoValue` for enums. +/// Currently, only unit enum variants are supported as it is not clear how other types of enums +/// should be represented in a `Value`. +/// For simple enums, we represent the enum as a `Value::String`. For other types of variants, we return an error. +/// The variant name will be case-converted as described by the `#[nu_value(rename_all = "...")]` helper attribute. +/// If no attribute is used, the default is `case_convert::Case::Snake`. +/// The implementation matches over all variants, uses the appropriate variant name, and constructs a `Value::String`. +/// +/// This is how such a derived implementation looks: +/// ```rust +/// #[derive(IntoValue)] +/// enum Weather { +/// Sunny, +/// Cloudy, +/// Raining +/// } +/// +/// impl nu_protocol::IntoValue for Weather { +/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value { +/// match self { +/// Self::Sunny => nu_protocol::Value::string("sunny", span), +/// Self::Cloudy => nu_protocol::Value::string("cloudy", span), +/// Self::Raining => nu_protocol::Value::string("raining", span), +/// } +/// } +/// } +/// ``` +fn enum_into_value( + ident: Ident, + data: DataEnum, + generics: Generics, + attrs: Vec, +) -> Result { + let container_attrs = ContainerAttributes::parse_attrs(attrs.iter())?; + let arms: Vec = data + .variants + .into_iter() + .map(|variant| { + attributes::deny(&variant.attrs)?; + let ident = variant.ident; + let ident_s = format!("{ident}") + .as_str() + .to_case(container_attrs.rename_all); + match &variant.fields { + // In the future we can implement more complexe enums here. + Fields::Named(fields) => Err(DeriveError::UnsupportedEnums { + fields_span: fields.span(), + }), + Fields::Unnamed(fields) => Err(DeriveError::UnsupportedEnums { + fields_span: fields.span(), + }), + Fields::Unit => { + Ok(quote!(Self::#ident => nu_protocol::Value::string(#ident_s, span))) + } + } + }) + .collect::>()?; + + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + Ok(quote! { + impl #impl_generics nu_protocol::IntoValue for #ident #ty_generics #where_clause { + fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value { + match self { + #(#arms,)* + } + } + } + }) +} + +/// Constructs the final `Value` that the macro generates. +/// +/// This function handles the construction of the final `Value` that the macro generates. +/// It is currently only used for structs but may be used for enums in the future. +/// The function takes two parameters: the `fields`, which allow iterating over each field of a data +/// type, and the `accessor`. +/// The fields determine whether we need to generate a `Value::Record`, `Value::List`, or +/// `Value::Nothing`. +/// For named fields, they are also directly used to generate the record key. +/// +/// The `accessor` parameter generalizes how the data is accessed. +/// For named fields, this is usually the name of the fields preceded by `self` in a struct, and +/// maybe something else for enums. +/// For unnamed fields, this should be an iterator similar to the one with named fields, but +/// accessing tuple fields, so we get `self.n`. +/// For unit structs, this parameter is ignored. +/// By using the accessor like this, we can have the same code for structs and enums with data +/// variants in the future. +fn fields_return_value( + fields: &Fields, + accessor: impl Iterator, +) -> TokenStream2 { + match fields { + Fields::Named(fields) => { + let items: Vec = fields + .named + .iter() + .zip(accessor) + .map(|(field, accessor)| { + let ident = field.ident.as_ref().expect("named has idents"); + let field = ident.to_string(); + quote!(#field => nu_protocol::IntoValue::into_value(#accessor, span)) + }) + .collect(); + quote! { + nu_protocol::Value::record(nu_protocol::record! { + #(#items),* + }, span) + } + } + Fields::Unnamed(fields) => { + let items = + fields.unnamed.iter().zip(accessor).map( + |(_, accessor)| quote!(nu_protocol::IntoValue::into_value(#accessor, span)), + ); + quote!(nu_protocol::Value::list(std::vec![#(#items),*], span)) + } + Fields::Unit => quote!(nu_protocol::Value::nothing(span)), + } +} diff --git a/crates/nu-derive-value/src/lib.rs b/crates/nu-derive-value/src/lib.rs new file mode 100644 index 0000000000..269566fc88 --- /dev/null +++ b/crates/nu-derive-value/src/lib.rs @@ -0,0 +1,69 @@ +//! Macro implementations of `#[derive(FromValue, IntoValue)]`. +//! +//! As this crate is a [`proc_macro`] crate, it is only allowed to export +//! [procedural macros](https://doc.rust-lang.org/reference/procedural-macros.html). +//! Therefore, it only exports [`IntoValue`] and [`FromValue`]. +//! +//! To get documentation for other functions and types used in this crate, run +//! `cargo doc -p nu-derive-value --document-private-items`. +//! +//! This crate uses a lot of +//! [`proc_macro2::TokenStream`](https://docs.rs/proc-macro2/1.0.24/proc_macro2/struct.TokenStream.html) +//! as `TokenStream2` to allow testing the behavior of the macros directly, including the output +//! token stream or if the macro errors as expected. +//! The tests for functionality can be found in `nu_protocol::value::test_derive`. +//! +//! This documentation is often less reference-heavy than typical Rust documentation. +//! This is because this crate is a dependency for `nu_protocol`, and linking to it would create a +//! cyclic dependency. +//! Also all examples in the documentation aren't tested as this crate cannot be compiled as a +//! normal library very easily. +//! This might change in the future if cargo allows building a proc-macro crate differently for +//! `cfg(doctest)` as they are already doing for `cfg(test)`. +//! +//! The generated code from the derive macros tries to be as +//! [hygienic](https://doc.rust-lang.org/reference/macros-by-example.html#hygiene) as possible. +//! This ensures that the macro can be called anywhere without requiring specific imports. +//! This results in obtuse code, which isn't recommended for manual, handwritten Rust +//! but ensures that no other code may influence this generated code or vice versa. + +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use proc_macro_error::{proc_macro_error, Diagnostic}; + +mod attributes; +mod error; +mod from; +mod into; +#[cfg(test)] +mod tests; + +const HELPER_ATTRIBUTE: &str = "nu_value"; + +/// Derive macro generating an impl of the trait `IntoValue`. +/// +/// For further information, see the docs on the trait itself. +#[proc_macro_derive(IntoValue, attributes(nu_value))] +#[proc_macro_error] +pub fn derive_into_value(input: TokenStream) -> TokenStream { + let input = TokenStream2::from(input); + let output = match into::derive_into_value(input) { + Ok(output) => output, + Err(e) => Diagnostic::from(e).abort(), + }; + TokenStream::from(output) +} + +/// Derive macro generating an impl of the trait `FromValue`. +/// +/// For further information, see the docs on the trait itself. +#[proc_macro_derive(FromValue, attributes(nu_value))] +#[proc_macro_error] +pub fn derive_from_value(input: TokenStream) -> TokenStream { + let input = TokenStream2::from(input); + let output = match from::derive_from_value(input) { + Ok(output) => output, + Err(e) => Diagnostic::from(e).abort(), + }; + TokenStream::from(output) +} diff --git a/crates/nu-derive-value/src/tests.rs b/crates/nu-derive-value/src/tests.rs new file mode 100644 index 0000000000..b94d12a732 --- /dev/null +++ b/crates/nu-derive-value/src/tests.rs @@ -0,0 +1,157 @@ +// These tests only check that the derive macros throw the relevant errors. +// Functionality of the derived types is tested in nu_protocol::value::test_derive. + +use crate::error::DeriveError; +use crate::from::derive_from_value; +use crate::into::derive_into_value; +use quote::quote; + +#[test] +fn unsupported_unions() { + let input = quote! { + #[nu_value] + union SomeUnion { + f1: u32, + f2: f32, + } + }; + + let from_res = derive_from_value(input.clone()); + assert!( + matches!(from_res, Err(DeriveError::UnsupportedUnions)), + "expected `DeriveError::UnsupportedUnions`, got {:?}", + from_res + ); + + let into_res = derive_into_value(input); + assert!( + matches!(into_res, Err(DeriveError::UnsupportedUnions)), + "expected `DeriveError::UnsupportedUnions`, got {:?}", + into_res + ); +} + +#[test] +fn unsupported_enums() { + let input = quote! { + #[nu_value(rename_all = "SCREAMING_SNAKE_CASE")] + enum ComplexEnum { + Unit, + Unnamed(u32, f32), + Named { + u: u32, + f: f32, + } + } + }; + + let from_res = derive_from_value(input.clone()); + assert!( + matches!(from_res, Err(DeriveError::UnsupportedEnums { .. })), + "expected `DeriveError::UnsupportedEnums`, got {:?}", + from_res + ); + + let into_res = derive_into_value(input); + assert!( + matches!(into_res, Err(DeriveError::UnsupportedEnums { .. })), + "expected `DeriveError::UnsupportedEnums`, got {:?}", + into_res + ); +} + +#[test] +fn unexpected_attribute() { + let input = quote! { + #[nu_value(what)] + enum SimpleEnum { + A, + B, + } + }; + + let from_res = derive_from_value(input.clone()); + assert!( + matches!(from_res, Err(DeriveError::UnexpectedAttribute { .. })), + "expected `DeriveError::UnexpectedAttribute`, got {:?}", + from_res + ); + + let into_res = derive_into_value(input); + assert!( + matches!(into_res, Err(DeriveError::UnexpectedAttribute { .. })), + "expected `DeriveError::UnexpectedAttribute`, got {:?}", + into_res + ); +} + +#[test] +fn deny_attribute_on_structs() { + let input = quote! { + #[nu_value] + struct SomeStruct; + }; + + let from_res = derive_from_value(input.clone()); + assert!( + matches!(from_res, Err(DeriveError::InvalidAttributePosition { .. })), + "expected `DeriveError::InvalidAttributePosition`, got {:?}", + from_res + ); + + let into_res = derive_into_value(input); + assert!( + matches!(into_res, Err(DeriveError::InvalidAttributePosition { .. })), + "expected `DeriveError::InvalidAttributePosition`, got {:?}", + into_res + ); +} + +#[test] +fn deny_attribute_on_fields() { + let input = quote! { + struct SomeStruct { + #[nu_value] + field: () + } + }; + + let from_res = derive_from_value(input.clone()); + assert!( + matches!(from_res, Err(DeriveError::InvalidAttributePosition { .. })), + "expected `DeriveError::InvalidAttributePosition`, got {:?}", + from_res + ); + + let into_res = derive_into_value(input); + assert!( + matches!(into_res, Err(DeriveError::InvalidAttributePosition { .. })), + "expected `DeriveError::InvalidAttributePosition`, got {:?}", + into_res + ); +} + +#[test] +fn invalid_attribute_value() { + let input = quote! { + #[nu_value(rename_all = "CrazY-CasE")] + enum SimpleEnum { + A, + B + } + }; + + let from_res = derive_from_value(input.clone()); + assert!( + matches!(from_res, Err(DeriveError::InvalidAttributeValue { .. })), + "expected `DeriveError::InvalidAttributeValue`, got {:?}", + from_res + ); + + let into_res = derive_into_value(input); + assert!( + matches!(into_res, Err(DeriveError::InvalidAttributeValue { .. })), + "expected `DeriveError::InvalidAttributeValue`, got {:?}", + into_res + ); +} diff --git a/crates/nu-engine/Cargo.toml b/crates/nu-engine/Cargo.toml index fae2b3eff4..c416a2f590 100644 --- a/crates/nu-engine/Cargo.toml +++ b/crates/nu-engine/Cargo.toml @@ -5,16 +5,17 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-engine" edition = "2021" license = "MIT" name = "nu-engine" -version = "0.94.3" +version = "0.95.1" [lib] bench = false [dependencies] -nu-protocol = { path = "../nu-protocol", features = ["plugin"], version = "0.94.3" } -nu-path = { path = "../nu-path", version = "0.94.3" } -nu-glob = { path = "../nu-glob", version = "0.94.3" } -nu-utils = { path = "../nu-utils", version = "0.94.3" } +nu-protocol = { path = "../nu-protocol", features = ["plugin"], version = "0.95.1" } +nu-path = { path = "../nu-path", version = "0.95.1" } +nu-glob = { path = "../nu-glob", version = "0.95.1" } +nu-utils = { path = "../nu-utils", version = "0.95.1" } +log = { workspace = true } [features] plugin = [] diff --git a/crates/nu-engine/README.md b/crates/nu-engine/README.md new file mode 100644 index 0000000000..d5198cfc3f --- /dev/null +++ b/crates/nu-engine/README.md @@ -0,0 +1,9 @@ +This crate primarily drives the evaluation of expressions. + +(Some overlap with nu-protocol) + +- Provides `CallExt` + +## Internal Nushell crate + +This crate implements components of Nushell and is not designed to support plugin authors or other users directly. diff --git a/crates/nu-engine/src/call_ext.rs b/crates/nu-engine/src/call_ext.rs index daedb24a1f..d3f36215e6 100644 --- a/crates/nu-engine/src/call_ext.rs +++ b/crates/nu-engine/src/call_ext.rs @@ -1,10 +1,10 @@ use crate::eval_expression; use nu_protocol::{ - ast::Call, + ast, debugger::WithoutDebug, - engine::{EngineState, Stack, StateWorkingSet}, + engine::{self, EngineState, Stack, StateWorkingSet}, eval_const::eval_constant, - FromValue, ShellError, Value, + ir, FromValue, ShellError, Span, Value, }; pub trait CallExt { @@ -23,6 +23,9 @@ pub trait CallExt { name: &str, ) -> Result, ShellError>; + /// Efficiently get the span of a flag argument + fn get_flag_span(&self, stack: &Stack, name: &str) -> Option; + fn rest( &self, engine_state: &EngineState, @@ -56,9 +59,12 @@ pub trait CallExt { stack: &mut Stack, name: &str, ) -> Result; + + /// True if the command has any positional or rest arguments, excluding before the given index. + fn has_positional_args(&self, stack: &Stack, starting_pos: usize) -> bool; } -impl CallExt for Call { +impl CallExt for ast::Call { fn has_flag( &self, engine_state: &EngineState, @@ -104,6 +110,10 @@ impl CallExt for Call { } } + fn get_flag_span(&self, _stack: &Stack, name: &str) -> Option { + self.get_named_arg(name).map(|arg| arg.span) + } + fn rest( &self, engine_state: &EngineState, @@ -189,4 +199,205 @@ impl CallExt for Call { }) } } + + fn has_positional_args(&self, _stack: &Stack, starting_pos: usize) -> bool { + self.rest_iter(starting_pos).next().is_some() + } +} + +impl CallExt for ir::Call { + fn has_flag( + &self, + _engine_state: &EngineState, + stack: &mut Stack, + flag_name: &str, + ) -> Result { + Ok(self + .named_iter(stack) + .find(|(name, _)| name.item == flag_name) + .is_some_and(|(_, value)| { + // Handle --flag=false + !matches!(value, Some(Value::Bool { val: false, .. })) + })) + } + + fn get_flag( + &self, + _engine_state: &EngineState, + stack: &mut Stack, + name: &str, + ) -> Result, ShellError> { + if let Some(val) = self.get_named_arg(stack, name) { + T::from_value(val.clone()).map(Some) + } else { + Ok(None) + } + } + + fn get_flag_span(&self, stack: &Stack, name: &str) -> Option { + self.named_iter(stack) + .find_map(|(i_name, _)| (i_name.item == name).then_some(i_name.span)) + } + + fn rest( + &self, + _engine_state: &EngineState, + stack: &mut Stack, + starting_pos: usize, + ) -> Result, ShellError> { + self.rest_iter_flattened(stack, starting_pos)? + .into_iter() + .map(T::from_value) + .collect() + } + + fn opt( + &self, + _engine_state: &EngineState, + stack: &mut Stack, + pos: usize, + ) -> Result, ShellError> { + self.positional_iter(stack) + .nth(pos) + .cloned() + .map(T::from_value) + .transpose() + } + + fn opt_const( + &self, + _working_set: &StateWorkingSet, + _pos: usize, + ) -> Result, ShellError> { + Err(ShellError::IrEvalError { + msg: "const evaluation is not yet implemented on ir::Call".into(), + span: Some(self.head), + }) + } + + fn req( + &self, + engine_state: &EngineState, + stack: &mut Stack, + pos: usize, + ) -> Result { + if let Some(val) = self.opt(engine_state, stack, pos)? { + Ok(val) + } else if self.positional_len(stack) == 0 { + Err(ShellError::AccessEmptyContent { span: self.head }) + } else { + Err(ShellError::AccessBeyondEnd { + max_idx: self.positional_len(stack) - 1, + span: self.head, + }) + } + } + + fn req_parser_info( + &self, + engine_state: &EngineState, + stack: &mut Stack, + name: &str, + ) -> Result { + // FIXME: this depends on the AST evaluator. We can fix this by making the parser info an + // enum rather than using expressions. It's not clear that evaluation of this is ever really + // needed. + if let Some(expr) = self.get_parser_info(stack, name) { + let expr = expr.clone(); + let stack = &mut stack.use_call_arg_out_dest(); + let result = eval_expression::(engine_state, stack, &expr)?; + FromValue::from_value(result) + } else { + Err(ShellError::CantFindColumn { + col_name: name.into(), + span: None, + src_span: self.head, + }) + } + } + + fn has_positional_args(&self, stack: &Stack, starting_pos: usize) -> bool { + self.rest_iter(stack, starting_pos).next().is_some() + } +} + +macro_rules! proxy { + ($self:ident . $method:ident ($($param:expr),*)) => (match &$self.inner { + engine::CallImpl::AstRef(call) => call.$method($($param),*), + engine::CallImpl::AstBox(call) => call.$method($($param),*), + engine::CallImpl::IrRef(call) => call.$method($($param),*), + engine::CallImpl::IrBox(call) => call.$method($($param),*), + }) +} + +impl CallExt for engine::Call<'_> { + fn has_flag( + &self, + engine_state: &EngineState, + stack: &mut Stack, + flag_name: &str, + ) -> Result { + proxy!(self.has_flag(engine_state, stack, flag_name)) + } + + fn get_flag( + &self, + engine_state: &EngineState, + stack: &mut Stack, + name: &str, + ) -> Result, ShellError> { + proxy!(self.get_flag(engine_state, stack, name)) + } + + fn get_flag_span(&self, stack: &Stack, name: &str) -> Option { + proxy!(self.get_flag_span(stack, name)) + } + + fn rest( + &self, + engine_state: &EngineState, + stack: &mut Stack, + starting_pos: usize, + ) -> Result, ShellError> { + proxy!(self.rest(engine_state, stack, starting_pos)) + } + + fn opt( + &self, + engine_state: &EngineState, + stack: &mut Stack, + pos: usize, + ) -> Result, ShellError> { + proxy!(self.opt(engine_state, stack, pos)) + } + + fn opt_const( + &self, + working_set: &StateWorkingSet, + pos: usize, + ) -> Result, ShellError> { + proxy!(self.opt_const(working_set, pos)) + } + + fn req( + &self, + engine_state: &EngineState, + stack: &mut Stack, + pos: usize, + ) -> Result { + proxy!(self.req(engine_state, stack, pos)) + } + + fn req_parser_info( + &self, + engine_state: &EngineState, + stack: &mut Stack, + name: &str, + ) -> Result { + proxy!(self.req_parser_info(engine_state, stack, name)) + } + + fn has_positional_args(&self, stack: &Stack, starting_pos: usize) -> bool { + proxy!(self.has_positional_args(stack, starting_pos)) + } } diff --git a/crates/nu-engine/src/closure_eval.rs b/crates/nu-engine/src/closure_eval.rs index f4bc40658b..b271d90cbe 100644 --- a/crates/nu-engine/src/closure_eval.rs +++ b/crates/nu-engine/src/closure_eval.rs @@ -62,8 +62,8 @@ pub struct ClosureEval { stack: Stack, block: Arc, arg_index: usize, - env_vars: Vec, - env_hidden: HashMap>, + env_vars: Vec>, + env_hidden: Arc>>, eval: EvalBlockWithEarlyReturnFn, } diff --git a/crates/nu-engine/src/command_prelude.rs b/crates/nu-engine/src/command_prelude.rs index 5c21af27e0..e6ddb5fb91 100644 --- a/crates/nu-engine/src/command_prelude.rs +++ b/crates/nu-engine/src/command_prelude.rs @@ -1,7 +1,7 @@ pub use crate::CallExt; pub use nu_protocol::{ - ast::{Call, CellPath}, - engine::{Command, EngineState, Stack, StateWorkingSet}, + ast::CellPath, + engine::{Call, Command, EngineState, Stack, StateWorkingSet}, record, ByteStream, ByteStreamType, Category, ErrSpan, Example, IntoInterruptiblePipelineData, IntoPipelineData, IntoSpanned, PipelineData, Record, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, diff --git a/crates/nu-engine/src/compile/builder.rs b/crates/nu-engine/src/compile/builder.rs new file mode 100644 index 0000000000..4294b2bd61 --- /dev/null +++ b/crates/nu-engine/src/compile/builder.rs @@ -0,0 +1,576 @@ +use nu_protocol::{ + ir::{DataSlice, Instruction, IrAstRef, IrBlock, Literal}, + CompileError, IntoSpanned, RegId, Span, Spanned, +}; + +/// A label identifier. Only exists while building code. Replaced with the actual target. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct LabelId(pub usize); + +/// Builds [`IrBlock`]s progressively by consuming instructions and handles register allocation. +#[derive(Debug)] +pub(crate) struct BlockBuilder { + pub(crate) block_span: Option, + pub(crate) instructions: Vec, + pub(crate) spans: Vec, + /// The actual instruction index that a label refers to. While building IR, branch targets are + /// specified as indices into this array rather than the true instruction index. This makes it + /// easier to make modifications to code, as just this array needs to be changed, and it's also + /// less error prone as during `finish()` we check to make sure all of the used labels have had + /// an index actually set. + pub(crate) labels: Vec>, + pub(crate) data: Vec, + pub(crate) ast: Vec>, + pub(crate) comments: Vec, + pub(crate) register_allocation_state: Vec, + pub(crate) file_count: u32, + pub(crate) loop_stack: Vec, +} + +impl BlockBuilder { + /// Starts a new block, with the first register (`%0`) allocated as input. + pub(crate) fn new(block_span: Option) -> Self { + BlockBuilder { + block_span, + instructions: vec![], + spans: vec![], + labels: vec![], + data: vec![], + ast: vec![], + comments: vec![], + register_allocation_state: vec![true], + file_count: 0, + loop_stack: vec![], + } + } + + /// Get the next unused register for code generation. + pub(crate) fn next_register(&mut self) -> Result { + if let Some(index) = self + .register_allocation_state + .iter_mut() + .position(|is_allocated| { + if !*is_allocated { + *is_allocated = true; + true + } else { + false + } + }) + { + Ok(RegId(index as u32)) + } else if self.register_allocation_state.len() < (u32::MAX as usize - 2) { + let reg_id = RegId(self.register_allocation_state.len() as u32); + self.register_allocation_state.push(true); + Ok(reg_id) + } else { + Err(CompileError::RegisterOverflow { + block_span: self.block_span, + }) + } + } + + /// Check if a register is initialized with a value. + pub(crate) fn is_allocated(&self, reg_id: RegId) -> bool { + self.register_allocation_state + .get(reg_id.0 as usize) + .is_some_and(|state| *state) + } + + /// Mark a register as initialized. + pub(crate) fn mark_register(&mut self, reg_id: RegId) -> Result<(), CompileError> { + if let Some(is_allocated) = self.register_allocation_state.get_mut(reg_id.0 as usize) { + *is_allocated = true; + Ok(()) + } else { + Err(CompileError::RegisterOverflow { + block_span: self.block_span, + }) + } + } + + /// Mark a register as empty, so that it can be used again by something else. + #[track_caller] + pub(crate) fn free_register(&mut self, reg_id: RegId) -> Result<(), CompileError> { + let index = reg_id.0 as usize; + + if self + .register_allocation_state + .get(index) + .is_some_and(|is_allocated| *is_allocated) + { + self.register_allocation_state[index] = false; + Ok(()) + } else { + log::warn!("register {reg_id} uninitialized, builder = {self:#?}"); + Err(CompileError::RegisterUninitialized { + reg_id, + caller: std::panic::Location::caller().to_string(), + }) + } + } + + /// Define a label, which can be used by branch instructions. The target can optionally be + /// specified now. + pub(crate) fn label(&mut self, target_index: Option) -> LabelId { + let label_id = self.labels.len(); + self.labels.push(target_index); + LabelId(label_id) + } + + /// Change the target of a label. + pub(crate) fn set_label( + &mut self, + label_id: LabelId, + target_index: usize, + ) -> Result<(), CompileError> { + *self + .labels + .get_mut(label_id.0) + .ok_or(CompileError::UndefinedLabel { + label_id: label_id.0, + span: None, + })? = Some(target_index); + Ok(()) + } + + /// Insert an instruction into the block, automatically marking any registers populated by + /// the instruction, and freeing any registers consumed by the instruction. + #[track_caller] + pub(crate) fn push(&mut self, instruction: Spanned) -> Result<(), CompileError> { + // Free read registers, and mark write registers. + // + // If a register is both read and written, it should be on both sides, so that we can verify + // that the register was in the right state beforehand. + let mut allocate = |read: &[RegId], write: &[RegId]| -> Result<(), CompileError> { + for reg in read { + self.free_register(*reg)?; + } + for reg in write { + self.mark_register(*reg)?; + } + Ok(()) + }; + + let allocate_result = match &instruction.item { + Instruction::Unreachable => Ok(()), + Instruction::LoadLiteral { dst, lit } => { + allocate(&[], &[*dst]).and( + // Free any registers on the literal + match lit { + Literal::Range { + start, + step, + end, + inclusion: _, + } => allocate(&[*start, *step, *end], &[]), + Literal::Bool(_) + | Literal::Int(_) + | Literal::Float(_) + | Literal::Filesize(_) + | Literal::Duration(_) + | Literal::Binary(_) + | Literal::Block(_) + | Literal::Closure(_) + | Literal::RowCondition(_) + | Literal::List { capacity: _ } + | Literal::Record { capacity: _ } + | Literal::Filepath { + val: _, + no_expand: _, + } + | Literal::Directory { + val: _, + no_expand: _, + } + | Literal::GlobPattern { + val: _, + no_expand: _, + } + | Literal::String(_) + | Literal::RawString(_) + | Literal::CellPath(_) + | Literal::Date(_) + | Literal::Nothing => Ok(()), + }, + ) + } + Instruction::LoadValue { dst, val: _ } => allocate(&[], &[*dst]), + Instruction::Move { dst, src } => allocate(&[*src], &[*dst]), + Instruction::Clone { dst, src } => allocate(&[*src], &[*dst, *src]), + Instruction::Collect { src_dst } => allocate(&[*src_dst], &[*src_dst]), + Instruction::Span { src_dst } => allocate(&[*src_dst], &[*src_dst]), + Instruction::Drop { src } => allocate(&[*src], &[]), + Instruction::Drain { src } => allocate(&[*src], &[]), + Instruction::LoadVariable { dst, var_id: _ } => allocate(&[], &[*dst]), + Instruction::StoreVariable { var_id: _, src } => allocate(&[*src], &[]), + Instruction::DropVariable { var_id: _ } => Ok(()), + Instruction::LoadEnv { dst, key: _ } => allocate(&[], &[*dst]), + Instruction::LoadEnvOpt { dst, key: _ } => allocate(&[], &[*dst]), + Instruction::StoreEnv { key: _, src } => allocate(&[*src], &[]), + Instruction::PushPositional { src } => allocate(&[*src], &[]), + Instruction::AppendRest { src } => allocate(&[*src], &[]), + Instruction::PushFlag { name: _ } => Ok(()), + Instruction::PushShortFlag { short: _ } => Ok(()), + Instruction::PushNamed { name: _, src } => allocate(&[*src], &[]), + Instruction::PushShortNamed { short: _, src } => allocate(&[*src], &[]), + Instruction::PushParserInfo { name: _, info: _ } => Ok(()), + Instruction::RedirectOut { mode: _ } => Ok(()), + Instruction::RedirectErr { mode: _ } => Ok(()), + Instruction::CheckErrRedirected { src } => allocate(&[*src], &[*src]), + Instruction::OpenFile { + file_num: _, + path, + append: _, + } => allocate(&[*path], &[]), + Instruction::WriteFile { file_num: _, src } => allocate(&[*src], &[]), + Instruction::CloseFile { file_num: _ } => Ok(()), + Instruction::Call { + decl_id: _, + src_dst, + } => allocate(&[*src_dst], &[*src_dst]), + Instruction::StringAppend { src_dst, val } => allocate(&[*src_dst, *val], &[*src_dst]), + Instruction::GlobFrom { + src_dst, + no_expand: _, + } => allocate(&[*src_dst], &[*src_dst]), + Instruction::ListPush { src_dst, item } => allocate(&[*src_dst, *item], &[*src_dst]), + Instruction::ListSpread { src_dst, items } => { + allocate(&[*src_dst, *items], &[*src_dst]) + } + Instruction::RecordInsert { src_dst, key, val } => { + allocate(&[*src_dst, *key, *val], &[*src_dst]) + } + Instruction::RecordSpread { src_dst, items } => { + allocate(&[*src_dst, *items], &[*src_dst]) + } + Instruction::Not { src_dst } => allocate(&[*src_dst], &[*src_dst]), + Instruction::BinaryOp { + lhs_dst, + op: _, + rhs, + } => allocate(&[*lhs_dst, *rhs], &[*lhs_dst]), + Instruction::FollowCellPath { src_dst, path } => { + allocate(&[*src_dst, *path], &[*src_dst]) + } + Instruction::CloneCellPath { dst, src, path } => { + allocate(&[*src, *path], &[*src, *dst]) + } + Instruction::UpsertCellPath { + src_dst, + path, + new_value, + } => allocate(&[*src_dst, *path, *new_value], &[*src_dst]), + Instruction::Jump { index: _ } => Ok(()), + Instruction::BranchIf { cond, index: _ } => allocate(&[*cond], &[]), + Instruction::BranchIfEmpty { src, index: _ } => allocate(&[*src], &[*src]), + Instruction::Match { + pattern: _, + src, + index: _, + } => allocate(&[*src], &[*src]), + Instruction::CheckMatchGuard { src } => allocate(&[*src], &[*src]), + Instruction::Iterate { + dst, + stream, + end_index: _, + } => allocate(&[*stream], &[*dst, *stream]), + Instruction::OnError { index: _ } => Ok(()), + Instruction::OnErrorInto { index: _, dst } => allocate(&[], &[*dst]), + Instruction::PopErrorHandler => Ok(()), + Instruction::CheckExternalFailed { dst, src } => allocate(&[*src], &[*dst, *src]), + Instruction::ReturnEarly { src } => allocate(&[*src], &[]), + Instruction::Return { src } => allocate(&[*src], &[]), + }; + + // Add more context to the error + match allocate_result { + Ok(()) => (), + Err(CompileError::RegisterUninitialized { reg_id, caller }) => { + return Err(CompileError::RegisterUninitializedWhilePushingInstruction { + reg_id, + caller, + instruction: format!("{:?}", instruction.item), + span: instruction.span, + }); + } + Err(err) => return Err(err), + } + + self.instructions.push(instruction.item); + self.spans.push(instruction.span); + self.ast.push(None); + self.comments.push(String::new()); + Ok(()) + } + + /// Set the AST of the last instruction. Separate method because it's rarely used. + pub(crate) fn set_last_ast(&mut self, ast_ref: Option) { + *self.ast.last_mut().expect("no last instruction") = ast_ref; + } + + /// Add a comment to the last instruction. + pub(crate) fn add_comment(&mut self, comment: impl std::fmt::Display) { + add_comment( + self.comments.last_mut().expect("no last instruction"), + comment, + ) + } + + /// Load a register with a literal. + pub(crate) fn load_literal( + &mut self, + reg_id: RegId, + literal: Spanned, + ) -> Result<(), CompileError> { + self.push( + Instruction::LoadLiteral { + dst: reg_id, + lit: literal.item, + } + .into_spanned(literal.span), + )?; + Ok(()) + } + + /// Allocate a new register and load a literal into it. + pub(crate) fn literal(&mut self, literal: Spanned) -> Result { + let reg_id = self.next_register()?; + self.load_literal(reg_id, literal)?; + Ok(reg_id) + } + + /// Deallocate a register and set it to `Empty`, if it is allocated + pub(crate) fn drop_reg(&mut self, reg_id: RegId) -> Result<(), CompileError> { + if self.is_allocated(reg_id) { + self.push(Instruction::Drop { src: reg_id }.into_spanned(Span::unknown()))?; + } + Ok(()) + } + + /// Set a register to `Empty`, but mark it as in-use, e.g. for input + pub(crate) fn load_empty(&mut self, reg_id: RegId) -> Result<(), CompileError> { + self.drop_reg(reg_id)?; + self.mark_register(reg_id) + } + + /// Drain the stream in a register (fully consuming it) + pub(crate) fn drain(&mut self, src: RegId, span: Span) -> Result<(), CompileError> { + self.push(Instruction::Drain { src }.into_spanned(span)) + } + + /// Add data to the `data` array and return a [`DataSlice`] referencing it. + pub(crate) fn data(&mut self, data: impl AsRef<[u8]>) -> Result { + let data = data.as_ref(); + let start = self.data.len(); + if data.is_empty() { + Ok(DataSlice::empty()) + } else if start + data.len() < u32::MAX as usize { + let slice = DataSlice { + start: start as u32, + len: data.len() as u32, + }; + self.data.extend_from_slice(data); + Ok(slice) + } else { + Err(CompileError::DataOverflow { + block_span: self.block_span, + }) + } + } + + /// Clone a register with a `clone` instruction. + pub(crate) fn clone_reg(&mut self, src: RegId, span: Span) -> Result { + let dst = self.next_register()?; + self.push(Instruction::Clone { dst, src }.into_spanned(span))?; + Ok(dst) + } + + /// Add a `branch-if` instruction + pub(crate) fn branch_if( + &mut self, + cond: RegId, + label_id: LabelId, + span: Span, + ) -> Result<(), CompileError> { + self.push( + Instruction::BranchIf { + cond, + index: label_id.0, + } + .into_spanned(span), + ) + } + + /// Add a `branch-if-empty` instruction + pub(crate) fn branch_if_empty( + &mut self, + src: RegId, + label_id: LabelId, + span: Span, + ) -> Result<(), CompileError> { + self.push( + Instruction::BranchIfEmpty { + src, + index: label_id.0, + } + .into_spanned(span), + ) + } + + /// Add a `jump` instruction + pub(crate) fn jump(&mut self, label_id: LabelId, span: Span) -> Result<(), CompileError> { + self.push(Instruction::Jump { index: label_id.0 }.into_spanned(span)) + } + + /// The index that the next instruction [`.push()`]ed will have. + pub(crate) fn here(&self) -> usize { + self.instructions.len() + } + + /// Allocate a new file number, for redirection. + pub(crate) fn next_file_num(&mut self) -> Result { + let next = self.file_count; + self.file_count = self + .file_count + .checked_add(1) + .ok_or(CompileError::FileOverflow { + block_span: self.block_span, + })?; + Ok(next) + } + + /// Push a new loop state onto the builder. Creates new labels that must be set. + pub(crate) fn begin_loop(&mut self) -> Loop { + let loop_ = Loop { + break_label: self.label(None), + continue_label: self.label(None), + }; + self.loop_stack.push(loop_); + loop_ + } + + /// True if we are currently in a loop. + pub(crate) fn is_in_loop(&self) -> bool { + !self.loop_stack.is_empty() + } + + /// Add a loop breaking jump instruction. + pub(crate) fn push_break(&mut self, span: Span) -> Result<(), CompileError> { + let loop_ = self + .loop_stack + .last() + .ok_or_else(|| CompileError::NotInALoop { + msg: "`break` called from outside of a loop".into(), + span: Some(span), + })?; + self.jump(loop_.break_label, span) + } + + /// Add a loop continuing jump instruction. + pub(crate) fn push_continue(&mut self, span: Span) -> Result<(), CompileError> { + let loop_ = self + .loop_stack + .last() + .ok_or_else(|| CompileError::NotInALoop { + msg: "`continue` called from outside of a loop".into(), + span: Some(span), + })?; + self.jump(loop_.continue_label, span) + } + + /// Pop the loop state. Checks that the loop being ended is the same one that was expected. + pub(crate) fn end_loop(&mut self, loop_: Loop) -> Result<(), CompileError> { + let ended_loop = self + .loop_stack + .pop() + .ok_or_else(|| CompileError::NotInALoop { + msg: "end_loop() called outside of a loop".into(), + span: None, + })?; + + if ended_loop == loop_ { + Ok(()) + } else { + Err(CompileError::IncoherentLoopState { + block_span: self.block_span, + }) + } + } + + /// Mark an unreachable code path. Produces an error at runtime if executed. + pub(crate) fn unreachable(&mut self, span: Span) -> Result<(), CompileError> { + self.push(Instruction::Unreachable.into_spanned(span)) + } + + /// Consume the builder and produce the final [`IrBlock`]. + pub(crate) fn finish(mut self) -> Result { + // Add comments to label targets + for (index, label_target) in self.labels.iter().enumerate() { + if let Some(label_target) = label_target { + add_comment( + &mut self.comments[*label_target], + format_args!("label({index})"), + ); + } + } + + // Populate the actual target indices of labels into the instructions + for ((index, instruction), span) in + self.instructions.iter_mut().enumerate().zip(&self.spans) + { + if let Some(label_id) = instruction.branch_target() { + let target_index = self.labels.get(label_id).cloned().flatten().ok_or( + CompileError::UndefinedLabel { + label_id, + span: Some(*span), + }, + )?; + // Add a comment to the target index that we come from here + add_comment( + &mut self.comments[target_index], + format_args!("from({index}:)"), + ); + instruction.set_branch_target(target_index).map_err(|_| { + CompileError::SetBranchTargetOfNonBranchInstruction { + instruction: format!("{:?}", instruction), + span: *span, + } + })?; + } + } + + Ok(IrBlock { + instructions: self.instructions, + spans: self.spans, + data: self.data.into(), + ast: self.ast, + comments: self.comments.into_iter().map(|s| s.into()).collect(), + register_count: self + .register_allocation_state + .len() + .try_into() + .expect("register count overflowed in finish() despite previous checks"), + file_count: self.file_count, + }) + } +} + +/// Keeps track of the `break` and `continue` target labels for a loop. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct Loop { + pub(crate) break_label: LabelId, + pub(crate) continue_label: LabelId, +} + +/// Add a new comment to an existing one +fn add_comment(comment: &mut String, new_comment: impl std::fmt::Display) { + use std::fmt::Write; + write!( + comment, + "{}{}", + if comment.is_empty() { "" } else { ", " }, + new_comment + ) + .expect("formatting failed"); +} diff --git a/crates/nu-engine/src/compile/call.rs b/crates/nu-engine/src/compile/call.rs new file mode 100644 index 0000000000..d9f1b8e581 --- /dev/null +++ b/crates/nu-engine/src/compile/call.rs @@ -0,0 +1,270 @@ +use std::sync::Arc; + +use nu_protocol::{ + ast::{Argument, Call, Expression, ExternalArgument}, + engine::StateWorkingSet, + ir::{Instruction, IrAstRef, Literal}, + IntoSpanned, RegId, Span, Spanned, +}; + +use super::{compile_expression, keyword::*, BlockBuilder, CompileError, RedirectModes}; + +pub(crate) fn compile_call( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + let decl = working_set.get_decl(call.decl_id); + + // Check if this call has --help - if so, just redirect to `help` + if call.named_iter().any(|(name, _, _)| name.item == "help") { + return compile_help( + working_set, + builder, + decl.name().into_spanned(call.head), + io_reg, + ); + } + + // Try to figure out if this is a keyword call like `if`, and handle those specially + if decl.is_keyword() { + match decl.name() { + "if" => { + return compile_if(working_set, builder, call, redirect_modes, io_reg); + } + "match" => { + return compile_match(working_set, builder, call, redirect_modes, io_reg); + } + "const" => { + // This differs from the behavior of the const command, which adds the const value + // to the stack. Since `load-variable` also checks `engine_state` for the variable + // and will get a const value though, is it really necessary to do that? + return builder.load_empty(io_reg); + } + "alias" => { + // Alias does nothing + return builder.load_empty(io_reg); + } + "let" | "mut" => { + return compile_let(working_set, builder, call, redirect_modes, io_reg); + } + "try" => { + return compile_try(working_set, builder, call, redirect_modes, io_reg); + } + "loop" => { + return compile_loop(working_set, builder, call, redirect_modes, io_reg); + } + "while" => { + return compile_while(working_set, builder, call, redirect_modes, io_reg); + } + "for" => { + return compile_for(working_set, builder, call, redirect_modes, io_reg); + } + "break" => { + return compile_break(working_set, builder, call, redirect_modes, io_reg); + } + "continue" => { + return compile_continue(working_set, builder, call, redirect_modes, io_reg); + } + "return" => { + return compile_return(working_set, builder, call, redirect_modes, io_reg); + } + _ => (), + } + } + + // Keep AST if the decl needs it. + let requires_ast = decl.requires_ast_for_arguments(); + + // It's important that we evaluate the args first before trying to set up the argument + // state for the call. + // + // We could technically compile anything that isn't another call safely without worrying about + // the argument state, but we'd have to check all of that first and it just isn't really worth + // it. + enum CompiledArg<'a> { + Positional(RegId, Span, Option), + Named( + &'a str, + Option<&'a str>, + Option, + Span, + Option, + ), + Spread(RegId, Span, Option), + } + + let mut compiled_args = vec![]; + + for arg in &call.arguments { + let arg_reg = arg + .expr() + .map(|expr| { + let arg_reg = builder.next_register()?; + + compile_expression( + working_set, + builder, + expr, + RedirectModes::capture_out(arg.span()), + None, + arg_reg, + )?; + + Ok(arg_reg) + }) + .transpose()?; + + let ast_ref = arg + .expr() + .filter(|_| requires_ast) + .map(|expr| IrAstRef(Arc::new(expr.clone()))); + + match arg { + Argument::Positional(_) | Argument::Unknown(_) => { + compiled_args.push(CompiledArg::Positional( + arg_reg.expect("expr() None in non-Named"), + arg.span(), + ast_ref, + )) + } + Argument::Named((name, short, _)) => compiled_args.push(CompiledArg::Named( + &name.item, + short.as_ref().map(|spanned| spanned.item.as_str()), + arg_reg, + arg.span(), + ast_ref, + )), + Argument::Spread(_) => compiled_args.push(CompiledArg::Spread( + arg_reg.expect("expr() None in non-Named"), + arg.span(), + ast_ref, + )), + } + } + + // Now that the args are all compiled, set up the call state (argument stack and redirections) + for arg in compiled_args { + match arg { + CompiledArg::Positional(reg, span, ast_ref) => { + builder.push(Instruction::PushPositional { src: reg }.into_spanned(span))?; + builder.set_last_ast(ast_ref); + } + CompiledArg::Named(name, short, Some(reg), span, ast_ref) => { + if !name.is_empty() { + let name = builder.data(name)?; + builder.push(Instruction::PushNamed { name, src: reg }.into_spanned(span))?; + } else { + let short = builder.data(short.unwrap_or(""))?; + builder + .push(Instruction::PushShortNamed { short, src: reg }.into_spanned(span))?; + } + builder.set_last_ast(ast_ref); + } + CompiledArg::Named(name, short, None, span, ast_ref) => { + if !name.is_empty() { + let name = builder.data(name)?; + builder.push(Instruction::PushFlag { name }.into_spanned(span))?; + } else { + let short = builder.data(short.unwrap_or(""))?; + builder.push(Instruction::PushShortFlag { short }.into_spanned(span))?; + } + builder.set_last_ast(ast_ref); + } + CompiledArg::Spread(reg, span, ast_ref) => { + builder.push(Instruction::AppendRest { src: reg }.into_spanned(span))?; + builder.set_last_ast(ast_ref); + } + } + } + + // Add any parser info from the call + for (name, info) in &call.parser_info { + let name = builder.data(name)?; + let info = Box::new(info.clone()); + builder.push(Instruction::PushParserInfo { name, info }.into_spanned(call.head))?; + } + + if let Some(mode) = redirect_modes.out { + builder.push(mode.map(|mode| Instruction::RedirectOut { mode }))?; + } + + if let Some(mode) = redirect_modes.err { + builder.push(mode.map(|mode| Instruction::RedirectErr { mode }))?; + } + + // The state is set up, so we can do the call into io_reg + builder.push( + Instruction::Call { + decl_id: call.decl_id, + src_dst: io_reg, + } + .into_spanned(call.head), + )?; + + Ok(()) +} + +pub(crate) fn compile_help( + working_set: &StateWorkingSet<'_>, + builder: &mut BlockBuilder, + decl_name: Spanned<&str>, + io_reg: RegId, +) -> Result<(), CompileError> { + let help_command_id = + working_set + .find_decl(b"help") + .ok_or_else(|| CompileError::MissingRequiredDeclaration { + decl_name: "help".into(), + span: decl_name.span, + })?; + + let name_data = builder.data(decl_name.item)?; + let name_literal = builder.literal(decl_name.map(|_| Literal::String(name_data)))?; + + builder.push(Instruction::PushPositional { src: name_literal }.into_spanned(decl_name.span))?; + + builder.push( + Instruction::Call { + decl_id: help_command_id, + src_dst: io_reg, + } + .into_spanned(decl_name.span), + )?; + + Ok(()) +} + +pub(crate) fn compile_external_call( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + head: &Expression, + args: &[ExternalArgument], + redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + // Pass everything to run-external + let run_external_id = working_set + .find_decl(b"run-external") + .ok_or(CompileError::RunExternalNotFound { span: head.span })?; + + let mut call = Call::new(head.span); + call.decl_id = run_external_id; + + call.arguments.push(Argument::Positional(head.clone())); + + for arg in args { + match arg { + ExternalArgument::Regular(expr) => { + call.arguments.push(Argument::Positional(expr.clone())); + } + ExternalArgument::Spread(expr) => { + call.arguments.push(Argument::Spread(expr.clone())); + } + } + } + + compile_call(working_set, builder, &call, redirect_modes, io_reg) +} diff --git a/crates/nu-engine/src/compile/expression.rs b/crates/nu-engine/src/compile/expression.rs new file mode 100644 index 0000000000..948eb5c238 --- /dev/null +++ b/crates/nu-engine/src/compile/expression.rs @@ -0,0 +1,556 @@ +use super::{ + compile_binary_op, compile_block, compile_call, compile_external_call, compile_load_env, + BlockBuilder, CompileError, RedirectModes, +}; + +use nu_protocol::{ + ast::{CellPath, Expr, Expression, ListItem, RecordItem, ValueWithUnit}, + engine::StateWorkingSet, + ir::{DataSlice, Instruction, Literal}, + IntoSpanned, RegId, Span, Value, ENV_VARIABLE_ID, +}; + +pub(crate) fn compile_expression( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + expr: &Expression, + redirect_modes: RedirectModes, + in_reg: Option, + out_reg: RegId, +) -> Result<(), CompileError> { + let drop_input = |builder: &mut BlockBuilder| { + if let Some(in_reg) = in_reg { + if in_reg != out_reg { + builder.drop_reg(in_reg)?; + } + } + Ok(()) + }; + + let lit = |builder: &mut BlockBuilder, literal: Literal| { + drop_input(builder)?; + + builder + .push( + Instruction::LoadLiteral { + dst: out_reg, + lit: literal, + } + .into_spanned(expr.span), + ) + .map(|_| ()) + }; + + let ignore = |builder: &mut BlockBuilder| { + drop_input(builder)?; + builder.load_empty(out_reg) + }; + + let unexpected = |expr_name: &str| CompileError::UnexpectedExpression { + expr_name: expr_name.into(), + span: expr.span, + }; + + let move_in_reg_to_out_reg = |builder: &mut BlockBuilder| { + // Ensure that out_reg contains the input value, because a call only uses one register + if let Some(in_reg) = in_reg { + if in_reg != out_reg { + // Have to move in_reg to out_reg so it can be used + builder.push( + Instruction::Move { + dst: out_reg, + src: in_reg, + } + .into_spanned(expr.span), + )?; + } + } else { + // Will have to initialize out_reg with Empty first + builder.load_empty(out_reg)?; + } + Ok(()) + }; + + match &expr.expr { + Expr::Bool(b) => lit(builder, Literal::Bool(*b)), + Expr::Int(i) => lit(builder, Literal::Int(*i)), + Expr::Float(f) => lit(builder, Literal::Float(*f)), + Expr::Binary(bin) => { + let data_slice = builder.data(bin)?; + lit(builder, Literal::Binary(data_slice)) + } + Expr::Range(range) => { + // Compile the subexpressions of the range + let compile_part = |builder: &mut BlockBuilder, + part_expr: Option<&Expression>| + -> Result { + let reg = builder.next_register()?; + if let Some(part_expr) = part_expr { + compile_expression( + working_set, + builder, + part_expr, + RedirectModes::capture_out(part_expr.span), + None, + reg, + )?; + } else { + builder.load_literal(reg, Literal::Nothing.into_spanned(expr.span))?; + } + Ok(reg) + }; + + drop_input(builder)?; + + let start = compile_part(builder, range.from.as_ref())?; + let step = compile_part(builder, range.next.as_ref())?; + let end = compile_part(builder, range.to.as_ref())?; + + // Assemble the range + builder.load_literal( + out_reg, + Literal::Range { + start, + step, + end, + inclusion: range.operator.inclusion, + } + .into_spanned(expr.span), + ) + } + Expr::Var(var_id) => { + drop_input(builder)?; + builder.push( + Instruction::LoadVariable { + dst: out_reg, + var_id: *var_id, + } + .into_spanned(expr.span), + )?; + Ok(()) + } + Expr::VarDecl(_) => Err(unexpected("VarDecl")), + Expr::Call(call) => { + move_in_reg_to_out_reg(builder)?; + + compile_call(working_set, builder, call, redirect_modes, out_reg) + } + Expr::ExternalCall(head, args) => { + move_in_reg_to_out_reg(builder)?; + + compile_external_call(working_set, builder, head, args, redirect_modes, out_reg) + } + Expr::Operator(_) => Err(unexpected("Operator")), + Expr::RowCondition(block_id) => lit(builder, Literal::RowCondition(*block_id)), + Expr::UnaryNot(subexpr) => { + drop_input(builder)?; + compile_expression( + working_set, + builder, + subexpr, + RedirectModes::capture_out(subexpr.span), + None, + out_reg, + )?; + builder.push(Instruction::Not { src_dst: out_reg }.into_spanned(expr.span))?; + Ok(()) + } + Expr::BinaryOp(lhs, op, rhs) => { + if let Expr::Operator(ref operator) = op.expr { + drop_input(builder)?; + compile_binary_op( + working_set, + builder, + lhs, + operator.clone().into_spanned(op.span), + rhs, + expr.span, + out_reg, + ) + } else { + Err(CompileError::UnsupportedOperatorExpression { span: op.span }) + } + } + Expr::Collect(var_id, expr) => { + let store_reg = if let Some(in_reg) = in_reg { + // Collect, clone, store + builder.push(Instruction::Collect { src_dst: in_reg }.into_spanned(expr.span))?; + builder.clone_reg(in_reg, expr.span)? + } else { + // Just store nothing in the variable + builder.literal(Literal::Nothing.into_spanned(Span::unknown()))? + }; + builder.push( + Instruction::StoreVariable { + var_id: *var_id, + src: store_reg, + } + .into_spanned(expr.span), + )?; + compile_expression(working_set, builder, expr, redirect_modes, in_reg, out_reg)?; + // Clean it up afterward + builder.push(Instruction::DropVariable { var_id: *var_id }.into_spanned(expr.span))?; + Ok(()) + } + Expr::Subexpression(block_id) => { + let block = working_set.get_block(*block_id); + compile_block(working_set, builder, block, redirect_modes, in_reg, out_reg) + } + Expr::Block(block_id) => lit(builder, Literal::Block(*block_id)), + Expr::Closure(block_id) => lit(builder, Literal::Closure(*block_id)), + Expr::MatchBlock(_) => Err(unexpected("MatchBlock")), // only for `match` keyword + Expr::List(items) => { + // Guess capacity based on items (does not consider spread as more than 1) + lit( + builder, + Literal::List { + capacity: items.len(), + }, + )?; + for item in items { + // Compile the expression of the item / spread + let reg = builder.next_register()?; + let expr = match item { + ListItem::Item(expr) | ListItem::Spread(_, expr) => expr, + }; + compile_expression( + working_set, + builder, + expr, + RedirectModes::capture_out(expr.span), + None, + reg, + )?; + + match item { + ListItem::Item(_) => { + // Add each item using list-push + builder.push( + Instruction::ListPush { + src_dst: out_reg, + item: reg, + } + .into_spanned(expr.span), + )?; + } + ListItem::Spread(spread_span, _) => { + // Spread the list using list-spread + builder.push( + Instruction::ListSpread { + src_dst: out_reg, + items: reg, + } + .into_spanned(*spread_span), + )?; + } + } + } + Ok(()) + } + Expr::Table(table) => { + lit( + builder, + Literal::List { + capacity: table.rows.len(), + }, + )?; + + // Evaluate the columns + let column_registers = table + .columns + .iter() + .map(|column| { + let reg = builder.next_register()?; + compile_expression( + working_set, + builder, + column, + RedirectModes::capture_out(column.span), + None, + reg, + )?; + Ok(reg) + }) + .collect::, CompileError>>()?; + + // Build records for each row + for row in table.rows.iter() { + let row_reg = builder.next_register()?; + builder.load_literal( + row_reg, + Literal::Record { + capacity: table.columns.len(), + } + .into_spanned(expr.span), + )?; + for (column_reg, item) in column_registers.iter().zip(row.iter()) { + let column_reg = builder.clone_reg(*column_reg, item.span)?; + let item_reg = builder.next_register()?; + compile_expression( + working_set, + builder, + item, + RedirectModes::capture_out(item.span), + None, + item_reg, + )?; + builder.push( + Instruction::RecordInsert { + src_dst: row_reg, + key: column_reg, + val: item_reg, + } + .into_spanned(item.span), + )?; + } + builder.push( + Instruction::ListPush { + src_dst: out_reg, + item: row_reg, + } + .into_spanned(expr.span), + )?; + } + + // Free the column registers, since they aren't needed anymore + for reg in column_registers { + builder.drop_reg(reg)?; + } + + Ok(()) + } + Expr::Record(items) => { + lit( + builder, + Literal::Record { + capacity: items.len(), + }, + )?; + + for item in items { + match item { + RecordItem::Pair(key, val) => { + // Add each item using record-insert + let key_reg = builder.next_register()?; + let val_reg = builder.next_register()?; + compile_expression( + working_set, + builder, + key, + RedirectModes::capture_out(key.span), + None, + key_reg, + )?; + compile_expression( + working_set, + builder, + val, + RedirectModes::capture_out(val.span), + None, + val_reg, + )?; + builder.push( + Instruction::RecordInsert { + src_dst: out_reg, + key: key_reg, + val: val_reg, + } + .into_spanned(expr.span), + )?; + } + RecordItem::Spread(spread_span, expr) => { + // Spread the expression using record-spread + let reg = builder.next_register()?; + compile_expression( + working_set, + builder, + expr, + RedirectModes::capture_out(expr.span), + None, + reg, + )?; + builder.push( + Instruction::RecordSpread { + src_dst: out_reg, + items: reg, + } + .into_spanned(*spread_span), + )?; + } + } + } + Ok(()) + } + Expr::Keyword(kw) => { + // keyword: just pass through expr, since commands that use it and are not being + // specially handled already are often just positional anyway + compile_expression( + working_set, + builder, + &kw.expr, + redirect_modes, + in_reg, + out_reg, + ) + } + Expr::ValueWithUnit(value_with_unit) => { + lit(builder, literal_from_value_with_unit(value_with_unit)?) + } + Expr::DateTime(dt) => lit(builder, Literal::Date(Box::new(*dt))), + Expr::Filepath(path, no_expand) => { + let val = builder.data(path)?; + lit( + builder, + Literal::Filepath { + val, + no_expand: *no_expand, + }, + ) + } + Expr::Directory(path, no_expand) => { + let val = builder.data(path)?; + lit( + builder, + Literal::Directory { + val, + no_expand: *no_expand, + }, + ) + } + Expr::GlobPattern(path, no_expand) => { + let val = builder.data(path)?; + lit( + builder, + Literal::GlobPattern { + val, + no_expand: *no_expand, + }, + ) + } + Expr::String(s) => { + let data_slice = builder.data(s)?; + lit(builder, Literal::String(data_slice)) + } + Expr::RawString(rs) => { + let data_slice = builder.data(rs)?; + lit(builder, Literal::RawString(data_slice)) + } + Expr::CellPath(path) => lit(builder, Literal::CellPath(Box::new(path.clone()))), + Expr::FullCellPath(full_cell_path) => { + if matches!(full_cell_path.head.expr, Expr::Var(ENV_VARIABLE_ID)) { + compile_load_env(builder, expr.span, &full_cell_path.tail, out_reg) + } else { + compile_expression( + working_set, + builder, + &full_cell_path.head, + RedirectModes::capture_out(expr.span), + in_reg, + out_reg, + )?; + // Only do the follow if this is actually needed + if !full_cell_path.tail.is_empty() { + let cell_path_reg = builder.literal( + Literal::CellPath(Box::new(CellPath { + members: full_cell_path.tail.clone(), + })) + .into_spanned(expr.span), + )?; + builder.push( + Instruction::FollowCellPath { + src_dst: out_reg, + path: cell_path_reg, + } + .into_spanned(expr.span), + )?; + } + Ok(()) + } + } + Expr::ImportPattern(_) => Err(unexpected("ImportPattern")), + Expr::Overlay(_) => Err(unexpected("Overlay")), + Expr::Signature(_) => ignore(builder), // no effect + Expr::StringInterpolation(exprs) | Expr::GlobInterpolation(exprs, _) => { + let mut exprs_iter = exprs.iter().peekable(); + + if exprs_iter + .peek() + .is_some_and(|e| matches!(e.expr, Expr::String(..) | Expr::RawString(..))) + { + // If the first expression is a string or raw string literal, just take it and build + // from that + compile_expression( + working_set, + builder, + exprs_iter.next().expect("peek() was Some"), + RedirectModes::capture_out(expr.span), + None, + out_reg, + )?; + } else { + // Start with an empty string + lit(builder, Literal::String(DataSlice::empty()))?; + } + + // Compile each expression and append to out_reg + for expr in exprs_iter { + let scratch_reg = builder.next_register()?; + compile_expression( + working_set, + builder, + expr, + RedirectModes::capture_out(expr.span), + None, + scratch_reg, + )?; + builder.push( + Instruction::StringAppend { + src_dst: out_reg, + val: scratch_reg, + } + .into_spanned(expr.span), + )?; + } + + // If it's a glob interpolation, change it to a glob + if let Expr::GlobInterpolation(_, no_expand) = expr.expr { + builder.push( + Instruction::GlobFrom { + src_dst: out_reg, + no_expand, + } + .into_spanned(expr.span), + )?; + } + + Ok(()) + } + Expr::Nothing => lit(builder, Literal::Nothing), + Expr::Garbage => Err(CompileError::Garbage { span: expr.span }), + } +} + +fn literal_from_value_with_unit(value_with_unit: &ValueWithUnit) -> Result { + let Expr::Int(int_value) = value_with_unit.expr.expr else { + return Err(CompileError::UnexpectedExpression { + expr_name: format!("{:?}", value_with_unit.expr), + span: value_with_unit.expr.span, + }); + }; + + match value_with_unit + .unit + .item + .build_value(int_value, Span::unknown()) + .map_err(|err| CompileError::InvalidLiteral { + msg: err.to_string(), + span: value_with_unit.expr.span, + })? { + Value::Filesize { val, .. } => Ok(Literal::Filesize(val)), + Value::Duration { val, .. } => Ok(Literal::Duration(val)), + other => Err(CompileError::InvalidLiteral { + msg: format!("bad value returned by Unit::build_value(): {other:?}"), + span: value_with_unit.unit.span, + }), + } +} diff --git a/crates/nu-engine/src/compile/keyword.rs b/crates/nu-engine/src/compile/keyword.rs new file mode 100644 index 0000000000..12f4a54c10 --- /dev/null +++ b/crates/nu-engine/src/compile/keyword.rs @@ -0,0 +1,902 @@ +use nu_protocol::{ + ast::{Block, Call, Expr, Expression}, + engine::StateWorkingSet, + ir::Instruction, + IntoSpanned, RegId, Type, VarId, +}; + +use super::{compile_block, compile_expression, BlockBuilder, CompileError, RedirectModes}; + +/// Compile a call to `if` as a branch-if +pub(crate) fn compile_if( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + // Pseudocode: + // + // %io_reg <- + // not %io_reg + // branch-if %io_reg, FALSE + // TRUE: ...... + // jump END + // FALSE: ...... OR drop %io_reg + // END: + let invalid = || CompileError::InvalidKeywordCall { + keyword: "if".into(), + span: call.head, + }; + + let condition = call.positional_nth(0).ok_or_else(invalid)?; + let true_block_arg = call.positional_nth(1).ok_or_else(invalid)?; + let else_arg = call.positional_nth(2); + + let true_block_id = true_block_arg.as_block().ok_or_else(invalid)?; + let true_block = working_set.get_block(true_block_id); + + let true_label = builder.label(None); + let false_label = builder.label(None); + let end_label = builder.label(None); + + let not_condition_reg = { + // Compile the condition first + let condition_reg = builder.next_register()?; + compile_expression( + working_set, + builder, + condition, + RedirectModes::capture_out(condition.span), + None, + condition_reg, + )?; + + // Negate the condition - we basically only want to jump if the condition is false + builder.push( + Instruction::Not { + src_dst: condition_reg, + } + .into_spanned(call.head), + )?; + + condition_reg + }; + + // Set up a branch if the condition is false. + builder.branch_if(not_condition_reg, false_label, call.head)?; + builder.add_comment("if false"); + + // Compile the true case + builder.set_label(true_label, builder.here())?; + compile_block( + working_set, + builder, + true_block, + redirect_modes.clone(), + Some(io_reg), + io_reg, + )?; + + // Add a jump over the false case + builder.jump(end_label, else_arg.map(|e| e.span).unwrap_or(call.head))?; + builder.add_comment("end if"); + + // On the else side now, assert that io_reg is still valid + builder.set_label(false_label, builder.here())?; + builder.mark_register(io_reg)?; + + if let Some(else_arg) = else_arg { + let Expression { + expr: Expr::Keyword(else_keyword), + .. + } = else_arg + else { + return Err(invalid()); + }; + + if else_keyword.keyword.as_ref() != b"else" { + return Err(invalid()); + } + + let else_expr = &else_keyword.expr; + + match &else_expr.expr { + Expr::Block(block_id) => { + let false_block = working_set.get_block(*block_id); + compile_block( + working_set, + builder, + false_block, + redirect_modes, + Some(io_reg), + io_reg, + )?; + } + _ => { + // The else case supports bare expressions too, not only blocks + compile_expression( + working_set, + builder, + else_expr, + redirect_modes, + Some(io_reg), + io_reg, + )?; + } + } + } else { + // We don't have an else expression/block, so just set io_reg = Empty + builder.load_empty(io_reg)?; + } + + // Set the end label + builder.set_label(end_label, builder.here())?; + + Ok(()) +} + +/// Compile a call to `match` +pub(crate) fn compile_match( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + // Pseudocode: + // + // %match_reg <- + // collect %match_reg + // match (pat1), %match_reg, PAT1 + // MATCH2: match (pat2), %match_reg, PAT2 + // FAIL: drop %io_reg + // drop %match_reg + // jump END + // PAT1: %guard_reg <- + // check-match-guard %guard_reg + // not %guard_reg + // branch-if %guard_reg, MATCH2 + // drop %match_reg + // <...expr...> + // jump END + // PAT2: drop %match_reg + // <...expr...> + // jump END + // END: + let invalid = || CompileError::InvalidKeywordCall { + keyword: "match".into(), + span: call.head, + }; + + let match_expr = call.positional_nth(0).ok_or_else(invalid)?; + + let match_block_arg = call.positional_nth(1).ok_or_else(invalid)?; + let match_block = match_block_arg.as_match_block().ok_or_else(invalid)?; + + let match_reg = builder.next_register()?; + + // Evaluate the match expression (patterns will be checked against this). + compile_expression( + working_set, + builder, + match_expr, + RedirectModes::capture_out(match_expr.span), + None, + match_reg, + )?; + + // Important to collect it first + builder.push(Instruction::Collect { src_dst: match_reg }.into_spanned(match_expr.span))?; + + // Generate the `match` instructions. Guards are not used at this stage. + let mut match_labels = Vec::with_capacity(match_block.len()); + let mut next_labels = Vec::with_capacity(match_block.len()); + let end_label = builder.label(None); + + for (pattern, _) in match_block { + let match_label = builder.label(None); + match_labels.push(match_label); + builder.push( + Instruction::Match { + pattern: Box::new(pattern.pattern.clone()), + src: match_reg, + index: match_label.0, + } + .into_spanned(pattern.span), + )?; + // Also add a label for the next match instruction or failure case + next_labels.push(builder.label(Some(builder.here()))); + } + + // Match fall-through to jump to the end, if no match + builder.load_empty(io_reg)?; + builder.drop_reg(match_reg)?; + builder.jump(end_label, call.head)?; + + // Generate each of the match expressions. Handle guards here, if present. + for (index, (pattern, expr)) in match_block.iter().enumerate() { + let match_label = match_labels[index]; + let next_label = next_labels[index]; + + // `io_reg` and `match_reg` are still valid at each of these branch targets + builder.mark_register(io_reg)?; + builder.mark_register(match_reg)?; + + // Set the original match instruction target here + builder.set_label(match_label, builder.here())?; + + // Handle guard, if present + if let Some(guard) = &pattern.guard { + let guard_reg = builder.next_register()?; + compile_expression( + working_set, + builder, + guard, + RedirectModes::capture_out(guard.span), + None, + guard_reg, + )?; + builder + .push(Instruction::CheckMatchGuard { src: guard_reg }.into_spanned(guard.span))?; + builder.push(Instruction::Not { src_dst: guard_reg }.into_spanned(guard.span))?; + // Branch to the next match instruction if the branch fails to match + builder.branch_if( + guard_reg, + next_label, + // Span the branch with the next pattern, or the head if this is the end + match_block + .get(index + 1) + .map(|b| b.0.span) + .unwrap_or(call.head), + )?; + builder.add_comment("if match guard false"); + } + + // match_reg no longer needed, successful match + builder.drop_reg(match_reg)?; + + // Execute match right hand side expression + if let Some(block_id) = expr.as_block() { + let block = working_set.get_block(block_id); + compile_block( + working_set, + builder, + block, + redirect_modes.clone(), + Some(io_reg), + io_reg, + )?; + } else { + compile_expression( + working_set, + builder, + expr, + redirect_modes.clone(), + Some(io_reg), + io_reg, + )?; + } + + // Jump to the end after the match logic is done + builder.jump(end_label, call.head)?; + builder.add_comment("end match"); + } + + // Set the end destination + builder.set_label(end_label, builder.here())?; + + Ok(()) +} + +/// Compile a call to `let` or `mut` (just do store-variable) +pub(crate) fn compile_let( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + _redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + // Pseudocode: + // + // %io_reg <- ...... <- %io_reg + // store-variable $var, %io_reg + let invalid = || CompileError::InvalidKeywordCall { + keyword: "let".into(), + span: call.head, + }; + + let var_decl_arg = call.positional_nth(0).ok_or_else(invalid)?; + let block_arg = call.positional_nth(1).ok_or_else(invalid)?; + + let var_id = var_decl_arg.as_var().ok_or_else(invalid)?; + let block_id = block_arg.as_block().ok_or_else(invalid)?; + let block = working_set.get_block(block_id); + + let variable = working_set.get_variable(var_id); + + compile_block( + working_set, + builder, + block, + RedirectModes::capture_out(call.head), + Some(io_reg), + io_reg, + )?; + + // If the variable is a glob type variable, we should cast it with GlobFrom + if variable.ty == Type::Glob { + builder.push( + Instruction::GlobFrom { + src_dst: io_reg, + no_expand: true, + } + .into_spanned(call.head), + )?; + } + + builder.push( + Instruction::StoreVariable { + var_id, + src: io_reg, + } + .into_spanned(call.head), + )?; + builder.add_comment("let"); + + // Don't forget to set io_reg to Empty afterward, as that's the result of an assignment + builder.load_empty(io_reg)?; + + Ok(()) +} + +/// Compile a call to `try`, setting an error handler over the evaluated block +pub(crate) fn compile_try( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + // Pseudocode (literal block): + // + // on-error-into ERR, %io_reg // or without + // %io_reg <- <...block...> <- %io_reg + // check-external-failed %failed_reg, %io_reg + // branch-if %failed_reg, FAIL + // pop-error-handler + // jump END + // FAIL: drain %io_reg + // unreachable + // ERR: clone %err_reg, %io_reg + // store-variable $err_var, %err_reg // or without + // %io_reg <- <...catch block...> <- %io_reg // set to empty if no catch block + // END: + // + // with expression that can't be inlined: + // + // %closure_reg <- + // on-error-into ERR, %io_reg + // %io_reg <- <...block...> <- %io_reg + // check-external-failed %failed_reg, %io_reg + // branch-if %failed_reg, FAIL + // pop-error-handler + // jump END + // FAIL: drain %io_reg + // unreachable + // ERR: clone %err_reg, %io_reg + // push-positional %closure_reg + // push-positional %err_reg + // call "do", %io_reg + // END: + let invalid = || CompileError::InvalidKeywordCall { + keyword: "try".into(), + span: call.head, + }; + + let block_arg = call.positional_nth(0).ok_or_else(invalid)?; + let block_id = block_arg.as_block().ok_or_else(invalid)?; + let block = working_set.get_block(block_id); + + let catch_expr = match call.positional_nth(1) { + Some(kw_expr) => Some(kw_expr.as_keyword().ok_or_else(invalid)?), + None => None, + }; + let catch_span = catch_expr.map(|e| e.span).unwrap_or(call.head); + + let err_label = builder.label(None); + let failed_label = builder.label(None); + let end_label = builder.label(None); + + // We have two ways of executing `catch`: if it was provided as a literal, we can inline it. + // Otherwise, we have to evaluate the expression and keep it as a register, and then call `do`. + enum CatchType<'a> { + Block { + block: &'a Block, + var_id: Option, + }, + Closure { + closure_reg: RegId, + }, + } + + let catch_type = catch_expr + .map(|catch_expr| match catch_expr.as_block() { + Some(block_id) => { + let block = working_set.get_block(block_id); + let var_id = block.signature.get_positional(0).and_then(|v| v.var_id); + Ok(CatchType::Block { block, var_id }) + } + None => { + // We have to compile the catch_expr and use it as a closure + let closure_reg = builder.next_register()?; + compile_expression( + working_set, + builder, + catch_expr, + RedirectModes::capture_out(catch_expr.span), + None, + closure_reg, + )?; + Ok(CatchType::Closure { closure_reg }) + } + }) + .transpose()?; + + // Put the error handler instruction. If we have a catch expression then we should capture the + // error. + if catch_type.is_some() { + builder.push( + Instruction::OnErrorInto { + index: err_label.0, + dst: io_reg, + } + .into_spanned(call.head), + )? + } else { + // Otherwise, we don't need the error value. + builder.push(Instruction::OnError { index: err_label.0 }.into_spanned(call.head))? + }; + + builder.add_comment("try"); + + // Compile the block + compile_block( + working_set, + builder, + block, + redirect_modes.clone(), + Some(io_reg), + io_reg, + )?; + + // Check for external command exit code failure, and also redirect that to the catch handler + let failed_reg = builder.next_register()?; + builder.push( + Instruction::CheckExternalFailed { + dst: failed_reg, + src: io_reg, + } + .into_spanned(catch_span), + )?; + builder.branch_if(failed_reg, failed_label, catch_span)?; + + // Successful case: pop the error handler + builder.push(Instruction::PopErrorHandler.into_spanned(call.head))?; + + // Jump over the failure case + builder.jump(end_label, catch_span)?; + + // Set up an error handler preamble for failed external. + // Draining the %io_reg results in the error handler being called with Empty, and sets + // $env.LAST_EXIT_CODE + builder.set_label(failed_label, builder.here())?; + builder.drain(io_reg, catch_span)?; + builder.add_comment("branches to err"); + builder.unreachable(catch_span)?; + + // This is the real error handler + builder.set_label(err_label, builder.here())?; + + // Mark out register as likely not clean - state in error handler is not well defined + builder.mark_register(io_reg)?; + + // Now compile whatever is necessary for the error handler + match catch_type { + Some(CatchType::Block { block, var_id }) => { + // Error will be in io_reg + builder.mark_register(io_reg)?; + if let Some(var_id) = var_id { + // Take a copy of the error as $err, since it will also be input + let err_reg = builder.next_register()?; + builder.push( + Instruction::Clone { + dst: err_reg, + src: io_reg, + } + .into_spanned(catch_span), + )?; + builder.push( + Instruction::StoreVariable { + var_id, + src: err_reg, + } + .into_spanned(catch_span), + )?; + } + // Compile the block, now that the variable is set + compile_block( + working_set, + builder, + block, + redirect_modes, + Some(io_reg), + io_reg, + )?; + } + Some(CatchType::Closure { closure_reg }) => { + // We should call `do`. Error will be in io_reg + let do_decl_id = working_set.find_decl(b"do").ok_or_else(|| { + CompileError::MissingRequiredDeclaration { + decl_name: "do".into(), + span: call.head, + } + })?; + + // Take a copy of io_reg, because we pass it both as an argument and input + builder.mark_register(io_reg)?; + let err_reg = builder.next_register()?; + builder.push( + Instruction::Clone { + dst: err_reg, + src: io_reg, + } + .into_spanned(catch_span), + )?; + + // Push the closure and the error + builder + .push(Instruction::PushPositional { src: closure_reg }.into_spanned(catch_span))?; + builder.push(Instruction::PushPositional { src: err_reg }.into_spanned(catch_span))?; + + // Call `$err | do $closure $err` + builder.push( + Instruction::Call { + decl_id: do_decl_id, + src_dst: io_reg, + } + .into_spanned(catch_span), + )?; + } + None => { + // Just set out to empty. + builder.load_empty(io_reg)?; + } + } + + // This is the end - if we succeeded, should jump here + builder.set_label(end_label, builder.here())?; + + Ok(()) +} + +/// Compile a call to `loop` (via `jump`) +pub(crate) fn compile_loop( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + _redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + // Pseudocode: + // + // drop %io_reg + // LOOP: %io_reg <- ...... + // drain %io_reg + // jump %LOOP + // END: drop %io_reg + let invalid = || CompileError::InvalidKeywordCall { + keyword: "loop".into(), + span: call.head, + }; + + let block_arg = call.positional_nth(0).ok_or_else(invalid)?; + let block_id = block_arg.as_block().ok_or_else(invalid)?; + let block = working_set.get_block(block_id); + + let loop_ = builder.begin_loop(); + builder.load_empty(io_reg)?; + + builder.set_label(loop_.continue_label, builder.here())?; + + compile_block( + working_set, + builder, + block, + RedirectModes::default(), + None, + io_reg, + )?; + + // Drain the output, just like for a semicolon + builder.drain(io_reg, call.head)?; + + builder.jump(loop_.continue_label, call.head)?; + builder.add_comment("loop"); + + builder.set_label(loop_.break_label, builder.here())?; + builder.end_loop(loop_)?; + + // State of %io_reg is not necessarily well defined here due to control flow, so make sure it's + // empty. + builder.mark_register(io_reg)?; + builder.load_empty(io_reg)?; + + Ok(()) +} + +/// Compile a call to `while`, via branch instructions +pub(crate) fn compile_while( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + _redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + // Pseudocode: + // + // LOOP: %io_reg <- + // branch-if %io_reg, TRUE + // jump FALSE + // TRUE: %io_reg <- ...... + // drain %io_reg + // jump LOOP + // FALSE: drop %io_reg + let invalid = || CompileError::InvalidKeywordCall { + keyword: "while".into(), + span: call.head, + }; + + let cond_arg = call.positional_nth(0).ok_or_else(invalid)?; + let block_arg = call.positional_nth(1).ok_or_else(invalid)?; + let block_id = block_arg.as_block().ok_or_else(invalid)?; + let block = working_set.get_block(block_id); + + let loop_ = builder.begin_loop(); + builder.set_label(loop_.continue_label, builder.here())?; + + let true_label = builder.label(None); + + compile_expression( + working_set, + builder, + cond_arg, + RedirectModes::capture_out(call.head), + None, + io_reg, + )?; + + builder.branch_if(io_reg, true_label, call.head)?; + builder.add_comment("while"); + builder.jump(loop_.break_label, call.head)?; + builder.add_comment("end while"); + + builder.set_label(true_label, builder.here())?; + + compile_block( + working_set, + builder, + block, + RedirectModes::default(), + None, + io_reg, + )?; + + // Drain the result, just like for a semicolon + builder.drain(io_reg, call.head)?; + + builder.jump(loop_.continue_label, call.head)?; + builder.add_comment("while"); + + builder.set_label(loop_.break_label, builder.here())?; + builder.end_loop(loop_)?; + + // State of %io_reg is not necessarily well defined here due to control flow, so make sure it's + // empty. + builder.mark_register(io_reg)?; + builder.load_empty(io_reg)?; + + Ok(()) +} + +/// Compile a call to `for` (via `iterate`) +pub(crate) fn compile_for( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + _redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + // Pseudocode: + // + // %stream_reg <- + // LOOP: iterate %io_reg, %stream_reg, END + // store-variable $var, %io_reg + // %io_reg <- <...block...> + // drain %io_reg + // jump LOOP + // END: drop %io_reg + let invalid = || CompileError::InvalidKeywordCall { + keyword: "for".into(), + span: call.head, + }; + + if call.get_named_arg("numbered").is_some() { + // This is deprecated and we don't support it. + return Err(invalid()); + } + + let var_decl_arg = call.positional_nth(0).ok_or_else(invalid)?; + let var_id = var_decl_arg.as_var().ok_or_else(invalid)?; + + let in_arg = call.positional_nth(1).ok_or_else(invalid)?; + let in_expr = in_arg.as_keyword().ok_or_else(invalid)?; + + let block_arg = call.positional_nth(2).ok_or_else(invalid)?; + let block_id = block_arg.as_block().ok_or_else(invalid)?; + let block = working_set.get_block(block_id); + + // Ensure io_reg is marked so we don't use it + builder.mark_register(io_reg)?; + + let stream_reg = builder.next_register()?; + + compile_expression( + working_set, + builder, + in_expr, + RedirectModes::capture_out(in_expr.span), + None, + stream_reg, + )?; + + // Set up loop state + let loop_ = builder.begin_loop(); + builder.set_label(loop_.continue_label, builder.here())?; + + // This gets a value from the stream each time it's executed + // io_reg basically will act as our scratch register here + builder.push( + Instruction::Iterate { + dst: io_reg, + stream: stream_reg, + end_index: loop_.break_label.0, + } + .into_spanned(call.head), + )?; + builder.add_comment("for"); + + // Put the received value in the variable + builder.push( + Instruction::StoreVariable { + var_id, + src: io_reg, + } + .into_spanned(var_decl_arg.span), + )?; + + // Do the body of the block + compile_block( + working_set, + builder, + block, + RedirectModes::default(), + None, + io_reg, + )?; + + // Drain the output, just like for a semicolon + builder.drain(io_reg, call.head)?; + + // Loop back to iterate to get the next value + builder.jump(loop_.continue_label, call.head)?; + + // Set the end of the loop + builder.set_label(loop_.break_label, builder.here())?; + builder.end_loop(loop_)?; + + // We don't need stream_reg anymore, after the loop + // io_reg may or may not be empty, so be sure it is + builder.free_register(stream_reg)?; + builder.mark_register(io_reg)?; + builder.load_empty(io_reg)?; + + Ok(()) +} + +/// Compile a call to `break`. +pub(crate) fn compile_break( + _working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + _redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + if builder.is_in_loop() { + builder.load_empty(io_reg)?; + builder.push_break(call.head)?; + builder.add_comment("break"); + } else { + // Fall back to calling the command if we can't find the loop target statically + builder.push( + Instruction::Call { + decl_id: call.decl_id, + src_dst: io_reg, + } + .into_spanned(call.head), + )?; + } + Ok(()) +} + +/// Compile a call to `continue`. +pub(crate) fn compile_continue( + _working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + _redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + if builder.is_in_loop() { + builder.load_empty(io_reg)?; + builder.push_continue(call.head)?; + builder.add_comment("continue"); + } else { + // Fall back to calling the command if we can't find the loop target statically + builder.push( + Instruction::Call { + decl_id: call.decl_id, + src_dst: io_reg, + } + .into_spanned(call.head), + )?; + } + Ok(()) +} + +/// Compile a call to `return` as a `return-early` instruction. +/// +/// This is not strictly necessary, but it is more efficient. +pub(crate) fn compile_return( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + _redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + // Pseudocode: + // + // %io_reg <- + // return-early %io_reg + if let Some(arg_expr) = call.positional_nth(0) { + compile_expression( + working_set, + builder, + arg_expr, + RedirectModes::capture_out(arg_expr.span), + None, + io_reg, + )?; + } else { + builder.load_empty(io_reg)?; + } + + // TODO: It would be nice if this could be `return` instead, but there is a little bit of + // behaviour remaining that still depends on `ShellError::Return` + builder.push(Instruction::ReturnEarly { src: io_reg }.into_spanned(call.head))?; + + // io_reg is supposed to remain allocated + builder.load_empty(io_reg)?; + + Ok(()) +} diff --git a/crates/nu-engine/src/compile/mod.rs b/crates/nu-engine/src/compile/mod.rs new file mode 100644 index 0000000000..8f6ae22682 --- /dev/null +++ b/crates/nu-engine/src/compile/mod.rs @@ -0,0 +1,204 @@ +use nu_protocol::{ + ast::{Block, Pipeline, PipelineRedirection, RedirectionSource, RedirectionTarget}, + engine::StateWorkingSet, + ir::{Instruction, IrBlock, RedirectMode}, + CompileError, IntoSpanned, RegId, Span, +}; + +mod builder; +mod call; +mod expression; +mod keyword; +mod operator; +mod redirect; + +use builder::BlockBuilder; +use call::*; +use expression::compile_expression; +use operator::*; +use redirect::*; + +const BLOCK_INPUT: RegId = RegId(0); + +/// Compile Nushell pipeline abstract syntax tree (AST) to internal representation (IR) instructions +/// for evaluation. +pub fn compile(working_set: &StateWorkingSet, block: &Block) -> Result { + let mut builder = BlockBuilder::new(block.span); + + let span = block.span.unwrap_or(Span::unknown()); + + compile_block( + working_set, + &mut builder, + block, + RedirectModes::caller(span), + Some(BLOCK_INPUT), + BLOCK_INPUT, + )?; + + // A complete block has to end with a `return` + builder.push(Instruction::Return { src: BLOCK_INPUT }.into_spanned(span))?; + + builder.finish() +} + +/// Compiles a [`Block`] in-place into an IR block. This can be used in a nested manner, for example +/// by [`compile_if()`], where the instructions for the blocks for the if/else are inlined into the +/// top-level IR block. +fn compile_block( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + block: &Block, + redirect_modes: RedirectModes, + in_reg: Option, + out_reg: RegId, +) -> Result<(), CompileError> { + let span = block.span.unwrap_or(Span::unknown()); + let mut redirect_modes = Some(redirect_modes); + if !block.pipelines.is_empty() { + let last_index = block.pipelines.len() - 1; + for (index, pipeline) in block.pipelines.iter().enumerate() { + compile_pipeline( + working_set, + builder, + pipeline, + span, + // the redirect mode only applies to the last pipeline. + if index == last_index { + redirect_modes + .take() + .expect("should only take redirect_modes once") + } else { + RedirectModes::default() + }, + // input is only passed to the first pipeline. + if index == 0 { in_reg } else { None }, + out_reg, + )?; + + if index != last_index { + // Explicitly drain the out reg after each non-final pipeline, because that's how + // the semicolon functions. + if builder.is_allocated(out_reg) { + builder.push(Instruction::Drain { src: out_reg }.into_spanned(span))?; + } + builder.load_empty(out_reg)?; + } + } + Ok(()) + } else if in_reg.is_none() { + builder.load_empty(out_reg) + } else { + Ok(()) + } +} + +fn compile_pipeline( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + pipeline: &Pipeline, + fallback_span: Span, + redirect_modes: RedirectModes, + in_reg: Option, + out_reg: RegId, +) -> Result<(), CompileError> { + let mut iter = pipeline.elements.iter().peekable(); + let mut in_reg = in_reg; + let mut redirect_modes = Some(redirect_modes); + while let Some(element) = iter.next() { + let span = element.pipe.unwrap_or(fallback_span); + + // We have to get the redirection mode from either the explicit redirection in the pipeline + // element, or from the next expression if it's specified there. If this is the last + // element, then it's from whatever is passed in as the mode to use. + + let next_redirect_modes = if let Some(next_element) = iter.peek() { + let mut modes = redirect_modes_of_expression(working_set, &next_element.expr, span)?; + + // If there's a next element with no inherent redirection we always pipe out *unless* + // this is a single redirection of stderr to pipe (e>|) + if modes.out.is_none() + && !matches!( + element.redirection, + Some(PipelineRedirection::Single { + source: RedirectionSource::Stderr, + target: RedirectionTarget::Pipe { .. } + }) + ) + { + let pipe_span = next_element.pipe.unwrap_or(next_element.expr.span); + modes.out = Some(RedirectMode::Pipe.into_spanned(pipe_span)); + } + + modes + } else { + redirect_modes + .take() + .expect("should only take redirect_modes once") + }; + + let spec_redirect_modes = match &element.redirection { + Some(PipelineRedirection::Single { source, target }) => { + let mode = redirection_target_to_mode(working_set, builder, target)?; + match source { + RedirectionSource::Stdout => RedirectModes { + out: Some(mode), + err: None, + }, + RedirectionSource::Stderr => RedirectModes { + out: None, + err: Some(mode), + }, + RedirectionSource::StdoutAndStderr => RedirectModes { + out: Some(mode), + err: Some(mode), + }, + } + } + Some(PipelineRedirection::Separate { out, err }) => { + // In this case, out and err must not both be Pipe + assert!( + !matches!( + (out, err), + ( + RedirectionTarget::Pipe { .. }, + RedirectionTarget::Pipe { .. } + ) + ), + "for Separate redirection, out and err targets must not both be Pipe" + ); + let out = redirection_target_to_mode(working_set, builder, out)?; + let err = redirection_target_to_mode(working_set, builder, err)?; + RedirectModes { + out: Some(out), + err: Some(err), + } + } + None => RedirectModes { + out: None, + err: None, + }, + }; + + let redirect_modes = RedirectModes { + out: spec_redirect_modes.out.or(next_redirect_modes.out), + err: spec_redirect_modes.err.or(next_redirect_modes.err), + }; + + compile_expression( + working_set, + builder, + &element.expr, + redirect_modes.clone(), + in_reg, + out_reg, + )?; + + // Clean up the redirection + finish_redirection(builder, redirect_modes, out_reg)?; + + // The next pipeline element takes input from this output + in_reg = Some(out_reg); + } + Ok(()) +} diff --git a/crates/nu-engine/src/compile/operator.rs b/crates/nu-engine/src/compile/operator.rs new file mode 100644 index 0000000000..a1ed3f66df --- /dev/null +++ b/crates/nu-engine/src/compile/operator.rs @@ -0,0 +1,378 @@ +use nu_protocol::{ + ast::{Assignment, Boolean, CellPath, Expr, Expression, Math, Operator, PathMember}, + engine::StateWorkingSet, + ir::{Instruction, Literal}, + IntoSpanned, RegId, Span, Spanned, ENV_VARIABLE_ID, +}; +use nu_utils::IgnoreCaseExt; + +use super::{compile_expression, BlockBuilder, CompileError, RedirectModes}; + +pub(crate) fn compile_binary_op( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + lhs: &Expression, + op: Spanned, + rhs: &Expression, + span: Span, + out_reg: RegId, +) -> Result<(), CompileError> { + if let Operator::Assignment(assign_op) = op.item { + if let Some(decomposed_op) = decompose_assignment(assign_op) { + // Compiling an assignment that uses a binary op with the existing value + compile_binary_op( + working_set, + builder, + lhs, + decomposed_op.into_spanned(op.span), + rhs, + span, + out_reg, + )?; + } else { + // Compiling a plain assignment, where the current left-hand side value doesn't matter + compile_expression( + working_set, + builder, + rhs, + RedirectModes::capture_out(rhs.span), + None, + out_reg, + )?; + } + + compile_assignment(working_set, builder, lhs, op.span, out_reg)?; + + // Load out_reg with Nothing, as that's the result of an assignment + builder.load_literal(out_reg, Literal::Nothing.into_spanned(op.span)) + } else { + // Not an assignment: just do the binary op + let lhs_reg = out_reg; + + compile_expression( + working_set, + builder, + lhs, + RedirectModes::capture_out(lhs.span), + None, + lhs_reg, + )?; + + match op.item { + // `and` / `or` are short-circuiting, and we can get by with one register and a branch + Operator::Boolean(Boolean::And) => { + let true_label = builder.label(None); + builder.branch_if(lhs_reg, true_label, op.span)?; + + // If the branch was not taken it's false, so short circuit to load false + let false_label = builder.label(None); + builder.jump(false_label, op.span)?; + + builder.set_label(true_label, builder.here())?; + compile_expression( + working_set, + builder, + rhs, + RedirectModes::capture_out(rhs.span), + None, + lhs_reg, + )?; + + let end_label = builder.label(None); + builder.jump(end_label, op.span)?; + + // Consumed by `branch-if`, so we have to set it false again + builder.set_label(false_label, builder.here())?; + builder.load_literal(lhs_reg, Literal::Bool(false).into_spanned(lhs.span))?; + + builder.set_label(end_label, builder.here())?; + } + Operator::Boolean(Boolean::Or) => { + let true_label = builder.label(None); + builder.branch_if(lhs_reg, true_label, op.span)?; + + // If the branch was not taken it's false, so do the right-side expression + compile_expression( + working_set, + builder, + rhs, + RedirectModes::capture_out(rhs.span), + None, + lhs_reg, + )?; + + let end_label = builder.label(None); + builder.jump(end_label, op.span)?; + + // Consumed by `branch-if`, so we have to set it true again + builder.set_label(true_label, builder.here())?; + builder.load_literal(lhs_reg, Literal::Bool(true).into_spanned(lhs.span))?; + + builder.set_label(end_label, builder.here())?; + } + _ => { + // Any other operator, via `binary-op` + let rhs_reg = builder.next_register()?; + + compile_expression( + working_set, + builder, + rhs, + RedirectModes::capture_out(rhs.span), + None, + rhs_reg, + )?; + + builder.push( + Instruction::BinaryOp { + lhs_dst: lhs_reg, + op: op.item, + rhs: rhs_reg, + } + .into_spanned(op.span), + )?; + } + } + + if lhs_reg != out_reg { + builder.push( + Instruction::Move { + dst: out_reg, + src: lhs_reg, + } + .into_spanned(op.span), + )?; + } + + builder.push(Instruction::Span { src_dst: out_reg }.into_spanned(span))?; + + Ok(()) + } +} + +/// The equivalent plain operator to use for an assignment, if any +pub(crate) fn decompose_assignment(assignment: Assignment) -> Option { + match assignment { + Assignment::Assign => None, + Assignment::PlusAssign => Some(Operator::Math(Math::Plus)), + Assignment::AppendAssign => Some(Operator::Math(Math::Append)), + Assignment::MinusAssign => Some(Operator::Math(Math::Minus)), + Assignment::MultiplyAssign => Some(Operator::Math(Math::Multiply)), + Assignment::DivideAssign => Some(Operator::Math(Math::Divide)), + } +} + +/// Compile assignment of the value in a register to a left-hand expression +pub(crate) fn compile_assignment( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + lhs: &Expression, + assignment_span: Span, + rhs_reg: RegId, +) -> Result<(), CompileError> { + match lhs.expr { + Expr::Var(var_id) => { + // Double check that the variable is supposed to be mutable + if !working_set.get_variable(var_id).mutable { + return Err(CompileError::AssignmentRequiresMutableVar { span: lhs.span }); + } + + builder.push( + Instruction::StoreVariable { + var_id, + src: rhs_reg, + } + .into_spanned(assignment_span), + )?; + Ok(()) + } + Expr::FullCellPath(ref path) => match (&path.head, &path.tail) { + ( + Expression { + expr: Expr::Var(var_id), + .. + }, + _, + ) if *var_id == ENV_VARIABLE_ID => { + // This will be an assignment to an environment variable. + let Some(PathMember::String { val: key, .. }) = path.tail.first() else { + return Err(CompileError::CannotReplaceEnv { span: lhs.span }); + }; + + // Some env vars can't be set by Nushell code. + const AUTOMATIC_NAMES: &[&str] = &["PWD", "FILE_PWD", "CURRENT_FILE"]; + if AUTOMATIC_NAMES.iter().any(|name| key.eq_ignore_case(name)) { + return Err(CompileError::AutomaticEnvVarSetManually { + envvar_name: "PWD".into(), + span: lhs.span, + }); + } + + let key_data = builder.data(key)?; + + let val_reg = if path.tail.len() > 1 { + // Get the current value of the head and first tail of the path, from env + let head_reg = builder.next_register()?; + + // We could use compile_load_env, but this shares the key data... + // Always use optional, because it doesn't matter if it's already there + builder.push( + Instruction::LoadEnvOpt { + dst: head_reg, + key: key_data, + } + .into_spanned(lhs.span), + )?; + + // Default to empty record so we can do further upserts + let default_label = builder.label(None); + let upsert_label = builder.label(None); + builder.branch_if_empty(head_reg, default_label, assignment_span)?; + builder.jump(upsert_label, assignment_span)?; + + builder.set_label(default_label, builder.here())?; + builder.load_literal( + head_reg, + Literal::Record { capacity: 0 }.into_spanned(lhs.span), + )?; + + // Do the upsert on the current value to incorporate rhs + builder.set_label(upsert_label, builder.here())?; + compile_upsert_cell_path( + builder, + (&path.tail[1..]).into_spanned(lhs.span), + head_reg, + rhs_reg, + assignment_span, + )?; + + head_reg + } else { + // Path has only one tail, so we don't need the current value to do an upsert, + // just set it directly to rhs + rhs_reg + }; + + // Finally, store the modified env variable + builder.push( + Instruction::StoreEnv { + key: key_data, + src: val_reg, + } + .into_spanned(assignment_span), + )?; + Ok(()) + } + (_, tail) if tail.is_empty() => { + // If the path tail is empty, we can really just treat this as if it were an + // assignment to the head + compile_assignment(working_set, builder, &path.head, assignment_span, rhs_reg) + } + _ => { + // Just a normal assignment to some path + let head_reg = builder.next_register()?; + + // Compile getting current value of the head expression + compile_expression( + working_set, + builder, + &path.head, + RedirectModes::capture_out(path.head.span), + None, + head_reg, + )?; + + // Upsert the tail of the path into the old value of the head expression + compile_upsert_cell_path( + builder, + path.tail.as_slice().into_spanned(lhs.span), + head_reg, + rhs_reg, + assignment_span, + )?; + + // Now compile the assignment of the updated value to the head + compile_assignment(working_set, builder, &path.head, assignment_span, head_reg) + } + }, + Expr::Garbage => Err(CompileError::Garbage { span: lhs.span }), + _ => Err(CompileError::AssignmentRequiresVar { span: lhs.span }), + } +} + +/// Compile an upsert-cell-path instruction, with known literal members +pub(crate) fn compile_upsert_cell_path( + builder: &mut BlockBuilder, + members: Spanned<&[PathMember]>, + src_dst: RegId, + new_value: RegId, + span: Span, +) -> Result<(), CompileError> { + let path_reg = builder.literal( + Literal::CellPath( + CellPath { + members: members.item.to_vec(), + } + .into(), + ) + .into_spanned(members.span), + )?; + builder.push( + Instruction::UpsertCellPath { + src_dst, + path: path_reg, + new_value, + } + .into_spanned(span), + )?; + Ok(()) +} + +/// Compile the correct sequence to get an environment variable + follow a path on it +pub(crate) fn compile_load_env( + builder: &mut BlockBuilder, + span: Span, + path: &[PathMember], + out_reg: RegId, +) -> Result<(), CompileError> { + if path.is_empty() { + builder.push( + Instruction::LoadVariable { + dst: out_reg, + var_id: ENV_VARIABLE_ID, + } + .into_spanned(span), + )?; + } else { + let (key, optional) = match &path[0] { + PathMember::String { val, optional, .. } => (builder.data(val)?, *optional), + PathMember::Int { span, .. } => { + return Err(CompileError::AccessEnvByInt { span: *span }) + } + }; + let tail = &path[1..]; + + if optional { + builder.push(Instruction::LoadEnvOpt { dst: out_reg, key }.into_spanned(span))?; + } else { + builder.push(Instruction::LoadEnv { dst: out_reg, key }.into_spanned(span))?; + } + + if !tail.is_empty() { + let path = builder.literal( + Literal::CellPath(Box::new(CellPath { + members: tail.to_vec(), + })) + .into_spanned(span), + )?; + builder.push( + Instruction::FollowCellPath { + src_dst: out_reg, + path, + } + .into_spanned(span), + )?; + } + } + Ok(()) +} diff --git a/crates/nu-engine/src/compile/redirect.rs b/crates/nu-engine/src/compile/redirect.rs new file mode 100644 index 0000000000..15af1a9f8c --- /dev/null +++ b/crates/nu-engine/src/compile/redirect.rs @@ -0,0 +1,157 @@ +use nu_protocol::{ + ast::{Expression, RedirectionTarget}, + engine::StateWorkingSet, + ir::{Instruction, RedirectMode}, + IntoSpanned, OutDest, RegId, Span, Spanned, +}; + +use super::{compile_expression, BlockBuilder, CompileError}; + +#[derive(Default, Clone)] +pub(crate) struct RedirectModes { + pub(crate) out: Option>, + pub(crate) err: Option>, +} + +impl RedirectModes { + pub(crate) fn capture_out(span: Span) -> Self { + RedirectModes { + out: Some(RedirectMode::Capture.into_spanned(span)), + err: None, + } + } + + pub(crate) fn caller(span: Span) -> RedirectModes { + RedirectModes { + out: Some(RedirectMode::Caller.into_spanned(span)), + err: Some(RedirectMode::Caller.into_spanned(span)), + } + } +} + +pub(crate) fn redirection_target_to_mode( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + target: &RedirectionTarget, +) -> Result, CompileError> { + Ok(match target { + RedirectionTarget::File { + expr, + append, + span: redir_span, + } => { + let file_num = builder.next_file_num()?; + let path_reg = builder.next_register()?; + compile_expression( + working_set, + builder, + expr, + RedirectModes::capture_out(*redir_span), + None, + path_reg, + )?; + builder.push( + Instruction::OpenFile { + file_num, + path: path_reg, + append: *append, + } + .into_spanned(*redir_span), + )?; + RedirectMode::File { file_num }.into_spanned(*redir_span) + } + RedirectionTarget::Pipe { span } => RedirectMode::Pipe.into_spanned(*span), + }) +} + +pub(crate) fn redirect_modes_of_expression( + working_set: &StateWorkingSet, + expression: &Expression, + redir_span: Span, +) -> Result { + let (out, err) = expression.expr.pipe_redirection(working_set); + Ok(RedirectModes { + out: out + .map(|r| r.into_spanned(redir_span)) + .map(out_dest_to_redirect_mode) + .transpose()?, + err: err + .map(|r| r.into_spanned(redir_span)) + .map(out_dest_to_redirect_mode) + .transpose()?, + }) +} + +/// Finish the redirection for an expression, writing to and closing files as necessary +pub(crate) fn finish_redirection( + builder: &mut BlockBuilder, + modes: RedirectModes, + out_reg: RegId, +) -> Result<(), CompileError> { + if let Some(Spanned { + item: RedirectMode::File { file_num }, + span, + }) = modes.out + { + // If out is a file and err is a pipe, we must not consume the expression result - + // that is actually the err, in that case. + if !matches!( + modes.err, + Some(Spanned { + item: RedirectMode::Pipe { .. }, + .. + }) + ) { + builder.push( + Instruction::WriteFile { + file_num, + src: out_reg, + } + .into_spanned(span), + )?; + builder.load_empty(out_reg)?; + } + builder.push(Instruction::CloseFile { file_num }.into_spanned(span))?; + } + + match modes.err { + Some(Spanned { + item: RedirectMode::File { file_num }, + span, + }) => { + // Close the file, unless it's the same as out (in which case it was already closed) + if !modes.out.is_some_and(|out_mode| match out_mode.item { + RedirectMode::File { + file_num: out_file_num, + } => file_num == out_file_num, + _ => false, + }) { + builder.push(Instruction::CloseFile { file_num }.into_spanned(span))?; + } + } + Some(Spanned { + item: RedirectMode::Pipe, + span, + }) => { + builder.push(Instruction::CheckErrRedirected { src: out_reg }.into_spanned(span))?; + } + _ => (), + } + + Ok(()) +} + +pub(crate) fn out_dest_to_redirect_mode( + out_dest: Spanned, +) -> Result, CompileError> { + let span = out_dest.span; + out_dest + .map(|out_dest| match out_dest { + OutDest::Pipe => Ok(RedirectMode::Pipe), + OutDest::Capture => Ok(RedirectMode::Capture), + OutDest::Null => Ok(RedirectMode::Null), + OutDest::Inherit => Ok(RedirectMode::Inherit), + OutDest::File(_) => Err(CompileError::InvalidRedirectMode { span }), + }) + .transpose() +} diff --git a/crates/nu-engine/src/documentation.rs b/crates/nu-engine/src/documentation.rs index 94cef3298e..a0dee3d343 100644 --- a/crates/nu-engine/src/documentation.rs +++ b/crates/nu-engine/src/documentation.rs @@ -3,8 +3,8 @@ use nu_protocol::{ ast::{Argument, Call, Expr, Expression, RecordItem}, debugger::WithoutDebug, engine::{Command, EngineState, Stack, UNKNOWN_SPAN_ID}, - record, Category, Example, IntoPipelineData, PipelineData, Signature, Span, SpanId, - SyntaxShape, Type, Value, + record, Category, Config, Example, IntoPipelineData, PipelineData, Signature, Span, SpanId, + Spanned, SyntaxShape, Type, Value, }; use std::{collections::HashMap, fmt::Write}; @@ -13,7 +13,7 @@ pub fn get_full_help( engine_state: &EngineState, stack: &mut Stack, ) -> String { - let config = engine_state.get_config(); + let config = stack.get_config(engine_state); let doc_config = DocumentationConfig { no_subcommands: false, no_color: !config.use_ansi_coloring, @@ -45,10 +45,12 @@ fn nu_highlight_string(code_string: &str, engine_state: &EngineState, stack: &mu if let Some(highlighter) = engine_state.find_decl(b"nu-highlight", &[]) { let decl = engine_state.get_decl(highlighter); + let call = Call::new(Span::unknown()); + if let Ok(output) = decl.run( engine_state, stack, - &Call::new(Span::unknown()), + &(&call).into(), Value::string(code_string, Span::unknown()).into_pipeline_data(), ) { let result = output.into_value(Span::unknown()); @@ -68,16 +70,30 @@ fn get_documentation( config: &DocumentationConfig, is_parser_keyword: bool, ) -> String { + let nu_config = stack.get_config(engine_state); + // Create ansi colors //todo make these configurable -- pull from enginestate.config - let help_section_name: String = - get_ansi_color_for_component_or_default(engine_state, "shape_string", "\x1b[32m"); // default: green + let help_section_name: String = get_ansi_color_for_component_or_default( + engine_state, + &nu_config, + "shape_string", + "\x1b[32m", + ); // default: green - let help_subcolor_one: String = - get_ansi_color_for_component_or_default(engine_state, "shape_external", "\x1b[36m"); // default: cyan - // was const bb: &str = "\x1b[1;34m"; // bold blue - let help_subcolor_two: String = - get_ansi_color_for_component_or_default(engine_state, "shape_block", "\x1b[94m"); // default: light blue (nobold, should be bolding the *names*) + let help_subcolor_one: String = get_ansi_color_for_component_or_default( + engine_state, + &nu_config, + "shape_external", + "\x1b[36m", + ); // default: cyan + // was const bb: &str = "\x1b[1;34m"; // bold blue + let help_subcolor_two: String = get_ansi_color_for_component_or_default( + engine_state, + &nu_config, + "shape_block", + "\x1b[94m", + ); // default: light blue (nobold, should be bolding the *names*) const RESET: &str = "\x1b[0m"; // reset @@ -137,13 +153,12 @@ fn get_documentation( } if !sig.named.is_empty() { - long_desc.push_str(&get_flags_section(Some(engine_state), sig, |v| { - nu_highlight_string( - &v.to_parsable_string(", ", &engine_state.config), - engine_state, - stack, - ) - })) + long_desc.push_str(&get_flags_section( + Some(engine_state), + Some(&nu_config), + sig, + |v| nu_highlight_string(&v.to_parsable_string(", ", &nu_config), engine_state, stack), + )) } if !sig.required_positional.is_empty() @@ -187,7 +202,7 @@ fn get_documentation( format!( " (optional, default: {})", nu_highlight_string( - &value.to_parsable_string(", ", &engine_state.config), + &value.to_parsable_string(", ", &nu_config), engine_state, stack ) @@ -269,11 +284,12 @@ fn get_documentation( let _ = write!(long_desc, "\n > {}\n", example.example); } else if let Some(highlighter) = engine_state.find_decl(b"nu-highlight", &[]) { let decl = engine_state.get_decl(highlighter); + let call = Call::new(Span::unknown()); match decl.run( engine_state, stack, - &Call::new(Span::unknown()), + &(&call).into(), Value::string(example.example, Span::unknown()).into_pipeline_data(), ) { Ok(output) => { @@ -296,6 +312,28 @@ fn get_documentation( } if let Some(result) = &example.result { + let mut table_call = Call::new(Span::unknown()); + if example.example.ends_with("--collapse") { + // collapse the result + table_call.add_named(( + Spanned { + item: "collapse".to_string(), + span: Span::unknown(), + }, + None, + None, + )) + } else { + // expand the result + table_call.add_named(( + Spanned { + item: "expand".to_string(), + span: Span::unknown(), + }, + None, + None, + )) + } let table = engine_state .find_decl("table".as_bytes(), &[]) .and_then(|decl_id| { @@ -304,7 +342,7 @@ fn get_documentation( .run( engine_state, stack, - &Call::new(Span::new(0, 0)), + &(&table_call).into(), PipelineData::Value(result.clone(), None), ) .ok() @@ -314,7 +352,7 @@ fn get_documentation( let _ = writeln!( long_desc, " {}", - item.to_expanded_string("", engine_state.get_config()) + item.to_expanded_string("", &nu_config) .replace('\n', "\n ") .trim() ); @@ -333,15 +371,16 @@ fn get_documentation( fn get_ansi_color_for_component_or_default( engine_state: &EngineState, + nu_config: &Config, theme_component: &str, default: &str, ) -> String { - if let Some(color) = &engine_state.get_config().color_config.get(theme_component) { + if let Some(color) = &nu_config.color_config.get(theme_component) { let caller_stack = &mut Stack::new().capture(); let span = Span::unknown(); let span_id = UNKNOWN_SPAN_ID; - let argument_opt = get_argument_for_color_value(engine_state, color, span, span_id); + let argument_opt = get_argument_for_color_value(nu_config, color, span, span_id); // Call ansi command using argument if let Some(argument) = argument_opt { @@ -369,8 +408,8 @@ fn get_ansi_color_for_component_or_default( } fn get_argument_for_color_value( - engine_state: &EngineState, - color: &&Value, + nu_config: &Config, + color: &Value, span: Span, span_id: SpanId, ) -> Option { @@ -387,9 +426,7 @@ fn get_argument_for_color_value( Type::String, ), Expression::new_existing( - Expr::String( - v.clone().to_expanded_string("", engine_state.get_config()), - ), + Expr::String(v.clone().to_expanded_string("", nu_config)), span, span_id, Type::String, @@ -431,6 +468,7 @@ pub fn document_shape(shape: SyntaxShape) -> SyntaxShape { pub fn get_flags_section( engine_state_opt: Option<&EngineState>, + nu_config_opt: Option<&Config>, signature: &Signature, mut value_formatter: F, // format default Value (because some calls cant access config or nu-highlight) ) -> String @@ -445,13 +483,26 @@ where // Sometimes we want to get the flags without engine_state // For example, in nu-plugin. In that case, we fall back on default values if let Some(engine_state) = engine_state_opt { - help_section_name = - get_ansi_color_for_component_or_default(engine_state, "shape_string", "\x1b[32m"); // default: green - help_subcolor_one = - get_ansi_color_for_component_or_default(engine_state, "shape_external", "\x1b[36m"); // default: cyan - // was const bb: &str = "\x1b[1;34m"; // bold blue - help_subcolor_two = - get_ansi_color_for_component_or_default(engine_state, "shape_block", "\x1b[94m"); + let nu_config = nu_config_opt.unwrap_or_else(|| engine_state.get_config()); + help_section_name = get_ansi_color_for_component_or_default( + engine_state, + nu_config, + "shape_string", + "\x1b[32m", + ); // default: green + help_subcolor_one = get_ansi_color_for_component_or_default( + engine_state, + nu_config, + "shape_external", + "\x1b[36m", + ); // default: cyan + // was const bb: &str = "\x1b[1;34m"; // bold blue + help_subcolor_two = get_ansi_color_for_component_or_default( + engine_state, + nu_config, + "shape_block", + "\x1b[94m", + ); // default: light blue (nobold, should be bolding the *names*) } else { help_section_name = "\x1b[32m".to_string(); diff --git a/crates/nu-engine/src/env.rs b/crates/nu-engine/src/env.rs index 048d9bfb99..f93e78af88 100644 --- a/crates/nu-engine/src/env.rs +++ b/crates/nu-engine/src/env.rs @@ -1,9 +1,9 @@ use crate::ClosureEvalOnce; use nu_path::canonicalize_with; use nu_protocol::{ - ast::{Call, Expr}, - engine::{EngineState, Stack, StateWorkingSet}, - Config, ShellError, Span, Value, VarId, + ast::Expr, + engine::{Call, EngineState, Stack, StateWorkingSet}, + ShellError, Span, Value, VarId, }; use std::{ collections::HashMap, @@ -244,14 +244,15 @@ pub fn path_str( } pub const DIR_VAR_PARSER_INFO: &str = "dirs_var"; -pub fn get_dirs_var_from_call(call: &Call) -> Option { - call.get_parser_info(DIR_VAR_PARSER_INFO).and_then(|x| { - if let Expr::Var(id) = x.expr { - Some(id) - } else { - None - } - }) +pub fn get_dirs_var_from_call(stack: &Stack, call: &Call) -> Option { + call.get_parser_info(stack, DIR_VAR_PARSER_INFO) + .and_then(|x| { + if let Expr::Var(id) = x.expr { + Some(id) + } else { + None + } + }) } /// This helper function is used to find files during eval @@ -322,18 +323,6 @@ pub fn find_in_dirs_env( Ok(check_dir(lib_dirs).or_else(|| check_dir(lib_dirs_fallback))) } -/// Get config -/// -/// This combines config stored in permanent state and any runtime updates to the environment. This -/// is the canonical way to fetch config at runtime when you have Stack available. -pub fn get_config(engine_state: &EngineState, stack: &Stack) -> Config { - if let Some(mut config_record) = stack.get_env_var(engine_state, "config") { - config_record.parse_as_config(engine_state.get_config()).0 - } else { - engine_state.get_config().clone() - } -} - fn get_converted_value( engine_state: &EngineState, stack: &Stack, diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs index 044bf86980..84fa700934 100644 --- a/crates/nu-engine/src/eval.rs +++ b/crates/nu-engine/src/eval.rs @@ -1,19 +1,20 @@ +use crate::eval_ir_block; #[allow(deprecated)] -use crate::{current_dir, get_config, get_full_help}; -use nu_path::expand_path_with; +use crate::{current_dir, get_full_help}; +use nu_path::{expand_path_with, AbsolutePathBuf}; use nu_protocol::{ ast::{ Assignment, Block, Call, Expr, Expression, ExternalArgument, PathMember, PipelineElement, PipelineRedirection, RedirectionSource, RedirectionTarget, }, debugger::DebugContext, - engine::{Closure, EngineState, Redirection, Stack}, + engine::{Closure, EngineState, Redirection, Stack, StateWorkingSet}, eval_base::Eval, - ByteStreamSource, Config, FromValue, IntoPipelineData, OutDest, PipelineData, ShellError, Span, - Spanned, Type, Value, VarId, ENV_VARIABLE_ID, + ByteStreamSource, Config, DataSource, FromValue, IntoPipelineData, OutDest, PipelineData, + PipelineMetadata, ShellError, Span, Spanned, Type, Value, VarId, ENV_VARIABLE_ID, }; use nu_utils::IgnoreCaseExt; -use std::{borrow::Cow, fs::OpenOptions, path::PathBuf}; +use std::{fs::OpenOptions, path::PathBuf, sync::Arc}; pub fn eval_call( engine_state: &EngineState, @@ -21,9 +22,7 @@ pub fn eval_call( call: &Call, input: PipelineData, ) -> Result { - if nu_utils::ctrl_c::was_pressed(&engine_state.ctrlc) { - return Ok(Value::nothing(call.head).into_pipeline_data()); - } + engine_state.signals().check(call.head)?; let decl = engine_state.get_decl(call.decl_id); if !decl.is_known_external() && call.named_iter().any(|(flag, _, _)| flag.item == "help") { @@ -176,7 +175,7 @@ pub fn eval_call( // 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) + decl.run(engine_state, caller_stack, &call.into(), input) } } @@ -197,6 +196,9 @@ pub fn redirect_env(engine_state: &EngineState, caller_stack: &mut Stack, callee for (var, value) in callee_stack.get_stack_env_vars() { caller_stack.add_env_var(var, value); } + + // set config to callee config, to capture any updates to that + caller_stack.config = callee_stack.config.clone(); } fn eval_external( @@ -225,7 +227,7 @@ fn eval_external( } } - command.run(engine_state, stack, &call, input) + command.run(engine_state, stack, &(&call).into(), input) } pub fn eval_expression( @@ -257,6 +259,10 @@ pub fn eval_expression_with_input( input = eval_external(engine_state, stack, head, args, input)?; } + Expr::Collect(var_id, expr) => { + input = eval_collect::(engine_state, stack, *var_id, expr, input)?; + } + Expr::Subexpression(block_id) => { let block = engine_state.get_block(*block_id); // FIXME: protect this collect with ctrl-c @@ -509,6 +515,11 @@ pub fn eval_block( block: &Block, mut input: PipelineData, ) -> Result { + // Remove once IR is the default. + if stack.use_ir { + return eval_ir_block::(engine_state, stack, block, input); + } + D::enter_block(engine_state, block); let num_pipelines = block.len(); @@ -523,7 +534,7 @@ pub fn eval_block( for (i, element) in elements.iter().enumerate() { let next = elements.get(i + 1).unwrap_or(last); - let (next_out, next_err) = next.pipe_redirection(engine_state); + let (next_out, next_err) = next.pipe_redirection(&StateWorkingSet::new(engine_state)); let (stdout, stderr) = eval_element_redirection::( engine_state, stack, @@ -598,6 +609,44 @@ pub fn eval_block( Ok(input) } +pub fn eval_collect( + engine_state: &EngineState, + stack: &mut Stack, + var_id: VarId, + expr: &Expression, + input: PipelineData, +) -> Result { + // Evaluate the expression with the variable set to the collected input + let span = input.span().unwrap_or(Span::unknown()); + + let metadata = match input.metadata() { + // Remove the `FilePath` metadata, because after `collect` it's no longer necessary to + // check where some input came from. + Some(PipelineMetadata { + data_source: DataSource::FilePath(_), + content_type: None, + }) => None, + other => other, + }; + + let input = input.into_value(span)?; + + stack.add_var(var_id, input.clone()); + + let result = eval_expression_with_input::( + engine_state, + stack, + expr, + // We still have to pass it as input + input.into_pipeline_data_with_metadata(metadata), + ) + .map(|(result, _failed)| result); + + stack.remove_var(var_id); + + result +} + pub fn eval_subexpression( engine_state: &EngineState, stack: &mut Stack, @@ -648,8 +697,8 @@ impl Eval for EvalRuntime { type MutState = Stack; - fn get_config<'a>(engine_state: Self::State<'a>, stack: &mut Stack) -> Cow<'a, Config> { - Cow::Owned(get_config(engine_state, stack)) + fn get_config(engine_state: Self::State<'_>, stack: &mut Stack) -> Arc { + stack.get_config(engine_state) } fn eval_filepath( @@ -681,7 +730,10 @@ impl Eval for EvalRuntime { } else if quoted { Ok(Value::string(path, span)) } else { - let cwd = engine_state.cwd(Some(stack)).unwrap_or_default(); + let cwd = engine_state + .cwd(Some(stack)) + .map(AbsolutePathBuf::into_std_path_buf) + .unwrap_or_default(); let path = expand_path_with(path, cwd, true); Ok(Value::string(path.to_string_lossy(), span)) @@ -719,6 +771,18 @@ impl Eval for EvalRuntime { eval_external(engine_state, stack, head, args, PipelineData::empty())?.into_value(span) } + fn eval_collect( + engine_state: &EngineState, + stack: &mut Stack, + var_id: VarId, + expr: &Expression, + ) -> Result { + // It's a little bizarre, but the expression can still have some kind of result even with + // nothing input + eval_collect::(engine_state, stack, var_id, expr, PipelineData::empty())? + .into_value(expr.span) + } + fn eval_subexpression( engine_state: &EngineState, stack: &mut Stack, @@ -836,7 +900,14 @@ impl Eval for EvalRuntime { }); } + let is_config = original_key == "config"; + stack.add_env_var(original_key, value); + + // Trigger the update to config, if we modified that. + if is_config { + stack.update_config(engine_state)?; + } } else { lhs.upsert_data_at_cell_path(&cell_path.tail, rhs)?; stack.add_var(*var_id, lhs); @@ -902,7 +973,7 @@ impl Eval for EvalRuntime { /// /// An automatic environment variable cannot be assigned to by user code. /// Current there are three of them: $env.PWD, $env.FILE_PWD, $env.CURRENT_FILE -fn is_automatic_env_var(var: &str) -> bool { +pub(crate) fn is_automatic_env_var(var: &str) -> bool { let names = ["PWD", "FILE_PWD", "CURRENT_FILE"]; names.iter().any(|&name| { if cfg!(windows) { diff --git a/crates/nu-engine/src/eval_helpers.rs b/crates/nu-engine/src/eval_helpers.rs index 66bda3e0eb..65ebc6b61d 100644 --- a/crates/nu-engine/src/eval_helpers.rs +++ b/crates/nu-engine/src/eval_helpers.rs @@ -1,6 +1,6 @@ use crate::{ eval_block, eval_block_with_early_return, eval_expression, eval_expression_with_input, - eval_subexpression, + eval_ir_block, eval_subexpression, }; use nu_protocol::{ ast::{Block, Expression}, @@ -13,6 +13,10 @@ use nu_protocol::{ pub type EvalBlockFn = fn(&EngineState, &mut Stack, &Block, PipelineData) -> Result; +/// Type of eval_ir_block() function +pub type EvalIrBlockFn = + fn(&EngineState, &mut Stack, &Block, PipelineData) -> Result; + /// Type of eval_block_with_early_return() function pub type EvalBlockWithEarlyReturnFn = fn(&EngineState, &mut Stack, &Block, PipelineData) -> Result; @@ -42,6 +46,16 @@ pub fn get_eval_block(engine_state: &EngineState) -> EvalBlockFn { } } +/// Helper function to fetch `eval_ir_block()` with the correct type parameter based on whether +/// engine_state is configured with or without a debugger. +pub fn get_eval_ir_block(engine_state: &EngineState) -> EvalIrBlockFn { + if engine_state.is_debugging() { + eval_ir_block:: + } else { + eval_ir_block:: + } +} + /// Helper function to fetch `eval_block_with_early_return()` with the correct type parameter based /// on whether engine_state is configured with or without a debugger. pub fn get_eval_block_with_early_return(engine_state: &EngineState) -> EvalBlockWithEarlyReturnFn { diff --git a/crates/nu-engine/src/eval_ir.rs b/crates/nu-engine/src/eval_ir.rs new file mode 100644 index 0000000000..8550cbab99 --- /dev/null +++ b/crates/nu-engine/src/eval_ir.rs @@ -0,0 +1,1489 @@ +use std::{borrow::Cow, fs::File, sync::Arc}; + +use nu_path::{expand_path_with, AbsolutePathBuf}; +use nu_protocol::{ + ast::{Bits, Block, Boolean, CellPath, Comparison, Math, Operator}, + debugger::DebugContext, + engine::{Argument, Closure, EngineState, ErrorHandler, Matcher, Redirection, Stack}, + ir::{Call, DataSlice, Instruction, IrAstRef, IrBlock, Literal, RedirectMode}, + record, ByteStreamSource, DataSource, DeclId, ErrSpan, Flag, IntoPipelineData, IntoSpanned, + ListStream, OutDest, PipelineData, PipelineMetadata, PositionalArg, Range, Record, RegId, + ShellError, Signals, Signature, Span, Spanned, Type, Value, VarId, ENV_VARIABLE_ID, +}; +use nu_utils::IgnoreCaseExt; + +use crate::{eval::is_automatic_env_var, eval_block_with_early_return}; + +/// Evaluate the compiled representation of a [`Block`]. +pub fn eval_ir_block( + engine_state: &EngineState, + stack: &mut Stack, + block: &Block, + input: PipelineData, +) -> Result { + // Rust does not check recursion limits outside of const evaluation. + // But nu programs run in the same process as the shell. + // To prevent a stack overflow in user code from crashing the shell, + // we limit the recursion depth of function calls. + let maximum_call_stack_depth: u64 = engine_state.config.recursion_limit as u64; + if stack.recursion_count > maximum_call_stack_depth { + return Err(ShellError::RecursionLimitReached { + recursion_limit: maximum_call_stack_depth, + span: block.span, + }); + } + + if let Some(ir_block) = &block.ir_block { + D::enter_block(engine_state, block); + + let args_base = stack.arguments.get_base(); + let error_handler_base = stack.error_handlers.get_base(); + + // Allocate and initialize registers. I've found that it's not really worth trying to avoid + // the heap allocation here by reusing buffers - our allocator is fast enough + let mut registers = Vec::with_capacity(ir_block.register_count as usize); + for _ in 0..ir_block.register_count { + registers.push(PipelineData::Empty); + } + + // Initialize file storage. + let mut files = vec![None; ir_block.file_count as usize]; + + let result = eval_ir_block_impl::( + &mut EvalContext { + engine_state, + stack, + data: &ir_block.data, + block_span: &block.span, + args_base, + error_handler_base, + redirect_out: None, + redirect_err: None, + matches: vec![], + registers: &mut registers[..], + files: &mut files[..], + }, + ir_block, + input, + ); + + stack.error_handlers.leave_frame(error_handler_base); + stack.arguments.leave_frame(args_base); + + D::leave_block(engine_state, block); + + result + } else { + // FIXME blocks having IR should not be optional + Err(ShellError::GenericError { + error: "Can't evaluate block in IR mode".into(), + msg: "block is missing compiled representation".into(), + span: block.span, + help: Some("the IrBlock is probably missing due to a compilation error".into()), + inner: vec![], + }) + } +} + +/// All of the pointers necessary for evaluation +struct EvalContext<'a> { + engine_state: &'a EngineState, + stack: &'a mut Stack, + data: &'a Arc<[u8]>, + /// The span of the block + block_span: &'a Option, + /// Base index on the argument stack to reset to after a call + args_base: usize, + /// Base index on the error handler stack to reset to after a call + error_handler_base: usize, + /// State set by redirect-out + redirect_out: Option, + /// State set by redirect-err + redirect_err: Option, + /// Scratch space to use for `match` + matches: Vec<(VarId, Value)>, + /// Intermediate pipeline data storage used by instructions, indexed by RegId + registers: &'a mut [PipelineData], + /// Holds open files used by redirections + files: &'a mut [Option>], +} + +impl<'a> EvalContext<'a> { + /// Replace the contents of a register with a new value + #[inline] + fn put_reg(&mut self, reg_id: RegId, new_value: PipelineData) { + // log::trace!("{reg_id} <- {new_value:?}"); + self.registers[reg_id.0 as usize] = new_value; + } + + /// Borrow the contents of a register. + #[inline] + fn borrow_reg(&self, reg_id: RegId) -> &PipelineData { + &self.registers[reg_id.0 as usize] + } + + /// Replace the contents of a register with `Empty` and then return the value that it contained + #[inline] + fn take_reg(&mut self, reg_id: RegId) -> PipelineData { + // log::trace!("<- {reg_id}"); + std::mem::replace(&mut self.registers[reg_id.0 as usize], PipelineData::Empty) + } + + /// Clone data from a register. Must be collected first. + fn clone_reg(&mut self, reg_id: RegId, error_span: Span) -> Result { + match &self.registers[reg_id.0 as usize] { + PipelineData::Empty => Ok(PipelineData::Empty), + PipelineData::Value(val, meta) => Ok(PipelineData::Value(val.clone(), meta.clone())), + _ => Err(ShellError::IrEvalError { + msg: "Must collect to value before using instruction that clones from a register" + .into(), + span: Some(error_span), + }), + } + } + + /// Clone a value from a register. Must be collected first. + fn clone_reg_value(&mut self, reg_id: RegId, fallback_span: Span) -> Result { + match self.clone_reg(reg_id, fallback_span)? { + PipelineData::Empty => Ok(Value::nothing(fallback_span)), + PipelineData::Value(val, _) => Ok(val), + _ => unreachable!("clone_reg should never return stream data"), + } + } + + /// Take and implicitly collect a register to a value + fn collect_reg(&mut self, reg_id: RegId, fallback_span: Span) -> Result { + let data = self.take_reg(reg_id); + let span = data.span().unwrap_or(fallback_span); + data.into_value(span) + } + + /// Get a string from data or produce evaluation error if it's invalid UTF-8 + fn get_str(&self, slice: DataSlice, error_span: Span) -> Result<&'a str, ShellError> { + std::str::from_utf8(&self.data[slice]).map_err(|_| ShellError::IrEvalError { + msg: format!("data slice does not refer to valid UTF-8: {slice:?}"), + span: Some(error_span), + }) + } +} + +/// Eval an IR block on the provided slice of registers. +fn eval_ir_block_impl( + ctx: &mut EvalContext<'_>, + ir_block: &IrBlock, + input: PipelineData, +) -> Result { + if !ctx.registers.is_empty() { + ctx.registers[0] = input; + } + + // Program counter, starts at zero. + let mut pc = 0; + + while pc < ir_block.instructions.len() { + let instruction = &ir_block.instructions[pc]; + let span = &ir_block.spans[pc]; + let ast = &ir_block.ast[pc]; + + D::enter_instruction(ctx.engine_state, ir_block, pc, ctx.registers); + + let result = eval_instruction::(ctx, instruction, span, ast); + + D::leave_instruction( + ctx.engine_state, + ir_block, + pc, + ctx.registers, + result.as_ref().err(), + ); + + match result { + Ok(InstructionResult::Continue) => { + pc += 1; + } + Ok(InstructionResult::Branch(next_pc)) => { + pc = next_pc; + } + Ok(InstructionResult::Return(reg_id)) => { + return Ok(ctx.take_reg(reg_id)); + } + Ok(InstructionResult::ExitCode(exit_code)) => { + if let Some(error_handler) = ctx.stack.error_handlers.pop(ctx.error_handler_base) { + // If an error handler is set, branch there + prepare_error_handler(ctx, error_handler, None); + pc = error_handler.handler_index; + } else { + // If not, exit the block with the exit code + return Ok(PipelineData::new_external_stream_with_only_exit_code( + exit_code, + )); + } + } + Err( + err @ (ShellError::Return { .. } + | ShellError::Continue { .. } + | ShellError::Break { .. }), + ) => { + // These block control related errors should be passed through + return Err(err); + } + Err(err) => { + if let Some(error_handler) = ctx.stack.error_handlers.pop(ctx.error_handler_base) { + // If an error handler is set, branch there + prepare_error_handler(ctx, error_handler, Some(err.into_spanned(*span))); + pc = error_handler.handler_index; + } else { + // If not, exit the block with the error + return Err(err); + } + } + } + } + + // Fell out of the loop, without encountering a Return. + Err(ShellError::IrEvalError { + msg: format!( + "Program counter out of range (pc={pc}, len={len})", + len = ir_block.instructions.len(), + ), + span: *ctx.block_span, + }) +} + +/// Prepare the context for an error handler +fn prepare_error_handler( + ctx: &mut EvalContext<'_>, + error_handler: ErrorHandler, + error: Option>, +) { + if let Some(reg_id) = error_handler.error_register { + if let Some(error) = error { + // Create the error value and put it in the register + let value = Value::record( + record! { + "msg" => Value::string(format!("{}", error.item), error.span), + "debug" => Value::string(format!("{:?}", error.item), error.span), + "raw" => Value::error(error.item, error.span), + }, + error.span, + ); + ctx.put_reg(reg_id, PipelineData::Value(value, None)); + } else { + // Set the register to empty + ctx.put_reg(reg_id, PipelineData::Empty); + } + } +} + +/// The result of performing an instruction. Describes what should happen next +#[derive(Debug)] +enum InstructionResult { + Continue, + Branch(usize), + Return(RegId), + ExitCode(i32), +} + +/// Perform an instruction +fn eval_instruction( + ctx: &mut EvalContext<'_>, + instruction: &Instruction, + span: &Span, + ast: &Option, +) -> Result { + use self::InstructionResult::*; + + // See the docs for `Instruction` for more information on what these instructions are supposed + // to do. + match instruction { + Instruction::Unreachable => Err(ShellError::IrEvalError { + msg: "Reached unreachable code".into(), + span: Some(*span), + }), + Instruction::LoadLiteral { dst, lit } => load_literal(ctx, *dst, lit, *span), + Instruction::LoadValue { dst, val } => { + ctx.put_reg(*dst, Value::clone(val).into_pipeline_data()); + Ok(Continue) + } + Instruction::Move { dst, src } => { + let val = ctx.take_reg(*src); + ctx.put_reg(*dst, val); + Ok(Continue) + } + Instruction::Clone { dst, src } => { + let data = ctx.clone_reg(*src, *span)?; + ctx.put_reg(*dst, data); + Ok(Continue) + } + Instruction::Collect { src_dst } => { + let data = ctx.take_reg(*src_dst); + let value = collect(data, *span)?; + ctx.put_reg(*src_dst, value); + Ok(Continue) + } + Instruction::Span { src_dst } => { + let data = ctx.take_reg(*src_dst); + let spanned = data.with_span(*span); + ctx.put_reg(*src_dst, spanned); + Ok(Continue) + } + Instruction::Drop { src } => { + ctx.take_reg(*src); + Ok(Continue) + } + Instruction::Drain { src } => { + let data = ctx.take_reg(*src); + drain(ctx, data) + } + Instruction::LoadVariable { dst, var_id } => { + let value = get_var(ctx, *var_id, *span)?; + ctx.put_reg(*dst, value.into_pipeline_data()); + Ok(Continue) + } + Instruction::StoreVariable { var_id, src } => { + let value = ctx.collect_reg(*src, *span)?; + ctx.stack.add_var(*var_id, value); + Ok(Continue) + } + Instruction::DropVariable { var_id } => { + ctx.stack.remove_var(*var_id); + Ok(Continue) + } + Instruction::LoadEnv { dst, key } => { + let key = ctx.get_str(*key, *span)?; + if let Some(value) = get_env_var_case_insensitive(ctx, key) { + let new_value = value.clone().into_pipeline_data(); + ctx.put_reg(*dst, new_value); + Ok(Continue) + } else { + // FIXME: using the same span twice, shouldn't this really be + // EnvVarNotFoundAtRuntime? There are tests that depend on CantFindColumn though... + Err(ShellError::CantFindColumn { + col_name: key.into(), + span: Some(*span), + src_span: *span, + }) + } + } + Instruction::LoadEnvOpt { dst, key } => { + let key = ctx.get_str(*key, *span)?; + let value = get_env_var_case_insensitive(ctx, key) + .cloned() + .unwrap_or(Value::nothing(*span)); + ctx.put_reg(*dst, value.into_pipeline_data()); + Ok(Continue) + } + Instruction::StoreEnv { key, src } => { + let key = ctx.get_str(*key, *span)?; + let value = ctx.collect_reg(*src, *span)?; + + let key = get_env_var_name_case_insensitive(ctx, key); + + if !is_automatic_env_var(&key) { + let is_config = key == "config"; + ctx.stack.add_env_var(key.into_owned(), value); + if is_config { + ctx.stack.update_config(ctx.engine_state)?; + } + Ok(Continue) + } else { + Err(ShellError::AutomaticEnvVarSetManually { + envvar_name: key.into(), + span: *span, + }) + } + } + Instruction::PushPositional { src } => { + let val = ctx.collect_reg(*src, *span)?.with_span(*span); + ctx.stack.arguments.push(Argument::Positional { + span: *span, + val, + ast: ast.clone().map(|ast_ref| ast_ref.0), + }); + Ok(Continue) + } + Instruction::AppendRest { src } => { + let vals = ctx.collect_reg(*src, *span)?.with_span(*span); + ctx.stack.arguments.push(Argument::Spread { + span: *span, + vals, + ast: ast.clone().map(|ast_ref| ast_ref.0), + }); + Ok(Continue) + } + Instruction::PushFlag { name } => { + let data = ctx.data.clone(); + ctx.stack.arguments.push(Argument::Flag { + data, + name: *name, + short: DataSlice::empty(), + span: *span, + }); + Ok(Continue) + } + Instruction::PushShortFlag { short } => { + let data = ctx.data.clone(); + ctx.stack.arguments.push(Argument::Flag { + data, + name: DataSlice::empty(), + short: *short, + span: *span, + }); + Ok(Continue) + } + Instruction::PushNamed { name, src } => { + let val = ctx.collect_reg(*src, *span)?.with_span(*span); + let data = ctx.data.clone(); + ctx.stack.arguments.push(Argument::Named { + data, + name: *name, + short: DataSlice::empty(), + span: *span, + val, + ast: ast.clone().map(|ast_ref| ast_ref.0), + }); + Ok(Continue) + } + Instruction::PushShortNamed { short, src } => { + let val = ctx.collect_reg(*src, *span)?.with_span(*span); + let data = ctx.data.clone(); + ctx.stack.arguments.push(Argument::Named { + data, + name: DataSlice::empty(), + short: *short, + span: *span, + val, + ast: ast.clone().map(|ast_ref| ast_ref.0), + }); + Ok(Continue) + } + Instruction::PushParserInfo { name, info } => { + let data = ctx.data.clone(); + ctx.stack.arguments.push(Argument::ParserInfo { + data, + name: *name, + info: info.clone(), + }); + Ok(Continue) + } + Instruction::RedirectOut { mode } => { + ctx.redirect_out = eval_redirection(ctx, mode, *span, RedirectionStream::Out)?; + Ok(Continue) + } + Instruction::RedirectErr { mode } => { + ctx.redirect_err = eval_redirection(ctx, mode, *span, RedirectionStream::Err)?; + Ok(Continue) + } + Instruction::CheckErrRedirected { src } => match ctx.borrow_reg(*src) { + PipelineData::ByteStream(stream, _) + if matches!(stream.source(), ByteStreamSource::Child(_)) => + { + Ok(Continue) + } + _ => Err(ShellError::GenericError { + error: "Can't redirect stderr of internal command output".into(), + msg: "piping stderr only works on external commands".into(), + span: Some(*span), + help: None, + inner: vec![], + }), + }, + Instruction::OpenFile { + file_num, + path, + append, + } => { + let path = ctx.collect_reg(*path, *span)?; + let file = open_file(ctx, &path, *append)?; + ctx.files[*file_num as usize] = Some(file); + Ok(Continue) + } + Instruction::WriteFile { file_num, src } => { + let src = ctx.take_reg(*src); + let file = ctx + .files + .get(*file_num as usize) + .cloned() + .flatten() + .ok_or_else(|| ShellError::IrEvalError { + msg: format!("Tried to write to file #{file_num}, but it is not open"), + span: Some(*span), + })?; + let result = { + let mut stack = ctx + .stack + .push_redirection(Some(Redirection::File(file)), None); + src.write_to_out_dests(ctx.engine_state, &mut stack)? + }; + // Abort execution if there's an exit code from a failed external + drain(ctx, result) + } + Instruction::CloseFile { file_num } => { + if ctx.files[*file_num as usize].take().is_some() { + Ok(Continue) + } else { + Err(ShellError::IrEvalError { + msg: format!("Tried to close file #{file_num}, but it is not open"), + span: Some(*span), + }) + } + } + Instruction::Call { decl_id, src_dst } => { + let input = ctx.take_reg(*src_dst); + let result = eval_call::(ctx, *decl_id, *span, input)?; + ctx.put_reg(*src_dst, result); + Ok(Continue) + } + Instruction::StringAppend { src_dst, val } => { + let string_value = ctx.collect_reg(*src_dst, *span)?; + let operand_value = ctx.collect_reg(*val, *span)?; + let string_span = string_value.span(); + + let mut string = string_value.into_string()?; + let operand = if let Value::String { val, .. } = operand_value { + // Small optimization, so we don't have to copy the string *again* + val + } else { + operand_value.to_expanded_string(", ", ctx.engine_state.get_config()) + }; + string.push_str(&operand); + + let new_string_value = Value::string(string, string_span); + ctx.put_reg(*src_dst, new_string_value.into_pipeline_data()); + Ok(Continue) + } + Instruction::GlobFrom { src_dst, no_expand } => { + let string_value = ctx.collect_reg(*src_dst, *span)?; + let glob_value = if matches!(string_value, Value::Glob { .. }) { + // It already is a glob, so don't touch it. + string_value + } else { + // Treat it as a string, then cast + let string = string_value.into_string()?; + Value::glob(string, *no_expand, *span) + }; + ctx.put_reg(*src_dst, glob_value.into_pipeline_data()); + Ok(Continue) + } + Instruction::ListPush { src_dst, item } => { + let list_value = ctx.collect_reg(*src_dst, *span)?; + let item = ctx.collect_reg(*item, *span)?; + let list_span = list_value.span(); + let mut list = list_value.into_list()?; + list.push(item); + ctx.put_reg(*src_dst, Value::list(list, list_span).into_pipeline_data()); + Ok(Continue) + } + Instruction::ListSpread { src_dst, items } => { + let list_value = ctx.collect_reg(*src_dst, *span)?; + let items = ctx.collect_reg(*items, *span)?; + let list_span = list_value.span(); + let items_span = items.span(); + let mut list = list_value.into_list()?; + list.extend( + items + .into_list() + .map_err(|_| ShellError::CannotSpreadAsList { span: items_span })?, + ); + ctx.put_reg(*src_dst, Value::list(list, list_span).into_pipeline_data()); + Ok(Continue) + } + Instruction::RecordInsert { src_dst, key, val } => { + let record_value = ctx.collect_reg(*src_dst, *span)?; + let key = ctx.collect_reg(*key, *span)?; + let val = ctx.collect_reg(*val, *span)?; + let record_span = record_value.span(); + let mut record = record_value.into_record()?; + + let key = key.coerce_into_string()?; + if let Some(old_value) = record.insert(&key, val) { + return Err(ShellError::ColumnDefinedTwice { + col_name: key, + second_use: *span, + first_use: old_value.span(), + }); + } + + ctx.put_reg( + *src_dst, + Value::record(record, record_span).into_pipeline_data(), + ); + Ok(Continue) + } + Instruction::RecordSpread { src_dst, items } => { + let record_value = ctx.collect_reg(*src_dst, *span)?; + let items = ctx.collect_reg(*items, *span)?; + let record_span = record_value.span(); + let items_span = items.span(); + let mut record = record_value.into_record()?; + // Not using .extend() here because it doesn't handle duplicates + for (key, val) in items + .into_record() + .map_err(|_| ShellError::CannotSpreadAsRecord { span: items_span })? + { + if let Some(first_value) = record.insert(&key, val) { + return Err(ShellError::ColumnDefinedTwice { + col_name: key, + second_use: *span, + first_use: first_value.span(), + }); + } + } + ctx.put_reg( + *src_dst, + Value::record(record, record_span).into_pipeline_data(), + ); + Ok(Continue) + } + Instruction::Not { src_dst } => { + let bool = ctx.collect_reg(*src_dst, *span)?; + let negated = !bool.as_bool()?; + ctx.put_reg( + *src_dst, + Value::bool(negated, bool.span()).into_pipeline_data(), + ); + Ok(Continue) + } + Instruction::BinaryOp { lhs_dst, op, rhs } => binary_op(ctx, *lhs_dst, op, *rhs, *span), + Instruction::FollowCellPath { src_dst, path } => { + let data = ctx.take_reg(*src_dst); + let path = ctx.take_reg(*path); + if let PipelineData::Value(Value::CellPath { val: path, .. }, _) = path { + let value = data.follow_cell_path(&path.members, *span, true)?; + ctx.put_reg(*src_dst, value.into_pipeline_data()); + Ok(Continue) + } else if let PipelineData::Value(Value::Error { error, .. }, _) = path { + Err(*error) + } else { + Err(ShellError::TypeMismatch { + err_message: "expected cell path".into(), + span: path.span().unwrap_or(*span), + }) + } + } + Instruction::CloneCellPath { dst, src, path } => { + let value = ctx.clone_reg_value(*src, *span)?; + let path = ctx.take_reg(*path); + if let PipelineData::Value(Value::CellPath { val: path, .. }, _) = path { + // TODO: make follow_cell_path() not have to take ownership, probably using Cow + let value = value.follow_cell_path(&path.members, true)?; + ctx.put_reg(*dst, value.into_pipeline_data()); + Ok(Continue) + } else if let PipelineData::Value(Value::Error { error, .. }, _) = path { + Err(*error) + } else { + Err(ShellError::TypeMismatch { + err_message: "expected cell path".into(), + span: path.span().unwrap_or(*span), + }) + } + } + Instruction::UpsertCellPath { + src_dst, + path, + new_value, + } => { + let data = ctx.take_reg(*src_dst); + let metadata = data.metadata(); + // Change the span because we're modifying it + let mut value = data.into_value(*span)?; + let path = ctx.take_reg(*path); + let new_value = ctx.collect_reg(*new_value, *span)?; + if let PipelineData::Value(Value::CellPath { val: path, .. }, _) = path { + value.upsert_data_at_cell_path(&path.members, new_value)?; + ctx.put_reg(*src_dst, value.into_pipeline_data_with_metadata(metadata)); + Ok(Continue) + } else if let PipelineData::Value(Value::Error { error, .. }, _) = path { + Err(*error) + } else { + Err(ShellError::TypeMismatch { + err_message: "expected cell path".into(), + span: path.span().unwrap_or(*span), + }) + } + } + Instruction::Jump { index } => Ok(Branch(*index)), + Instruction::BranchIf { cond, index } => { + let data = ctx.take_reg(*cond); + let data_span = data.span(); + let val = match data { + PipelineData::Value(Value::Bool { val, .. }, _) => val, + PipelineData::Value(Value::Error { error, .. }, _) => { + return Err(*error); + } + _ => { + return Err(ShellError::TypeMismatch { + err_message: "expected bool".into(), + span: data_span.unwrap_or(*span), + }); + } + }; + if val { + Ok(Branch(*index)) + } else { + Ok(Continue) + } + } + Instruction::BranchIfEmpty { src, index } => { + let is_empty = matches!( + ctx.borrow_reg(*src), + PipelineData::Empty | PipelineData::Value(Value::Nothing { .. }, _) + ); + + if is_empty { + Ok(Branch(*index)) + } else { + Ok(Continue) + } + } + Instruction::Match { + pattern, + src, + index, + } => { + let value = ctx.clone_reg_value(*src, *span)?; + ctx.matches.clear(); + if pattern.match_value(&value, &mut ctx.matches) { + // Match succeeded: set variables and branch + for (var_id, match_value) in ctx.matches.drain(..) { + ctx.stack.add_var(var_id, match_value); + } + Ok(Branch(*index)) + } else { + // Failed to match, put back original value + ctx.matches.clear(); + Ok(Continue) + } + } + Instruction::CheckMatchGuard { src } => { + if matches!( + ctx.borrow_reg(*src), + PipelineData::Value(Value::Bool { .. }, _) + ) { + Ok(Continue) + } else { + Err(ShellError::MatchGuardNotBool { span: *span }) + } + } + Instruction::Iterate { + dst, + stream, + end_index, + } => eval_iterate(ctx, *dst, *stream, *end_index), + Instruction::OnError { index } => { + ctx.stack.error_handlers.push(ErrorHandler { + handler_index: *index, + error_register: None, + }); + Ok(Continue) + } + Instruction::OnErrorInto { index, dst } => { + ctx.stack.error_handlers.push(ErrorHandler { + handler_index: *index, + error_register: Some(*dst), + }); + Ok(Continue) + } + Instruction::PopErrorHandler => { + ctx.stack.error_handlers.pop(ctx.error_handler_base); + Ok(Continue) + } + Instruction::CheckExternalFailed { dst, src } => { + let data = ctx.take_reg(*src); + let (data, failed) = data.check_external_failed()?; + ctx.put_reg(*src, data); + ctx.put_reg(*dst, Value::bool(failed, *span).into_pipeline_data()); + Ok(Continue) + } + Instruction::ReturnEarly { src } => { + let val = ctx.collect_reg(*src, *span)?; + Err(ShellError::Return { + span: *span, + value: Box::new(val), + }) + } + Instruction::Return { src } => Ok(Return(*src)), + } +} + +/// Load a literal value into a register +fn load_literal( + ctx: &mut EvalContext<'_>, + dst: RegId, + lit: &Literal, + span: Span, +) -> Result { + let value = literal_value(ctx, lit, span)?; + ctx.put_reg(dst, PipelineData::Value(value, None)); + Ok(InstructionResult::Continue) +} + +fn literal_value( + ctx: &mut EvalContext<'_>, + lit: &Literal, + span: Span, +) -> Result { + Ok(match lit { + Literal::Bool(b) => Value::bool(*b, span), + Literal::Int(i) => Value::int(*i, span), + Literal::Float(f) => Value::float(*f, span), + Literal::Filesize(q) => Value::filesize(*q, span), + Literal::Duration(q) => Value::duration(*q, span), + Literal::Binary(bin) => Value::binary(&ctx.data[*bin], span), + Literal::Block(block_id) | Literal::RowCondition(block_id) | Literal::Closure(block_id) => { + let block = ctx.engine_state.get_block(*block_id); + let captures = block + .captures + .iter() + .map(|var_id| get_var(ctx, *var_id, span).map(|val| (*var_id, val))) + .collect::, ShellError>>()?; + Value::closure( + Closure { + block_id: *block_id, + captures, + }, + span, + ) + } + Literal::Range { + start, + step, + end, + inclusion, + } => { + let start = ctx.collect_reg(*start, span)?; + let step = ctx.collect_reg(*step, span)?; + let end = ctx.collect_reg(*end, span)?; + let range = Range::new(start, step, end, *inclusion, span)?; + Value::range(range, span) + } + Literal::List { capacity } => Value::list(Vec::with_capacity(*capacity), span), + Literal::Record { capacity } => Value::record(Record::with_capacity(*capacity), span), + Literal::Filepath { + val: path, + no_expand, + } => { + let path = ctx.get_str(*path, span)?; + if *no_expand { + Value::string(path, span) + } else { + let cwd = ctx.engine_state.cwd(Some(ctx.stack))?; + let path = expand_path_with(path, cwd, true); + + Value::string(path.to_string_lossy(), span) + } + } + Literal::Directory { + val: path, + no_expand, + } => { + let path = ctx.get_str(*path, span)?; + if path == "-" { + Value::string("-", span) + } else if *no_expand { + Value::string(path, span) + } else { + let cwd = ctx + .engine_state + .cwd(Some(ctx.stack)) + .map(AbsolutePathBuf::into_std_path_buf) + .unwrap_or_default(); + let path = expand_path_with(path, cwd, true); + + Value::string(path.to_string_lossy(), span) + } + } + Literal::GlobPattern { val, no_expand } => { + Value::glob(ctx.get_str(*val, span)?, *no_expand, span) + } + Literal::String(s) => Value::string(ctx.get_str(*s, span)?, span), + Literal::RawString(s) => Value::string(ctx.get_str(*s, span)?, span), + Literal::CellPath(path) => Value::cell_path(CellPath::clone(path), span), + Literal::Date(dt) => Value::date(**dt, span), + Literal::Nothing => Value::nothing(span), + }) +} + +fn binary_op( + ctx: &mut EvalContext<'_>, + lhs_dst: RegId, + op: &Operator, + rhs: RegId, + span: Span, +) -> Result { + let lhs_val = ctx.collect_reg(lhs_dst, span)?; + let rhs_val = ctx.collect_reg(rhs, span)?; + + // Handle binary op errors early + if let Value::Error { error, .. } = lhs_val { + return Err(*error); + } + if let Value::Error { error, .. } = rhs_val { + return Err(*error); + } + + // We only have access to one span here, but the generated code usually adds a `span` + // instruction to set the output span to the right span. + let op_span = span; + + let result = match op { + Operator::Comparison(cmp) => match cmp { + Comparison::Equal => lhs_val.eq(op_span, &rhs_val, span)?, + Comparison::NotEqual => lhs_val.ne(op_span, &rhs_val, span)?, + Comparison::LessThan => lhs_val.lt(op_span, &rhs_val, span)?, + Comparison::GreaterThan => lhs_val.gt(op_span, &rhs_val, span)?, + Comparison::LessThanOrEqual => lhs_val.lte(op_span, &rhs_val, span)?, + Comparison::GreaterThanOrEqual => lhs_val.gte(op_span, &rhs_val, span)?, + Comparison::RegexMatch => { + lhs_val.regex_match(ctx.engine_state, op_span, &rhs_val, false, span)? + } + Comparison::NotRegexMatch => { + lhs_val.regex_match(ctx.engine_state, op_span, &rhs_val, true, span)? + } + Comparison::In => lhs_val.r#in(op_span, &rhs_val, span)?, + Comparison::NotIn => lhs_val.not_in(op_span, &rhs_val, span)?, + Comparison::StartsWith => lhs_val.starts_with(op_span, &rhs_val, span)?, + Comparison::EndsWith => lhs_val.ends_with(op_span, &rhs_val, span)?, + }, + Operator::Math(mat) => match mat { + Math::Plus => lhs_val.add(op_span, &rhs_val, span)?, + Math::Append => lhs_val.append(op_span, &rhs_val, span)?, + Math::Minus => lhs_val.sub(op_span, &rhs_val, span)?, + Math::Multiply => lhs_val.mul(op_span, &rhs_val, span)?, + Math::Divide => lhs_val.div(op_span, &rhs_val, span)?, + Math::Modulo => lhs_val.modulo(op_span, &rhs_val, span)?, + Math::FloorDivision => lhs_val.floor_div(op_span, &rhs_val, span)?, + Math::Pow => lhs_val.pow(op_span, &rhs_val, span)?, + }, + Operator::Boolean(bl) => match bl { + Boolean::And => lhs_val.and(op_span, &rhs_val, span)?, + Boolean::Or => lhs_val.or(op_span, &rhs_val, span)?, + Boolean::Xor => lhs_val.xor(op_span, &rhs_val, span)?, + }, + Operator::Bits(bit) => match bit { + Bits::BitOr => lhs_val.bit_or(op_span, &rhs_val, span)?, + Bits::BitXor => lhs_val.bit_xor(op_span, &rhs_val, span)?, + Bits::BitAnd => lhs_val.bit_and(op_span, &rhs_val, span)?, + Bits::ShiftLeft => lhs_val.bit_shl(op_span, &rhs_val, span)?, + Bits::ShiftRight => lhs_val.bit_shr(op_span, &rhs_val, span)?, + }, + Operator::Assignment(_asg) => { + return Err(ShellError::IrEvalError { + msg: "can't eval assignment with the `binary-op` instruction".into(), + span: Some(span), + }) + } + }; + + ctx.put_reg(lhs_dst, PipelineData::Value(result, None)); + + Ok(InstructionResult::Continue) +} + +/// Evaluate a call +fn eval_call( + ctx: &mut EvalContext<'_>, + decl_id: DeclId, + head: Span, + input: PipelineData, +) -> Result { + let EvalContext { + engine_state, + stack: caller_stack, + args_base, + redirect_out, + redirect_err, + .. + } = ctx; + + let args_len = caller_stack.arguments.get_len(*args_base); + let decl = engine_state.get_decl(decl_id); + + // Set up redirect modes + let mut caller_stack = caller_stack.push_redirection(redirect_out.take(), redirect_err.take()); + + let result; + + if let Some(block_id) = decl.block_id() { + // If the decl is a custom command + let block = engine_state.get_block(block_id); + + // Set up a callee stack with the captures and move arguments from the stack into variables + let mut callee_stack = caller_stack.gather_captures(engine_state, &block.captures); + + gather_arguments( + engine_state, + block, + &mut caller_stack, + &mut callee_stack, + *args_base, + args_len, + head, + )?; + + // Add one to the recursion count, so we don't recurse too deep. Stack overflows are not + // recoverable in Rust. + callee_stack.recursion_count += 1; + + result = eval_block_with_early_return::(engine_state, &mut callee_stack, block, input); + + // Move environment variables back into the caller stack scope if requested to do so + if block.redirect_env { + redirect_env(engine_state, &mut caller_stack, &callee_stack); + } + } else { + // FIXME: precalculate this and save it somewhere + let span = Span::merge_many( + std::iter::once(head).chain( + caller_stack + .arguments + .get_args(*args_base, args_len) + .iter() + .flat_map(|arg| arg.span()), + ), + ); + + let call = Call { + decl_id, + head, + span, + args_base: *args_base, + args_len, + }; + + // Run the call + result = decl.run(engine_state, &mut caller_stack, &(&call).into(), input); + }; + + drop(caller_stack); + + // Important that this runs, to reset state post-call: + ctx.stack.arguments.leave_frame(ctx.args_base); + ctx.redirect_out = None; + ctx.redirect_err = None; + + result +} + +fn find_named_var_id( + sig: &Signature, + name: &[u8], + short: &[u8], + span: Span, +) -> Result { + sig.named + .iter() + .find(|n| { + if !n.long.is_empty() { + n.long.as_bytes() == name + } else { + // It's possible to only have a short name and no long name + n.short + .is_some_and(|s| s.encode_utf8(&mut [0; 4]).as_bytes() == short) + } + }) + .ok_or_else(|| ShellError::IrEvalError { + msg: format!( + "block does not have an argument named `{}`", + String::from_utf8_lossy(name) + ), + span: Some(span), + }) + .and_then(|flag| expect_named_var_id(flag, span)) +} + +fn expect_named_var_id(arg: &Flag, span: Span) -> Result { + arg.var_id.ok_or_else(|| ShellError::IrEvalError { + msg: format!( + "block signature is missing var id for named arg `{}`", + arg.long + ), + span: Some(span), + }) +} + +fn expect_positional_var_id(arg: &PositionalArg, span: Span) -> Result { + arg.var_id.ok_or_else(|| ShellError::IrEvalError { + msg: format!( + "block signature is missing var id for positional arg `{}`", + arg.name + ), + span: Some(span), + }) +} + +/// Move arguments from the stack into variables for a custom command +fn gather_arguments( + engine_state: &EngineState, + block: &Block, + caller_stack: &mut Stack, + callee_stack: &mut Stack, + args_base: usize, + args_len: usize, + call_head: Span, +) -> Result<(), ShellError> { + let mut positional_iter = block + .signature + .required_positional + .iter() + .map(|p| (p, true)) + .chain( + block + .signature + .optional_positional + .iter() + .map(|p| (p, false)), + ); + + // Arguments that didn't get consumed by required/optional + let mut rest = vec![]; + + // If we encounter a spread, all further positionals should go to rest + let mut always_spread = false; + + for arg in caller_stack.arguments.drain_args(args_base, args_len) { + match arg { + Argument::Positional { span, val, .. } => { + // Don't check next positional arg if we encountered a spread previously + let next = (!always_spread).then(|| positional_iter.next()).flatten(); + if let Some((positional_arg, required)) = next { + let var_id = expect_positional_var_id(positional_arg, span)?; + if required { + // By checking the type of the bound variable rather than converting the + // SyntaxShape here, we might be able to save some allocations and effort + let variable = engine_state.get_var(var_id); + check_type(&val, &variable.ty)?; + } + callee_stack.add_var(var_id, val); + } else { + rest.push(val); + } + } + Argument::Spread { vals, .. } => { + if let Value::List { vals, .. } = vals { + rest.extend(vals); + // All further positional args should go to spread + always_spread = true; + } else if let Value::Error { error, .. } = vals { + return Err(*error); + } else { + return Err(ShellError::CannotSpreadAsList { span: vals.span() }); + } + } + Argument::Flag { + data, + name, + short, + span, + } => { + let var_id = find_named_var_id(&block.signature, &data[name], &data[short], span)?; + callee_stack.add_var(var_id, Value::bool(true, span)) + } + Argument::Named { + data, + name, + short, + span, + val, + .. + } => { + let var_id = find_named_var_id(&block.signature, &data[name], &data[short], span)?; + callee_stack.add_var(var_id, val) + } + Argument::ParserInfo { .. } => (), + } + } + + // Add the collected rest of the arguments if a spread argument exists + if let Some(rest_arg) = &block.signature.rest_positional { + let rest_span = rest.first().map(|v| v.span()).unwrap_or(call_head); + let var_id = expect_positional_var_id(rest_arg, rest_span)?; + callee_stack.add_var(var_id, Value::list(rest, rest_span)); + } + + // Check for arguments that haven't yet been set and set them to their defaults + for (positional_arg, _) in positional_iter { + let var_id = expect_positional_var_id(positional_arg, call_head)?; + callee_stack.add_var( + var_id, + positional_arg + .default_value + .clone() + .unwrap_or(Value::nothing(call_head)), + ); + } + + for named_arg in &block.signature.named { + if let Some(var_id) = named_arg.var_id { + // For named arguments, we do this check by looking to see if the variable was set yet on + // the stack. This assumes that the stack's variables was previously empty, but that's a + // fair assumption for a brand new callee stack. + if !callee_stack.vars.iter().any(|(id, _)| *id == var_id) { + let val = if named_arg.arg.is_none() { + Value::bool(false, call_head) + } else if let Some(value) = &named_arg.default_value { + value.clone() + } else { + Value::nothing(call_head) + }; + callee_stack.add_var(var_id, val); + } + } + } + + Ok(()) +} + +/// Type check helper. Produces `CantConvert` error if `val` is not compatible with `ty`. +fn check_type(val: &Value, ty: &Type) -> Result<(), ShellError> { + if match val { + // An empty list is compatible with any list or table type + Value::List { vals, .. } if vals.is_empty() => { + matches!(ty, Type::Any | Type::List(_) | Type::Table(_)) + } + // FIXME: the allocation that might be required here is not great, it would be nice to be + // able to just directly check whether a value is compatible with a type + _ => val.get_type().is_subtype(ty), + } { + Ok(()) + } else { + Err(ShellError::CantConvert { + to_type: ty.to_string(), + from_type: val.get_type().to_string(), + span: val.span(), + help: None, + }) + } +} + +/// Get variable from [`Stack`] or [`EngineState`] +fn get_var(ctx: &EvalContext<'_>, var_id: VarId, span: Span) -> Result { + match var_id { + // $env + ENV_VARIABLE_ID => { + let env_vars = ctx.stack.get_env_vars(ctx.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)); + + Ok(Value::record(pairs.into_iter().collect(), span)) + } + _ => ctx.stack.get_var(var_id, span).or_else(|err| { + // $nu is handled by getting constant + if let Some(const_val) = ctx.engine_state.get_constant(var_id).cloned() { + Ok(const_val.with_span(span)) + } else { + Err(err) + } + }), + } +} + +/// Get an environment variable, case-insensitively +fn get_env_var_case_insensitive<'a>(ctx: &'a mut EvalContext<'_>, key: &str) -> Option<&'a Value> { + // Read scopes in order + ctx.stack + .env_vars + .iter() + .rev() + .chain(std::iter::once(&ctx.engine_state.env_vars)) + .flat_map(|overlays| { + // Read overlays in order + ctx.stack + .active_overlays + .iter() + .rev() + .filter_map(|name| overlays.get(name)) + }) + .find_map(|map| { + // Use the hashmap first to try to be faster? + map.get(key).or_else(|| { + // Check to see if it exists at all in the map + map.iter() + .find_map(|(k, v)| k.eq_ignore_case(key).then_some(v)) + }) + }) +} + +/// Get the existing name of an environment variable, case-insensitively. This is used to implement +/// case preservation of environment variables, so that changing an environment variable that +/// already exists always uses the same case. +fn get_env_var_name_case_insensitive<'a>(ctx: &mut EvalContext<'_>, key: &'a str) -> Cow<'a, str> { + // Read scopes in order + ctx.stack + .env_vars + .iter() + .rev() + .chain(std::iter::once(&ctx.engine_state.env_vars)) + .flat_map(|overlays| { + // Read overlays in order + ctx.stack + .active_overlays + .iter() + .rev() + .filter_map(|name| overlays.get(name)) + }) + .find_map(|map| { + // Use the hashmap first to try to be faster? + if map.contains_key(key) { + Some(Cow::Borrowed(key)) + } else { + map.keys().find(|k| k.eq_ignore_case(key)).map(|k| { + // it exists, but with a different case + Cow::Owned(k.to_owned()) + }) + } + }) + // didn't exist. + .unwrap_or(Cow::Borrowed(key)) +} + +/// Helper to collect values into [`PipelineData`], preserving original span and metadata +/// +/// The metadata is removed if it is the file data source, as that's just meant to mark streams. +fn collect(data: PipelineData, fallback_span: Span) -> Result { + let span = data.span().unwrap_or(fallback_span); + let metadata = match data.metadata() { + // Remove the `FilePath` metadata, because after `collect` it's no longer necessary to + // check where some input came from. + Some(PipelineMetadata { + data_source: DataSource::FilePath(_), + content_type: None, + }) => None, + other => other, + }; + let value = data.into_value(span)?; + Ok(PipelineData::Value(value, metadata)) +} + +/// Helper for drain behavior. Returns `Ok(ExitCode)` on failed external. +fn drain(ctx: &mut EvalContext<'_>, data: PipelineData) -> Result { + use self::InstructionResult::*; + let span = data.span().unwrap_or(Span::unknown()); + if let Some(exit_status) = data.drain()? { + ctx.stack.add_env_var( + "LAST_EXIT_CODE".into(), + Value::int(exit_status.code() as i64, span), + ); + if exit_status.code() == 0 { + Ok(Continue) + } else { + Ok(ExitCode(exit_status.code())) + } + } else { + Ok(Continue) + } +} + +enum RedirectionStream { + Out, + Err, +} + +/// Open a file for redirection +fn open_file(ctx: &EvalContext<'_>, path: &Value, append: bool) -> Result, ShellError> { + let path_expanded = + expand_path_with(path.as_str()?, ctx.engine_state.cwd(Some(ctx.stack))?, true); + let mut options = File::options(); + if append { + options.append(true); + } else { + options.write(true).truncate(true); + } + let file = options + .create(true) + .open(path_expanded) + .err_span(path.span())?; + Ok(Arc::new(file)) +} + +/// Set up a [`Redirection`] from a [`RedirectMode`] +fn eval_redirection( + ctx: &mut EvalContext<'_>, + mode: &RedirectMode, + span: Span, + which: RedirectionStream, +) -> Result, ShellError> { + match mode { + RedirectMode::Pipe => Ok(Some(Redirection::Pipe(OutDest::Pipe))), + RedirectMode::Capture => Ok(Some(Redirection::Pipe(OutDest::Capture))), + RedirectMode::Null => Ok(Some(Redirection::Pipe(OutDest::Null))), + RedirectMode::Inherit => Ok(Some(Redirection::Pipe(OutDest::Inherit))), + RedirectMode::File { file_num } => { + let file = ctx + .files + .get(*file_num as usize) + .cloned() + .flatten() + .ok_or_else(|| ShellError::IrEvalError { + msg: format!("Tried to redirect to file #{file_num}, but it is not open"), + span: Some(span), + })?; + Ok(Some(Redirection::File(file))) + } + RedirectMode::Caller => Ok(match which { + RedirectionStream::Out => ctx.stack.pipe_stdout().cloned().map(Redirection::Pipe), + RedirectionStream::Err => ctx.stack.pipe_stderr().cloned().map(Redirection::Pipe), + }), + } +} + +/// Do an `iterate` instruction. This can be called repeatedly to get more values from an iterable +fn eval_iterate( + ctx: &mut EvalContext<'_>, + dst: RegId, + stream: RegId, + end_index: usize, +) -> Result { + let mut data = ctx.take_reg(stream); + if let PipelineData::ListStream(list_stream, _) = &mut data { + // Modify the stream, taking one value off, and branching if it's empty + if let Some(val) = list_stream.next_value() { + ctx.put_reg(dst, val.into_pipeline_data()); + ctx.put_reg(stream, data); // put the stream back so it can be iterated on again + Ok(InstructionResult::Continue) + } else { + ctx.put_reg(dst, PipelineData::Empty); + Ok(InstructionResult::Branch(end_index)) + } + } else { + // Convert the PipelineData to an iterator, and wrap it in a ListStream so it can be + // iterated on + let metadata = data.metadata(); + let span = data.span().unwrap_or(Span::unknown()); + ctx.put_reg( + stream, + PipelineData::ListStream( + ListStream::new(data.into_iter(), span, Signals::EMPTY), + metadata, + ), + ); + eval_iterate(ctx, dst, stream, end_index) + } +} + +/// Redirect environment from the callee stack to the caller stack +fn redirect_env(engine_state: &EngineState, caller_stack: &mut Stack, callee_stack: &Stack) { + // TODO: make this more efficient + // Grab all environment variables from the callee + 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 (var, value) in callee_stack.get_stack_env_vars() { + caller_stack.add_env_var(var, value); + } +} diff --git a/crates/nu-engine/src/lib.rs b/crates/nu-engine/src/lib.rs index e3c8f8eede..5a5bb9a5d0 100644 --- a/crates/nu-engine/src/lib.rs +++ b/crates/nu-engine/src/lib.rs @@ -1,17 +1,21 @@ +#![doc = include_str!("../README.md")] mod call_ext; mod closure_eval; pub mod column; pub mod command_prelude; +mod compile; pub mod documentation; pub mod env; mod eval; mod eval_helpers; +mod eval_ir; mod glob_from; pub mod scope; pub use call_ext::CallExt; pub use closure_eval::*; pub use column::get_columns; +pub use compile::compile; pub use documentation::get_full_help; pub use env::*; pub use eval::{ @@ -19,4 +23,5 @@ pub use eval::{ eval_expression_with_input, eval_subexpression, eval_variable, redirect_env, }; pub use eval_helpers::*; +pub use eval_ir::eval_ir_block; pub use glob_from::glob_from; diff --git a/crates/nu-explore/Cargo.toml b/crates/nu-explore/Cargo.toml index dc17e6087d..6d0816ee3e 100644 --- a/crates/nu-explore/Cargo.toml +++ b/crates/nu-explore/Cargo.toml @@ -5,21 +5,21 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-explore" edition = "2021" license = "MIT" name = "nu-explore" -version = "0.94.3" +version = "0.95.1" [lib] bench = false [dependencies] -nu-protocol = { path = "../nu-protocol", version = "0.94.3" } -nu-parser = { path = "../nu-parser", version = "0.94.3" } -nu-color-config = { path = "../nu-color-config", version = "0.94.3" } -nu-engine = { path = "../nu-engine", version = "0.94.3" } -nu-table = { path = "../nu-table", version = "0.94.3" } -nu-json = { path = "../nu-json", version = "0.94.3" } -nu-utils = { path = "../nu-utils", version = "0.94.3" } +nu-protocol = { path = "../nu-protocol", version = "0.95.1" } +nu-parser = { path = "../nu-parser", version = "0.95.1" } +nu-color-config = { path = "../nu-color-config", version = "0.95.1" } +nu-engine = { path = "../nu-engine", version = "0.95.1" } +nu-table = { path = "../nu-table", version = "0.95.1" } +nu-json = { path = "../nu-json", version = "0.95.1" } +nu-utils = { path = "../nu-utils", version = "0.95.1" } nu-ansi-term = { workspace = true } -nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.94.3" } +nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.95.1" } anyhow = { workspace = true } log = { workspace = true } @@ -32,4 +32,4 @@ ansi-str = { workspace = true } unicode-width = { workspace = true } lscolors = { workspace = true, default-features = false, features = [ "nu-ansi-term", -] } +] } \ No newline at end of file diff --git a/crates/nu-explore/README.md b/crates/nu-explore/README.md new file mode 100644 index 0000000000..d18a576c43 --- /dev/null +++ b/crates/nu-explore/README.md @@ -0,0 +1,5 @@ +Implementation of the interactive `explore` command pager. + +## Internal Nushell crate + +This crate implements components of Nushell and is not designed to support plugin authors or other users directly. diff --git a/crates/nu-explore/src/commands/expand.rs b/crates/nu-explore/src/commands/expand.rs index d7d75af6c5..38f92c0b50 100644 --- a/crates/nu-explore/src/commands/expand.rs +++ b/crates/nu-explore/src/commands/expand.rs @@ -1,7 +1,7 @@ use super::ViewCommand; use crate::{ nu_common::{self, collect_input}, - views::Preview, + views::{Preview, ViewConfig}, }; use anyhow::Result; use nu_color_config::StyleComputer; @@ -43,6 +43,7 @@ impl ViewCommand for ExpandCmd { engine_state: &EngineState, stack: &mut Stack, value: Option, + _: &ViewConfig, ) -> Result { if let Some(value) = value { let value_as_string = convert_value_to_string(value, engine_state, stack)?; @@ -63,15 +64,14 @@ fn convert_value_to_string( let has_no_head = cols.is_empty() || (cols.len() == 1 && cols[0].is_empty()); let has_single_value = vals.len() == 1 && vals[0].len() == 1; if !has_no_head && has_single_value { - let config = engine_state.get_config(); - Ok(vals[0][0].to_abbreviated_string(config)) + let config = stack.get_config(engine_state); + Ok(vals[0][0].to_abbreviated_string(&config)) } else { - let ctrlc = engine_state.ctrlc.clone(); let config = engine_state.get_config(); let style_computer = StyleComputer::from_config(engine_state, stack); Ok(nu_common::try_build_table( - ctrlc, + engine_state.signals(), config, &style_computer, value, diff --git a/crates/nu-explore/src/commands/help.rs b/crates/nu-explore/src/commands/help.rs index 8be468383e..684e713939 100644 --- a/crates/nu-explore/src/commands/help.rs +++ b/crates/nu-explore/src/commands/help.rs @@ -1,5 +1,5 @@ use super::ViewCommand; -use crate::views::Preview; +use crate::views::{Preview, ViewConfig}; use anyhow::Result; use nu_ansi_term::Color; use nu_protocol::{ @@ -99,7 +99,13 @@ impl ViewCommand for HelpCmd { Ok(()) } - fn spawn(&mut self, _: &EngineState, _: &mut Stack, _: Option) -> Result { + fn spawn( + &mut self, + _: &EngineState, + _: &mut Stack, + _: Option, + _: &ViewConfig, + ) -> Result { Ok(HelpCmd::view()) } } diff --git a/crates/nu-explore/src/commands/mod.rs b/crates/nu-explore/src/commands/mod.rs index 141dc25f56..a748ddaaa3 100644 --- a/crates/nu-explore/src/commands/mod.rs +++ b/crates/nu-explore/src/commands/mod.rs @@ -1,3 +1,5 @@ +use crate::views::ViewConfig; + use super::pager::{Pager, Transition}; use anyhow::Result; use nu_protocol::{ @@ -49,5 +51,6 @@ pub trait ViewCommand { engine_state: &EngineState, stack: &mut Stack, value: Option, + config: &ViewConfig, ) -> Result; } diff --git a/crates/nu-explore/src/commands/nu.rs b/crates/nu-explore/src/commands/nu.rs index 1310dfbd1f..84e1fb9706 100644 --- a/crates/nu-explore/src/commands/nu.rs +++ b/crates/nu-explore/src/commands/nu.rs @@ -27,7 +27,7 @@ impl NuCmd { } impl ViewCommand for NuCmd { - type View = NuView<'static>; + type View = NuView; fn name(&self) -> &'static str { Self::NAME @@ -48,6 +48,7 @@ impl ViewCommand for NuCmd { engine_state: &EngineState, stack: &mut Stack, value: Option, + config: &ViewConfig, ) -> Result { let value = value.unwrap_or_default(); @@ -62,22 +63,22 @@ impl ViewCommand for NuCmd { return Ok(NuView::Preview(Preview::new(&text))); } - let mut view = RecordView::new(columns, values); + let mut view = RecordView::new(columns, values, config.explore_config.clone()); if is_record { - view.set_orientation_current(Orientation::Left); + view.set_top_layer_orientation(Orientation::Left); } Ok(NuView::Records(view)) } } -pub enum NuView<'a> { - Records(RecordView<'a>), +pub enum NuView { + Records(RecordView), Preview(Preview), } -impl View for NuView<'_> { +impl View for NuView { fn draw(&mut self, f: &mut Frame, area: Rect, cfg: ViewConfig<'_>, layout: &mut Layout) { match self { NuView::Records(v) => v.draw(f, area, cfg, layout), @@ -119,11 +120,4 @@ impl View for NuView<'_> { NuView::Preview(v) => v.exit(), } } - - fn setup(&mut self, config: ViewConfig<'_>) { - match self { - NuView::Records(v) => v.setup(config), - NuView::Preview(v) => v.setup(config), - } - } } diff --git a/crates/nu-explore/src/commands/table.rs b/crates/nu-explore/src/commands/table.rs index 6bc6268067..9ab1e39b98 100644 --- a/crates/nu-explore/src/commands/table.rs +++ b/crates/nu-explore/src/commands/table.rs @@ -1,7 +1,7 @@ use super::ViewCommand; use crate::{ nu_common::collect_input, - views::{Orientation, RecordView}, + views::{Orientation, RecordView, ViewConfig}, }; use anyhow::Result; use nu_protocol::{ @@ -30,7 +30,7 @@ impl TableCmd { } impl ViewCommand for TableCmd { - type View = RecordView<'static>; + type View = RecordView; fn name(&self) -> &'static str { Self::NAME @@ -49,20 +49,21 @@ impl ViewCommand for TableCmd { _: &EngineState, _: &mut Stack, value: Option, + config: &ViewConfig, ) -> Result { let value = value.unwrap_or_default(); let is_record = matches!(value, Value::Record { .. }); let (columns, data) = collect_input(value)?; - let mut view = RecordView::new(columns, data); + let mut view = RecordView::new(columns, data, config.explore_config.clone()); if is_record { - view.set_orientation_current(Orientation::Left); + view.set_top_layer_orientation(Orientation::Left); } if let Some(o) = self.settings.orientation { - view.set_orientation_current(o); + view.set_top_layer_orientation(o); } if self.settings.turn_on_cursor_mode { diff --git a/crates/nu-explore/src/commands/try.rs b/crates/nu-explore/src/commands/try.rs index a81ce13e21..e1290651c8 100644 --- a/crates/nu-explore/src/commands/try.rs +++ b/crates/nu-explore/src/commands/try.rs @@ -1,5 +1,5 @@ use super::ViewCommand; -use crate::views::TryView; +use crate::views::{TryView, ViewConfig}; use anyhow::Result; use nu_protocol::{ engine::{EngineState, Stack}, @@ -22,7 +22,7 @@ impl TryCmd { } impl ViewCommand for TryCmd { - type View = TryView<'static>; + type View = TryView; fn name(&self) -> &'static str { Self::NAME @@ -43,9 +43,10 @@ impl ViewCommand for TryCmd { engine_state: &EngineState, stack: &mut Stack, value: Option, + config: &ViewConfig, ) -> Result { let value = value.unwrap_or_default(); - let mut view = TryView::new(value); + let mut view = TryView::new(value, config.explore_config.clone()); view.init(self.command.clone()); view.try_run(engine_state, stack)?; diff --git a/crates/nu-explore/src/explore.rs b/crates/nu-explore/src/explore.rs index e5498ce5b3..28a3f04a30 100644 --- a/crates/nu-explore/src/explore.rs +++ b/crates/nu-explore/src/explore.rs @@ -63,11 +63,10 @@ impl Command for Explore { let tail: bool = call.has_flag(engine_state, stack, "tail")?; let peek_value: bool = call.has_flag(engine_state, stack, "peek")?; - let ctrlc = engine_state.ctrlc.clone(); - let nu_config = engine_state.get_config(); + let nu_config = stack.get_config(engine_state); let style_computer = StyleComputer::from_config(engine_state, stack); - let mut explore_config = ExploreConfig::from_nu_config(nu_config); + let mut explore_config = ExploreConfig::from_nu_config(&nu_config); explore_config.table.show_header = show_head; explore_config.table.show_index = show_index; explore_config.table.separator_style = lookup_color(&style_computer, "separator"); @@ -75,7 +74,7 @@ impl Command for Explore { let lscolors = create_lscolors(engine_state, stack); let config = PagerConfig::new( - nu_config, + &nu_config, &explore_config, &style_computer, &lscolors, @@ -83,7 +82,7 @@ impl Command for Explore { tail, ); - let result = run_pager(engine_state, &mut stack.clone(), ctrlc, input, config); + let result = run_pager(engine_state, &mut stack.clone(), input, config); match result { Ok(Some(value)) => Ok(PipelineData::Value(value, None)), diff --git a/crates/nu-explore/src/lib.rs b/crates/nu-explore/src/lib.rs index 562602c394..d645572c89 100644 --- a/crates/nu-explore/src/lib.rs +++ b/crates/nu-explore/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] mod commands; mod default_context; mod explore; @@ -10,7 +11,8 @@ use anyhow::Result; use commands::{ExpandCmd, HelpCmd, NuCmd, QuitCmd, TableCmd, TryCmd}; pub use default_context::add_explore_context; pub use explore::Explore; -use nu_common::{collect_pipeline, has_simple_value, CtrlC}; +use explore::ExploreConfig; +use nu_common::{collect_pipeline, has_simple_value}; use nu_protocol::{ engine::{EngineState, Stack}, PipelineData, Value, @@ -27,7 +29,6 @@ mod util { fn run_pager( engine_state: &EngineState, stack: &mut Stack, - ctrlc: CtrlC, input: PipelineData, config: PagerConfig, ) -> Result> { @@ -43,15 +44,15 @@ fn run_pager( if is_binary { p.show_message("For help type :help"); - let view = binary_view(input)?; - return p.run(engine_state, stack, ctrlc, Some(view), commands); + let view = binary_view(input, config.explore_config)?; + return p.run(engine_state, stack, Some(view), commands); } let (columns, data) = collect_pipeline(input)?; let has_no_input = columns.is_empty() && data.is_empty(); if has_no_input { - return p.run(engine_state, stack, ctrlc, help_view(), commands); + return p.run(engine_state, stack, help_view(), commands); } p.show_message("For help type :help"); @@ -59,11 +60,11 @@ fn run_pager( if let Some(value) = has_simple_value(&data) { let text = value.to_abbreviated_string(config.nu_config); let view = Some(Page::new(Preview::new(&text), false)); - return p.run(engine_state, stack, ctrlc, view, commands); + return p.run(engine_state, stack, view, commands); } let view = create_record_view(columns, data, is_record, config); - p.run(engine_state, stack, ctrlc, view, commands) + p.run(engine_state, stack, view, commands) } fn create_record_view( @@ -73,9 +74,9 @@ fn create_record_view( is_record: bool, config: PagerConfig, ) -> Option { - let mut view = RecordView::new(columns, data); + let mut view = RecordView::new(columns, data, config.explore_config.clone()); if is_record { - view.set_orientation_current(Orientation::Left); + view.set_top_layer_orientation(Orientation::Left); } if config.tail { @@ -91,14 +92,14 @@ fn help_view() -> Option { Some(Page::new(HelpCmd::view(), false)) } -fn binary_view(input: PipelineData) -> Result { +fn binary_view(input: PipelineData, config: &ExploreConfig) -> Result { let data = match input { PipelineData::Value(Value::Binary { val, .. }, _) => val, PipelineData::ByteStream(bs, _) => bs.into_bytes()?, _ => unreachable!("checked beforehand"), }; - let view = BinaryView::new(data); + let view = BinaryView::new(data, config); Ok(Page::new(view, true)) } diff --git a/crates/nu-explore/src/nu_common/mod.rs b/crates/nu-explore/src/nu_common/mod.rs index 091a7d6f36..42ba2f0896 100644 --- a/crates/nu-explore/src/nu_common/mod.rs +++ b/crates/nu-explore/src/nu_common/mod.rs @@ -6,13 +6,11 @@ mod value; use nu_color_config::TextStyle; use nu_protocol::Value; -use std::sync::{atomic::AtomicBool, Arc}; pub use nu_ansi_term::{Color as NuColor, Style as NuStyle}; pub use nu_protocol::{Config as NuConfig, Span as NuSpan}; pub type NuText = (String, TextStyle); -pub type CtrlC = Option>; pub use command::run_command_with_value; pub use lscolor::{create_lscolors, lscolorize}; diff --git a/crates/nu-explore/src/nu_common/table.rs b/crates/nu-explore/src/nu_common/table.rs index cb580e404b..164c837bf9 100644 --- a/crates/nu-explore/src/nu_common/table.rs +++ b/crates/nu-explore/src/nu_common/table.rs @@ -1,22 +1,21 @@ use crate::nu_common::NuConfig; use nu_color_config::StyleComputer; -use nu_protocol::{Record, Span, Value}; +use nu_protocol::{Record, Signals, Span, Value}; use nu_table::{ common::{nu_value_to_string, nu_value_to_string_clean}, ExpandedTable, TableOpts, }; -use std::sync::{atomic::AtomicBool, Arc}; pub fn try_build_table( - ctrlc: Option>, + signals: &Signals, config: &NuConfig, style_computer: &StyleComputer, value: Value, ) -> String { let span = value.span(); match value { - Value::List { vals, .. } => try_build_list(vals, ctrlc, config, span, style_computer), - Value::Record { val, .. } => try_build_map(&val, span, style_computer, ctrlc, config), + Value::List { vals, .. } => try_build_list(vals, signals, config, span, style_computer), + Value::Record { val, .. } => try_build_map(&val, span, style_computer, signals, config), val if matches!(val, Value::String { .. }) => { nu_value_to_string_clean(&val, config, style_computer).0 } @@ -28,13 +27,13 @@ fn try_build_map( record: &Record, span: Span, style_computer: &StyleComputer, - ctrlc: Option>, + signals: &Signals, config: &NuConfig, ) -> String { let opts = TableOpts::new( config, style_computer, - ctrlc, + signals, Span::unknown(), usize::MAX, (config.table_indent.left, config.table_indent.right), @@ -53,7 +52,7 @@ fn try_build_map( fn try_build_list( vals: Vec, - ctrlc: Option>, + signals: &Signals, config: &NuConfig, span: Span, style_computer: &StyleComputer, @@ -61,7 +60,7 @@ fn try_build_list( let opts = TableOpts::new( config, style_computer, - ctrlc, + signals, Span::unknown(), usize::MAX, (config.table_indent.left, config.table_indent.right), diff --git a/crates/nu-explore/src/pager/mod.rs b/crates/nu-explore/src/pager/mod.rs index a56df95bc7..4a043476bb 100644 --- a/crates/nu-explore/src/pager/mod.rs +++ b/crates/nu-explore/src/pager/mod.rs @@ -11,7 +11,7 @@ use self::{ use super::views::{Layout, View}; use crate::{ explore::ExploreConfig, - nu_common::{CtrlC, NuColor, NuConfig, NuStyle}, + nu_common::{NuColor, NuConfig, NuStyle}, registry::{Command, CommandRegistry}, views::{util::nu_style_to_tui, ViewConfig}, }; @@ -36,7 +36,6 @@ use std::{ cmp::min, io::{self, Stdout}, result, - sync::atomic::Ordering, }; pub type Frame<'a> = ratatui::Frame<'a>; @@ -89,20 +88,41 @@ impl<'a> Pager<'a> { &mut self, engine_state: &EngineState, stack: &mut Stack, - ctrlc: CtrlC, - mut view: Option, + view: Option, commands: CommandRegistry, ) -> Result> { - if let Some(page) = &mut view { - page.view.setup(ViewConfig::new( - self.config.nu_config, - self.config.explore_config, - self.config.style_computer, - self.config.lscolors, - )) + // setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, Clear(ClearType::All))?; + + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + let mut info = ViewInfo { + status: Some(Report::default()), + ..Default::default() + }; + + if let Some(text) = self.message.take() { + info.status = Some(Report::message(text, Severity::Info)); } - run_pager(engine_state, stack, ctrlc, self, view, commands) + let result = render_ui( + &mut terminal, + engine_state, + stack, + self, + &mut info, + view, + commands, + )?; + + // restore terminal + disable_raw_mode()?; + execute!(io::stdout(), LeaveAlternateScreen)?; + + Ok(result) } } @@ -145,55 +165,11 @@ impl<'a> PagerConfig<'a> { } } -fn run_pager( - engine_state: &EngineState, - stack: &mut Stack, - ctrlc: CtrlC, - pager: &mut Pager, - view: Option, - commands: CommandRegistry, -) -> Result> { - // setup terminal - enable_raw_mode()?; - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, Clear(ClearType::All))?; - - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - - let mut info = ViewInfo { - status: Some(Report::default()), - ..Default::default() - }; - - if let Some(text) = pager.message.take() { - info.status = Some(Report::message(text, Severity::Info)); - } - - let result = render_ui( - &mut terminal, - engine_state, - stack, - ctrlc, - pager, - &mut info, - view, - commands, - )?; - - // restore terminal - disable_raw_mode()?; - execute!(io::stdout(), LeaveAlternateScreen)?; - - Ok(result) -} - #[allow(clippy::too_many_arguments)] fn render_ui( term: &mut Terminal, engine_state: &EngineState, stack: &mut Stack, - ctrlc: CtrlC, pager: &mut Pager<'_>, info: &mut ViewInfo, view: Option, @@ -203,11 +179,8 @@ fn render_ui( let mut view_stack = ViewStack::new(view, Vec::new()); loop { - // handle CTRLC event - if let Some(ctrlc) = ctrlc.clone() { - if ctrlc.load(Ordering::SeqCst) { - break Ok(None); - } + if engine_state.signals().interrupted() { + break Ok(None); } let mut layout = Layout::default(); @@ -440,15 +413,20 @@ fn run_command( Command::View { mut cmd, stackable } => { // what we do we just replace the view. let value = view_stack.curr_view.as_mut().and_then(|p| p.view.exit()); - let mut new_view = cmd.spawn(engine_state, stack, value)?; + let view_cfg = ViewConfig::new( + pager.config.nu_config, + pager.config.explore_config, + pager.config.style_computer, + pager.config.lscolors, + ); + + let new_view = cmd.spawn(engine_state, stack, value, &view_cfg)?; if let Some(view) = view_stack.curr_view.take() { if !view.stackable { view_stack.stack.push(view); } } - setup_view(&mut new_view, &pager.config); - view_stack.curr_view = Some(Page::raw(new_view, stackable)); Ok(CmdResult::new(false, true, cmd.name().to_owned())) @@ -456,16 +434,6 @@ fn run_command( } } -fn setup_view(view: &mut Box, cfg: &PagerConfig<'_>) { - let cfg = ViewConfig::new( - cfg.nu_config, - cfg.explore_config, - cfg.style_computer, - cfg.lscolors, - ); - view.setup(cfg); -} - fn set_cursor_cmd_bar(f: &mut Frame, area: Rect, pager: &Pager) { if pager.cmd_buf.is_cmd_input { // todo: deal with a situation where we exceed the bar width diff --git a/crates/nu-explore/src/registry/command.rs b/crates/nu-explore/src/registry/command.rs index 7e0bc131b5..6a3e9a128f 100644 --- a/crates/nu-explore/src/registry/command.rs +++ b/crates/nu-explore/src/registry/command.rs @@ -1,6 +1,6 @@ use crate::{ commands::{SimpleCommand, ViewCommand}, - views::View, + views::{View, ViewConfig}, }; use anyhow::Result; @@ -78,8 +78,9 @@ where engine_state: &nu_protocol::engine::EngineState, stack: &mut nu_protocol::engine::Stack, value: Option, + cfg: &ViewConfig, ) -> Result { - let view = self.0.spawn(engine_state, stack, value)?; + let view = self.0.spawn(engine_state, stack, value, cfg)?; Ok(Box::new(view) as Box) } } diff --git a/crates/nu-explore/src/views/binary/mod.rs b/crates/nu-explore/src/views/binary/mod.rs index f288861be8..2a35b7c180 100644 --- a/crates/nu-explore/src/views/binary/mod.rs +++ b/crates/nu-explore/src/views/binary/mod.rs @@ -40,11 +40,15 @@ struct Settings { } impl BinaryView { - pub fn new(data: Vec) -> Self { + pub fn new(data: Vec, cfg: &ExploreConfig) -> Self { + let settings = settings_from_config(cfg); + // There's gotta be a nicer way of doing this than creating a widget just to count lines + let count_rows = BinaryWidget::new(&data, settings.opts, Default::default()).count_lines(); + Self { data, - cursor: WindowCursor2D::default(), - settings: Settings::default(), + cursor: WindowCursor2D::new(count_rows, 1).expect("Failed to create XYCursor"), + settings, } } } @@ -87,15 +91,6 @@ impl View for BinaryView { // todo: impl Cursor + peek of a value None } - - fn setup(&mut self, cfg: ViewConfig<'_>) { - self.settings = settings_from_config(cfg.explore_config); - - let count_rows = - BinaryWidget::new(&self.data, self.settings.opts, Default::default()).count_lines(); - // TODO: refactor View so setup() is fallible and we don't have to panic here - self.cursor = WindowCursor2D::new(count_rows, 1).expect("Failed to create XYCursor"); - } } fn create_binary_widget(v: &BinaryView) -> BinaryWidget<'_> { diff --git a/crates/nu-explore/src/views/mod.rs b/crates/nu-explore/src/views/mod.rs index 7aab197eb5..bdbf3129db 100644 --- a/crates/nu-explore/src/views/mod.rs +++ b/crates/nu-explore/src/views/mod.rs @@ -99,8 +99,6 @@ pub trait View { fn exit(&mut self) -> Option { None } - - fn setup(&mut self, _: ViewConfig<'_>) {} } impl View for Box { @@ -131,8 +129,4 @@ impl View for Box { fn show_data(&mut self, i: usize) -> bool { self.as_mut().show_data(i) } - - fn setup(&mut self, cfg: ViewConfig<'_>) { - self.as_mut().setup(cfg) - } } diff --git a/crates/nu-explore/src/views/record/mod.rs b/crates/nu-explore/src/views/record/mod.rs index 557995c40b..4f617686f1 100644 --- a/crates/nu-explore/src/views/record/mod.rs +++ b/crates/nu-explore/src/views/record/mod.rs @@ -20,33 +20,28 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use nu_color_config::StyleComputer; use nu_protocol::{ engine::{EngineState, Stack}, - Config, Record, Span, Value, + Config, Record, Value, }; use ratatui::{layout::Rect, widgets::Block}; -use std::{borrow::Cow, collections::HashMap}; +use std::collections::HashMap; pub use self::table_widget::Orientation; #[derive(Debug, Clone)] -pub struct RecordView<'a> { - layer_stack: Vec>, +pub struct RecordView { + layer_stack: Vec, mode: UIMode, orientation: Orientation, cfg: ExploreConfig, } -impl<'a> RecordView<'a> { - pub fn new( - columns: impl Into>, - records: impl Into]>>, - ) -> Self { +impl RecordView { + pub fn new(columns: Vec, records: Vec>, cfg: ExploreConfig) -> Self { Self { layer_stack: vec![RecordLayer::new(columns, records)], mode: UIMode::View, orientation: Orientation::Top, - // TODO: It's kind of gross how this temporarily has an incorrect/default config. - // See if we can pass correct config in through the constructor - cfg: ExploreConfig::default(), + cfg, } } @@ -57,63 +52,45 @@ impl<'a> RecordView<'a> { } pub fn transpose(&mut self) { - let layer = self.get_layer_last_mut(); + let layer = self.get_top_layer_mut(); transpose_table(layer); layer.reset_cursor(); } - // todo: rename to get_layer - pub fn get_layer_last(&self) -> &RecordLayer<'a> { + pub fn get_top_layer(&self) -> &RecordLayer { self.layer_stack .last() .expect("we guarantee that 1 entry is always in a list") } - pub fn get_layer_last_mut(&mut self) -> &mut RecordLayer<'a> { + pub fn get_top_layer_mut(&mut self) -> &mut RecordLayer { self.layer_stack .last_mut() .expect("we guarantee that 1 entry is always in a list") } - pub fn get_orientation_current(&mut self) -> Orientation { - self.get_layer_last().orientation - } - - pub fn set_orientation(&mut self, orientation: Orientation) { - self.orientation = orientation; - - // we need to reset all indexes as we can't no more use them. - self.reset_cursors(); - } - - fn reset_cursors(&mut self) { - for layer in &mut self.layer_stack { - layer.reset_cursor(); - } - } - - pub fn set_orientation_current(&mut self, orientation: Orientation) { - let layer = self.get_layer_last_mut(); + pub fn set_top_layer_orientation(&mut self, orientation: Orientation) { + let layer = self.get_top_layer_mut(); layer.orientation = orientation; layer.reset_cursor(); } /// Get the current position of the cursor in the table as a whole pub fn get_cursor_position(&self) -> Position { - let layer = self.get_layer_last(); + let layer = self.get_top_layer(); layer.cursor.position() } /// Get the current position of the cursor in the window being shown pub fn get_cursor_position_in_window(&self) -> Position { - let layer = self.get_layer_last(); + let layer = self.get_top_layer(); layer.cursor.window_relative_position() } /// Get the origin of the window being shown. (0,0), top left corner. pub fn get_window_origin(&self) -> Position { - let layer = self.get_layer_last(); + let layer = self.get_top_layer(); layer.cursor.window_origin() } @@ -125,57 +102,62 @@ impl<'a> RecordView<'a> { self.mode = UIMode::View; } - pub fn get_current_value(&self) -> Value { + pub fn get_current_value(&self) -> &Value { let Position { row, column } = self.get_cursor_position(); - let layer = self.get_layer_last(); + let layer = self.get_top_layer(); let (row, column) = match layer.orientation { Orientation::Top => (row, column), Orientation::Left => (column, row), }; - if layer.records.len() > row && layer.records[row].len() > column { - layer.records[row][column].clone() - } else { - Value::nothing(Span::unknown()) - } + // These should never happen as long as the cursor is working correctly + assert!(row < layer.record_values.len(), "row out of bounds"); + assert!(column < layer.column_names.len(), "column out of bounds"); + + &layer.record_values[row][column] } - /// Create a table widget. - /// WARNING: this is currently really slow on large data sets. - /// It creates a string representation of every cell in the table and looks at every row for lscolorize. - fn create_table_widget(&'a self, cfg: ViewConfig<'a>) -> TableWidget<'a> { - let layer = self.get_layer_last(); - let mut data = convert_records_to_string(&layer.records, cfg.nu_config, cfg.style_computer); - lscolorize(&layer.columns, &mut data, cfg.lscolors); - - let headers = layer.columns.as_ref(); + fn create_table_widget<'a>(&'a mut self, cfg: ViewConfig<'a>) -> TableWidget<'a> { + let style = self.cfg.table; let style_computer = cfg.style_computer; let Position { row, column } = self.get_window_origin(); + let layer = self.get_top_layer_mut(); + if layer.record_text.is_none() { + let mut data = + convert_records_to_string(&layer.record_values, cfg.nu_config, cfg.style_computer); + lscolorize(&layer.column_names, &mut data, cfg.lscolors); + + layer.record_text = Some(data); + } + + let headers = &layer.column_names; + let data = layer.record_text.as_ref().expect("always ok"); + TableWidget::new( headers, data, style_computer, row, column, - self.cfg.table, + style, layer.orientation, ) } fn update_cursors(&mut self, rows: usize, columns: usize) { - match self.get_layer_last().orientation { + match self.get_top_layer().orientation { Orientation::Top => { let _ = self - .get_layer_last_mut() + .get_top_layer_mut() .cursor .set_window_size(rows, columns); } Orientation::Left => { let _ = self - .get_layer_last_mut() + .get_top_layer_mut() .cursor .set_window_size(rows, columns); } @@ -183,7 +165,7 @@ impl<'a> RecordView<'a> { } fn create_records_report(&self) -> Report { - let layer = self.get_layer_last(); + let layer = self.get_top_layer(); let covered_percent = report_row_position(layer.cursor); let cursor = report_cursor_position(self.mode, layer.cursor); let message = layer.name.clone().unwrap_or_default(); @@ -197,12 +179,9 @@ impl<'a> RecordView<'a> { } } -impl View for RecordView<'_> { +impl View for RecordView { fn draw(&mut self, f: &mut Frame, area: Rect, cfg: ViewConfig<'_>, layout: &mut Layout) { let mut table_layout = TableWidgetState::default(); - // TODO: creating the table widget is O(N) where N is the number of cells in the grid. - // Way too slow to do on every draw call! - // To make explore work for larger data sets, this needs to be improved. let table = self.create_table_widget(cfg); f.render_stateful_widget(table, area, &mut table_layout); @@ -217,7 +196,7 @@ impl View for RecordView<'_> { row, column, table_layout.count_rows, - self.get_layer_last().orientation, + self.get_top_layer().orientation, self.cfg.table.show_header, ); @@ -265,7 +244,7 @@ impl View for RecordView<'_> { let style_computer = StyleComputer::new(&dummy_engine_state, &dummy_stack, HashMap::new()); let data = convert_records_to_string( - &self.get_layer_last().records, + &self.get_top_layer().record_values, &nu_protocol::Config::default(), &style_computer, ); @@ -274,7 +253,7 @@ impl View for RecordView<'_> { } fn show_data(&mut self, pos: usize) -> bool { - let data = &self.get_layer_last().records; + let data = &self.get_top_layer().record_values; let mut i = 0; for (row, cells) in data.iter().enumerate() { @@ -285,7 +264,7 @@ impl View for RecordView<'_> { for (column, _) in cells.iter().enumerate() { if i == pos { - self.get_layer_last_mut() + self.get_top_layer_mut() .cursor .set_window_start_position(row, column); return true; @@ -301,11 +280,6 @@ impl View for RecordView<'_> { fn exit(&mut self) -> Option { Some(build_last_value(self)) } - - // todo: move the method to Command? - fn setup(&mut self, cfg: ViewConfig<'_>) { - self.cfg = cfg.explore_config.clone(); - } } fn get_element_info( @@ -332,30 +306,35 @@ enum UIMode { } #[derive(Debug, Clone)] -pub struct RecordLayer<'a> { - columns: Cow<'a, [String]>, - records: Cow<'a, [Vec]>, +pub struct RecordLayer { + column_names: Vec, + // These are the raw records in the current layer. The sole reason we keep this around is so we can return the original value + // if it's being peeked. Otherwise we could accept an iterator over it. + // or if it could be Clonable we could do that anyway; + // cause it would keep memory footprint lower while keep everything working + // (yee would make return O(n); we would need to traverse iterator once again; but maybe worth it) + record_values: Vec>, + // This is the text representation of the record values (the actual text that will be displayed to users). + // It's an Option because we need configuration to set it and we (currently) don't have access to configuration when things are created. + record_text: Option>>, orientation: Orientation, name: Option, was_transposed: bool, cursor: WindowCursor2D, } -impl<'a> RecordLayer<'a> { - fn new( - columns: impl Into>, - records: impl Into]>>, - ) -> Self { - let columns = columns.into(); - let records = records.into(); - +impl RecordLayer { + fn new(columns: Vec, records: Vec>) -> Self { // TODO: refactor so this is fallible and returns a Result instead of panicking let cursor = WindowCursor2D::new(records.len(), columns.len()).expect("Failed to create cursor"); + let column_names = columns.iter().map(|s| strip_string(s)).collect(); + Self { - columns, - records, + column_names, + record_values: records, + record_text: None, cursor, orientation: Orientation::Top, name: None, @@ -369,21 +348,21 @@ impl<'a> RecordLayer<'a> { fn count_rows(&self) -> usize { match self.orientation { - Orientation::Top => self.records.len(), - Orientation::Left => self.columns.len(), + Orientation::Top => self.record_values.len(), + Orientation::Left => self.column_names.len(), } } fn count_columns(&self) -> usize { match self.orientation { - Orientation::Top => self.columns.len(), - Orientation::Left => self.records.len(), + Orientation::Top => self.column_names.len(), + Orientation::Left => self.record_values.len(), } } fn get_column_header(&self) -> Option { let col = self.cursor.column(); - self.columns.get(col).map(|header| header.to_string()) + self.column_names.get(col).map(|header| header.to_string()) } fn reset_cursor(&mut self) { @@ -404,7 +383,7 @@ fn handle_key_event_view_mode(view: &mut RecordView, key: &KeyEvent) -> Option { - view.get_layer_last_mut().cursor.prev_row_page(); + view.get_top_layer_mut().cursor.prev_row_page(); return Some(Transition::Ok); } @@ -417,7 +396,7 @@ fn handle_key_event_view_mode(view: &mut RecordView, key: &KeyEvent) -> Option { - view.get_layer_last_mut().cursor.next_row_page(); + view.get_top_layer_mut().cursor.next_row_page(); return Some(Transition::Ok); } @@ -447,32 +426,32 @@ fn handle_key_event_view_mode(view: &mut RecordView, key: &KeyEvent) -> Option Some(Transition::Cmd(String::from("expand"))), KeyCode::Up | KeyCode::Char('k') => { - view.get_layer_last_mut().cursor.prev_row_i(); + view.get_top_layer_mut().cursor.prev_row_i(); Some(Transition::Ok) } KeyCode::Down | KeyCode::Char('j') => { - view.get_layer_last_mut().cursor.next_row_i(); + view.get_top_layer_mut().cursor.next_row_i(); Some(Transition::Ok) } KeyCode::Left | KeyCode::Char('h') => { - view.get_layer_last_mut().cursor.prev_column_i(); + view.get_top_layer_mut().cursor.prev_column_i(); Some(Transition::Ok) } KeyCode::Right | KeyCode::Char('l') => { - view.get_layer_last_mut().cursor.next_column_i(); + view.get_top_layer_mut().cursor.next_column_i(); Some(Transition::Ok) } KeyCode::Home | KeyCode::Char('g') => { - view.get_layer_last_mut().cursor.row_move_to_start(); + view.get_top_layer_mut().cursor.row_move_to_start(); Some(Transition::Ok) } KeyCode::End | KeyCode::Char('G') => { - view.get_layer_last_mut().cursor.row_move_to_end(); + view.get_top_layer_mut().cursor.row_move_to_end(); Some(Transition::Ok) } @@ -494,7 +473,7 @@ fn handle_key_event_cursor_mode( code: KeyCode::PageUp, .. } => { - view.get_layer_last_mut().cursor.prev_row_page(); + view.get_top_layer_mut().cursor.prev_row_page(); return Ok(Some(Transition::Ok)); } @@ -507,7 +486,7 @@ fn handle_key_event_cursor_mode( code: KeyCode::PageDown, .. } => { - view.get_layer_last_mut().cursor.next_row_page(); + view.get_top_layer_mut().cursor.next_row_page(); return Ok(Some(Transition::Ok)); } @@ -521,47 +500,55 @@ fn handle_key_event_cursor_mode( Ok(Some(Transition::Ok)) } KeyCode::Up | KeyCode::Char('k') => { - view.get_layer_last_mut().cursor.prev_row(); + view.get_top_layer_mut().cursor.prev_row(); Ok(Some(Transition::Ok)) } KeyCode::Down | KeyCode::Char('j') => { - view.get_layer_last_mut().cursor.next_row(); + view.get_top_layer_mut().cursor.next_row(); Ok(Some(Transition::Ok)) } KeyCode::Left | KeyCode::Char('h') => { - view.get_layer_last_mut().cursor.prev_column(); + view.get_top_layer_mut().cursor.prev_column(); Ok(Some(Transition::Ok)) } KeyCode::Right | KeyCode::Char('l') => { - view.get_layer_last_mut().cursor.next_column(); + view.get_top_layer_mut().cursor.next_column(); Ok(Some(Transition::Ok)) } KeyCode::Home | KeyCode::Char('g') => { - view.get_layer_last_mut().cursor.row_move_to_start(); + view.get_top_layer_mut().cursor.row_move_to_start(); Ok(Some(Transition::Ok)) } KeyCode::End | KeyCode::Char('G') => { - view.get_layer_last_mut().cursor.row_move_to_end(); + view.get_top_layer_mut().cursor.row_move_to_end(); Ok(Some(Transition::Ok)) } + // Try to "drill down" into the selected value KeyCode::Enter => { let value = view.get_current_value(); + + // ...but it only makes sense to drill down into a few types of values + if !matches!( + value, + Value::Record { .. } | Value::List { .. } | Value::Custom { .. } + ) { + return Ok(None); + } + let is_record = matches!(value, Value::Record { .. }); - let next_layer = create_layer(value)?; + let next_layer = create_layer(value.clone())?; push_layer(view, next_layer); if is_record { - view.set_orientation_current(Orientation::Left); - } else if view.orientation == view.get_layer_last().orientation { - view.get_layer_last_mut().orientation = view.orientation; + view.set_top_layer_orientation(Orientation::Left); } else { - view.set_orientation_current(view.orientation); + view.set_top_layer_orientation(view.orientation); } Ok(Some(Transition::Ok)) @@ -570,14 +557,14 @@ fn handle_key_event_cursor_mode( } } -fn create_layer(value: Value) -> Result> { +fn create_layer(value: Value) -> Result { let (columns, values) = collect_input(value)?; Ok(RecordLayer::new(columns, values)) } -fn push_layer(view: &mut RecordView<'_>, mut next_layer: RecordLayer<'static>) { - let layer = view.get_layer_last(); +fn push_layer(view: &mut RecordView, mut next_layer: RecordLayer) { + let layer = view.get_top_layer(); let header = layer.get_column_header(); if let Some(header) = header { @@ -599,9 +586,9 @@ fn estimate_page_size(area: Rect, show_head: bool) -> u16 { } /// scroll to the end of the data -fn tail_data(state: &mut RecordView<'_>, page_size: usize) { - let layer = state.get_layer_last_mut(); - let count_rows = layer.records.len(); +fn tail_data(state: &mut RecordView, page_size: usize) { + let layer = state.get_top_layer_mut(); + let count_rows = layer.record_values.len(); if count_rows > page_size { layer .cursor @@ -620,6 +607,7 @@ fn convert_records_to_string( row.iter() .map(|value| { let text = value.clone().to_abbreviated_string(cfg); + let text = strip_string(&text); let float_precision = cfg.float_precision as usize; make_styled_string(style_computer, text, Some(value), float_precision) @@ -638,8 +626,8 @@ fn highlight_selected_cell(f: &mut Frame, info: ElementInfo, cfg: &ExploreConfig fn build_last_value(v: &RecordView) -> Value { if v.mode == UIMode::Cursor { - v.get_current_value() - } else if v.get_layer_last().count_rows() < 2 { + v.get_current_value().clone() + } else if v.get_top_layer().count_rows() < 2 { build_table_as_record(v) } else { build_table_as_list(v) @@ -647,14 +635,18 @@ fn build_last_value(v: &RecordView) -> Value { } fn build_table_as_list(v: &RecordView) -> Value { - let layer = v.get_layer_last(); + let layer = v.get_top_layer(); - let cols = &layer.columns; let vals = layer - .records + .record_values .iter() .map(|vals| { - let record = cols.iter().cloned().zip(vals.iter().cloned()).collect(); + let record = layer + .column_names + .iter() + .cloned() + .zip(vals.iter().cloned()) + .collect(); Value::record(record, NuSpan::unknown()) }) .collect(); @@ -663,18 +655,17 @@ fn build_table_as_list(v: &RecordView) -> Value { } fn build_table_as_record(v: &RecordView) -> Value { - let layer = v.get_layer_last(); + let layer = v.get_top_layer(); - let record = if let Some(row) = layer.records.first() { - layer - .columns + let mut record = Record::new(); + if let Some(row) = layer.record_values.first() { + record = layer + .column_names .iter() .cloned() .zip(row.iter().cloned()) - .collect() - } else { - Record::new() - }; + .collect(); + } Value::record(record, NuSpan::unknown()) } @@ -708,47 +699,67 @@ fn get_percentage(value: usize, max: usize) -> usize { ((value as f32 / max as f32) * 100.0).floor() as usize } -fn transpose_table(layer: &mut RecordLayer<'_>) { - let count_rows = layer.records.len(); - let count_columns = layer.columns.len(); - +fn transpose_table(layer: &mut RecordLayer) { if layer.was_transposed { - let data = match &mut layer.records { - Cow::Owned(data) => data, - Cow::Borrowed(_) => unreachable!("must never happen"), - }; - - let headers = pop_first_column(data); - let headers = headers - .into_iter() - .map(|value| match value { - Value::String { val, .. } => val, - _ => unreachable!("must never happen"), - }) - .collect(); - - let data = _transpose_table(data, count_rows, count_columns - 1); - - layer.records = Cow::Owned(data); - layer.columns = Cow::Owned(headers); + transpose_from(layer); } else { - let mut data = _transpose_table(&layer.records, count_rows, count_columns); - - for (column, column_name) in layer.columns.iter().enumerate() { - let value = Value::string(column_name, NuSpan::unknown()); - - data[column].insert(0, value); - } - - layer.records = Cow::Owned(data); - layer.columns = (1..count_rows + 1 + 1).map(|i| i.to_string()).collect(); + transpose_to(layer); } layer.was_transposed = !layer.was_transposed; } -fn pop_first_column(values: &mut [Vec]) -> Vec { - let mut data = vec![Value::default(); values.len()]; +fn transpose_from(layer: &mut RecordLayer) { + let count_rows = layer.record_values.len(); + let count_columns = layer.column_names.len(); + + if let Some(data) = &mut layer.record_text { + pop_first_column(data); + *data = _transpose_table(data, count_rows, count_columns - 1); + } + + let headers = pop_first_column(&mut layer.record_values); + let headers = headers + .into_iter() + .map(|value| match value { + Value::String { val, .. } => val, + _ => unreachable!("must never happen"), + }) + .collect(); + + let data = _transpose_table(&layer.record_values, count_rows, count_columns - 1); + + layer.record_values = data; + layer.column_names = headers; +} + +fn transpose_to(layer: &mut RecordLayer) { + let count_rows = layer.record_values.len(); + let count_columns = layer.column_names.len(); + + if let Some(data) = &mut layer.record_text { + *data = _transpose_table(data, count_rows, count_columns); + for (column, column_name) in layer.column_names.iter().enumerate() { + let value = (column_name.to_owned(), Default::default()); + data[column].insert(0, value); + } + } + + let mut data = _transpose_table(&layer.record_values, count_rows, count_columns); + for (column, column_name) in layer.column_names.iter().enumerate() { + let value = Value::string(column_name, NuSpan::unknown()); + data[column].insert(0, value); + } + + layer.record_values = data; + layer.column_names = (1..count_rows + 1 + 1).map(|i| i.to_string()).collect(); +} + +fn pop_first_column(values: &mut [Vec]) -> Vec +where + T: Default + Clone, +{ + let mut data = vec![T::default(); values.len()]; for (row, values) in values.iter_mut().enumerate() { data[row] = values.remove(0); } @@ -756,12 +767,11 @@ fn pop_first_column(values: &mut [Vec]) -> Vec { data } -fn _transpose_table( - values: &[Vec], - count_rows: usize, - count_columns: usize, -) -> Vec> { - let mut data = vec![vec![Value::default(); count_rows]; count_columns]; +fn _transpose_table(values: &[Vec], count_rows: usize, count_columns: usize) -> Vec> +where + T: Clone + Default, +{ + let mut data = vec![vec![T::default(); count_rows]; count_columns]; for (row, values) in values.iter().enumerate() { for (column, value) in values.iter().enumerate() { data[column][row].clone_from(value); @@ -770,3 +780,9 @@ fn _transpose_table( data } + +fn strip_string(text: &str) -> String { + String::from_utf8(strip_ansi_escapes::strip(text)) + .map_err(|_| ()) + .unwrap_or_else(|_| text.to_owned()) +} diff --git a/crates/nu-explore/src/views/record/table_widget.rs b/crates/nu-explore/src/views/record/table_widget.rs index 2410d1bd11..ae39d46858 100644 --- a/crates/nu-explore/src/views/record/table_widget.rs +++ b/crates/nu-explore/src/views/record/table_widget.rs @@ -13,15 +13,12 @@ use ratatui::{ text::Span, widgets::{Block, Borders, Paragraph, StatefulWidget, Widget}, }; -use std::{ - borrow::Cow, - cmp::{max, Ordering}, -}; +use std::cmp::{max, Ordering}; #[derive(Debug, Clone)] pub struct TableWidget<'a> { - columns: Cow<'a, [String]>, - data: Cow<'a, [Vec]>, + columns: &'a [String], + data: &'a [Vec], index_row: usize, index_column: usize, config: TableConfig, @@ -39,8 +36,8 @@ pub enum Orientation { impl<'a> TableWidget<'a> { #[allow(clippy::too_many_arguments)] pub fn new( - columns: impl Into>, - data: impl Into]>>, + columns: &'a [String], + data: &'a [Vec], style_computer: &'a StyleComputer<'a>, index_row: usize, index_column: usize, @@ -48,8 +45,8 @@ impl<'a> TableWidget<'a> { header_position: Orientation, ) -> Self { Self { - columns: columns.into(), - data: data.into(), + columns, + data, style_computer, index_row, index_column, @@ -91,6 +88,7 @@ impl StatefulWidget for TableWidget<'_> { // todo: refactoring these to methods as they have quite a bit in common. impl<'a> TableWidget<'a> { + // header at the top; header is always 1 line fn render_table_horizontal(self, area: Rect, buf: &mut Buffer, state: &mut TableWidgetState) { let padding_l = self.config.column_padding_left as u16; let padding_r = self.config.column_padding_right as u16; @@ -133,25 +131,16 @@ impl<'a> TableWidget<'a> { } if show_index { - let area = Rect::new(width, data_y, area.width, data_height); width += render_index( buf, - area, + Rect::new(width, data_y, area.width, data_height), self.style_computer, self.index_row, padding_l, padding_r, ); - width += render_vertical_line_with_split( - buf, - width, - data_y, - data_height, - show_head, - false, - separator_s, - ); + width += render_split_line(buf, width, area.y, area.height, show_head, separator_s); } // if there is more data than we can show, add an ellipsis to the column headers to hint at that @@ -165,6 +154,11 @@ impl<'a> TableWidget<'a> { } for col in self.index_column..self.columns.len() { + let need_split_line = state.count_columns > 0 && width < area.width; + if need_split_line { + width += render_split_line(buf, width, area.y, area.height, show_head, separator_s); + } + let mut column = create_column(data, col); let column_width = calculate_column_width(&column); @@ -197,22 +191,26 @@ impl<'a> TableWidget<'a> { } if show_head { - let mut header = [head_row_text(&head, self.style_computer)]; + let head_style = head_style(&head, self.style_computer); if head_width > use_space as usize { - truncate_str(&mut header[0].0, use_space as usize) + truncate_str(&mut head, use_space as usize) } + let head_iter = [(&head, head_style)].into_iter(); + // we don't change width here cause the whole column have the same width; so we add it when we print data let mut w = width; w += render_space(buf, w, head_y, 1, padding_l); - w += render_column(buf, w, head_y, use_space, &header); + w += render_column(buf, w, head_y, use_space, head_iter); w += render_space(buf, w, head_y, 1, padding_r); let x = w - padding_r - use_space; - state.layout.push(&header[0].0, x, head_y, use_space, 1); + state.layout.push(&head, x, head_y, use_space, 1); } + let column_rows = column.iter().map(|(t, s)| (t, *s)); + width += render_space(buf, width, data_y, data_height, padding_l); - width += render_column(buf, width, data_y, use_space, &column); + width += render_column(buf, width, data_y, use_space, column_rows); width += render_space(buf, width, data_y, data_height, padding_r); for (row, (text, _)) in column.iter().enumerate() { @@ -235,15 +233,7 @@ impl<'a> TableWidget<'a> { } if width < area.width { - width += render_vertical_line_with_split( - buf, - width, - data_y, - data_height, - show_head, - false, - separator_s, - ); + width += render_split_line(buf, width, area.y, area.height, show_head, separator_s); } let rest = area.width.saturating_sub(width); @@ -255,6 +245,7 @@ impl<'a> TableWidget<'a> { } } + // header at the left; header is always 1 line fn render_table_vertical(self, area: Rect, buf: &mut Buffer, state: &mut TableWidgetState) { if area.width == 0 || area.height == 0 { return; @@ -305,10 +296,9 @@ impl<'a> TableWidget<'a> { return; } - let columns = columns + let columns_iter = columns .iter() - .map(|s| head_row_text(s, self.style_computer)) - .collect::>(); + .map(|s| (s.clone(), head_style(s, self.style_computer))); if !show_index { let x = area.x + left_w; @@ -326,12 +316,12 @@ impl<'a> TableWidget<'a> { let x = area.x + left_w; left_w += render_space(buf, x, area.y, 1, padding_l); let x = area.x + left_w; - left_w += render_column(buf, x, area.y, columns_width as u16, &columns); + left_w += render_column(buf, x, area.y, columns_width as u16, columns_iter); let x = area.x + left_w; left_w += render_space(buf, x, area.y, 1, padding_r); let layout_x = left_w - padding_r - columns_width as u16; - for (i, (text, _)) in columns.iter().enumerate() { + for (i, text) in columns.iter().enumerate() { state .layout .push(text, layout_x, area.y + i as u16, columns_width as u16, 1); @@ -354,6 +344,9 @@ impl<'a> TableWidget<'a> { state.count_rows = columns.len(); state.count_columns = 0; + // note: is there a time where we would have more then 1 column? + // seems like not really; cause it's literally KV table, or am I wrong? + for col in self.index_column..self.data.len() { let mut column = self.data[col][self.index_row..self.index_row + columns.len()].to_vec(); @@ -362,6 +355,13 @@ impl<'a> TableWidget<'a> { break; } + // see KV comment; this block might never got used + let need_split_line = state.count_columns > 0 && left_w < area.width; + if need_split_line { + render_vertical_line(buf, area.x + left_w, area.y, area.height, separator_s); + left_w += 1; + } + let column_width = column_width as u16; let available = area.width - left_w; let is_last = col + 1 == self.data.len(); @@ -377,10 +377,12 @@ impl<'a> TableWidget<'a> { break; } + let head_rows = column.iter().map(|(t, s)| (t, *s)); + let x = area.x + left_w; left_w += render_space(buf, x, area.y, area.height, padding_l); let x = area.x + left_w; - left_w += render_column(buf, x, area.y, column_width, &column); + left_w += render_column(buf, x, area.y, column_width, head_rows); let x = area.x + left_w; left_w += render_space(buf, x, area.y, area.height, padding_r); @@ -554,6 +556,51 @@ fn render_index( width } +fn render_split_line( + buf: &mut Buffer, + x: u16, + y: u16, + height: u16, + has_head: bool, + style: NuStyle, +) -> u16 { + if has_head { + render_vertical_split_line(buf, x, y, height, &[0], &[2], &[], style); + } else { + render_vertical_split_line(buf, x, y, height, &[], &[], &[], style); + } + + 1 +} + +#[allow(clippy::too_many_arguments)] +fn render_vertical_split_line( + buf: &mut Buffer, + x: u16, + y: u16, + height: u16, + top_slit: &[u16], + inner_slit: &[u16], + bottom_slit: &[u16], + style: NuStyle, +) -> u16 { + render_vertical_line(buf, x, y, height, style); + + for &y in top_slit { + render_top_connector(buf, x, y, style); + } + + for &y in inner_slit { + render_inner_connector(buf, x, y, style); + } + + for &y in bottom_slit { + render_bottom_connector(buf, x, y, style); + } + + 1 +} + fn render_vertical_line_with_split( buf: &mut Buffer, x: u16, @@ -619,14 +666,14 @@ fn repeat_vertical( c: char, style: TextStyle, ) { - let text = std::iter::repeat(c) - .take(width as usize) - .collect::(); + let text = String::from(c); let style = text_style_to_tui_style(style); - let span = Span::styled(text, style); + let span = Span::styled(&text, style); for row in 0..height { - buf.set_span(x, y + row, &span, width); + for col in 0..width { + buf.set_span(x + col, y + row, &span, 1); + } } } @@ -667,6 +714,12 @@ fn render_bottom_connector(buf: &mut Buffer, x: u16, y: u16, style: NuStyle) { buf.set_span(x, y, &span, 1); } +fn render_inner_connector(buf: &mut Buffer, x: u16, y: u16, style: NuStyle) { + let style = nu_style_to_tui(style); + let span = Span::styled("┼", style); + buf.set_span(x, y, &span, 1); +} + fn calculate_column_width(column: &[NuText]) -> usize { column .iter() @@ -676,35 +729,28 @@ fn calculate_column_width(column: &[NuText]) -> usize { .unwrap_or(0) } -fn render_column( +fn render_column( buf: &mut ratatui::buffer::Buffer, x: u16, y: u16, available_width: u16, - rows: &[NuText], -) -> u16 { - for (row, (text, style)) in rows.iter().enumerate() { - let style = text_style_to_tui_style(*style); - let text = strip_string(text); - let span = Span::styled(text, style); + rows: impl Iterator, +) -> u16 +where + T: AsRef, + S: Into, +{ + for (row, (text, style)) in rows.enumerate() { + let style = text_style_to_tui_style(style.into()); + let span = Span::styled(text.as_ref(), style); buf.set_span(x, y + row as u16, &span, available_width); } available_width } -fn strip_string(text: &str) -> String { - String::from_utf8(strip_ansi_escapes::strip(text)) - .map_err(|_| ()) - .unwrap_or_else(|_| text.to_owned()) -} - -fn head_row_text(head: &str, style_computer: &StyleComputer) -> NuText { - ( - String::from(head), - TextStyle::with_style( - Alignment::Center, - style_computer.compute("header", &Value::string(head, nu_protocol::Span::unknown())), - ), - ) +fn head_style(head: &str, style_computer: &StyleComputer) -> TextStyle { + let style = + style_computer.compute("header", &Value::string(head, nu_protocol::Span::unknown())); + TextStyle::with_style(Alignment::Center, style) } diff --git a/crates/nu-explore/src/views/try.rs b/crates/nu-explore/src/views/try.rs index 9acbb84208..099e792e38 100644 --- a/crates/nu-explore/src/views/try.rs +++ b/crates/nu-explore/src/views/try.rs @@ -1,5 +1,6 @@ use super::{record::RecordView, util::nu_style_to_tui, Layout, Orientation, View, ViewConfig}; use crate::{ + explore::ExploreConfig, nu_common::{collect_pipeline, run_command_with_value}, pager::{report::Report, Frame, Transition, ViewInfo}, }; @@ -16,24 +17,26 @@ use ratatui::{ }; use std::cmp::min; -pub struct TryView<'a> { +pub struct TryView { input: Value, command: String, - reactive: bool, - table: Option>, + immediate: bool, + table: Option, view_mode: bool, border_color: Style, + config: ExploreConfig, } -impl<'a> TryView<'a> { - pub fn new(input: Value) -> Self { +impl TryView { + pub fn new(input: Value, config: ExploreConfig) -> Self { Self { input, table: None, - reactive: false, - border_color: Style::default(), + immediate: config.try_reactive, + border_color: nu_style_to_tui(config.table.separator_style), view_mode: false, command: String::new(), + config, } } @@ -42,13 +45,19 @@ impl<'a> TryView<'a> { } pub fn try_run(&mut self, engine_state: &EngineState, stack: &mut Stack) -> Result<()> { - let view = run_command(&self.command, &self.input, engine_state, stack)?; + let view = run_command( + &self.command, + &self.input, + engine_state, + stack, + &self.config, + )?; self.table = Some(view); Ok(()) } } -impl View for TryView<'_> { +impl View for TryView { fn draw(&mut self, f: &mut Frame, area: Rect, cfg: ViewConfig<'_>, layout: &mut Layout) { let border_color = self.border_color; @@ -122,7 +131,6 @@ impl View for TryView<'_> { f.render_widget(table_block, table_area); if let Some(table) = &mut self.table { - table.setup(cfg); let area = Rect::new( area.x + 2, area.y + 4, @@ -178,7 +186,7 @@ impl View for TryView<'_> { if !self.command.is_empty() { self.command.pop(); - if self.reactive { + if self.immediate { match self.try_run(engine_state, stack) { Ok(_) => info.report = Some(Report::default()), Err(err) => info.report = Some(Report::error(format!("Error: {err}"))), @@ -191,7 +199,7 @@ impl View for TryView<'_> { KeyCode::Char(c) => { self.command.push(*c); - if self.reactive { + if self.immediate { match self.try_run(engine_state, stack) { Ok(_) => info.report = Some(Report::default()), Err(err) => info.report = Some(Report::error(format!("Error: {err}"))), @@ -232,20 +240,6 @@ impl View for TryView<'_> { fn show_data(&mut self, i: usize) -> bool { self.table.as_mut().map_or(false, |v| v.show_data(i)) } - - fn setup(&mut self, config: ViewConfig<'_>) { - self.border_color = nu_style_to_tui(config.explore_config.table.separator_style); - self.reactive = config.explore_config.try_reactive; - - let mut r = RecordView::new(vec![], vec![]); - r.setup(config); - - if let Some(view) = &mut self.table { - view.setup(config); - view.set_orientation(r.get_orientation_current()); - view.set_orientation_current(r.get_orientation_current()); - } - } } fn run_command( @@ -253,16 +247,17 @@ fn run_command( input: &Value, engine_state: &EngineState, stack: &mut Stack, -) -> Result> { + config: &ExploreConfig, +) -> Result { let pipeline = run_command_with_value(command, input, engine_state, stack)?; let is_record = matches!(pipeline, PipelineData::Value(Value::Record { .. }, ..)); let (columns, values) = collect_pipeline(pipeline)?; - let mut view = RecordView::new(columns, values); + let mut view = RecordView::new(columns, values, config.clone()); if is_record { - view.set_orientation_current(Orientation::Left); + view.set_top_layer_orientation(Orientation::Left); } Ok(view) diff --git a/crates/nu-glob/Cargo.toml b/crates/nu-glob/Cargo.toml index 3e0dbfbd12..932958910c 100644 --- a/crates/nu-glob/Cargo.toml +++ b/crates/nu-glob/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nu-glob" -version = "0.94.3" +version = "0.95.1" authors = ["The Nushell Project Developers", "The Rust Project Developers"] license = "MIT/Apache-2.0" description = """ @@ -14,4 +14,4 @@ categories = ["filesystem"] bench = false [dev-dependencies] -doc-comment = "0.3" +doc-comment = "0.3" \ No newline at end of file diff --git a/crates/nu-json/Cargo.toml b/crates/nu-json/Cargo.toml index 9d348d00d4..a3b61042b1 100644 --- a/crates/nu-json/Cargo.toml +++ b/crates/nu-json/Cargo.toml @@ -1,11 +1,14 @@ [package] -authors = ["The Nushell Project Developers", "Christian Zangl "] +authors = [ + "The Nushell Project Developers", + "Christian Zangl ", +] description = "Fork of serde-hjson" repository = "https://github.com/nushell/nushell/tree/main/crates/nu-json" edition = "2021" license = "MIT" name = "nu-json" -version = "0.94.3" +version = "0.95.1" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -19,9 +22,11 @@ default = ["preserve_order"] [dependencies] linked-hash-map = { version = "0.5", optional = true } num-traits = { workspace = true } -serde = { workspace = true } +serde = { workspace = true } serde_json = { workspace = true } [dev-dependencies] -# nu-path = { path="../nu-path", version = "0.94.3" } -# serde_json = "1.0" +nu-test-support = { path = "../nu-test-support", version = "0.95.1" } +nu-path = { path = "../nu-path", version = "0.95.1" } +serde_json = "1.0" +fancy-regex = "0.13.0" diff --git a/crates/nu-json/README.md b/crates/nu-json/README.md index acf3c6b296..c982f1a818 100644 --- a/crates/nu-json/README.md +++ b/crates/nu-json/README.md @@ -24,7 +24,7 @@ nu-json = "0.76" ## From the Commandline Add with: -``` +```sh cargo add serde cargo add nu-json ``` @@ -43,7 +43,7 @@ fn main() { let sample_text=r#" { - # specify rate in requests/second + ## specify rate in requests/second rate: 1000 array: [ diff --git a/crates/nu-json/src/lib.rs b/crates/nu-json/src/lib.rs index eead282786..1cf2c0a9fd 100644 --- a/crates/nu-json/src/lib.rs +++ b/crates/nu-json/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] pub use self::de::{ from_iter, from_reader, from_slice, from_str, Deserializer, StreamDeserializer, }; diff --git a/crates/nu-json/src/ser.rs b/crates/nu-json/src/ser.rs index d9df1aea56..4b15740602 100644 --- a/crates/nu-json/src/ser.rs +++ b/crates/nu-json/src/ser.rs @@ -714,7 +714,7 @@ impl<'a> HjsonFormatter<'a> { stack: Vec::new(), at_colon: false, indent, - braces_same_line: false, + braces_same_line: true, } } } diff --git a/crates/nu-json/tests/main.rs b/crates/nu-json/tests/main.rs index c209bc0a52..73c68f3d8e 100644 --- a/crates/nu-json/tests/main.rs +++ b/crates/nu-json/tests/main.rs @@ -1,7 +1,5 @@ -// FIXME: re-enable tests -/* -use nu_json::Value; use fancy_regex::Regex; +use nu_json::Value; use std::fs; use std::io; use std::path::{Path, PathBuf}; @@ -11,7 +9,7 @@ fn txt(text: &str) -> String { #[cfg(windows)] { - out.replace("\r\n", "").replace("\n", "") + out.replace("\r\n", "").replace('\n', "") } #[cfg(not(windows))] @@ -21,15 +19,7 @@ fn txt(text: &str) -> 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 - ) - }) + nu_test_support::fs::assets().join("nu_json").into() } fn get_test_content(name: &str) -> io::Result { @@ -50,7 +40,7 @@ fn get_result_content(name: &str) -> io::Result<(String, String)> { 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)?)) + Ok((fs::read_to_string(p1)?, fs::read_to_string(p2)?)) } macro_rules! run_test { @@ -73,7 +63,8 @@ macro_rules! run_test { 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 { + // nu_json::to_string now outputs json instead of hjson! + if rjson != actual_hjson { println!( "{:?}\n---hjson expected\n{}\n---hjson actual\n{}\n---\n", name, rhjson, actual_hjson @@ -85,7 +76,7 @@ macro_rules! run_test { name, rjson, actual_json ); } - assert!(rhjson == actual_hjson && rjson == actual_json); + assert!(rjson == actual_hjson && rjson == actual_json); } }}; } @@ -198,7 +189,7 @@ fn test_hjson() { let missing = all .into_iter() - .filter(|x| done.iter().find(|y| &x == y) == None) + .filter(|x| !done.iter().any(|y| x == y)) .collect::>(); if !missing.is_empty() { @@ -208,5 +199,3 @@ fn test_hjson() { panic!(); } } - -*/ diff --git a/crates/nu-lsp/Cargo.toml b/crates/nu-lsp/Cargo.toml index c4e495f040..0f9ef01e69 100644 --- a/crates/nu-lsp/Cargo.toml +++ b/crates/nu-lsp/Cargo.toml @@ -3,14 +3,14 @@ authors = ["The Nushell Project Developers"] description = "Nushell's integrated LSP server" repository = "https://github.com/nushell/nushell/tree/main/crates/nu-lsp" name = "nu-lsp" -version = "0.94.3" +version = "0.95.1" edition = "2021" license = "MIT" [dependencies] -nu-cli = { path = "../nu-cli", version = "0.94.3" } -nu-parser = { path = "../nu-parser", version = "0.94.3" } -nu-protocol = { path = "../nu-protocol", version = "0.94.3" } +nu-cli = { path = "../nu-cli", version = "0.95.1" } +nu-parser = { path = "../nu-parser", version = "0.95.1" } +nu-protocol = { path = "../nu-protocol", version = "0.95.1" } reedline = { workspace = true } @@ -23,8 +23,8 @@ serde = { workspace = true } serde_json = { workspace = true } [dev-dependencies] -nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.94.3" } -nu-command = { path = "../nu-command", version = "0.94.3" } -nu-test-support = { path = "../nu-test-support", version = "0.94.3" } +nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.95.1" } +nu-command = { path = "../nu-command", version = "0.95.1" } +nu-test-support = { path = "../nu-test-support", version = "0.95.1" } -assert-json-diff = "2.0" +assert-json-diff = "2.0" \ No newline at end of file diff --git a/crates/nu-lsp/README.md b/crates/nu-lsp/README.md new file mode 100644 index 0000000000..d9e666d6c2 --- /dev/null +++ b/crates/nu-lsp/README.md @@ -0,0 +1,7 @@ +Implementation of the Nushell language server. + +See [the Language Server Protocol specification](https://microsoft.github.io/language-server-protocol/) + +## Internal Nushell crate + +This crate implements components of Nushell and is not designed to support plugin authors or other users directly. diff --git a/crates/nu-lsp/src/lib.rs b/crates/nu-lsp/src/lib.rs index 44eeeb5756..aee2ab240d 100644 --- a/crates/nu-lsp/src/lib.rs +++ b/crates/nu-lsp/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] use lsp_server::{Connection, IoThreads, Message, Response, ResponseError}; use lsp_types::{ request::{Completion, GotoDefinition, HoverRequest, Request}, @@ -17,10 +18,7 @@ use ropey::Rope; use std::{ collections::BTreeMap, path::{Path, PathBuf}, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, + sync::Arc, time::Duration, }; @@ -57,11 +55,7 @@ impl LanguageServer { }) } - pub fn serve_requests( - mut self, - engine_state: EngineState, - ctrlc: Arc, - ) -> Result<()> { + pub fn serve_requests(mut self, engine_state: EngineState) -> Result<()> { let server_capabilities = serde_json::to_value(ServerCapabilities { text_document_sync: Some(lsp_types::TextDocumentSyncCapability::Kind( TextDocumentSyncKind::INCREMENTAL, @@ -75,10 +69,12 @@ impl LanguageServer { let _initialization_params = self .connection - .initialize_while(server_capabilities, || !ctrlc.load(Ordering::SeqCst)) + .initialize_while(server_capabilities, || { + !engine_state.signals().interrupted() + }) .into_diagnostic()?; - while !ctrlc.load(Ordering::SeqCst) { + while !engine_state.signals().interrupted() { let msg = match self .connection .receiver @@ -631,7 +627,7 @@ mod tests { std::thread::spawn(move || { let engine_state = nu_cmd_lang::create_default_context(); let engine_state = nu_command::add_shell_command_context(engine_state); - send.send(lsp_server.serve_requests(engine_state, Arc::new(AtomicBool::new(false)))) + send.send(lsp_server.serve_requests(engine_state)) }); client_connection diff --git a/crates/nu-parser/Cargo.toml b/crates/nu-parser/Cargo.toml index 742009424a..b06e81387d 100644 --- a/crates/nu-parser/Cargo.toml +++ b/crates/nu-parser/Cargo.toml @@ -5,17 +5,17 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-parser" edition = "2021" license = "MIT" name = "nu-parser" -version = "0.94.3" +version = "0.95.1" exclude = ["/fuzz"] [lib] bench = false [dependencies] -nu-engine = { path = "../nu-engine", version = "0.94.3" } -nu-path = { path = "../nu-path", version = "0.94.3" } -nu-plugin-engine = { path = "../nu-plugin-engine", optional = true, version = "0.94.3" } -nu-protocol = { path = "../nu-protocol", version = "0.94.3" } +nu-engine = { path = "../nu-engine", version = "0.95.1" } +nu-path = { path = "../nu-path", version = "0.95.1" } +nu-plugin-engine = { path = "../nu-plugin-engine", optional = true, version = "0.95.1" } +nu-protocol = { path = "../nu-protocol", version = "0.95.1" } bytesize = { workspace = true } chrono = { default-features = false, features = ['std'], workspace = true } @@ -27,4 +27,4 @@ serde_json = { workspace = true } rstest = { workspace = true, default-features = false } [features] -plugin = ["nu-plugin-engine"] +plugin = ["nu-plugin-engine"] \ No newline at end of file diff --git a/crates/nu-parser/README.md b/crates/nu-parser/README.md index 02f61a31a6..cbb7729e4d 100644 --- a/crates/nu-parser/README.md +++ b/crates/nu-parser/README.md @@ -4,7 +4,7 @@ Nushell's parser is a type-directed parser, meaning that the parser will use typ Nushell's base language is whitespace-separated tokens with the command (Nushell's term for a function) name in the head position: -``` +```nushell head1 arg1 arg2 | head2 ``` @@ -12,7 +12,7 @@ head1 arg1 arg2 | head2 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: -``` +```text , , , , ``` @@ -24,7 +24,7 @@ As Nushell is a language of pipelines, pipes form a key role in both separating The above tokens are converted the following during the lite parse phase: -``` +```text Pipeline: Command #1: , , @@ -45,7 +45,7 @@ Each command has a shape assigned to each of the arguments it reads in. These sh For example, if the command is written as: -```sql +```text where $x > 10 ``` @@ -53,7 +53,7 @@ When the parsing happens, the parser will look up the `where` command and find i In the above example, if the Signature of `where` said that it took three String values, the result would be: -``` +```text CallInfo: Name: `where` Args: @@ -64,7 +64,7 @@ CallInfo: Or, the Signature could state that it takes in three positional arguments: a Variable, an Operator, and a Number, which would give: -``` +```text CallInfo: Name: `where` Args: @@ -77,7 +77,7 @@ Note that in this case, each would be checked at compile time to confirm that th 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. -``` +```text CallInfo: Name: `where` Args: diff --git a/crates/nu-parser/src/flatten.rs b/crates/nu-parser/src/flatten.rs index 92b424783a..8a05edaef4 100644 --- a/crates/nu-parser/src/flatten.rs +++ b/crates/nu-parser/src/flatten.rs @@ -26,6 +26,7 @@ pub enum FlatShape { Flag, Float, Garbage, + GlobInterpolation, GlobPattern, Int, InternalCall(DeclId), @@ -67,6 +68,7 @@ impl FlatShape { FlatShape::Flag => "shape_flag", FlatShape::Float => "shape_float", FlatShape::Garbage => "shape_garbage", + FlatShape::GlobInterpolation => "shape_glob_interpolation", FlatShape::GlobPattern => "shape_globpattern", FlatShape::Int => "shape_int", FlatShape::InternalCall(_) => "shape_internalcall", @@ -187,6 +189,9 @@ fn flatten_expression_into( )); flatten_expression_into(working_set, not, output); } + Expr::Collect(_, expr) => { + flatten_expression_into(working_set, expr, output); + } Expr::Closure(block_id) => { let outer_span = expr.span; @@ -277,7 +282,7 @@ fn flatten_expression_into( output[arg_start..].sort(); } Expr::ExternalCall(head, args) => { - if let Expr::String(..) = &head.expr { + if let Expr::String(..) | Expr::GlobPattern(..) = &head.expr { output.push((head.span, FlatShape::External)); } else { flatten_expression_into(working_set, head, output); @@ -286,7 +291,7 @@ fn flatten_expression_into( for arg in args.as_ref() { match arg { ExternalArgument::Regular(expr) => { - if let Expr::String(..) = &expr.expr { + if let Expr::String(..) | Expr::GlobPattern(..) = &expr.expr { output.push((expr.span, FlatShape::ExternalArg)); } else { flatten_expression_into(working_set, expr, output); @@ -431,6 +436,25 @@ fn flatten_expression_into( } output.extend(flattened); } + Expr::GlobInterpolation(exprs, quoted) => { + let mut flattened = vec![]; + for expr in exprs { + flatten_expression_into(working_set, expr, &mut flattened); + } + + if *quoted { + // If we aren't a bare word interpolation, also highlight the outer quotes + output.push(( + Span::new(expr.span.start, expr.span.start + 2), + FlatShape::GlobInterpolation, + )); + flattened.push(( + Span::new(expr.span.end - 1, expr.span.end), + FlatShape::GlobInterpolation, + )); + } + output.extend(flattened); + } Expr::Record(list) => { let outer_span = expr.span; let mut last_end = outer_span.start; diff --git a/crates/nu-parser/src/known_external.rs b/crates/nu-parser/src/known_external.rs index 453112aa32..a41cf3a4e8 100644 --- a/crates/nu-parser/src/known_external.rs +++ b/crates/nu-parser/src/known_external.rs @@ -1,7 +1,8 @@ use nu_engine::command_prelude::*; use nu_protocol::{ - ast::{Argument, Expr, Expression}, - engine::{CommandType, UNKNOWN_SPAN_ID}, + ast::{self, Expr, Expression}, + engine::{self, CallImpl, CommandType, UNKNOWN_SPAN_ID}, + ir::{self, DataSlice}, }; #[derive(Clone)] @@ -43,8 +44,6 @@ impl Command for KnownExternal { let command = engine_state.get_decl(decl_id); - let mut extern_call = Call::new(head_span); - let extern_name = if let Some(name_bytes) = engine_state.find_decl_name(call.decl_id, &[]) { String::from_utf8_lossy(name_bytes) } else { @@ -56,59 +55,166 @@ impl Command for KnownExternal { }; let extern_name: Vec<_> = extern_name.split(' ').collect(); - let call_head_id = engine_state - .find_span_id(call.head) - .unwrap_or(UNKNOWN_SPAN_ID); - let arg_extern_name = Expression::new_existing( - Expr::String(extern_name[0].to_string()), + match &call.inner { + CallImpl::AstRef(call) => { + let extern_call = ast_call_to_extern_call(engine_state, call, &extern_name)?; + command.run(engine_state, stack, &(&extern_call).into(), input) + } + CallImpl::AstBox(call) => { + let extern_call = ast_call_to_extern_call(engine_state, call, &extern_name)?; + command.run(engine_state, stack, &(&extern_call).into(), input) + } + CallImpl::IrRef(call) => { + let extern_call = ir_call_to_extern_call(stack, call, &extern_name)?; + command.run(engine_state, stack, &(&extern_call).into(), input) + } + CallImpl::IrBox(call) => { + let extern_call = ir_call_to_extern_call(stack, call, &extern_name)?; + command.run(engine_state, stack, &(&extern_call).into(), input) + } + } + } +} + +/// Transform the args from an `ast::Call` onto a `run-external` call +fn ast_call_to_extern_call( + engine_state: &EngineState, + call: &ast::Call, + extern_name: &[&str], +) -> Result { + let head_span = call.head; + + let mut extern_call = ast::Call::new(head_span); + + let call_head_id = engine_state + .find_span_id(call.head) + .unwrap_or(UNKNOWN_SPAN_ID); + + let arg_extern_name = Expression::new_existing( + Expr::String(extern_name[0].to_string()), + call.head, + call_head_id, + Type::String, + ); + + extern_call.add_positional(arg_extern_name); + + for subcommand in extern_name.iter().skip(1) { + extern_call.add_positional(Expression::new_existing( + Expr::String(subcommand.to_string()), call.head, call_head_id, Type::String, - ); + )); + } - extern_call.add_positional(arg_extern_name); - - for subcommand in extern_name.into_iter().skip(1) { - extern_call.add_positional(Expression::new_existing( - Expr::String(subcommand.to_string()), - call.head, - call_head_id, - Type::String, - )); - } - - for arg in &call.arguments { - match arg { - Argument::Positional(positional) => extern_call.add_positional(positional.clone()), - Argument::Named(named) => { - let named_span_id = engine_state - .find_span_id(named.0.span) - .unwrap_or(UNKNOWN_SPAN_ID); - if let Some(short) = &named.1 { - extern_call.add_positional(Expression::new_existing( - Expr::String(format!("-{}", short.item)), - named.0.span, - named_span_id, - Type::String, - )); - } else { - extern_call.add_positional(Expression::new_existing( - Expr::String(format!("--{}", named.0.item)), - named.0.span, - named_span_id, - Type::String, - )); - } - if let Some(arg) = &named.2 { - extern_call.add_positional(arg.clone()); - } + for arg in &call.arguments { + match arg { + ast::Argument::Positional(positional) => extern_call.add_positional(positional.clone()), + ast::Argument::Named(named) => { + let named_span_id = engine_state + .find_span_id(named.0.span) + .unwrap_or(UNKNOWN_SPAN_ID); + if let Some(short) = &named.1 { + extern_call.add_positional(Expression::new_existing( + Expr::String(format!("-{}", short.item)), + named.0.span, + named_span_id, + Type::String, + )); + } else { + extern_call.add_positional(Expression::new_existing( + Expr::String(format!("--{}", named.0.item)), + named.0.span, + named_span_id, + Type::String, + )); } - Argument::Unknown(unknown) => extern_call.add_unknown(unknown.clone()), - Argument::Spread(args) => extern_call.add_spread(args.clone()), + if let Some(arg) = &named.2 { + extern_call.add_positional(arg.clone()); + } + } + ast::Argument::Unknown(unknown) => extern_call.add_unknown(unknown.clone()), + ast::Argument::Spread(args) => extern_call.add_spread(args.clone()), + } + } + + Ok(extern_call) +} + +/// Transform the args from an `ir::Call` onto a `run-external` call +fn ir_call_to_extern_call( + stack: &mut Stack, + call: &ir::Call, + extern_name: &[&str], +) -> Result { + let mut extern_call = ir::Call::build(call.decl_id, call.head); + + // Add the command and subcommands + for name in extern_name { + extern_call.add_positional(stack, call.head, Value::string(*name, call.head)); + } + + // Add the arguments, reformatting named arguments into string positionals + for index in 0..call.args_len { + match &call.arguments(stack)[index] { + engine::Argument::Flag { + data, + name, + short, + span, + } => { + let name_arg = engine::Argument::Positional { + span: *span, + val: Value::string(known_external_option_name(data, *name, *short), *span), + ast: None, + }; + extern_call.add_argument(stack, name_arg); + } + engine::Argument::Named { + data, + name, + short, + span, + val, + .. + } => { + let name_arg = engine::Argument::Positional { + span: *span, + val: Value::string(known_external_option_name(data, *name, *short), *span), + ast: None, + }; + let val_arg = engine::Argument::Positional { + span: *span, + val: val.clone(), + ast: None, + }; + extern_call.add_argument(stack, name_arg); + extern_call.add_argument(stack, val_arg); + } + a @ (engine::Argument::Positional { .. } + | engine::Argument::Spread { .. } + | engine::Argument::ParserInfo { .. }) => { + let argument = a.clone(); + extern_call.add_argument(stack, argument); } } + } - command.run(engine_state, stack, &extern_call, input) + Ok(extern_call.finish()) +} + +fn known_external_option_name(data: &[u8], name: DataSlice, short: DataSlice) -> String { + if !data[name].is_empty() { + format!( + "--{}", + std::str::from_utf8(&data[name]).expect("invalid utf-8 in flag name") + ) + } else { + format!( + "-{}", + std::str::from_utf8(&data[short]).expect("invalid utf-8 in flag short name") + ) } } diff --git a/crates/nu-parser/src/lex.rs b/crates/nu-parser/src/lex.rs index 4639cae25d..3290a774f4 100644 --- a/crates/nu-parser/src/lex.rs +++ b/crates/nu-parser/src/lex.rs @@ -238,6 +238,10 @@ pub fn lex_item( Some(e), ); } + } else if c == b'|' && is_redirection(&input[token_start..*curr_offset]) { + // matches err>| etc. + *curr_offset += 1; + break; } else if is_item_terminator(&block_level, c, additional_whitespace, special_tokens) { break; } @@ -301,6 +305,16 @@ pub fn lex_item( contents: TokenContents::OutGreaterGreaterThan, span, }, + b"out>|" | b"o>|" => { + err = Some(ParseError::Expected( + "`|`. Redirecting stdout to a pipe is the same as normal piping.", + span, + )); + Token { + contents: TokenContents::Item, + span, + } + } b"err>" | b"e>" => Token { contents: TokenContents::ErrGreaterThan, span, @@ -309,6 +323,10 @@ pub fn lex_item( contents: TokenContents::ErrGreaterGreaterThan, span, }, + b"err>|" | b"e>|" => Token { + contents: TokenContents::ErrGreaterPipe, + span, + }, b"out+err>" | b"err+out>" | b"o+e>" | b"e+o>" => Token { contents: TokenContents::OutErrGreaterThan, span, @@ -317,6 +335,10 @@ pub fn lex_item( contents: TokenContents::OutErrGreaterGreaterThan, span, }, + b"out+err>|" | b"err+out>|" | b"o+e>|" | b"e+o>|" => Token { + contents: TokenContents::OutErrGreaterPipe, + span, + }, b"&&" => { err = Some(ParseError::ShellAndAnd(span)); Token { @@ -580,14 +602,6 @@ fn lex_internal( // If the next character is non-newline whitespace, skip it. curr_offset += 1; } else { - let token = try_lex_special_piped_item(input, &mut curr_offset, span_offset); - if let Some(token) = token { - output.push(token); - is_complete = false; - continue; - } - - // Otherwise, try to consume an unclassified token. let (token, err) = lex_item( input, &mut curr_offset, @@ -606,48 +620,10 @@ fn lex_internal( (output, error) } -/// trying to lex for the following item: -/// e>|, e+o>|, o+e>| -/// -/// It returns Some(token) if we find the item, or else return None. -fn try_lex_special_piped_item( - input: &[u8], - curr_offset: &mut usize, - span_offset: usize, -) -> Option { - let c = input[*curr_offset]; - let e_pipe_len = 3; - let eo_pipe_len = 5; - let offset = *curr_offset; - if c == b'e' { - // expect `e>|` - if (offset + e_pipe_len <= input.len()) && (&input[offset..offset + e_pipe_len] == b"e>|") { - *curr_offset += e_pipe_len; - return Some(Token::new( - TokenContents::ErrGreaterPipe, - Span::new(span_offset + offset, span_offset + offset + e_pipe_len), - )); - } - if (offset + eo_pipe_len <= input.len()) - && (&input[offset..offset + eo_pipe_len] == b"e+o>|") - { - *curr_offset += eo_pipe_len; - return Some(Token::new( - TokenContents::OutErrGreaterPipe, - Span::new(span_offset + offset, span_offset + offset + eo_pipe_len), - )); - } - } else if c == b'o' { - // it can be the following case: `o+e>|` - if (offset + eo_pipe_len <= input.len()) - && (&input[offset..offset + eo_pipe_len] == b"o+e>|") - { - *curr_offset += eo_pipe_len; - return Some(Token::new( - TokenContents::OutErrGreaterPipe, - Span::new(span_offset + offset, span_offset + offset + eo_pipe_len), - )); - } - } - None +/// True if this the start of a redirection. Does not match `>>` or `>|` forms. +fn is_redirection(token: &[u8]) -> bool { + matches!( + token, + b"o>" | b"out>" | b"e>" | b"err>" | b"o+e>" | b"e+o>" | b"out+err>" | b"err+out>" + ) } diff --git a/crates/nu-parser/src/lib.rs b/crates/nu-parser/src/lib.rs index eeb6a590b8..8f871aa815 100644 --- a/crates/nu-parser/src/lib.rs +++ b/crates/nu-parser/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] mod deparse; mod exportable; mod flatten; @@ -8,7 +9,6 @@ mod parse_keywords; mod parse_patterns; mod parse_shape_specs; mod parser; -mod parser_path; mod type_check; pub use deparse::{escape_for_script_arg, escape_quote_string}; @@ -18,13 +18,10 @@ pub use flatten::{ pub use known_external::KnownExternal; pub use lex::{lex, lex_signature, Token, TokenContents}; pub use lite_parser::{lite_parse, LiteBlock, LiteCommand}; +pub use nu_protocol::parser_path::*; pub use parse_keywords::*; -pub use parser_path::*; pub use parser::{ is_math_expression_like, parse, parse_block, parse_expression, parse_external_call, parse_unit_value, trim_quotes, trim_quotes_str, unescape_unquote_string, DURATION_UNIT_GROUPS, }; - -#[cfg(feature = "plugin")] -pub use parse_keywords::parse_register; diff --git a/crates/nu-parser/src/lite_parser.rs b/crates/nu-parser/src/lite_parser.rs index f04b8befdc..c9e9578698 100644 --- a/crates/nu-parser/src/lite_parser.rs +++ b/crates/nu-parser/src/lite_parser.rs @@ -2,6 +2,7 @@ //! can be parsed. use crate::{Token, TokenContents}; +use itertools::{Either, Itertools}; use nu_protocol::{ast::RedirectionSource, ParseError, Span}; use std::mem; @@ -24,6 +25,15 @@ impl LiteRedirectionTarget { | LiteRedirectionTarget::Pipe { connector } => *connector, } } + + pub fn spans(&self) -> impl Iterator { + match *self { + LiteRedirectionTarget::File { + connector, file, .. + } => Either::Left([connector, file].into_iter()), + LiteRedirectionTarget::Pipe { connector } => Either::Right(std::iter::once(connector)), + } + } } #[derive(Debug, Clone)] @@ -38,6 +48,17 @@ pub enum LiteRedirection { }, } +impl LiteRedirection { + pub fn spans(&self) -> impl Iterator { + match self { + LiteRedirection::Single { target, .. } => Either::Left(target.spans()), + LiteRedirection::Separate { out, err } => { + Either::Right(out.spans().chain(err.spans()).sorted()) + } + } + } +} + #[derive(Debug, Clone, Default)] pub struct LiteCommand { pub pipe: Option, @@ -113,6 +134,14 @@ impl LiteCommand { Ok(()) } + + pub fn parts_including_redirection(&self) -> impl Iterator + '_ { + self.parts.iter().copied().chain( + self.redirection + .iter() + .flat_map(|redirection| redirection.spans()), + ) + } } #[derive(Debug, Clone, Default)] diff --git a/crates/nu-parser/src/parse_keywords.rs b/crates/nu-parser/src/parse_keywords.rs index 2849b4e39c..4ac1d20699 100644 --- a/crates/nu-parser/src/parse_keywords.rs +++ b/crates/nu-parser/src/parse_keywords.rs @@ -2,7 +2,6 @@ use crate::{ exportable::Exportable, parse_block, parser::{parse_redirection, redirecting_builtin_error}, - parser_path::ParserPath, type_check::{check_block_input_output, type_compatible}, }; use itertools::Itertools; @@ -15,6 +14,7 @@ use nu_protocol::{ }, engine::{StateWorkingSet, DEFAULT_OVERLAY_NAME}, eval_const::eval_constant, + parser_path::ParserPath, Alias, BlockId, DeclId, Module, ModuleId, ParseError, PositionalArg, ResolvedImportPattern, Span, Spanned, SyntaxShape, Type, Value, VarId, }; @@ -42,35 +42,46 @@ use crate::{ }; /// These parser keywords can be aliased -pub const ALIASABLE_PARSER_KEYWORDS: &[&[u8]] = &[b"overlay hide", b"overlay new", b"overlay use"]; +pub const ALIASABLE_PARSER_KEYWORDS: &[&[u8]] = &[ + b"if", + b"match", + b"try", + b"overlay", + b"overlay hide", + b"overlay new", + b"overlay use", +]; pub const RESERVED_VARIABLE_NAMES: [&str; 3] = ["in", "nu", "env"]; /// These parser keywords cannot be aliased (either not possible, or support not yet added) pub const UNALIASABLE_PARSER_KEYWORDS: &[&[u8]] = &[ - b"export", - b"def", - b"export def", - b"for", - b"extern", - b"export extern", b"alias", - b"export alias", - b"export-env", + b"const", + b"def", + b"extern", b"module", b"use", + b"export", + b"export alias", + b"export const", + b"export def", + b"export extern", + b"export module", b"export use", - b"hide", - // b"overlay", - // b"overlay hide", - // b"overlay new", - // b"overlay use", + b"for", + b"loop", + b"while", + b"return", + b"break", + b"continue", b"let", - b"const", b"mut", + b"hide", + b"export-env", + b"source-env", b"source", b"where", - b"register", b"plugin use", ]; @@ -1192,7 +1203,7 @@ pub fn parse_export_in_block( "export alias" => parse_alias(working_set, lite_command, None), "export def" => parse_def(working_set, lite_command, None).0, "export const" => parse_const(working_set, &lite_command.parts[1..]), - "export use" => parse_use(working_set, lite_command).0, + "export use" => parse_use(working_set, lite_command, None).0, "export module" => parse_module(working_set, lite_command, None).0, "export extern" => parse_extern(working_set, lite_command, None), _ => { @@ -1211,6 +1222,7 @@ pub fn parse_export_in_module( working_set: &mut StateWorkingSet, lite_command: &LiteCommand, module_name: &[u8], + parent_module: &mut Module, ) -> (Pipeline, Vec) { let spans = &lite_command.parts[..]; @@ -1416,7 +1428,8 @@ pub fn parse_export_in_module( pipe: lite_command.pipe, redirection: lite_command.redirection.clone(), }; - let (pipeline, exportables) = parse_use(working_set, &lite_command); + let (pipeline, exportables) = + parse_use(working_set, &lite_command, Some(parent_module)); let export_use_decl_id = if let Some(id) = working_set.find_decl(b"export use") { id @@ -1759,7 +1772,7 @@ pub fn parse_module_block( )) } b"use" => { - let (pipeline, _) = parse_use(working_set, command); + let (pipeline, _) = parse_use(working_set, command, Some(&mut module)); block.pipelines.push(pipeline) } @@ -1774,7 +1787,7 @@ pub fn parse_module_block( } b"export" => { let (pipe, exportables) = - parse_export_in_module(working_set, command, module_name); + parse_export_in_module(working_set, command, module_name, &mut module); for exportable in exportables { match exportable { @@ -1884,6 +1897,48 @@ pub fn parse_module_block( (block, module, module_comments) } +fn module_needs_reloading(working_set: &StateWorkingSet, module_id: ModuleId) -> bool { + let module = working_set.get_module(module_id); + + fn submodule_need_reloading(working_set: &StateWorkingSet, submodule_id: ModuleId) -> bool { + let submodule = working_set.get_module(submodule_id); + let submodule_changed = if let Some((file_path, file_id)) = &submodule.file { + let existing_contents = working_set.get_contents_of_file(*file_id); + let file_contents = file_path.read(working_set); + + if let (Some(existing), Some(new)) = (existing_contents, file_contents) { + existing != new + } else { + false + } + } else { + false + }; + + if submodule_changed { + true + } else { + module_needs_reloading(working_set, submodule_id) + } + } + + let export_submodule_changed = module + .submodules + .iter() + .any(|(_, submodule_id)| submodule_need_reloading(working_set, *submodule_id)); + + if export_submodule_changed { + return true; + } + + let private_submodule_changed = module + .imported_modules + .iter() + .any(|submodule_id| submodule_need_reloading(working_set, *submodule_id)); + + private_submodule_changed +} + /// Parse a module from a file. /// /// The module name is inferred from the stem of the file, unless specified in `name_override`. @@ -1922,23 +1977,26 @@ fn parse_module_file( // Check if we've parsed the module before. if let Some(module_id) = working_set.find_module_by_span(new_span) { - return Some(module_id); + if !module_needs_reloading(working_set, module_id) { + return Some(module_id); + } } // Add the file to the stack of files being processed. - if let Err(e) = working_set.files.push(path.path_buf(), path_span) { + if let Err(e) = working_set.files.push(path.clone().path_buf(), path_span) { working_set.error(e); return None; } // Parse the module - let (block, module, module_comments) = + let (block, mut module, module_comments) = parse_module_block(working_set, new_span, module_name.as_bytes()); // Remove the file from the stack of files being processed. working_set.files.pop(); let _ = working_set.add_block(Arc::new(block)); + module.file = Some((path, file_id)); let module_id = working_set.add_module(&module_name, module, module_comments); Some(module_id) @@ -2228,6 +2286,7 @@ pub fn parse_module( pub fn parse_use( working_set: &mut StateWorkingSet, lite_command: &LiteCommand, + parent_module: Option<&mut Module>, ) -> (Pipeline, Vec) { let spans = &lite_command.parts; @@ -2373,12 +2432,14 @@ pub fn parse_use( ); }; + let mut imported_modules = vec![]; let (definitions, errors) = module.resolve_import_pattern( working_set, module_id, &import_pattern.members, None, name_span, + &mut imported_modules, ); working_set.parse_errors.extend(errors); @@ -2420,6 +2481,9 @@ pub fn parse_use( import_pattern.constants = constants.iter().map(|(_, id)| *id).collect(); + if let Some(m) = parent_module { + m.track_imported_modules(&imported_modules) + } // Extend the current scope with the module's exportables working_set.use_decls(definitions.decls); working_set.use_modules(definitions.modules); @@ -2853,6 +2917,7 @@ pub fn parse_overlay_use(working_set: &mut StateWorkingSet, call: Box) -> &[], Some(final_overlay_name.as_bytes()), call.head, + &mut vec![], ) } else { origin_module.resolve_import_pattern( @@ -2863,6 +2928,7 @@ pub fn parse_overlay_use(working_set: &mut StateWorkingSet, call: Box) -> }], Some(final_overlay_name.as_bytes()), call.head, + &mut vec![], ) } } else { @@ -3560,235 +3626,6 @@ pub fn parse_where(working_set: &mut StateWorkingSet, lite_command: &LiteCommand } } -/// `register` is deprecated and will be removed in 0.94. Use `plugin add` and `plugin use` instead. -#[cfg(feature = "plugin")] -pub fn parse_register(working_set: &mut StateWorkingSet, lite_command: &LiteCommand) -> Pipeline { - use nu_plugin_engine::PluginDeclaration; - use nu_protocol::{ - engine::Stack, ErrSpan, ParseWarning, PluginIdentity, PluginRegistryItem, PluginSignature, - RegisteredPlugin, - }; - - let spans = &lite_command.parts; - - #[allow(deprecated)] - 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" { - working_set.error(ParseError::UnknownState( - "internal error: Wrong call name for 'register' function".into(), - Span::concat(spans), - )); - return garbage_pipeline(working_set, spans); - } - if let Some(redirection) = lite_command.redirection.as_ref() { - working_set.error(redirecting_builtin_error("register", redirection)); - return garbage_pipeline(working_set, 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 => { - working_set.error(ParseError::UnknownState( - "internal error: Register declaration not found".into(), - Span::concat(spans), - )); - return garbage_pipeline(working_set, spans); - } - Some(decl_id) => { - let ParsedInternalCall { call, output } = - parse_internal_call(working_set, spans[0], &spans[1..], decl_id); - let decl = working_set.get_decl(decl_id); - - let call_span = Span::concat(spans); - - let starting_error_count = working_set.parse_errors.len(); - check_call(working_set, call_span, &decl.signature(), &call); - - let Ok(is_help) = has_flag_const(working_set, &call, "help") else { - return garbage_pipeline(working_set, spans); - }; - - if starting_error_count != working_set.parse_errors.len() || is_help { - return Pipeline::from_vec(vec![Expression::new( - working_set, - Expr::Call(call), - call_span, - output, - )]); - } - - (call, call_span) - } - }; - - // Now that the call is parsed, add the deprecation warning - working_set - .parse_warnings - .push(ParseWarning::DeprecatedWarning { - old_command: "register".into(), - new_suggestion: "use `plugin add` and `plugin use`".into(), - span: call.head, - url: "https://www.nushell.sh/book/plugins.html".into(), - }); - - // Extracting the required arguments from the call and keeping them together in a tuple - let arguments = call - .positional_nth(0) - .map(|expr| { - let val = - eval_constant(working_set, expr).map_err(|err| err.wrap(working_set, call.head))?; - let filename = val - .coerce_into_string() - .map_err(|err| err.wrap(working_set, call.head))?; - - let Some(path) = find_in_dirs(&filename, working_set, &cwd, Some(PLUGIN_DIRS_VAR)) - else { - return Err(ParseError::RegisteredFileNotFound(filename, expr.span)); - }; - - if path.exists() && path.is_file() { - Ok((path, expr.span)) - } else { - Err(ParseError::RegisteredFileNotFound(filename, expr.span)) - } - }) - .expect("required positional has being checked"); - - // 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_nth(1).map(|expr| { - let signature = working_set.get_span_contents(expr.span); - serde_json::from_slice::(signature).map_err(|e| { - ParseError::LabeledError( - "Signature deserialization error".into(), - format!("unable to deserialize signature: {e}"), - 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::RegisteredFileNotFound(name, expr.span)) - }) - .and_then(|path| { - if path.exists() & path.is_file() { - Ok(path) - } else { - Err(ParseError::RegisteredFileNotFound( - format!("{path:?}"), - expr.span, - )) - } - }) - }); - - let shell = match shell { - None => None, - Some(path) => match path { - Ok(path) => Some(path), - Err(err) => { - working_set.error(err); - return Pipeline::from_vec(vec![Expression::new( - working_set, - Expr::Call(call), - call_span, - Type::Any, - )]); - } - }, - }; - - // We need the current environment variables for `python` based plugins - // Or we'll likely have a problem when a plugin is implemented in a virtual Python environment. - let get_envs = || { - let stack = Stack::new().capture(); - nu_engine::env::env_to_strings(working_set.permanent_state, &stack) - }; - - let error = arguments.and_then(|(path, path_span)| { - let path = path.path_buf(); - - // Create the plugin identity. This validates that the plugin name starts with `nu_plugin_` - let identity = PluginIdentity::new(path, shell).err_span(path_span)?; - - let plugin = nu_plugin_engine::add_plugin_to_working_set(working_set, &identity) - .map_err(|err| err.wrap(working_set, call.head))?; - - let signatures = signature.map_or_else( - || { - // It's important that the plugin is restarted if we're going to get signatures - // - // The user would expect that `register` would always run the binary to get new - // signatures, in case it was replaced with an updated binary - plugin.reset().map_err(|err| { - ParseError::LabeledError( - "Failed to restart plugin to get new signatures".into(), - err.to_string(), - spans[0], - ) - })?; - - let signatures = plugin - .clone() - .get(get_envs) - .and_then(|p| p.get_signature()) - .map_err(|err| { - log::warn!("Error getting signatures: {err:?}"); - ParseError::LabeledError( - "Error getting signatures".into(), - err.to_string(), - spans[0], - ) - }); - - if let Ok(ref signatures) = signatures { - // Add the loaded plugin to the delta - working_set.update_plugin_registry(PluginRegistryItem::new( - &identity, - signatures.clone(), - )); - } - - signatures - }, - |sig| sig.map(|sig| vec![sig]), - )?; - - for signature in signatures { - // create plugin command declaration (need struct impl Command) - // store declaration in working set - let plugin_decl = PluginDeclaration::new(plugin.clone(), signature); - - working_set.add_decl(Box::new(plugin_decl)); - } - - Ok(()) - }); - - if let Err(err) = error { - working_set.error(err); - } - - Pipeline::from_vec(vec![Expression::new( - working_set, - Expr::Call(call), - call_span, - Type::Nothing, - )]) -} - #[cfg(feature = "plugin")] pub fn parse_plugin_use(working_set: &mut StateWorkingSet, call: Box) -> Pipeline { use nu_protocol::{FromValue, PluginRegistryFile}; diff --git a/crates/nu-parser/src/parse_patterns.rs b/crates/nu-parser/src/parse_patterns.rs index 73668b7d04..dc4a64ce37 100644 --- a/crates/nu-parser/src/parse_patterns.rs +++ b/crates/nu-parser/src/parse_patterns.rs @@ -39,7 +39,7 @@ pub fn parse_pattern(working_set: &mut StateWorkingSet, span: Span) -> MatchPatt let value = parse_value(working_set, span, &SyntaxShape::Any); MatchPattern { - pattern: Pattern::Value(value), + pattern: Pattern::Value(Box::new(value)), guard: None, span, } diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 149bce8958..bc48b9bd0c 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -16,7 +16,6 @@ use nu_protocol::{ IN_VARIABLE_ID, }; use std::{ - borrow::Cow, collections::{HashMap, HashSet}, num::ParseIntError, str, @@ -222,6 +221,209 @@ pub(crate) fn check_call( } } +/// Parses a string in the arg or head position of an external call. +/// +/// If the string begins with `r#`, it is parsed as a raw string. If it doesn't contain any quotes +/// or parentheses, it is parsed as a glob pattern so that tilde and glob expansion can be handled +/// by `run-external`. Otherwise, we use a custom state machine to put together an interpolated +/// string, where each balanced pair of quotes is parsed as a separate part of the string, and then +/// concatenated together. +/// +/// For example, `-foo="bar\nbaz"` becomes `$"-foo=bar\nbaz"` +fn parse_external_string(working_set: &mut StateWorkingSet, span: Span) -> Expression { + let contents = &working_set.get_span_contents(span); + + if contents.starts_with(b"r#") { + parse_raw_string(working_set, span) + } else if contents + .iter() + .any(|b| matches!(b, b'"' | b'\'' | b'(' | b')')) + { + enum State { + Bare { + from: usize, + }, + Quote { + from: usize, + quote_char: u8, + escaped: bool, + depth: i32, + }, + } + // Find the spans of parts of the string that can be parsed as their own strings for + // concatenation. + // + // By passing each of these parts to `parse_string()`, we can eliminate the quotes and also + // handle string interpolation. + let make_span = |from: usize, index: usize| Span { + start: span.start + from, + end: span.start + index, + }; + let mut spans = vec![]; + let mut state = State::Bare { from: 0 }; + let mut index = 0; + while index < contents.len() { + let ch = contents[index]; + match &mut state { + State::Bare { from } => match ch { + b'"' | b'\'' => { + // Push bare string + if index != *from { + spans.push(make_span(*from, index)); + } + // then transition to other state + state = State::Quote { + from: index, + quote_char: ch, + escaped: false, + depth: 1, + }; + } + b'$' => { + if let Some("e_char @ (b'"' | b'\'')) = contents.get(index + 1) { + // Start a dollar quote (interpolated string) + if index != *from { + spans.push(make_span(*from, index)); + } + state = State::Quote { + from: index, + quote_char, + escaped: false, + depth: 1, + }; + // Skip over two chars (the dollar sign and the quote) + index += 2; + continue; + } + } + // Continue to consume + _ => (), + }, + State::Quote { + from, + quote_char, + escaped, + depth, + } => match ch { + ch if ch == *quote_char && !*escaped => { + // Count if there are more than `depth` quotes remaining + if contents[index..] + .iter() + .filter(|b| *b == quote_char) + .count() as i32 + > *depth + { + // Increment depth to be greedy + *depth += 1; + } else { + // Decrement depth + *depth -= 1; + } + if *depth == 0 { + // End of string + spans.push(make_span(*from, index + 1)); + // go back to Bare state + state = State::Bare { from: index + 1 }; + } + } + b'\\' if !*escaped && *quote_char == b'"' => { + // The next token is escaped so it doesn't count (only for double quote) + *escaped = true; + } + _ => { + *escaped = false; + } + }, + } + index += 1; + } + + // Add the final span + match state { + State::Bare { from } | State::Quote { from, .. } => { + if from < contents.len() { + spans.push(make_span(from, contents.len())); + } + } + } + + // Log the spans that will be parsed + if log::log_enabled!(log::Level::Trace) { + let contents = spans + .iter() + .map(|span| String::from_utf8_lossy(working_set.get_span_contents(*span))) + .collect::>(); + + trace!("parsing: external string, parts: {contents:?}") + } + + // Check if the whole thing is quoted. If not, it should be a glob + let quoted = + (contents.len() >= 3 && contents.starts_with(b"$\"") && contents.ends_with(b"\"")) + || is_quoted(contents); + + // Parse each as its own string + let exprs: Vec = spans + .into_iter() + .map(|span| parse_string(working_set, span)) + .collect(); + + if exprs + .iter() + .all(|expr| matches!(expr.expr, Expr::String(..))) + { + // If the exprs are all strings anyway, just collapse into a single string. + let string = exprs + .into_iter() + .map(|expr| { + let Expr::String(contents) = expr.expr else { + unreachable!("already checked that this was a String") + }; + contents + }) + .collect::(); + if quoted { + Expression::new(working_set, Expr::String(string), span, Type::String) + } else { + Expression::new( + working_set, + Expr::GlobPattern(string, false), + span, + Type::Glob, + ) + } + } else { + // Flatten any string interpolations contained with the exprs. + let exprs = exprs + .into_iter() + .flat_map(|expr| match expr.expr { + Expr::StringInterpolation(subexprs) => subexprs, + _ => vec![expr], + }) + .collect(); + // Make an interpolation out of the expressions. Use `GlobInterpolation` if it's a bare + // word, so that the unquoted state can get passed through to `run-external`. + if quoted { + Expression::new( + working_set, + Expr::StringInterpolation(exprs), + span, + Type::String, + ) + } else { + Expression::new( + working_set, + Expr::GlobInterpolation(exprs, false), + span, + Type::Glob, + ) + } + } + } else { + parse_glob_pattern(working_set, span) + } +} + fn parse_external_arg(working_set: &mut StateWorkingSet, span: Span) -> ExternalArgument { let contents = working_set.get_span_contents(span); @@ -229,8 +431,6 @@ fn parse_external_arg(working_set: &mut StateWorkingSet, span: Span) -> External ExternalArgument::Regular(parse_dollar_expr(working_set, span)) } else if contents.starts_with(b"[") { ExternalArgument::Regular(parse_list_expression(working_set, span, &SyntaxShape::Any)) - } else if contents.starts_with(b"r#") { - ExternalArgument::Regular(parse_raw_string(working_set, span)) } else if contents.len() > 3 && contents.starts_with(b"...") && (contents[3] == b'$' || contents[3] == b'[' || contents[3] == b'(') @@ -241,18 +441,7 @@ fn parse_external_arg(working_set: &mut StateWorkingSet, span: Span) -> External &SyntaxShape::List(Box::new(SyntaxShape::Any)), )) } else { - // Eval stage trims the quotes, so we don't have to do the same thing when parsing. - let (contents, err) = unescape_string_preserving_quotes(contents, span); - if let Some(err) = err { - working_set.error(err); - } - - ExternalArgument::Regular(Expression::new( - working_set, - Expr::String(contents), - span, - Type::String, - )) + ExternalArgument::Regular(parse_external_string(working_set, span)) } } @@ -274,18 +463,7 @@ pub fn parse_external_call(working_set: &mut StateWorkingSet, spans: &[Span]) -> let arg = parse_expression(working_set, &[head_span]); Box::new(arg) } else { - // Eval stage will unquote the string, so we don't bother with that here - let (contents, err) = unescape_string_preserving_quotes(&head_contents, head_span); - if let Some(err) = err { - working_set.error(err) - } - - Box::new(Expression::new( - working_set, - Expr::String(contents), - head_span, - Type::String, - )) + Box::new(parse_external_string(working_set, head_span)) }; let args = spans[1..] @@ -756,15 +934,12 @@ pub fn parse_internal_call( let output = signature.get_output_type(); // storing the var ID for later due to borrowing issues - let lib_dirs_var_id = if decl.is_builtin() { - match decl.name() { - "use" | "overlay use" | "source-env" | "nu-check" => { - find_dirs_var(working_set, LIB_DIRS_VAR) - } - _ => None, + let lib_dirs_var_id = match decl.name() { + "use" | "overlay use" | "source-env" if decl.is_keyword() => { + find_dirs_var(working_set, LIB_DIRS_VAR) } - } else { - None + "nu-check" if decl.is_builtin() => find_dirs_var(working_set, LIB_DIRS_VAR), + _ => None, }; // The index into the positional parameter in the definition @@ -2639,23 +2814,6 @@ pub fn unescape_unquote_string(bytes: &[u8], span: Span) -> (String, Option (String, Option) { - let (bytes, err) = if bytes.starts_with(b"\"") { - let (bytes, err) = unescape_string(bytes, span); - (Cow::Owned(bytes), err) - } else { - (Cow::Borrowed(bytes), None) - }; - - // The original code for args used lossy conversion here, even though that's not what we - // typically use for strings. Revisit whether that's actually desirable later, but don't - // want to introduce a breaking change for this patch. - let token = String::from_utf8_lossy(&bytes).into_owned(); - (token, err) -} - pub fn parse_string(working_set: &mut StateWorkingSet, span: Span) -> Expression { trace!("parsing: string"); @@ -2670,6 +2828,36 @@ pub fn parse_string(working_set: &mut StateWorkingSet, span: Span) -> Expression if bytes[0] != b'\'' && bytes[0] != b'"' && bytes[0] != b'`' && bytes.contains(&b'(') { return parse_string_interpolation(working_set, span); } + // Check for unbalanced quotes: + { + if bytes.starts_with(b"\"") + && (bytes.iter().filter(|ch| **ch == b'"').count() > 1 && !bytes.ends_with(b"\"")) + { + let close_delimiter_index = bytes + .iter() + .skip(1) + .position(|ch| *ch == b'"') + .expect("Already check input bytes contains at least two double quotes"); + // needs `+2` rather than `+1`, because we have skip 1 to find close_delimiter_index before. + let span = Span::new(span.start + close_delimiter_index + 2, span.end); + working_set.error(ParseError::ExtraTokensAfterClosingDelimiter(span)); + return garbage(working_set, span); + } + + if bytes.starts_with(b"\'") + && (bytes.iter().filter(|ch| **ch == b'\'').count() > 1 && !bytes.ends_with(b"\'")) + { + let close_delimiter_index = bytes + .iter() + .skip(1) + .position(|ch| *ch == b'\'') + .expect("Already check input bytes contains at least two double quotes"); + // needs `+2` rather than `+1`, because we have skip 1 to find close_delimiter_index before. + let span = Span::new(span.start + close_delimiter_index + 2, span.end); + working_set.error(ParseError::ExtraTokensAfterClosingDelimiter(span)); + return garbage(working_set, span); + } + } let (s, err) = unescape_unquote_string(bytes, span); if let Some(err) = err { @@ -3114,6 +3302,8 @@ pub fn parse_row_condition(working_set: &mut StateWorkingSet, spans: &[Span]) -> default_value: None, }); + compile_block(working_set, &mut block); + working_set.add_block(Arc::new(block)) } }; @@ -4257,7 +4447,7 @@ pub fn parse_match_block_expression(working_set: &mut StateWorkingSet, span: Spa &SyntaxShape::MathExpression, ); - pattern.guard = Some(guard); + pattern.guard = Some(Box::new(guard)); position += if found { start + 1 } else { start }; connector = working_set.get_span_contents(output[position].span); } @@ -4762,7 +4952,7 @@ pub fn parse_math_expression( let mut expr_stack: Vec = vec![]; let mut idx = 0; - let mut last_prec = 1000000; + let mut last_prec = u8::MAX; let first_span = working_set.get_span_contents(spans[0]); @@ -5087,15 +5277,6 @@ pub fn parse_expression(working_set: &mut StateWorkingSet, spans: &[Span]) -> Ex } b"where" => parse_where_expr(working_set, &spans[pos..]), #[cfg(feature = "plugin")] - b"register" => { - working_set.error(ParseError::BuiltinCommandInPipeline( - "register".into(), - spans[0], - )); - - parse_call(working_set, &spans[pos..], spans[0]) - } - #[cfg(feature = "plugin")] b"plugin" => { if spans.len() > 1 && working_set.get_span_contents(spans[1]) == b"use" { // only 'plugin use' is banned @@ -5118,6 +5299,9 @@ pub fn parse_expression(working_set: &mut StateWorkingSet, spans: &[Span]) -> Ex let mut block = Block::default(); let ty = output.ty.clone(); block.pipelines = vec![Pipeline::from_vec(vec![output])]; + block.span = Some(Span::concat(spans)); + + compile_block(working_set, &mut block); let block_id = working_set.add_block(Arc::new(block)); @@ -5210,16 +5394,26 @@ pub fn parse_builtin_commands( match name { b"def" => parse_def(working_set, lite_command, None).0, b"extern" => parse_extern(working_set, lite_command, None), - b"let" => parse_let(working_set, &lite_command.parts), + b"let" => parse_let( + working_set, + &lite_command + .parts_including_redirection() + .collect::>(), + ), b"const" => parse_const(working_set, &lite_command.parts), - b"mut" => parse_mut(working_set, &lite_command.parts), + b"mut" => parse_mut( + working_set, + &lite_command + .parts_including_redirection() + .collect::>(), + ), b"for" => { let expr = parse_for(working_set, lite_command); Pipeline::from_vec(vec![expr]) } b"alias" => parse_alias(working_set, lite_command, None), b"module" => parse_module(working_set, lite_command, None).0, - b"use" => parse_use(working_set, lite_command).0, + b"use" => parse_use(working_set, lite_command, None).0, b"overlay" => { if let Some(redirection) = lite_command.redirection.as_ref() { working_set.error(redirecting_builtin_error("overlay", redirection)); @@ -5231,8 +5425,6 @@ pub fn parse_builtin_commands( b"export" => parse_export_in_block(working_set, lite_command), b"hide" => parse_hide(working_set, lite_command), b"where" => parse_where(working_set, lite_command), - #[cfg(feature = "plugin")] - b"register" => parse_register(working_set, lite_command), // Only "plugin use" is a keyword #[cfg(feature = "plugin")] b"plugin" @@ -5466,169 +5658,73 @@ pub(crate) fn redirecting_builtin_error( } } -pub fn parse_pipeline( - working_set: &mut StateWorkingSet, - pipeline: &LitePipeline, - is_subexpression: bool, - pipeline_index: usize, -) -> Pipeline { +pub fn parse_pipeline(working_set: &mut StateWorkingSet, pipeline: &LitePipeline) -> Pipeline { + let first_command = pipeline.commands.first(); + let first_command_name = first_command + .and_then(|command| command.parts.first()) + .map(|span| working_set.get_span_contents(*span)); + if pipeline.commands.len() > 1 { - // Special case: allow `let` and `mut` to consume the whole pipeline, eg) `let abc = "foo" | str length` - if let Some(&first) = pipeline.commands[0].parts.first() { - let first = working_set.get_span_contents(first); - if first == b"let" || first == b"mut" { - let name = if first == b"let" { "let" } else { "mut" }; - let mut new_command = LiteCommand { - comments: vec![], - parts: pipeline.commands[0].parts.clone(), - pipe: None, - redirection: None, - }; + // Special case: allow "let" or "mut" to consume the whole pipeline, if this is a pipeline + // with multiple commands + if matches!(first_command_name, Some(b"let" | b"mut")) { + // Merge the pipeline into one command + let first_command = first_command.expect("must be Some"); - if let Some(redirection) = pipeline.commands[0].redirection.as_ref() { - working_set.error(redirecting_builtin_error(name, redirection)); - } + let remainder_span = first_command + .parts_including_redirection() + .skip(3) + .chain( + pipeline.commands[1..] + .iter() + .flat_map(|command| command.parts_including_redirection()), + ) + .reduce(Span::append); - for element in &pipeline.commands[1..] { - if let Some(redirection) = pipeline.commands[0].redirection.as_ref() { - working_set.error(redirecting_builtin_error(name, redirection)); - } else { - new_command.parts.push(element.pipe.expect("pipe span")); - new_command.comments.extend_from_slice(&element.comments); - new_command.parts.extend_from_slice(&element.parts); - } - } + let parts = first_command + .parts + .iter() + .take(3) // the let/mut start itself + .copied() + .chain(remainder_span) // everything else + .collect(); - // if the 'let' is complete enough, use it, if not, fall through for now - if new_command.parts.len() > 3 { - let rhs_span = Span::concat(&new_command.parts[3..]); + let comments = pipeline + .commands + .iter() + .flat_map(|command| command.comments.iter()) + .copied() + .collect(); - new_command.parts.truncate(3); - new_command.parts.push(rhs_span); - - let mut pipeline = parse_builtin_commands(working_set, &new_command); - - if pipeline_index == 0 { - let let_decl_id = working_set.find_decl(b"let"); - let mut_decl_id = working_set.find_decl(b"mut"); - for element in pipeline.elements.iter_mut() { - if let Expr::Call(call) = &element.expr.expr { - if Some(call.decl_id) == let_decl_id - || Some(call.decl_id) == mut_decl_id - { - // Do an expansion - if let Some(Expression { - expr: Expr::Block(block_id), - .. - }) = call.positional_iter().nth(1) - { - let block = working_set.get_block(*block_id); - - if let Some(element) = block - .pipelines - .first() - .and_then(|p| p.elements.first()) - .cloned() - { - if element.has_in_variable(working_set) { - let element = wrap_element_with_collect( - working_set, - &element, - ); - let block = working_set.get_block_mut(*block_id); - block.pipelines[0].elements[0] = element; - } - } - } - continue; - } else if element.has_in_variable(working_set) && !is_subexpression - { - *element = wrap_element_with_collect(working_set, element); - } - } else if element.has_in_variable(working_set) && !is_subexpression { - *element = wrap_element_with_collect(working_set, element); - } - } - } - - return pipeline; - } - } - } - - let mut elements = pipeline - .commands - .iter() - .map(|element| parse_pipeline_element(working_set, element)) - .collect::>(); - - if is_subexpression { - for element in elements.iter_mut().skip(1) { - if element.has_in_variable(working_set) { - *element = wrap_element_with_collect(working_set, element); - } - } + let new_command = LiteCommand { + pipe: None, + comments, + parts, + redirection: None, + }; + parse_builtin_commands(working_set, &new_command) } else { - for element in elements.iter_mut() { - if element.has_in_variable(working_set) { - *element = wrap_element_with_collect(working_set, element); - } - } - } - - Pipeline { elements } - } else { - if let Some(&first) = pipeline.commands[0].parts.first() { - let first = working_set.get_span_contents(first); - if first == b"let" || first == b"mut" { - if let Some(redirection) = pipeline.commands[0].redirection.as_ref() { - let name = if first == b"let" { "let" } else { "mut" }; - working_set.error(redirecting_builtin_error(name, redirection)); - } - } - } - - let mut pipeline = parse_builtin_commands(working_set, &pipeline.commands[0]); - - let let_decl_id = working_set.find_decl(b"let"); - let mut_decl_id = working_set.find_decl(b"mut"); - - if pipeline_index == 0 { - for element in pipeline.elements.iter_mut() { - if let Expr::Call(call) = &element.expr.expr { - if Some(call.decl_id) == let_decl_id || Some(call.decl_id) == mut_decl_id { - // Do an expansion - if let Some(Expression { - expr: Expr::Block(block_id), - .. - }) = call.positional_iter().nth(1) - { - let block = working_set.get_block(*block_id); - - if let Some(element) = block - .pipelines - .first() - .and_then(|p| p.elements.first()) - .cloned() - { - if element.has_in_variable(working_set) { - let element = wrap_element_with_collect(working_set, &element); - let block = working_set.get_block_mut(*block_id); - block.pipelines[0].elements[0] = element; - } - } - } - continue; - } else if element.has_in_variable(working_set) && !is_subexpression { - *element = wrap_element_with_collect(working_set, element); + // Parse a normal multi command pipeline + let elements: Vec<_> = pipeline + .commands + .iter() + .enumerate() + .map(|(index, element)| { + let element = parse_pipeline_element(working_set, element); + // Handle $in for pipeline elements beyond the first one + if index > 0 && element.has_in_variable(working_set) { + wrap_element_with_collect(working_set, element.clone()) + } else { + element } - } else if element.has_in_variable(working_set) && !is_subexpression { - *element = wrap_element_with_collect(working_set, element); - } - } - } + }) + .collect(); - pipeline + Pipeline { elements } + } + } else { + // If there's only one command in the pipeline, this could be a builtin command + parse_builtin_commands(working_set, &pipeline.commands[0]) } } @@ -5659,26 +5755,69 @@ pub fn parse_block( } let mut block = Block::new_with_capacity(lite_block.block.len()); + block.span = Some(span); - for (idx, lite_pipeline) in lite_block.block.iter().enumerate() { - let pipeline = parse_pipeline(working_set, lite_pipeline, is_subexpression, idx); + for lite_pipeline in &lite_block.block { + let pipeline = parse_pipeline(working_set, lite_pipeline); block.pipelines.push(pipeline); } + // If this is not a subexpression and there are any pipelines where the first element has $in, + // we can wrap the whole block in collect so that they all reference the same $in + if !is_subexpression + && block + .pipelines + .iter() + .flat_map(|pipeline| pipeline.elements.first()) + .any(|element| element.has_in_variable(working_set)) + { + // Move the block out to prepare it to become a subexpression + let inner_block = std::mem::take(&mut block); + block.span = inner_block.span; + let ty = inner_block.output_type(); + let block_id = working_set.add_block(Arc::new(inner_block)); + + // Now wrap it in a Collect expression, and put it in the block as the only pipeline + let subexpression = Expression::new(working_set, Expr::Subexpression(block_id), span, ty); + let collect = wrap_expr_with_collect(working_set, subexpression); + + block.pipelines.push(Pipeline { + elements: vec![PipelineElement { + pipe: None, + expr: collect, + redirection: None, + }], + }); + } + if scoped { working_set.exit_scope(); } - block.span = Some(span); - let errors = type_check::check_block_input_output(working_set, &block); if !errors.is_empty() { working_set.parse_errors.extend_from_slice(&errors); } + // Do not try to compile blocks that are subexpressions, or when we've already had a parse + // failure as that definitely will fail to compile + if !is_subexpression && working_set.parse_errors.is_empty() { + compile_block(working_set, &mut block); + } + block } +/// Compile an IR block for the `Block`, adding a compile error on failure +fn compile_block(working_set: &mut StateWorkingSet<'_>, block: &mut Block) { + match nu_engine::compile(working_set, block) { + Ok(ir_block) => { + block.ir_block = Some(ir_block); + } + Err(err) => working_set.compile_errors.push(err), + } +} + pub fn discover_captures_in_closure( working_set: &StateWorkingSet, block: &Block, @@ -6012,7 +6151,7 @@ pub fn discover_captures_in_expr( } Expr::String(_) => {} Expr::RawString(_) => {} - Expr::StringInterpolation(exprs) => { + Expr::StringInterpolation(exprs) | Expr::GlobInterpolation(exprs, _) => { for expr in exprs { discover_captures_in_expr(working_set, expr, seen, seen_blocks, output)?; } @@ -6023,6 +6162,10 @@ pub fn discover_captures_in_expr( discover_captures_in_expr(working_set, &match_.1, seen, seen_blocks, output)?; } } + Expr::Collect(var_id, expr) => { + seen.push(*var_id); + discover_captures_in_expr(working_set, expr, seen, seen_blocks, output)? + } Expr::RowCondition(block_id) | Expr::Subexpression(block_id) => { let block = working_set.get_block(*block_id); @@ -6073,28 +6216,28 @@ pub fn discover_captures_in_expr( fn wrap_redirection_with_collect( working_set: &mut StateWorkingSet, - target: &RedirectionTarget, + target: RedirectionTarget, ) -> RedirectionTarget { match target { RedirectionTarget::File { expr, append, span } => RedirectionTarget::File { expr: wrap_expr_with_collect(working_set, expr), - span: *span, - append: *append, + span, + append, }, - RedirectionTarget::Pipe { span } => RedirectionTarget::Pipe { span: *span }, + RedirectionTarget::Pipe { span } => RedirectionTarget::Pipe { span }, } } fn wrap_element_with_collect( working_set: &mut StateWorkingSet, - element: &PipelineElement, + element: PipelineElement, ) -> PipelineElement { PipelineElement { pipe: element.pipe, - expr: wrap_expr_with_collect(working_set, &element.expr), - redirection: element.redirection.as_ref().map(|r| match r { + expr: wrap_expr_with_collect(working_set, element.expr), + redirection: element.redirection.map(|r| match r { PipelineRedirection::Single { source, target } => PipelineRedirection::Single { - source: *source, + source, target: wrap_redirection_with_collect(working_set, target), }, PipelineRedirection::Separate { out, err } => PipelineRedirection::Separate { @@ -6105,63 +6248,24 @@ fn wrap_element_with_collect( } } -fn wrap_expr_with_collect(working_set: &mut StateWorkingSet, expr: &Expression) -> Expression { +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![]; + // IN_VARIABLE_ID should get replaced with a unique variable, so that we don't have to + // execute as a closure + let var_id = working_set.add_variable(b"$in".into(), expr.span, Type::Any, false); + let mut expr = expr.clone(); + expr.replace_in_variable(working_set, var_id); - let var_id = IN_VARIABLE_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, - default_value: None, - }); - - let block = Block { - pipelines: vec![Pipeline::from_vec(vec![expr.clone()])], - signature: Box::new(signature), - ..Default::default() - }; - - let block_id = working_set.add_block(Arc::new(block)); - - output.push(Argument::Positional(Expression::new( - working_set, - Expr::Closure(block_id), - span, - Type::Any, - ))); - - output.push(Argument::Named(( - Spanned { - item: "keep-env".to_string(), - span: Span::new(0, 0), - }, - None, - 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::new( - working_set, - Expr::Call(Box::new(Call { - head: Span::new(0, 0), - arguments: output, - decl_id, - parser_info: HashMap::new(), - })), - span, - Type::Any, - ) - } else { - Expression::garbage(working_set, span) - } + // Bind the custom `$in` variable for that particular expression + let ty = expr.ty.clone(); + Expression::new( + working_set, + Expr::Collect(var_id, Box::new(expr)), + span, + // We can expect it to have the same result type + ty, + ) } // Parses a vector of u8 to create an AST Block. If a file name is given, then diff --git a/crates/nu-parser/src/type_check.rs b/crates/nu-parser/src/type_check.rs index 1c76afb621..6cfe8f64b3 100644 --- a/crates/nu-parser/src/type_check.rs +++ b/crates/nu-parser/src/type_check.rs @@ -278,8 +278,7 @@ pub fn math_result_type( ) } }, - Operator::Math(Math::Divide) | Operator::Math(Math::Modulo) => match (&lhs.ty, &rhs.ty) - { + Operator::Math(Math::Divide) => 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), @@ -328,6 +327,55 @@ pub fn math_result_type( ) } }, + Operator::Math(Math::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::Number, Type::Number) => (Type::Number, None), + (Type::Number, Type::Int) => (Type::Number, None), + (Type::Int, Type::Number) => (Type::Number, None), + (Type::Number, Type::Float) => (Type::Number, None), + (Type::Float, Type::Number) => (Type::Number, None), + (Type::Filesize, Type::Filesize) => (Type::Filesize, None), + (Type::Filesize, Type::Int) => (Type::Filesize, None), + (Type::Filesize, Type::Float) => (Type::Filesize, None), + (Type::Duration, Type::Duration) => (Type::Duration, None), + (Type::Duration, Type::Int) => (Type::Duration, None), + (Type::Duration, Type::Float) => (Type::Duration, None), + + (Type::Custom(a), Type::Custom(b)) if a == b => (Type::Custom(a.clone()), None), + (Type::Custom(a), _) => (Type::Custom(a.clone()), None), + + (Type::Any, _) => (Type::Any, None), + (_, Type::Any) => (Type::Any, None), + (Type::Int | Type::Float | Type::Filesize | Type::Duration, _) => { + *op = Expression::garbage(working_set, op.span); + ( + Type::Any, + Some(ParseError::UnsupportedOperationRHS( + "division".into(), + op.span, + lhs.span, + lhs.ty.clone(), + rhs.span, + rhs.ty.clone(), + )), + ) + } + _ => { + *op = Expression::garbage(working_set, op.span); + ( + Type::Any, + Some(ParseError::UnsupportedOperationLHS( + "division".into(), + op.span, + lhs.span, + lhs.ty.clone(), + )), + ) + } + }, Operator::Math(Math::FloorDivision) => match (&lhs.ty, &rhs.ty) { (Type::Int, Type::Int) => (Type::Int, None), (Type::Float, Type::Int) => (Type::Int, None), @@ -1007,7 +1055,11 @@ pub fn check_block_input_output(working_set: &StateWorkingSet, block: &Block) -> .span }; - output_errors.push(ParseError::OutputMismatch(output_type.clone(), span)) + output_errors.push(ParseError::OutputMismatch( + output_type.clone(), + current_output_type.clone(), + span, + )) } } diff --git a/crates/nu-parser/tests/test_parser.rs b/crates/nu-parser/tests/test_parser.rs index 2646b3cc90..4d6778d201 100644 --- a/crates/nu-parser/tests/test_parser.rs +++ b/crates/nu-parser/tests/test_parser.rs @@ -1,8 +1,8 @@ use nu_parser::*; use nu_protocol::{ - ast::{Argument, Call, Expr, ExternalArgument, PathMember, Range}, - engine::{Command, EngineState, Stack, StateWorkingSet}, - ParseError, PipelineData, ShellError, Signature, Span, SyntaxShape, + ast::{Argument, Expr, Expression, ExternalArgument, PathMember, Range}, + engine::{Call, Command, EngineState, Stack, StateWorkingSet}, + ParseError, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, }; use rstest::rstest; @@ -41,6 +41,41 @@ impl Command for Let { } } +#[cfg(test)] +#[derive(Clone)] +pub struct Mut; + +#[cfg(test)] +impl Command for Mut { + fn name(&self) -> &str { + "mut" + } + + fn usage(&self) -> &str { + "Mock mut command." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("mut") + .required("var_name", SyntaxShape::VarWithOptType, "variable name") + .required( + "initial_value", + SyntaxShape::Keyword(b"=".to_vec(), Box::new(SyntaxShape::MathExpression)), + "equals sign followed by value", + ) + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + todo!() + } +} + fn test_int( test_tag: &str, // name of sub-test test: &[u8], // input expression @@ -182,7 +217,7 @@ pub fn multi_test_parse_int() { Test( "ranges or relative paths not confused for int", b"./a/b", - Expr::String("./a/b".into()), + Expr::GlobPattern("./a/b".into(), false), None, ), Test( @@ -694,6 +729,50 @@ pub fn parse_call_missing_req_flag() { )); } +fn test_external_call(input: &str, tag: &str, f: impl FnOnce(&Expression, &[ExternalArgument])) { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + let block = parse(&mut working_set, None, input.as_bytes(), true); + assert!( + working_set.parse_errors.is_empty(), + "{tag}: errors: {:?}", + working_set.parse_errors + ); + + let pipeline = &block.pipelines[0]; + assert_eq!(1, pipeline.len()); + let element = &pipeline.elements[0]; + match &element.expr.expr { + Expr::ExternalCall(name, args) => f(name, args), + other => { + panic!("{tag}: Unexpected expression in pipeline: {other:?}"); + } + } +} + +fn check_external_call_interpolation( + tag: &str, + subexpr_count: usize, + quoted: bool, + expr: &Expression, +) -> bool { + match &expr.expr { + Expr::StringInterpolation(exprs) => { + assert!(quoted, "{tag}: quoted"); + assert_eq!(expr.ty, Type::String, "{tag}: expr.ty"); + assert_eq!(subexpr_count, exprs.len(), "{tag}: subexpr_count"); + true + } + Expr::GlobInterpolation(exprs, is_quoted) => { + assert_eq!(quoted, *is_quoted, "{tag}: quoted"); + assert_eq!(expr.ty, Type::Glob, "{tag}: expr.ty"); + assert_eq!(subexpr_count, exprs.len(), "{tag}: subexpr_count"); + true + } + _ => false, + } +} + #[rstest] #[case("foo-external-call", "foo-external-call", "bare word")] #[case("^foo-external-call", "foo-external-call", "bare word with caret")] @@ -713,200 +792,370 @@ pub fn parse_call_missing_req_flag() { r"foo\external-call", "bare word with backslash and caret" )] -#[case( - "^'foo external call'", - "'foo external call'", - "single quote with caret" -)] -#[case( - "^'foo/external call'", - "'foo/external call'", - "single quote with forward slash and caret" -)] -#[case( - r"^'foo\external call'", - r"'foo\external call'", - "single quote with backslash and caret" -)] -#[case( - r#"^"foo external call""#, - r#""foo external call""#, - "double quote with caret" -)] -#[case( - r#"^"foo/external call""#, - r#""foo/external call""#, - "double quote with forward slash and caret" -)] -#[case( - r#"^"foo\\external call""#, - r#""foo\external call""#, - "double quote with backslash and caret" -)] -#[case("`foo external call`", "`foo external call`", "backtick quote")] +#[case("`foo external call`", "foo external call", "backtick quote")] #[case( "^`foo external call`", - "`foo external call`", + "foo external call", "backtick quote with caret" )] #[case( "`foo/external call`", - "`foo/external call`", + "foo/external call", "backtick quote with forward slash" )] #[case( "^`foo/external call`", - "`foo/external call`", + "foo/external call", "backtick quote with forward slash and caret" )] #[case( - r"^`foo\external call`", r"`foo\external call`", + r"foo\external call", "backtick quote with backslash" )] #[case( r"^`foo\external call`", - r"`foo\external call`", + r"foo\external call", "backtick quote with backslash and caret" )] -fn test_external_call_name(#[case] input: &str, #[case] expected: &str, #[case] tag: &str) { - let engine_state = EngineState::new(); - let mut working_set = StateWorkingSet::new(&engine_state); - let block = parse(&mut working_set, None, input.as_bytes(), true); - assert!( - working_set.parse_errors.is_empty(), - "{tag}: errors: {:?}", - working_set.parse_errors - ); - - let pipeline = &block.pipelines[0]; - assert_eq!(1, pipeline.len()); - let element = &pipeline.elements[0]; - match &element.expr.expr { - Expr::ExternalCall(name, args) => { - match &name.expr { - Expr::String(string) => { - assert_eq!(expected, string); - } - other => { - panic!("{tag}: Unexpected expression in command name position: {other:?}"); - } - } - assert_eq!(0, args.len()); - } - other => { - panic!("{tag}: Unexpected expression in pipeline: {other:?}"); - } - } -} - -#[rstest] -#[case("^foo bar-baz", "bar-baz", "bare word")] -#[case("^foo bar/baz", "bar/baz", "bare word with forward slash")] -#[case(r"^foo bar\baz", r"bar\baz", "bare word with backslash")] -#[case("^foo 'bar baz'", "'bar baz'", "single quote")] -#[case("foo 'bar/baz'", "'bar/baz'", "single quote with forward slash")] -#[case(r"foo 'bar\baz'", r"'bar\baz'", "single quote with backslash")] -#[case(r#"^foo "bar baz""#, r#""bar baz""#, "double quote")] -#[case(r#"^foo "bar/baz""#, r#""bar/baz""#, "double quote with forward slash")] -#[case(r#"^foo "bar\\baz""#, r#""bar\baz""#, "double quote with backslash")] -#[case("^foo `bar baz`", "`bar baz`", "backtick quote")] -#[case("^foo `bar/baz`", "`bar/baz`", "backtick quote with forward slash")] -#[case(r"^foo `bar\baz`", r"`bar\baz`", "backtick quote with backslash")] -fn test_external_call_argument_regular( +pub fn test_external_call_head_glob( #[case] input: &str, #[case] expected: &str, #[case] tag: &str, ) { - let engine_state = EngineState::new(); - let mut working_set = StateWorkingSet::new(&engine_state); - let block = parse(&mut working_set, None, input.as_bytes(), true); - assert!( - working_set.parse_errors.is_empty(), - "{tag}: errors: {:?}", - working_set.parse_errors - ); + test_external_call(input, tag, |name, args| { + match &name.expr { + Expr::GlobPattern(string, is_quoted) => { + assert_eq!(expected, string, "{tag}: incorrect name"); + assert!(!*is_quoted); + } + other => { + panic!("{tag}: Unexpected expression in command name position: {other:?}"); + } + } + assert_eq!(0, args.len()); + }) +} - let pipeline = &block.pipelines[0]; - assert_eq!(1, pipeline.len()); - let element = &pipeline.elements[0]; - match &element.expr.expr { - Expr::ExternalCall(name, args) => { - match &name.expr { - Expr::String(string) => { - assert_eq!("foo", string, "{tag}: incorrect name"); +#[rstest] +#[case( + r##"^r#'foo-external-call'#"##, + "foo-external-call", + "raw string with caret" +)] +#[case( + r##"^r#'foo/external-call'#"##, + "foo/external-call", + "raw string with forward slash and caret" +)] +#[case( + r##"^r#'foo\external-call'#"##, + r"foo\external-call", + "raw string with backslash and caret" +)] +pub fn test_external_call_head_raw_string( + #[case] input: &str, + #[case] expected: &str, + #[case] tag: &str, +) { + test_external_call(input, tag, |name, args| { + match &name.expr { + Expr::RawString(string) => { + assert_eq!(expected, string, "{tag}: incorrect name"); + } + other => { + panic!("{tag}: Unexpected expression in command name position: {other:?}"); + } + } + assert_eq!(0, args.len()); + }) +} + +#[rstest] +#[case("^'foo external call'", "foo external call", "single quote with caret")] +#[case( + "^'foo/external call'", + "foo/external call", + "single quote with forward slash and caret" +)] +#[case( + r"^'foo\external call'", + r"foo\external call", + "single quote with backslash and caret" +)] +#[case( + r#"^"foo external call""#, + r#"foo external call"#, + "double quote with caret" +)] +#[case( + r#"^"foo/external call""#, + r#"foo/external call"#, + "double quote with forward slash and caret" +)] +#[case( + r#"^"foo\\external call""#, + r#"foo\external call"#, + "double quote with backslash and caret" +)] +pub fn test_external_call_head_string( + #[case] input: &str, + #[case] expected: &str, + #[case] tag: &str, +) { + test_external_call(input, tag, |name, args| { + match &name.expr { + Expr::String(string) => { + assert_eq!(expected, string); + } + other => { + panic!("{tag}: Unexpected expression in command name position: {other:?}"); + } + } + assert_eq!(0, args.len()); + }) +} + +#[rstest] +#[case(r"~/.foo/(1)", 2, false, "unquoted interpolated string")] +#[case( + r"~\.foo(2)\(1)", + 4, + false, + "unquoted interpolated string with backslash" +)] +#[case(r"^~/.foo/(1)", 2, false, "unquoted interpolated string with caret")] +#[case(r#"^$"~/.foo/(1)""#, 2, true, "quoted interpolated string with caret")] +pub fn test_external_call_head_interpolated_string( + #[case] input: &str, + #[case] subexpr_count: usize, + #[case] quoted: bool, + #[case] tag: &str, +) { + test_external_call(input, tag, |name, args| { + if !check_external_call_interpolation(tag, subexpr_count, quoted, name) { + panic!("{tag}: Unexpected expression in command name position: {name:?}"); + } + assert_eq!(0, args.len()); + }) +} + +#[rstest] +#[case("^foo foo-external-call", "foo-external-call", "bare word")] +#[case( + "^foo foo/external-call", + "foo/external-call", + "bare word with forward slash" +)] +#[case( + r"^foo foo\external-call", + r"foo\external-call", + "bare word with backslash" +)] +#[case( + "^foo `foo external call`", + "foo external call", + "backtick quote with caret" +)] +#[case( + "^foo `foo/external call`", + "foo/external call", + "backtick quote with forward slash" +)] +#[case( + r"^foo `foo\external call`", + r"foo\external call", + "backtick quote with backslash" +)] +pub fn test_external_call_arg_glob(#[case] input: &str, #[case] expected: &str, #[case] tag: &str) { + test_external_call(input, tag, |name, args| { + match &name.expr { + Expr::GlobPattern(string, _) => { + assert_eq!("foo", string, "{tag}: incorrect name"); + } + other => { + panic!("{tag}: Unexpected expression in command name position: {other:?}"); + } + } + assert_eq!(1, args.len()); + match &args[0] { + ExternalArgument::Regular(expr) => match &expr.expr { + Expr::GlobPattern(string, is_quoted) => { + assert_eq!(expected, string, "{tag}: incorrect arg"); + assert!(!*is_quoted); } other => { - panic!("{tag}: Unexpected expression in command name position: {other:?}"); - } - } - assert_eq!(1, args.len()); - match &args[0] { - ExternalArgument::Regular(expr) => match &expr.expr { - Expr::String(string) => { - assert_eq!(expected, string, "{tag}: incorrect arg"); - } - other => { - panic!("Unexpected expression in command arg position: {other:?}") - } - }, - other @ ExternalArgument::Spread(..) => { - panic!("Unexpected external spread argument in command arg position: {other:?}") + panic!("Unexpected expression in command arg position: {other:?}") } + }, + other @ ExternalArgument::Spread(..) => { + panic!("Unexpected external spread argument in command arg position: {other:?}") } } - other => { - panic!("{tag}: Unexpected expression in pipeline: {other:?}"); + }) +} + +#[rstest] +#[case(r##"^foo r#'foo-external-call'#"##, "foo-external-call", "raw string")] +#[case( + r##"^foo r#'foo/external-call'#"##, + "foo/external-call", + "raw string with forward slash" +)] +#[case( + r##"^foo r#'foo\external-call'#"##, + r"foo\external-call", + "raw string with backslash" +)] +pub fn test_external_call_arg_raw_string( + #[case] input: &str, + #[case] expected: &str, + #[case] tag: &str, +) { + test_external_call(input, tag, |name, args| { + match &name.expr { + Expr::GlobPattern(string, _) => { + assert_eq!("foo", string, "{tag}: incorrect name"); + } + other => { + panic!("{tag}: Unexpected expression in command name position: {other:?}"); + } } - } + assert_eq!(1, args.len()); + match &args[0] { + ExternalArgument::Regular(expr) => match &expr.expr { + Expr::RawString(string) => { + assert_eq!(expected, string, "{tag}: incorrect arg"); + } + other => { + panic!("Unexpected expression in command arg position: {other:?}") + } + }, + other @ ExternalArgument::Spread(..) => { + panic!("Unexpected external spread argument in command arg position: {other:?}") + } + } + }) +} + +#[rstest] +#[case("^foo 'foo external call'", "foo external call", "single quote")] +#[case( + "^foo 'foo/external call'", + "foo/external call", + "single quote with forward slash" +)] +#[case( + r"^foo 'foo\external call'", + r"foo\external call", + "single quote with backslash" +)] +#[case(r#"^foo "foo external call""#, r#"foo external call"#, "double quote")] +#[case( + r#"^foo "foo/external call""#, + r#"foo/external call"#, + "double quote with forward slash" +)] +#[case( + r#"^foo "foo\\external call""#, + r#"foo\external call"#, + "double quote with backslash" +)] +pub fn test_external_call_arg_string( + #[case] input: &str, + #[case] expected: &str, + #[case] tag: &str, +) { + test_external_call(input, tag, |name, args| { + match &name.expr { + Expr::GlobPattern(string, _) => { + assert_eq!("foo", string, "{tag}: incorrect name"); + } + other => { + panic!("{tag}: Unexpected expression in command name position: {other:?}"); + } + } + assert_eq!(1, args.len()); + match &args[0] { + ExternalArgument::Regular(expr) => match &expr.expr { + Expr::String(string) => { + assert_eq!(expected, string, "{tag}: incorrect arg"); + } + other => { + panic!("{tag}: Unexpected expression in command arg position: {other:?}") + } + }, + other @ ExternalArgument::Spread(..) => { + panic!( + "{tag}: Unexpected external spread argument in command arg position: {other:?}" + ) + } + } + }) +} + +#[rstest] +#[case(r"^foo ~/.foo/(1)", 2, false, "unquoted interpolated string")] +#[case(r#"^foo $"~/.foo/(1)""#, 2, true, "quoted interpolated string")] +pub fn test_external_call_arg_interpolated_string( + #[case] input: &str, + #[case] subexpr_count: usize, + #[case] quoted: bool, + #[case] tag: &str, +) { + test_external_call(input, tag, |name, args| { + match &name.expr { + Expr::GlobPattern(string, _) => { + assert_eq!("foo", string, "{tag}: incorrect name"); + } + other => { + panic!("{tag}: Unexpected expression in command name position: {other:?}"); + } + } + assert_eq!(1, args.len()); + match &args[0] { + ExternalArgument::Regular(expr) => { + if !check_external_call_interpolation(tag, subexpr_count, quoted, expr) { + panic!("Unexpected expression in command arg position: {expr:?}") + } + } + other @ ExternalArgument::Spread(..) => { + panic!("Unexpected external spread argument in command arg position: {other:?}") + } + } + }) } #[test] fn test_external_call_argument_spread() { - let engine_state = EngineState::new(); - let mut working_set = StateWorkingSet::new(&engine_state); - let block = parse(&mut working_set, None, b"^foo ...[a b c]", true); - assert!( - working_set.parse_errors.is_empty(), - "errors: {:?}", - working_set.parse_errors - ); + let input = r"^foo ...[a b c]"; + let tag = "spread"; - let pipeline = &block.pipelines[0]; - assert_eq!(1, pipeline.len()); - let element = &pipeline.elements[0]; - match &element.expr.expr { - Expr::ExternalCall(name, args) => { - match &name.expr { - Expr::String(string) => { - assert_eq!("foo", string, "incorrect name"); + test_external_call(input, tag, |name, args| { + match &name.expr { + Expr::GlobPattern(string, _) => { + assert_eq!("foo", string, "incorrect name"); + } + other => { + panic!("Unexpected expression in command name position: {other:?}"); + } + } + assert_eq!(1, args.len()); + match &args[0] { + ExternalArgument::Spread(expr) => match &expr.expr { + Expr::List(items) => { + assert_eq!(3, items.len()); + // that's good enough, don't really need to go so deep into it... } other => { - panic!("Unexpected expression in command name position: {other:?}"); - } - } - assert_eq!(1, args.len()); - match &args[0] { - ExternalArgument::Spread(expr) => match &expr.expr { - Expr::List(items) => { - assert_eq!(3, items.len()); - // that's good enough, don't really need to go so deep into it... - } - other => { - panic!("Unexpected expression in command arg position: {other:?}") - } - }, - other @ ExternalArgument::Regular(..) => { - panic!( - "Unexpected external regular argument in command arg position: {other:?}" - ) + panic!("Unexpected expression in command arg position: {other:?}") } + }, + other @ ExternalArgument::Regular(..) => { + panic!("Unexpected external regular argument in command arg position: {other:?}") } } - other => { - panic!("Unexpected expression in pipeline: {other:?}"); - } - } + }) } #[test] @@ -935,11 +1184,29 @@ fn test_nothing_comparison_eq() { fn test_redirection_with_letmut(#[case] phase: &[u8]) { let engine_state = EngineState::new(); let mut working_set = StateWorkingSet::new(&engine_state); - let _block = parse(&mut working_set, None, phase, true); - assert!(matches!( - working_set.parse_errors.first(), - Some(ParseError::RedirectingBuiltinCommand(_, _, _)) - )); + working_set.add_decl(Box::new(Let)); + working_set.add_decl(Box::new(Mut)); + + let block = parse(&mut working_set, None, phase, true); + assert!( + working_set.parse_errors.is_empty(), + "parse errors: {:?}", + working_set.parse_errors + ); + assert_eq!(1, block.pipelines[0].elements.len()); + + let element = &block.pipelines[0].elements[0]; + assert!(element.redirection.is_none()); // it should be in the let block, not here + + if let Expr::Call(call) = &element.expr.expr { + let arg = call.positional_nth(1).expect("no positional args"); + let block_id = arg.as_block().expect("arg 1 is not a block"); + let block = working_set.get_block(block_id); + let inner_element = &block.pipelines[0].elements[0]; + assert!(inner_element.redirection.is_some()); + } else { + panic!("expected Call: {:?}", block.pipelines[0].elements[0]) + } } #[rstest] @@ -1132,6 +1399,44 @@ mod string { assert_eq!(subexprs[0], &Expr::String("(1 + 3)(7 - 5)".to_string())); } + #[test] + pub fn parse_string_interpolation_bare() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + let block = parse( + &mut working_set, + None, + b"\"\" ++ foo(1 + 3)bar(7 - 5)", + true, + ); + + assert!(working_set.parse_errors.is_empty()); + + assert_eq!(block.len(), 1); + let pipeline = &block.pipelines[0]; + assert_eq!(pipeline.len(), 1); + let element = &pipeline.elements[0]; + assert!(element.redirection.is_none()); + + let subexprs: Vec<&Expr> = match &element.expr.expr { + Expr::BinaryOp(_, _, rhs) => match &rhs.expr { + Expr::StringInterpolation(expressions) => { + expressions.iter().map(|e| &e.expr).collect() + } + _ => panic!("Expected an `Expr::StringInterpolation`"), + }, + _ => panic!("Expected an `Expr::BinaryOp`"), + }; + + assert_eq!(subexprs.len(), 4); + + assert_eq!(subexprs[0], &Expr::String("foo".to_string())); + assert!(matches!(subexprs[1], &Expr::FullCellPath(..))); + assert_eq!(subexprs[2], &Expr::String("bar".to_string())); + assert!(matches!(subexprs[3], &Expr::FullCellPath(..))); + } + #[test] pub fn parse_nested_expressions() { let engine_state = EngineState::new(); @@ -1507,10 +1812,7 @@ mod range { #[cfg(test)] mod input_types { use super::*; - use nu_protocol::{ - ast::{Argument, Call}, - Category, PipelineData, ShellError, Type, - }; + use nu_protocol::{ast::Argument, engine::Call, Category, PipelineData, ShellError, Type}; #[derive(Clone)] pub struct LsTest; diff --git a/crates/nu-path/Cargo.toml b/crates/nu-path/Cargo.toml index e3c84acdb3..62908e7cb7 100644 --- a/crates/nu-path/Cargo.toml +++ b/crates/nu-path/Cargo.toml @@ -5,14 +5,14 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-path" edition = "2021" license = "MIT" name = "nu-path" -version = "0.94.3" +version = "0.95.1" exclude = ["/fuzz"] [lib] bench = false [dependencies] -dirs-next = { workspace = true } +dirs = { workspace = true } [target.'cfg(windows)'.dependencies] omnipath = { workspace = true } diff --git a/crates/nu-path/src/form.rs b/crates/nu-path/src/form.rs new file mode 100644 index 0000000000..edec72c08c --- /dev/null +++ b/crates/nu-path/src/form.rs @@ -0,0 +1,158 @@ +use std::ffi::OsStr; + +mod private { + use std::ffi::OsStr; + + // This trait should not be extended by external crates in order to uphold safety guarantees. + // As such, this trait is put inside a private module to prevent external impls. + // This ensures that all possible [`PathForm`]s can only be defined here and will: + // - be zero sized (enforced anyways by the `repr(transparent)` on `Path`) + // - have a no-op [`Drop`] implementation + pub trait Sealed: 'static { + fn invariants_satisfied + ?Sized>(path: &P) -> bool; + } +} + +/// A marker trait for the different kinds of path forms. +/// Each form has its own invariants that are guaranteed be upheld. +/// The list of path forms are: +/// - [`Any`]: a path with no invariants. It may be a relative or an absolute path. +/// - [`Relative`]: a strictly relative path. +/// - [`Absolute`]: a strictly absolute path. +/// - [`Canonical`]: a path that must be in canonicalized form. +pub trait PathForm: private::Sealed {} +impl PathForm for Any {} +impl PathForm for Relative {} +impl PathForm for Absolute {} +impl PathForm for Canonical {} + +/// A path whose form is unknown. It could be a relative, absolute, or canonical path. +/// +/// The path is not guaranteed to be normalized. It may contain unresolved symlinks, +/// trailing slashes, dot components (`..` or `.`), and repeated path separators. +pub struct Any; + +impl private::Sealed for Any { + fn invariants_satisfied + ?Sized>(_: &P) -> bool { + true + } +} + +/// A strictly relative path. +/// +/// The path is not guaranteed to be normalized. It may contain unresolved symlinks, +/// trailing slashes, dot components (`..` or `.`), and repeated path separators. +pub struct Relative; + +impl private::Sealed for Relative { + fn invariants_satisfied + ?Sized>(path: &P) -> bool { + std::path::Path::new(path).is_relative() + } +} + +/// An absolute path. +/// +/// The path is not guaranteed to be normalized. It may contain unresolved symlinks, +/// trailing slashes, dot components (`..` or `.`), and repeated path separators. +pub struct Absolute; + +impl private::Sealed for Absolute { + fn invariants_satisfied + ?Sized>(path: &P) -> bool { + std::path::Path::new(path).is_absolute() + } +} + +// A canonical path. +// +// An absolute path with all intermediate components normalized and symbolic links resolved. +pub struct Canonical; + +impl private::Sealed for Canonical { + fn invariants_satisfied + ?Sized>(_: &P) -> bool { + true + } +} + +/// A marker trait for [`PathForm`]s that may be relative paths. +/// This includes only the [`Any`] and [`Relative`] path forms. +pub trait MaybeRelative: PathForm {} +impl MaybeRelative for Any {} +impl MaybeRelative for Relative {} + +/// A marker trait for [`PathForm`]s that may be absolute paths. +/// This includes the [`Any`], [`Absolute`], and [`Canonical`] path forms. +pub trait MaybeAbsolute: PathForm {} +impl MaybeAbsolute for Any {} +impl MaybeAbsolute for Absolute {} +impl MaybeAbsolute for Canonical {} + +/// A marker trait for [`PathForm`]s that are absolute paths. +/// This includes only the [`Absolute`] and [`Canonical`] path forms. +/// +/// Only [`PathForm`]s that implement this trait can be easily converted to [`std::path::Path`] +/// or [`std::path::PathBuf`]. This is to encourage/force other Nushell crates to account for +/// the emulated current working directory, instead of using the [`std::env::current_dir`]. +pub trait IsAbsolute: PathForm {} +impl IsAbsolute for Absolute {} +impl IsAbsolute for Canonical {} + +/// A marker trait that signifies one [`PathForm`] can be used as or trivially converted to +/// another [`PathForm`]. +/// +/// The list of possible conversions are: +/// - [`Relative`], [`Absolute`], or [`Canonical`] into [`Any`]. +/// - [`Canonical`] into [`Absolute`]. +/// - Any form into itself. +pub trait PathCast: PathForm {} +impl PathCast
for Form {} +impl PathCast for Relative {} +impl PathCast for Absolute {} +impl PathCast for Canonical {} +impl PathCast for Canonical {} + +/// A trait used to specify the output [`PathForm`] of a path join operation. +/// +/// The output path forms based on the left hand side path form are as follows: +/// +/// | Left hand side | Output form | +/// | --------------:|:------------ | +/// | [`Any`] | [`Any`] | +/// | [`Relative`] | [`Any`] | +/// | [`Absolute`] | [`Absolute`] | +/// | [`Canonical`] | [`Absolute`] | +pub trait PathJoin: PathForm { + type Output: PathForm; +} +impl PathJoin for Any { + type Output = Self; +} +impl PathJoin for Relative { + type Output = Any; +} +impl PathJoin for Absolute { + type Output = Self; +} +impl PathJoin for Canonical { + type Output = Absolute; +} + +/// A marker trait for [`PathForm`]s that support setting the file name or extension. +/// +/// This includes the [`Any`], [`Relative`], and [`Absolute`] path forms. +/// [`Canonical`] paths do not support this, since appending file names and extensions that contain +/// path separators can cause the path to no longer be canonical. +pub trait PathSet: PathForm {} +impl PathSet for Any {} +impl PathSet for Relative {} +impl PathSet for Absolute {} + +/// A marker trait for [`PathForm`]s that support pushing paths. +/// +/// This includes only [`Any`] and [`Absolute`] path forms. +/// Pushing onto a [`Relative`] path could cause it to become [`Absolute`], +/// which is why they do not support pushing. +/// In the future, a `push_rel` and/or a `try_push` method could be added as an alternative. +/// Similarly, [`Canonical`] paths may become uncanonical if a path is pushed onto it. +pub trait PathPush: PathSet {} +impl PathPush for Any {} +impl PathPush for Absolute {} diff --git a/crates/nu-path/src/helpers.rs b/crates/nu-path/src/helpers.rs index aaa53eab71..a6e35bddfe 100644 --- a/crates/nu-path/src/helpers.rs +++ b/crates/nu-path/src/helpers.rs @@ -3,21 +3,39 @@ use omnipath::WinPathExt; use std::path::PathBuf; pub fn home_dir() -> Option { - dirs_next::home_dir() + dirs::home_dir() } +/// Return the data directory for the current platform or XDG_DATA_HOME if specified. +pub fn data_dir() -> Option { + match std::env::var("XDG_DATA_HOME").map(PathBuf::from) { + Ok(xdg_data) if xdg_data.is_absolute() => Some(canonicalize(&xdg_data).unwrap_or(xdg_data)), + _ => get_canonicalized_path(dirs::data_dir()), + } +} + +/// Return the cache directory for the current platform or XDG_CACHE_HOME if specified. +pub fn cache_dir() -> Option { + match std::env::var("XDG_CACHE_HOME").map(PathBuf::from) { + Ok(xdg_cache) if xdg_cache.is_absolute() => { + Some(canonicalize(&xdg_cache).unwrap_or(xdg_cache)) + } + _ => get_canonicalized_path(dirs::cache_dir()), + } +} + +/// Return the config directory for the current platform or XDG_CONFIG_HOME if specified. pub fn config_dir() -> Option { match std::env::var("XDG_CONFIG_HOME").map(PathBuf::from) { Ok(xdg_config) if xdg_config.is_absolute() => { Some(canonicalize(&xdg_config).unwrap_or(xdg_config)) } - _ => config_dir_old(), + _ => get_canonicalized_path(dirs::config_dir()), } } -/// Get the old default config directory. Outside of Linux, this will ignore `XDG_CONFIG_HOME` -pub fn config_dir_old() -> Option { - let path = dirs_next::config_dir()?; +pub fn get_canonicalized_path(path: Option) -> Option { + let path = path?; Some(canonicalize(&path).unwrap_or(path)) } diff --git a/crates/nu-path/src/lib.rs b/crates/nu-path/src/lib.rs index 6c064d4b57..2cb415264a 100644 --- a/crates/nu-path/src/lib.rs +++ b/crates/nu-path/src/lib.rs @@ -1,13 +1,17 @@ +#![doc = include_str!("../README.md")] mod assert_path_eq; mod components; pub mod dots; pub mod expansions; +pub mod form; mod helpers; +mod path; mod tilde; mod trailing_slash; pub use components::components; pub use expansions::{canonicalize_with, expand_path_with, expand_to_real_path, locate_in_dirs}; -pub use helpers::{config_dir, config_dir_old, home_dir}; +pub use helpers::{cache_dir, config_dir, data_dir, get_canonicalized_path, home_dir}; +pub use path::*; pub use tilde::expand_tilde; pub use trailing_slash::{has_trailing_slash, strip_trailing_slash}; diff --git a/crates/nu-path/src/path.rs b/crates/nu-path/src/path.rs new file mode 100644 index 0000000000..916fffdf72 --- /dev/null +++ b/crates/nu-path/src/path.rs @@ -0,0 +1,3095 @@ +use crate::form::{ + Absolute, Any, Canonical, IsAbsolute, MaybeRelative, PathCast, PathForm, PathJoin, PathPush, + PathSet, Relative, +}; +use std::{ + borrow::{Borrow, Cow}, + cmp::Ordering, + collections::TryReserveError, + convert::Infallible, + ffi::{OsStr, OsString}, + fmt, fs, + hash::{Hash, Hasher}, + io, + iter::FusedIterator, + marker::PhantomData, + ops::{Deref, DerefMut}, + path::StripPrefixError, + rc::Rc, + str::FromStr, + sync::Arc, +}; + +/// A wrapper around [`std::path::Path`] with extra invariants determined by its `Form`. +/// +/// The possible path forms are [`Any`], [`Relative`], [`Absolute`], or [`Canonical`]. +/// To learn more, view the documentation on [`PathForm`] or any of the individual forms. +/// +/// There are also several type aliases available, corresponding to each [`PathForm`]: +/// - [`RelativePath`] (same as [`Path`]) +/// - [`AbsolutePath`] (same as [`Path`]) +/// - [`CanonicalPath`] (same as [`Path`]) +/// +/// If the `Form` is not specified, then it defaults to [`Any`], so [`Path`] and [`Path`] +/// are one in the same. +/// +/// # Converting to [`std::path`] types +/// +/// [`Path`]s with form [`Any`] cannot be easily referenced as a [`std::path::Path`] by design. +/// Other Nushell crates need to account for the emulated current working directory +/// before passing a path to functions in [`std`] or other third party crates. +/// You can [`join`](Path::join) a [`Path`] onto an [`AbsolutePath`] or a [`CanonicalPath`]. +/// This will return an [`AbsolutePathBuf`] which can be easily referenced as a [`std::path::Path`]. +/// If you really mean it, you can instead use [`as_relative_std_path`](Path::as_relative_std_path) +/// to get the underlying [`std::path::Path`] from a [`Path`]. +/// But this may cause third-party code to use [`std::env::current_dir`] to resolve +/// the path which is almost always incorrect behavior. Extra care is needed to ensure that this +/// is not the case after using [`as_relative_std_path`](Path::as_relative_std_path). +#[repr(transparent)] +pub struct Path { + _form: PhantomData, + inner: std::path::Path, +} + +/// A path that is strictly relative. +/// +/// I.e., this path is guaranteed to never be absolute. +/// +/// [`RelativePath`]s cannot be easily converted into a [`std::path::Path`] by design. +/// Other Nushell crates need to account for the emulated current working directory +/// before passing a path to functions in [`std`] or other third party crates. +/// You can [`join`](Path::join) a [`RelativePath`] onto an [`AbsolutePath`] or a [`CanonicalPath`]. +/// This will return an [`AbsolutePathBuf`] which can be referenced as a [`std::path::Path`]. +/// If you really mean it, you can use [`as_relative_std_path`](RelativePath::as_relative_std_path) +/// to get the underlying [`std::path::Path`] from a [`RelativePath`]. +/// But this may cause third-party code to use [`std::env::current_dir`] to resolve +/// the path which is almost always incorrect behavior. Extra care is needed to ensure that this +/// is not the case after using [`as_relative_std_path`](RelativePath::as_relative_std_path). +/// +/// # Examples +/// +/// [`RelativePath`]s can be created by using [`try_relative`](Path::try_relative) +/// on a [`Path`], by using [`try_new`](Path::try_new), or by using +/// [`strip_prefix`](Path::strip_prefix) on a [`Path`] of any form. +/// +/// ``` +/// use nu_path::{Path, RelativePath}; +/// +/// let path1 = Path::new("foo.txt"); +/// let path1 = path1.try_relative().unwrap(); +/// +/// let path2 = RelativePath::try_new("foo.txt").unwrap(); +/// +/// let path3 = Path::new("/prefix/foo.txt").strip_prefix("/prefix").unwrap(); +/// +/// assert_eq!(path1, path2); +/// assert_eq!(path2, path3); +/// ``` +/// +/// You can also use `RelativePath::try_from` or `try_into`. +/// This supports attempted conversions from [`Path`] as well as types in [`std::path`]. +/// +/// ``` +/// use nu_path::{Path, RelativePath}; +/// +/// let path1 = Path::new("foo.txt"); +/// let path1: &RelativePath = path1.try_into().unwrap(); +/// +/// let path2 = std::path::Path::new("foo.txt"); +/// let path2: &RelativePath = path2.try_into().unwrap(); +/// +/// assert_eq!(path1, path2) +/// ``` +pub type RelativePath = Path; + +/// A path that is strictly absolute. +/// +/// I.e., this path is guaranteed to never be relative. +/// +/// # Examples +/// +/// [`AbsolutePath`]s can be created by using [`try_absolute`](Path::try_absolute) on a [`Path`] +/// or by using [`try_new`](AbsolutePath::try_new). +/// +#[cfg_attr(not(windows), doc = "```")] +#[cfg_attr(windows, doc = "```no_run")] +/// use nu_path::{AbsolutePath, Path}; +/// +/// let path1 = Path::new("/foo").try_absolute().unwrap(); +/// let path2 = AbsolutePath::try_new("/foo").unwrap(); +/// +/// assert_eq!(path1, path2); +/// ``` +/// +/// You can also use `AbsolutePath::try_from` or `try_into`. +/// This supports attempted conversions from [`Path`] as well as types in [`std::path`]. +/// +#[cfg_attr(not(windows), doc = "```")] +#[cfg_attr(windows, doc = "```no_run")] +/// use nu_path::{AbsolutePath, Path}; +/// +/// let path1 = Path::new("/foo"); +/// let path1: &AbsolutePath = path1.try_into().unwrap(); +/// +/// let path2 = std::path::Path::new("/foo"); +/// let path2: &AbsolutePath = path2.try_into().unwrap(); +/// +/// assert_eq!(path1, path2) +/// ``` +pub type AbsolutePath = Path; + +/// An absolute, canonical path. +/// +/// # Examples +/// +/// [`CanonicalPath`]s can only be created by using [`canonicalize`](Path::canonicalize) on +/// an [`AbsolutePath`]. References to [`CanonicalPath`]s can be converted to +/// [`AbsolutePath`] references using `as_ref`, [`cast`](Path::cast), +/// or [`as_absolute`](CanonicalPath::as_absolute). +/// +/// ```no_run +/// use nu_path::AbsolutePath; +/// +/// let path = AbsolutePath::try_new("/foo").unwrap(); +/// +/// let canonical = path.canonicalize().expect("canonicalization failed"); +/// +/// assert_eq!(path, canonical.as_absolute()); +/// ``` +pub type CanonicalPath = Path; + +impl Path { + /// Create a new path of any form without validating invariants. + #[inline] + fn new_unchecked + ?Sized>(path: &P) -> &Self { + debug_assert!(Form::invariants_satisfied(path)); + // Safety: `Path` is a repr(transparent) wrapper around `std::path::Path`. + let path = std::path::Path::new(path.as_ref()); + let ptr = std::ptr::from_ref(path) as *const Self; + unsafe { &*ptr } + } + + /// Attempt to create a new [`Path`] from a reference of another type. + /// + /// This is a convenience method instead of having to use `try_into` with a type annotation. + /// + /// # Examples + /// + /// ``` + /// use nu_path::{AbsolutePath, RelativePath}; + /// + /// assert!(AbsolutePath::try_new("foo.txt").is_err()); + /// assert!(RelativePath::try_new("foo.txt").is_ok()); + /// ``` + #[inline] + pub fn try_new<'a, T>(path: &'a T) -> Result<&'a Self, <&'a T as TryInto<&'a Self>>::Error> + where + T: ?Sized, + &'a T: TryInto<&'a Self>, + { + path.try_into() + } + + /// Returns the underlying [`OsStr`] slice. + /// + /// # Examples + /// + /// ``` + /// use nu_path::Path; + /// + /// let os_str = Path::new("foo.txt").as_os_str(); + /// assert_eq!(os_str, std::ffi::OsStr::new("foo.txt")); + /// ``` + #[must_use] + #[inline] + pub fn as_os_str(&self) -> &OsStr { + self.inner.as_os_str() + } + + /// Returns a [`str`] slice if the [`Path`] is valid unicode. + /// + /// # Examples + /// + /// ``` + /// use nu_path::Path; + /// + /// let path = Path::new("foo.txt"); + /// assert_eq!(path.to_str(), Some("foo.txt")); + /// ``` + #[inline] + pub fn to_str(&self) -> Option<&str> { + self.inner.to_str() + } + + /// Converts a [`Path`] to a `Cow`. + /// + /// Any non-Unicode sequences are replaced with `U+FFFD REPLACEMENT CHARACTER`. + /// + /// # Examples + /// + /// Calling `to_string_lossy` on a [`Path`] with valid unicode: + /// + /// ``` + /// use nu_path::Path; + /// + /// let path = Path::new("foo.txt"); + /// assert_eq!(path.to_string_lossy(), "foo.txt"); + /// ``` + /// + /// Had `path` contained invalid unicode, the `to_string_lossy` call might have returned + /// `"fo�.txt"`. + #[inline] + pub fn to_string_lossy(&self) -> Cow<'_, str> { + self.inner.to_string_lossy() + } + + /// Converts a [`Path`] to an owned [`PathBuf`]. + /// + /// # Examples + /// + /// ``` + /// use nu_path::{Path, PathBuf}; + /// + /// let path_buf = Path::new("foo.txt").to_path_buf(); + /// assert_eq!(path_buf, PathBuf::from("foo.txt")); + /// ``` + #[inline] + pub fn to_path_buf(&self) -> PathBuf { + PathBuf::new_unchecked(self.inner.to_path_buf()) + } + + /// Returns the [`Path`] without its final component, if there is one. + /// + /// This means it returns `Some("")` for relative paths with one component. + /// + /// Returns [`None`] if the path terminates in a root or prefix, or if it's + /// the empty string. + /// + /// # Examples + /// + /// ``` + /// use nu_path::Path; + /// + /// let path = Path::new("/foo/bar"); + /// let parent = path.parent().unwrap(); + /// assert_eq!(parent, Path::new("/foo")); + /// + /// let grand_parent = parent.parent().unwrap(); + /// assert_eq!(grand_parent, Path::new("/")); + /// assert_eq!(grand_parent.parent(), None); + /// + /// let relative_path = Path::new("foo/bar"); + /// let parent = relative_path.parent(); + /// assert_eq!(parent, Some(Path::new("foo"))); + /// let grand_parent = parent.and_then(Path::parent); + /// assert_eq!(grand_parent, Some(Path::new(""))); + /// let great_grand_parent = grand_parent.and_then(Path::parent); + /// assert_eq!(great_grand_parent, None); + /// ``` + #[must_use] + #[inline] + pub fn parent(&self) -> Option<&Self> { + self.inner.parent().map(Self::new_unchecked) + } + + /// Produces an iterator over a [`Path`] and its ancestors. + /// + /// The iterator will yield the [`Path`] that is returned if the [`parent`](Path::parent) method + /// is used zero or more times. That means, the iterator will yield `&self`, + /// `&self.parent().unwrap()`, `&self.parent().unwrap().parent().unwrap()` and so on. + /// If the [`parent`](Path::parent) method returns [`None`], the iterator will do likewise. + /// The iterator will always yield at least one value, namely `&self`. + /// + /// # Examples + /// + /// ``` + /// use nu_path::Path; + /// + /// let mut ancestors = Path::new("/foo/bar").ancestors(); + /// assert_eq!(ancestors.next(), Some(Path::new("/foo/bar"))); + /// assert_eq!(ancestors.next(), Some(Path::new("/foo"))); + /// assert_eq!(ancestors.next(), Some(Path::new("/"))); + /// assert_eq!(ancestors.next(), None); + /// + /// let mut ancestors = Path::new("../foo/bar").ancestors(); + /// assert_eq!(ancestors.next(), Some(Path::new("../foo/bar"))); + /// assert_eq!(ancestors.next(), Some(Path::new("../foo"))); + /// assert_eq!(ancestors.next(), Some(Path::new(".."))); + /// assert_eq!(ancestors.next(), Some(Path::new(""))); + /// assert_eq!(ancestors.next(), None); + /// ``` + #[inline] + pub fn ancestors(&self) -> Ancestors<'_, Form> { + Ancestors { + _form: PhantomData, + inner: self.inner.ancestors(), + } + } + + /// Returns the final component of a [`Path`], if there is one. + /// + /// If the path is a normal file, this is the file name. If it's the path of a directory, this + /// is the directory name. + /// + /// Returns [`None`] if the path terminates in `..`. + /// + /// # Examples + /// + /// ``` + /// use nu_path::Path; + /// use std::ffi::OsStr; + /// + /// assert_eq!(Some(OsStr::new("bin")), Path::new("/usr/bin/").file_name()); + /// assert_eq!(Some(OsStr::new("foo.txt")), Path::new("tmp/foo.txt").file_name()); + /// assert_eq!(Some(OsStr::new("foo.txt")), Path::new("foo.txt/.").file_name()); + /// assert_eq!(Some(OsStr::new("foo.txt")), Path::new("foo.txt/.//").file_name()); + /// assert_eq!(None, Path::new("foo.txt/..").file_name()); + /// assert_eq!(None, Path::new("/").file_name()); + /// ``` + #[must_use] + #[inline] + pub fn file_name(&self) -> Option<&OsStr> { + self.inner.file_name() + } + + /// Returns a relative path that, when joined onto `base`, yields `self`. + /// + /// # Examples + /// + /// ``` + /// use nu_path::{Path, PathBuf}; + /// + /// let path = Path::new("/test/haha/foo.txt"); + /// + /// assert_eq!(path.strip_prefix("/").unwrap(), Path::new("test/haha/foo.txt")); + /// assert_eq!(path.strip_prefix("/test").unwrap(), Path::new("haha/foo.txt")); + /// assert_eq!(path.strip_prefix("/test/").unwrap(), Path::new("haha/foo.txt")); + /// assert_eq!(path.strip_prefix("/test/haha/foo.txt").unwrap(), Path::new("")); + /// assert_eq!(path.strip_prefix("/test/haha/foo.txt/").unwrap(), Path::new("")); + /// + /// assert!(path.strip_prefix("test").is_err()); + /// assert!(path.strip_prefix("/haha").is_err()); + /// + /// let prefix = PathBuf::from("/test/"); + /// assert_eq!(path.strip_prefix(prefix).unwrap(), Path::new("haha/foo.txt")); + /// ``` + #[inline] + pub fn strip_prefix(&self, base: impl AsRef) -> Result<&RelativePath, StripPrefixError> { + self.inner + .strip_prefix(&base.as_ref().inner) + .map(RelativePath::new_unchecked) + } + + /// Determines whether `base` is a prefix of `self`. + /// + /// Only considers whole path components to match. + /// + /// # Examples + /// + /// ``` + /// use nu_path::Path; + /// + /// let path = Path::new("/etc/passwd"); + /// + /// assert!(path.starts_with("/etc")); + /// assert!(path.starts_with("/etc/")); + /// assert!(path.starts_with("/etc/passwd")); + /// assert!(path.starts_with("/etc/passwd/")); // extra slash is okay + /// assert!(path.starts_with("/etc/passwd///")); // multiple extra slashes are okay + /// + /// assert!(!path.starts_with("/e")); + /// assert!(!path.starts_with("/etc/passwd.txt")); + /// + /// assert!(!Path::new("/etc/foo.rs").starts_with("/etc/foo")); + /// ``` + #[must_use] + #[inline] + pub fn starts_with(&self, base: impl AsRef) -> bool { + self.inner.starts_with(&base.as_ref().inner) + } + + /// Determines whether `child` is a suffix of `self`. + /// + /// Only considers whole path components to match. + /// + /// # Examples + /// + /// ``` + /// use nu_path::Path; + /// + /// let path = Path::new("/etc/resolv.conf"); + /// + /// assert!(path.ends_with("resolv.conf")); + /// assert!(path.ends_with("etc/resolv.conf")); + /// assert!(path.ends_with("/etc/resolv.conf")); + /// + /// assert!(!path.ends_with("/resolv.conf")); + /// assert!(!path.ends_with("conf")); // use .extension() instead + /// ``` + #[must_use] + #[inline] + pub fn ends_with(&self, child: impl AsRef) -> bool { + self.inner.ends_with(&child.as_ref().inner) + } + + /// Extracts the stem (non-extension) portion of [`self.file_name`](Path::file_name). + /// + /// The stem is: + /// + /// * [`None`], if there is no file name; + /// * The entire file name if there is no embedded `.`; + /// * The entire file name if the file name begins with `.` and has no other `.`s within; + /// * Otherwise, the portion of the file name before the final `.` + /// + /// # Examples + /// + /// ``` + /// use nu_path::Path; + /// + /// assert_eq!("foo", Path::new("foo.rs").file_stem().unwrap()); + /// assert_eq!("foo.tar", Path::new("foo.tar.gz").file_stem().unwrap()); + /// ``` + #[must_use] + #[inline] + pub fn file_stem(&self) -> Option<&OsStr> { + self.inner.file_stem() + } + + /// Extracts the extension (without the leading dot) of [`self.file_name`](Path::file_name), + /// if possible. + /// + /// The extension is: + /// + /// * [`None`], if there is no file name; + /// * [`None`], if there is no embedded `.`; + /// * [`None`], if the file name begins with `.` and has no other `.`s within; + /// * Otherwise, the portion of the file name after the final `.` + /// + /// # Examples + /// + /// ``` + /// use nu_path::Path; + /// + /// assert_eq!("rs", Path::new("foo.rs").extension().unwrap()); + /// assert_eq!("gz", Path::new("foo.tar.gz").extension().unwrap()); + /// ``` + #[must_use] + #[inline] + pub fn extension(&self) -> Option<&OsStr> { + self.inner.extension() + } + + /// Produces an iterator over the [`Component`](std::path::Component)s of the path. + /// + /// When parsing the path, there is a small amount of normalization: + /// + /// * Repeated separators are ignored, so `a/b` and `a//b` both have + /// `a` and `b` as components. + /// + /// * Occurrences of `.` are normalized away, except if they are at the + /// beginning of the path. For example, `a/./b`, `a/b/`, `a/b/.` and + /// `a/b` all have `a` and `b` as components, but `./a/b` starts with + /// an additional [`CurDir`](std::path::Component) component. + /// + /// * A trailing slash is normalized away, `/a/b` and `/a/b/` are equivalent. + /// + /// Note that no other normalization takes place; in particular, `a/c` + /// and `a/b/../c` are distinct, to account for the possibility that `b` + /// is a symbolic link (so its parent isn't `a`). + /// + /// # Examples + /// + /// ``` + /// use nu_path::Path; + /// use std::path::Component; + /// use std::ffi::OsStr; + /// + /// let mut components = Path::new("/tmp/foo.txt").components(); + /// + /// assert_eq!(components.next(), Some(Component::RootDir)); + /// assert_eq!(components.next(), Some(Component::Normal(OsStr::new("tmp")))); + /// assert_eq!(components.next(), Some(Component::Normal(OsStr::new("foo.txt")))); + /// assert_eq!(components.next(), None) + /// ``` + #[inline] + pub fn components(&self) -> std::path::Components<'_> { + self.inner.components() + } + + /// Produces an iterator over the path's components viewed as [`OsStr`] slices. + /// + /// For more information about the particulars of how the path is separated into components, + /// see [`components`](Path::components). + /// + /// # Examples + /// + /// ``` + /// use nu_path::Path; + /// use std::ffi::OsStr; + /// + /// let mut it = Path::new("/tmp/foo.txt").iter(); + /// assert_eq!(it.next(), Some(OsStr::new(&std::path::MAIN_SEPARATOR.to_string()))); + /// assert_eq!(it.next(), Some(OsStr::new("tmp"))); + /// assert_eq!(it.next(), Some(OsStr::new("foo.txt"))); + /// assert_eq!(it.next(), None) + /// ``` + #[inline] + pub fn iter(&self) -> std::path::Iter<'_> { + self.inner.iter() + } + + /// Returns an object that implements [`Display`](fmt::Display) for safely printing paths + /// that may contain non-Unicode data. This may perform lossy conversion, + /// depending on the platform. If you would like an implementation which escapes the path + /// please use [`Debug`](fmt::Debug) instead. + /// + /// # Examples + /// + /// ``` + /// use nu_path::Path; + /// + /// let path = Path::new("/tmp/foo.rs"); + /// + /// println!("{}", path.display()); + /// ``` + #[inline] + pub fn display(&self) -> std::path::Display<'_> { + self.inner.display() + } + + /// Converts a [`Box`](Box) into a [`PathBuf`] without copying or allocating. + #[inline] + pub fn into_path_buf(self: Box) -> PathBuf { + // Safety: `Path` is a repr(transparent) wrapper around `std::path::Path`. + let ptr = Box::into_raw(self) as *mut std::path::Path; + let boxed = unsafe { Box::from_raw(ptr) }; + PathBuf::new_unchecked(boxed.into_path_buf()) + } + + /// Returns a reference to the same [`Path`] in a different form. + /// + /// [`PathForm`]s can be converted to one another based on [`PathCast`] implementations. + /// Namely, the following form conversions are possible: + /// - [`Relative`], [`Absolute`], or [`Canonical`] into [`Any`]. + /// - [`Canonical`] into [`Absolute`]. + /// - Any form into itself. + /// + /// # Examples + /// + /// ``` + /// use nu_path::{Path, RelativePath}; + /// + /// let relative = RelativePath::try_new("test.txt").unwrap(); + /// let p: &Path = relative.cast(); + /// assert_eq!(p, relative); + /// ``` + #[inline] + pub fn cast(&self) -> &Path + where + To: PathForm, + Form: PathCast, + { + Path::new_unchecked(self) + } + + /// Returns a reference to a path with its form as [`Any`]. + /// + /// # Examples + /// + /// ``` + /// use nu_path::{Path, RelativePath}; + /// + /// let p = RelativePath::try_new("test.txt").unwrap(); + /// assert_eq!(Path::new("test.txt"), p.as_any()); + /// ``` + #[inline] + pub fn as_any(&self) -> &Path { + Path::new_unchecked(self) + } +} + +impl Path { + /// Create a new [`Path`] by wrapping a string slice. + /// + /// This is a cost-free conversion. + /// + /// # Examples + /// + /// ``` + /// use nu_path::Path; + /// + /// Path::new("foo.txt"); + /// ``` + /// + /// You can create [`Path`]s from [`String`]s, or even other [`Path`]s: + /// + /// ``` + /// use nu_path::Path; + /// + /// let string = String::from("foo.txt"); + /// let from_string = Path::new(&string); + /// let from_path = Path::new(&from_string); + /// assert_eq!(from_string, from_path); + /// ``` + #[inline] + pub fn new + ?Sized>(path: &P) -> &Self { + Self::new_unchecked(path) + } + + /// Returns a mutable reference to the underlying [`OsStr`] slice. + /// + /// # Examples + /// + /// ``` + /// use nu_path::{Path, PathBuf}; + /// + /// let mut path = PathBuf::from("Foo.TXT"); + /// + /// assert_ne!(path, Path::new("foo.txt")); + /// + /// path.as_mut_os_str().make_ascii_lowercase(); + /// assert_eq!(path, Path::new("foo.txt")); + /// ``` + #[must_use] + #[inline] + pub fn as_mut_os_str(&mut self) -> &mut OsStr { + self.inner.as_mut_os_str() + } + + /// Returns `true` if the [`Path`] is absolute, i.e., if it is independent of + /// the current directory. + /// + /// * On Unix, a path is absolute if it starts with the root, + /// so [`is_absolute`](Path::is_absolute) and [`has_root`](Path::has_root) are equivalent. + /// + /// * On Windows, a path is absolute if it has a prefix and starts with the root: + /// `c:\windows` is absolute, while `c:temp` and `\temp` are not. + /// + /// # Examples + /// + /// ``` + /// use nu_path::Path; + /// + /// assert!(!Path::new("foo.txt").is_absolute()); + /// ``` + #[must_use] + #[inline] + pub fn is_absolute(&self) -> bool { + self.inner.is_absolute() + } + + // Returns `true` if the [`Path`] is relative, i.e., not absolute. + /// + /// See [`is_absolute`](Path::is_absolute)'s documentation for more details. + /// + /// # Examples + /// + /// ``` + /// use nu_path::Path; + /// + /// assert!(Path::new("foo.txt").is_relative()); + /// ``` + #[must_use] + #[inline] + pub fn is_relative(&self) -> bool { + self.inner.is_relative() + } + + /// Returns an `Ok` [`AbsolutePath`] if the [`Path`] is absolute. + /// Otherwise, returns an `Err` [`RelativePath`]. + /// + /// # Examples + /// + /// ``` + /// use nu_path::Path; + /// + /// assert!(Path::new("test.txt").try_absolute().is_err()); + /// ``` + #[inline] + pub fn try_absolute(&self) -> Result<&AbsolutePath, &RelativePath> { + if self.is_absolute() { + Ok(AbsolutePath::new_unchecked(&self.inner)) + } else { + Err(RelativePath::new_unchecked(&self.inner)) + } + } + + /// Returns an `Ok` [`RelativePath`] if the [`Path`] is relative. + /// Otherwise, returns an `Err` [`AbsolutePath`]. + /// + /// # Examples + /// + /// ``` + /// use nu_path::Path; + /// + /// assert!(Path::new("test.txt").try_relative().is_ok()); + /// ``` + #[inline] + pub fn try_relative(&self) -> Result<&RelativePath, &AbsolutePath> { + if self.is_relative() { + Ok(RelativePath::new_unchecked(&self.inner)) + } else { + Err(AbsolutePath::new_unchecked(&self.inner)) + } + } +} + +impl Path { + /// Creates an owned [`PathBuf`] with `path` adjoined to `self`. + /// + /// If `path` is absolute, it replaces the current path. + /// + /// See [`PathBuf::push`] for more details on what it means to adjoin a path. + /// + /// # Examples + /// + /// ``` + /// use nu_path::{Path, PathBuf}; + /// + /// assert_eq!(Path::new("/etc").join("passwd"), PathBuf::from("/etc/passwd")); + /// assert_eq!(Path::new("/etc").join("/bin/sh"), PathBuf::from("/bin/sh")); + /// ``` + #[must_use] + #[inline] + pub fn join(&self, path: impl AsRef) -> PathBuf { + PathBuf::new_unchecked(self.inner.join(&path.as_ref().inner)) + } +} + +impl Path { + /// Creates an owned [`PathBuf`] like `self` but with the given file name. + /// + /// See [`PathBuf::set_file_name`] for more details. + /// + /// # Examples + /// + /// ``` + /// use nu_path::{Path, PathBuf}; + /// + /// let path = Path::new("/tmp/foo.png"); + /// assert_eq!(path.with_file_name("bar"), PathBuf::from("/tmp/bar")); + /// assert_eq!(path.with_file_name("bar.txt"), PathBuf::from("/tmp/bar.txt")); + /// + /// let path = Path::new("/tmp"); + /// assert_eq!(path.with_file_name("var"), PathBuf::from("/var")); + /// ``` + #[inline] + pub fn with_file_name(&self, file_name: impl AsRef) -> PathBuf { + PathBuf::new_unchecked(self.inner.with_file_name(file_name)) + } + + /// Creates an owned [`PathBuf`] like `self` but with the given extension. + /// + /// See [`PathBuf::set_extension`] for more details. + /// + /// # Examples + /// + /// ``` + /// use nu_path::{Path, PathBuf}; + /// + /// let path = Path::new("foo.rs"); + /// assert_eq!(path.with_extension("txt"), PathBuf::from("foo.txt")); + /// + /// let path = Path::new("foo.tar.gz"); + /// assert_eq!(path.with_extension(""), PathBuf::from("foo.tar")); + /// assert_eq!(path.with_extension("xz"), PathBuf::from("foo.tar.xz")); + /// assert_eq!(path.with_extension("").with_extension("txt"), PathBuf::from("foo.txt")); + /// ``` + #[inline] + pub fn with_extension(&self, extension: impl AsRef) -> PathBuf { + PathBuf::new_unchecked(self.inner.with_extension(extension)) + } +} + +impl Path { + /// Returns the, potentially relative, underlying [`std::path::Path`]. + /// + /// # Note + /// + /// Caution should be taken when using this function. Nushell keeps track of an emulated current + /// working directory, and using the [`std::path::Path`] returned from this method will likely + /// use [`std::env::current_dir`] to resolve the path instead of using the emulated current + /// working directory. + /// + /// Instead, you should probably join this path onto the emulated current working directory. + /// Any [`AbsolutePath`] or [`CanonicalPath`] will also suffice. + /// + /// # Examples + /// + /// ``` + /// use nu_path::Path; + /// + /// let p = Path::new("test.txt"); + /// assert_eq!(std::path::Path::new("test.txt"), p.as_relative_std_path()); + /// ``` + #[inline] + pub fn as_relative_std_path(&self) -> &std::path::Path { + &self.inner + } + + // Returns `true` if the [`Path`] has a root. + /// + /// * On Unix, a path has a root if it begins with `/`. + /// + /// * On Windows, a path has a root if it: + /// * has no prefix and begins with a separator, e.g., `\windows` + /// * has a prefix followed by a separator, e.g., `c:\windows` but not `c:windows` + /// * has any non-disk prefix, e.g., `\\server\share` + /// + /// # Examples + /// + /// ``` + /// use nu_path::Path; + /// + /// assert!(Path::new("/etc/passwd").has_root()); + /// ``` + #[must_use] + #[inline] + pub fn has_root(&self) -> bool { + self.inner.has_root() + } +} + +impl Path { + /// Returns the underlying [`std::path::Path`]. + /// + /// # Examples + /// + #[cfg_attr(not(windows), doc = "```")] + #[cfg_attr(windows, doc = "```no_run")] + /// use nu_path::AbsolutePath; + /// + /// let p = AbsolutePath::try_new("/test").unwrap(); + /// assert_eq!(std::path::Path::new("/test"), p.as_std_path()); + /// ``` + #[inline] + pub fn as_std_path(&self) -> &std::path::Path { + &self.inner + } + + /// Converts a [`Path`] to an owned [`std::path::PathBuf`]. + /// + /// # Examples + /// + #[cfg_attr(not(windows), doc = "```")] + #[cfg_attr(windows, doc = "```no_run")] + /// use nu_path::AbsolutePath; + /// + /// let path = AbsolutePath::try_new("/foo").unwrap(); + /// assert_eq!(path.to_std_path_buf(), std::path::PathBuf::from("/foo")); + /// ``` + #[inline] + pub fn to_std_path_buf(&self) -> std::path::PathBuf { + self.inner.to_path_buf() + } + + /// Queries the file system to get information about a file, directory, etc. + /// + /// This function will traverse symbolic links to query information about the destination file. + /// + /// This is an alias to [`std::fs::metadata`]. + /// + /// # Examples + /// + /// ```no_run + /// use nu_path::AbsolutePath; + /// + /// let path = AbsolutePath::try_new("/Minas/tirith").unwrap(); + /// let metadata = path.metadata().expect("metadata call failed"); + /// println!("{:?}", metadata.file_type()); + /// ``` + #[inline] + pub fn metadata(&self) -> io::Result { + self.inner.metadata() + } + + /// Returns an iterator over the entries within a directory. + /// + /// The iterator will yield instances of [io::Result]<[fs::DirEntry]>. + /// New errors may be encountered after an iterator is initially constructed. + /// + /// This is an alias to [`std::fs::read_dir`]. + /// + /// # Examples + /// + /// ```no_run + /// use nu_path::AbsolutePath; + /// + /// let path = AbsolutePath::try_new("/laputa").unwrap(); + /// for entry in path.read_dir().expect("read_dir call failed") { + /// if let Ok(entry) = entry { + /// println!("{:?}", entry.path()); + /// } + /// } + /// ``` + #[inline] + pub fn read_dir(&self) -> io::Result { + self.inner.read_dir() + } + + /// Returns `true` if the path points at an existing entity. + /// + /// Warning: this method may be error-prone, consider using [`try_exists`](Path::try_exists) + /// instead! It also has a risk of introducing time-of-check to time-of-use (TOCTOU) bugs. + /// + /// This function will traverse symbolic links to query information about the destination file. + /// + /// If you cannot access the metadata of the file, e.g. because of a permission error + /// or broken symbolic links, this will return `false`. + /// + /// # Examples + /// + /// ```no_run + /// use nu_path::AbsolutePath; + /// + /// let path = AbsolutePath::try_new("/does_not_exist").unwrap(); + /// assert!(!path.exists()); + /// ``` + #[must_use] + #[inline] + pub fn exists(&self) -> bool { + self.inner.exists() + } + + /// Returns `true` if the path exists on disk and is pointing at a regular file. + /// + /// This function will traverse symbolic links to query information about the destination file. + /// + /// If you cannot access the metadata of the file, e.g. because of a permission error + /// or broken symbolic links, this will return `false`. + /// + /// # Examples + /// + /// ```no_run + /// use nu_path::AbsolutePath; + /// + /// let path = AbsolutePath::try_new("/is_a_directory/").unwrap(); + /// assert_eq!(path.is_file(), false); + /// + /// let path = AbsolutePath::try_new("/a_file.txt").unwrap(); + /// assert_eq!(path.is_file(), true); + /// ``` + /// + /// # See Also + /// + /// When the goal is simply to read from (or write to) the source, the most reliable way + /// to test the source can be read (or written to) is to open it. Only using `is_file` can + /// break workflows like `diff <( prog_a )` on a Unix-like system for example. + /// See [`std::fs::File::open`] or [`std::fs::OpenOptions::open`] for more information. + #[must_use] + #[inline] + pub fn is_file(&self) -> bool { + self.inner.is_file() + } + + /// Returns `true` if the path exists on disk and is pointing at a directory. + /// + /// This function will traverse symbolic links to query information about the destination file. + /// + /// If you cannot access the metadata of the file, e.g. because of a permission error + /// or broken symbolic links, this will return `false`. + /// + /// # Examples + /// + /// ```no_run + /// use nu_path::AbsolutePath; + /// + /// let path = AbsolutePath::try_new("/is_a_directory/").unwrap(); + /// assert_eq!(path.is_dir(), true); + /// + /// let path = AbsolutePath::try_new("/a_file.txt").unwrap(); + /// assert_eq!(path.is_dir(), false); + /// ``` + #[must_use] + #[inline] + pub fn is_dir(&self) -> bool { + self.inner.is_dir() + } +} + +impl AbsolutePath { + /// Returns the canonical, absolute form of the path with all intermediate components + /// normalized and symbolic links resolved. + /// + /// On Windows, this will also simplify to a winuser path. + /// + /// This is an alias to [`std::fs::canonicalize`]. + /// + /// # Examples + /// + /// ```no_run + /// use nu_path::{AbsolutePath, PathBuf}; + /// + /// let path = AbsolutePath::try_new("/foo/test/../test/bar.rs").unwrap(); + /// assert_eq!(path.canonicalize().unwrap(), PathBuf::from("/foo/test/bar.rs")); + /// ``` + #[cfg(not(windows))] + #[inline] + pub fn canonicalize(&self) -> io::Result { + self.inner + .canonicalize() + .map(CanonicalPathBuf::new_unchecked) + } + + /// Returns the canonical, absolute form of the path with all intermediate components + /// normalized and symbolic links resolved. + /// + /// On Windows, this will also simplify to a winuser path. + /// + /// This is an alias to [`std::fs::canonicalize`]. + /// + /// # Examples + /// + /// ```no_run + /// use nu_path::{AbsolutePath, PathBuf}; + /// + /// let path = AbsolutePath::try_new("/foo/test/../test/bar.rs").unwrap(); + /// assert_eq!(path.canonicalize().unwrap(), PathBuf::from("/foo/test/bar.rs")); + /// ``` + #[cfg(windows)] + pub fn canonicalize(&self) -> io::Result { + use omnipath::WinPathExt; + + let path = self.inner.canonicalize()?.to_winuser_path()?; + Ok(CanonicalPathBuf::new_unchecked(path)) + } + + /// Reads a symbolic link, returning the file that the link points to. + /// + /// This is an alias to [`std::fs::read_link`]. + /// + /// # Examples + /// + /// ```no_run + /// use nu_path::AbsolutePath; + /// + /// let path = AbsolutePath::try_new("/laputa/sky_castle.rs").unwrap(); + /// let path_link = path.read_link().expect("read_link call failed"); + /// ``` + #[inline] + pub fn read_link(&self) -> io::Result { + self.inner.read_link().map(PathBuf::new_unchecked) + } + + /// Returns `Ok(true)` if the path points at an existing entity. + /// + /// This function will traverse symbolic links to query information about the destination file. + /// In case of broken symbolic links this will return `Ok(false)`. + /// + /// [`Path::exists`] only checks whether or not a path was both found and readable. + /// By contrast, [`try_exists`](Path::try_exists) will return `Ok(true)` or `Ok(false)`, + /// respectively, if the path was _verified_ to exist or not exist. + /// If its existence can neither be confirmed nor denied, it will propagate an `Err` instead. + /// This can be the case if e.g. listing permission is denied on one of the parent directories. + /// + /// Note that while this avoids some pitfalls of the [`exists`](Path::exists) method, + /// it still can not prevent time-of-check to time-of-use (TOCTOU) bugs. + /// You should only use it in scenarios where those bugs are not an issue. + /// + /// # Examples + /// + /// ```no_run + /// use nu_path::AbsolutePath; + /// + /// let path = AbsolutePath::try_new("/does_not_exist").unwrap(); + /// assert!(!path.try_exists().unwrap()); + /// + /// let path = AbsolutePath::try_new("/root/secret_file.txt").unwrap(); + /// assert!(path.try_exists().is_err()); + /// ``` + #[inline] + pub fn try_exists(&self) -> io::Result { + self.inner.try_exists() + } + + /// Returns `true` if the path exists on disk and is pointing at a symbolic link. + /// + /// This function will not traverse symbolic links. + /// In case of a broken symbolic link this will also return true. + /// + /// If you cannot access the directory containing the file, e.g., because of a permission error, + /// this will return false. + /// + /// # Examples + /// + #[cfg_attr(unix, doc = "```no_run")] + #[cfg_attr(not(unix), doc = "```ignore")] + /// use nu_path::AbsolutePath; + /// use std::os::unix::fs::symlink; + /// + /// let link_path = AbsolutePath::try_new("/link").unwrap(); + /// symlink("/origin_does_not_exist/", link_path).unwrap(); + /// assert_eq!(link_path.is_symlink(), true); + /// assert_eq!(link_path.exists(), false); + /// ``` + #[must_use] + #[inline] + pub fn is_symlink(&self) -> bool { + self.inner.is_symlink() + } + + /// Queries the metadata about a file without following symlinks. + /// + /// This is an alias to [`std::fs::symlink_metadata`]. + /// + /// # Examples + /// + /// ```no_run + /// use nu_path::AbsolutePath; + /// + /// let path = AbsolutePath::try_new("/Minas/tirith").unwrap(); + /// let metadata = path.symlink_metadata().expect("symlink_metadata call failed"); + /// println!("{:?}", metadata.file_type()); + /// ``` + #[inline] + pub fn symlink_metadata(&self) -> io::Result { + self.inner.symlink_metadata() + } +} + +impl CanonicalPath { + /// Returns a [`CanonicalPath`] as a [`AbsolutePath`]. + /// + /// # Examples + /// + /// ```no_run + /// use nu_path::AbsolutePath; + /// + /// let absolute = AbsolutePath::try_new("/test").unwrap(); + /// let p = absolute.canonicalize().unwrap(); + /// assert_eq!(absolute, p.as_absolute()); + /// ``` + #[inline] + pub fn as_absolute(&self) -> &AbsolutePath { + self.cast() + } +} + +impl fmt::Debug for Path { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self.inner, fmt) + } +} + +impl Clone for Box> { + #[inline] + fn clone(&self) -> Self { + std_box_to_box(self.inner.into()) + } +} + +impl ToOwned for Path { + type Owned = PathBuf; + + #[inline] + fn to_owned(&self) -> Self::Owned { + self.to_path_buf() + } + + #[inline] + fn clone_into(&self, target: &mut PathBuf) { + self.inner.clone_into(&mut target.inner); + } +} + +impl<'a, Form: PathForm> IntoIterator for &'a Path { + type Item = &'a OsStr; + + type IntoIter = std::path::Iter<'a>; + + #[inline] + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +/// An iterator over [`Path`] and its ancestors. +/// +/// This `struct` is created by the [`ancestors`](Path::ancestors) method on [`Path`]. +/// See its documentation for more. +/// +/// # Examples +/// +/// ``` +/// use nu_path::Path; +/// +/// let path = Path::new("/foo/bar"); +/// +/// for ancestor in path.ancestors() { +/// println!("{}", ancestor.display()); +/// } +/// ``` +#[derive(Clone, Copy)] +pub struct Ancestors<'a, Form: PathForm> { + _form: PhantomData, + inner: std::path::Ancestors<'a>, +} + +impl fmt::Debug for Ancestors<'_, Form> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self.inner, f) + } +} + +impl<'a, Form: PathForm> Iterator for Ancestors<'a, Form> { + type Item = &'a Path; + + fn next(&mut self) -> Option { + self.inner.next().map(Path::new_unchecked) + } +} + +impl FusedIterator for Ancestors<'_, Form> {} + +/// A wrapper around [`std::path::PathBuf`] with extra invariants determined by its `Form`. +/// +/// The possible path forms are [`Any`], [`Relative`], [`Absolute`], or [`Canonical`]. +/// To learn more, view the documentation on [`PathForm`] or any of the individual forms. +/// +/// There are also several type aliases available, corresponding to each [`PathForm`]: +/// - [`RelativePathBuf`] (same as [`PathBuf`]) +/// - [`AbsolutePathBuf`] (same as [`PathBuf`]) +/// - [`CanonicalPathBuf`] (same as [`PathBuf`]) +/// +/// If the `Form` is not specified, then it defaults to [`Any`], +/// so [`PathBuf`] and [`PathBuf`] are one in the same. +/// +/// # Examples +/// +/// To create a [`PathBuf`] with [`Any`] form, you can use the same techniques as when creating +/// a [`std::path::PathBuf`]. +/// +/// ``` +/// use nu_path::PathBuf; +/// +/// let path = PathBuf::from(r"C:\windows\system32.dll"); +/// +/// let mut path1 = PathBuf::new(); +/// path1.push(r"C:\"); +/// path1.push("windows"); +/// path1.push("system32"); +/// path1.set_extension("dll"); +/// +/// let path2: PathBuf = [r"C:\", "windows", "system32.dll"].iter().collect(); +/// +/// assert_eq!(path1, path2); +/// ``` +/// +/// # Converting to [`std::path`] types +/// +/// [`PathBuf`]s with form [`Any`] cannot be easily referenced as a [`std::path::Path`] +/// or converted to a [`std::path::PathBuf`] by design. +/// Other Nushell crates need to account for the emulated current working directory +/// before passing a path to functions in [`std`] or other third party crates. +/// You can [`join`](Path::join) a [`Path`] onto an [`AbsolutePath`] or a [`CanonicalPath`]. +/// This will return an [`AbsolutePathBuf`] which can be easily referenced as a [`std::path::Path`]. +/// If you really mean it, you can instead use [`as_relative_std_path`](Path::as_relative_std_path) +/// or [`into_relative_std_path_buf`](PathBuf::into_relative_std_path_buf) +/// to get the underlying [`std::path::Path`] or [`std::path::PathBuf`] from a [`PathBuf`]. +/// But this may cause third-party code to use [`std::env::current_dir`] to resolve +/// the path which is almost always incorrect behavior. Extra care is needed to ensure that this +/// is not the case after using [`as_relative_std_path`](Path::as_relative_std_path) +/// or [`into_relative_std_path_buf`](PathBuf::into_relative_std_path_buf). +#[repr(transparent)] +pub struct PathBuf { + _form: PhantomData, + inner: std::path::PathBuf, +} + +/// A path buf that is strictly relative. +/// +/// I.e., this path buf is guaranteed to never be absolute. +/// +/// [`RelativePathBuf`]s cannot be easily referenced as a [`std::path::Path`] +/// or converted to a [`std::path::PathBuf`] by design. +/// Other Nushell crates need to account for the emulated current working directory +/// before passing a path to functions in [`std`] or other third party crates. +/// You can [`join`](Path::join) a [`RelativePath`] onto an [`AbsolutePath`] or a [`CanonicalPath`]. +/// This will return an [`AbsolutePathBuf`] which can be easily referenced as a [`std::path::Path`]. +/// If you really mean it, you can instead use +/// [`as_relative_std_path`](RelativePath::as_relative_std_path) +/// or [`into_relative_std_path_buf`](RelativePathBuf::into_relative_std_path_buf) +/// to get the underlying [`std::path::Path`] or [`std::path::PathBuf`] from a [`RelativePathBuf`]. +/// But this may cause third-party code to use [`std::env::current_dir`] to resolve +/// the path which is almost always incorrect behavior. Extra care is needed to ensure that this +/// is not the case after using [`as_relative_std_path`](RelativePath::as_relative_std_path) +/// or [`into_relative_std_path_buf`](RelativePathBuf::into_relative_std_path_buf). +/// +/// # Examples +/// +/// [`RelativePathBuf`]s can be created by using [`try_into_relative`](PathBuf::try_into_relative) +/// on a [`PathBuf`] or by using [`to_path_buf`](Path::to_path_buf) on a [`RelativePath`]. +/// +/// ``` +/// use nu_path::{PathBuf, RelativePath, RelativePathBuf}; +/// +/// let path_buf = PathBuf::from("foo.txt"); +/// let path_buf = path_buf.try_into_relative().unwrap(); +/// +/// let path = RelativePath::try_new("foo.txt").unwrap(); +/// let path_buf2 = path.to_path_buf(); +/// +/// assert_eq!(path_buf, path_buf2); +/// ``` +/// +/// You can also use `RelativePathBuf::try_from` or `try_into`. +/// This supports attempted conversions from [`Path`] as well as types in [`std::path`]. +/// +/// ``` +/// use nu_path::{Path, RelativePathBuf}; +/// +/// let path1 = RelativePathBuf::try_from("foo.txt").unwrap(); +/// +/// let path2 = Path::new("foo.txt"); +/// let path2 = RelativePathBuf::try_from(path2).unwrap(); +/// +/// let path3 = std::path::PathBuf::from("foo.txt"); +/// let path3: RelativePathBuf = path3.try_into().unwrap(); +/// +/// assert_eq!(path1, path2); +/// assert_eq!(path2, path3); +/// ``` +pub type RelativePathBuf = PathBuf; + +/// A path buf that is strictly absolute. +/// +/// I.e., this path buf is guaranteed to never be relative. +/// +/// # Examples +/// +/// [`AbsolutePathBuf`]s can be created by using [`try_into_absolute`](PathBuf::try_into_absolute) +/// on a [`PathBuf`] or by using [`to_path_buf`](Path::to_path_buf) on an [`AbsolutePath`]. +/// +#[cfg_attr(not(windows), doc = "```")] +#[cfg_attr(windows, doc = "```no_run")] +/// use nu_path::{AbsolutePath, AbsolutePathBuf, PathBuf}; +/// +/// let path_buf1 = PathBuf::from("/foo"); +/// let path_buf1 = path_buf1.try_into_absolute().unwrap(); +/// +/// let path = AbsolutePath::try_new("/foo").unwrap(); +/// let path_buf2 = path.to_path_buf(); +/// +/// assert_eq!(path_buf1, path_buf2); +/// ``` +/// +/// You can also use `AbsolutePathBuf::try_from` or `try_into`. +/// This supports attempted conversions from [`Path`] as well as types in [`std::path`]. +/// +#[cfg_attr(not(windows), doc = "```")] +#[cfg_attr(windows, doc = "```no_run")] +/// use nu_path::{AbsolutePathBuf, Path}; +/// +/// let path1 = AbsolutePathBuf::try_from("/foo").unwrap(); +/// +/// let path2 = Path::new("/foo"); +/// let path2 = AbsolutePathBuf::try_from(path2).unwrap(); +/// +/// let path3 = std::path::PathBuf::from("/foo"); +/// let path3: AbsolutePathBuf = path3.try_into().unwrap(); +/// +/// assert_eq!(path1, path2); +/// assert_eq!(path2, path3); +/// ``` +pub type AbsolutePathBuf = PathBuf; + +/// An absolute, canonical path buf. +/// +/// # Examples +/// +/// [`CanonicalPathBuf`]s can only be created by using [`canonicalize`](Path::canonicalize) on +/// an [`AbsolutePath`]. [`CanonicalPathBuf`]s can be converted back to [`AbsolutePathBuf`]s via +/// [`into_absolute`](CanonicalPathBuf::into_absolute). +/// +/// ```no_run +/// use nu_path::AbsolutePathBuf; +/// +/// let path = AbsolutePathBuf::try_from("/foo").unwrap(); +/// +/// let canonical = path.canonicalize().expect("canonicalization failed"); +/// +/// assert_eq!(path, canonical.into_absolute()); +/// ``` +pub type CanonicalPathBuf = PathBuf; + +impl PathBuf { + /// Create a new [`PathBuf`] of any form without validiting invariants. + #[inline] + pub(crate) fn new_unchecked(buf: std::path::PathBuf) -> Self { + debug_assert!(Form::invariants_satisfied(&buf)); + Self { + _form: PhantomData, + inner: buf, + } + } + + /// Coerces to a [`Path`] slice. + /// + /// # Examples + /// + /// ``` + /// use nu_path::{Path, PathBuf}; + /// + /// let p = PathBuf::from("/test"); + /// assert_eq!(Path::new("/test"), p.as_path()); + /// ``` + #[must_use] + #[inline] + pub fn as_path(&self) -> &Path { + Path::new_unchecked(&self.inner) + } + + /// Truncates `self` to [`self.parent`](Path::parent). + /// + /// Returns `false` and does nothing if [`self.parent`](Path::parent) is [`None`]. + /// Otherwise, returns `true`. + /// + /// # Examples + /// + /// ``` + /// use nu_path::{Path, PathBuf}; + /// + /// let mut p = PathBuf::from("/spirited/away.rs"); + /// + /// p.pop(); + /// assert_eq!(Path::new("/spirited"), p); + /// p.pop(); + /// assert_eq!(Path::new("/"), p); + /// ``` + #[inline] + pub fn pop(&mut self) -> bool { + self.inner.pop() + } + + /// Consumes the [`PathBuf`], returning its internal [`OsString`] storage. + /// + /// # Examples + /// + /// ``` + /// use nu_path::PathBuf; + /// + /// let p = PathBuf::from("/the/head"); + /// let os_str = p.into_os_string(); + /// ``` + #[inline] + pub fn into_os_string(self) -> OsString { + self.inner.into_os_string() + } + + /// Converts this [`PathBuf`] into a [boxed](Box) [`Path`]. + #[inline] + pub fn into_boxed_path(self) -> Box> { + std_box_to_box(self.inner.into_boxed_path()) + } + + /// Returns the [`capacity`](OsString::capacity) of the underlying [`OsString`]. + #[must_use] + #[inline] + pub fn capacity(&self) -> usize { + self.inner.capacity() + } + + /// Invokes [`reserve`](OsString::reserve) on the underlying [`OsString`]. + #[inline] + pub fn reserve(&mut self, additional: usize) { + self.inner.reserve(additional) + } + + /// Invokes [`try_reserve`](OsString::try_reserve) on the underlying [`OsString`]. + #[inline] + pub fn try_reserve(&mut self, additional: usize) -> Result<(), TryReserveError> { + self.inner.try_reserve(additional) + } + + /// Invokes [`reserve_exact`](OsString::reserve_exact) on the underlying [`OsString`]. + #[inline] + pub fn reserve_exact(&mut self, additional: usize) { + self.inner.reserve_exact(additional) + } + + /// Invokes [`try_reserve_exact`](OsString::try_reserve_exact) on the underlying [`OsString`]. + #[inline] + pub fn try_reserve_exact(&mut self, additional: usize) -> Result<(), TryReserveError> { + self.inner.try_reserve_exact(additional) + } + + /// Invokes [`shrink_to_fit`](OsString::shrink_to_fit) on the underlying [`OsString`]. + #[inline] + pub fn shrink_to_fit(&mut self) { + self.inner.shrink_to_fit() + } + + /// Invokes [`shrink_to`](OsString::shrink_to) on the underlying [`OsString`]. + #[inline] + pub fn shrink_to(&mut self, min_capacity: usize) { + self.inner.shrink_to(min_capacity) + } + + /// Consumes a [`PathBuf`], returning it with a different form. + /// + /// [`PathForm`]s can be converted to one another based on [`PathCast`] implementations. + /// Namely, the following form conversions are possible: + /// - [`Relative`], [`Absolute`], or [`Canonical`] into [`Any`]. + /// - [`Canonical`] into [`Absolute`]. + /// - Any form into itself. + /// + /// # Examples + /// + /// ``` + /// use nu_path::{PathBuf, RelativePathBuf}; + /// + /// let p = RelativePathBuf::try_from("test.txt").unwrap(); + /// let p: PathBuf = p.cast_into(); + /// assert_eq!(PathBuf::from("test.txt"), p); + /// ``` + #[inline] + pub fn cast_into(self) -> PathBuf + where + To: PathForm, + Form: PathCast, + { + PathBuf::new_unchecked(self.inner) + } + + /// Consumes a [`PathBuf`], returning it with form [`Any`]. + /// + /// # Examples + /// + /// ``` + /// use nu_path::{PathBuf, RelativePathBuf}; + /// + /// let p = RelativePathBuf::try_from("test.txt").unwrap(); + /// assert_eq!(PathBuf::from("test.txt"), p.into_any()); + /// ``` + #[inline] + pub fn into_any(self) -> PathBuf { + PathBuf::new_unchecked(self.inner) + } +} + +impl PathBuf { + /// Creates an empty [`PathBuf`]. + /// + /// # Examples + /// + /// ``` + /// use nu_path::PathBuf; + /// + /// let path = PathBuf::new(); + /// ``` + #[must_use] + #[inline] + pub fn new() -> Self { + Self::new_unchecked(std::path::PathBuf::new()) + } + + /// Creates a new [`PathBuf`] with a given capacity used to create the internal [`OsString`]. + /// See [`with_capacity`](OsString::with_capacity) defined on [`OsString`]. + /// + /// # Examples + /// + /// ``` + /// use nu_path::PathBuf; + /// + /// let mut path = PathBuf::with_capacity(10); + /// let capacity = path.capacity(); + /// + /// // This push is done without reallocating + /// path.push(r"C:\"); + /// + /// assert_eq!(capacity, path.capacity()); + /// ``` + #[inline] + #[must_use] + pub fn with_capacity(capacity: usize) -> Self { + Self::new_unchecked(std::path::PathBuf::with_capacity(capacity)) + } + + /// Returns a mutable reference to the underlying [`OsString`]. + /// + /// # Examples + /// + /// ``` + /// use nu_path::{Path, PathBuf}; + /// + /// let mut path = PathBuf::from("/foo"); + /// + /// path.push("bar"); + /// assert_eq!(path, Path::new("/foo/bar")); + /// + /// // OsString's `push` does not add a separator. + /// path.as_mut_os_string().push("baz"); + /// assert_eq!(path, Path::new("/foo/barbaz")); + /// ``` + #[must_use] + #[inline] + pub fn as_mut_os_string(&mut self) -> &mut OsString { + self.inner.as_mut_os_string() + } + + /// Invokes [`clear`](OsString::clear) on the underlying [`OsString`]. + #[inline] + pub fn clear(&mut self) { + self.inner.clear() + } + + /// Consumes a [`PathBuf`], returning an `Ok` [`RelativePathBuf`] if the [`PathBuf`] + /// is relative. Otherwise, returns the original [`PathBuf`] as an `Err`. + /// + /// # Examples + /// + /// ``` + /// use nu_path::PathBuf; + /// + /// assert!(PathBuf::from("test.txt").try_into_relative().is_ok()); + /// ``` + #[inline] + pub fn try_into_relative(self) -> Result { + if self.inner.is_relative() { + Ok(PathBuf::new_unchecked(self.inner)) + } else { + Err(self) + } + } + + /// Consumes a [`PathBuf`], returning an `Ok` [`AbsolutePathBuf`] if the [`PathBuf`] + /// is absolute. Otherwise, returns the original [`PathBuf`] as an `Err`. + /// + /// # Examples + /// + /// ``` + /// use nu_path::PathBuf; + /// + /// assert!(PathBuf::from("test.txt").try_into_absolute().is_err()); + /// ``` + #[inline] + pub fn try_into_absolute(self) -> Result { + if self.inner.is_absolute() { + Ok(PathBuf::new_unchecked(self.inner)) + } else { + Err(self) + } + } +} + +impl PathBuf { + /// Extends `self` with `path`. + /// + /// If `path` is absolute, it replaces the current path. + /// + /// On Windows: + /// + /// * if `path` has a root but no prefix (e.g., `\windows`), it + /// replaces everything except for the prefix (if any) of `self`. + /// * if `path` has a prefix but no root, it replaces `self`. + /// * if `self` has a verbatim prefix (e.g. `\\?\C:\windows`) + /// and `path` is not empty, the new path is normalized: all references + /// to `.` and `..` are removed. + /// + /// Consider using [`Path::join`] if you need a new [`PathBuf`] instead of + /// using this function on a cloned [`PathBuf`]. + /// + /// # Examples + /// + /// Pushing a relative path extends the existing path: + /// + /// ``` + /// use nu_path::PathBuf; + /// + /// let mut path = PathBuf::from("/tmp"); + /// path.push("file.bk"); + /// assert_eq!(path, PathBuf::from("/tmp/file.bk")); + /// ``` + /// + /// Pushing an absolute path replaces the existing path: + /// + /// ``` + /// use nu_path::PathBuf; + /// + /// let mut path = PathBuf::from("/tmp"); + /// path.push("/etc"); + /// assert_eq!(path, PathBuf::from("/etc")); + /// ``` + #[inline] + pub fn push(&mut self, path: impl AsRef) { + self.inner.push(&path.as_ref().inner) + } +} + +impl PathBuf { + /// Updates [`self.file_name`](Path::file_name) to `file_name`. + /// + /// If [`self.file_name`](Path::file_name) was [`None`], + /// this is equivalent to pushing `file_name`. + /// + /// Otherwise it is equivalent to calling [`pop`](PathBuf::pop) and then pushing `file_name`. + /// The new path will be a sibling of the original path. + /// (That is, it will have the same parent.) + /// + /// # Examples + /// + /// ``` + /// use nu_path::PathBuf; + /// + /// let mut buf = PathBuf::from("/"); + /// assert!(buf.file_name() == None); + /// + /// buf.set_file_name("foo.txt"); + /// assert!(buf == PathBuf::from("/foo.txt")); + /// assert!(buf.file_name().is_some()); + /// + /// buf.set_file_name("bar.txt"); + /// assert!(buf == PathBuf::from("/bar.txt")); + /// + /// buf.set_file_name("baz"); + /// assert!(buf == PathBuf::from("/baz")); + /// ``` + #[inline] + pub fn set_file_name(&mut self, file_name: impl AsRef) { + self.inner.set_file_name(file_name) + } + + /// Updates [`self.extension`](Path::extension) to `Some(extension)` or to [`None`] if + /// `extension` is empty. + /// + /// Returns `false` and does nothing if [`self.file_name`](Path::file_name) is [`None`], + /// returns `true` and updates the extension otherwise. + /// + /// If [`self.extension`](Path::extension) is [`None`], the extension is added; otherwise + /// it is replaced. + /// + /// If `extension` is the empty string, [`self.extension`](Path::extension) will be [`None`] + /// afterwards, not `Some("")`. + /// + /// # Caveats + /// + /// The new `extension` may contain dots and will be used in its entirety, + /// but only the part after the final dot will be reflected in + /// [`self.extension`](Path::extension). + /// + /// If the file stem contains internal dots and `extension` is empty, part of the + /// old file stem will be considered the new [`self.extension`](Path::extension). + /// + /// # Examples + /// + /// ``` + /// use nu_path::{Path, PathBuf}; + /// + /// let mut p = PathBuf::from("/feel/the"); + /// + /// p.set_extension("force"); + /// assert_eq!(Path::new("/feel/the.force"), p.as_path()); + /// + /// p.set_extension("dark.side"); + /// assert_eq!(Path::new("/feel/the.dark.side"), p.as_path()); + /// + /// p.set_extension("cookie"); + /// assert_eq!(Path::new("/feel/the.dark.cookie"), p.as_path()); + /// + /// p.set_extension(""); + /// assert_eq!(Path::new("/feel/the.dark"), p.as_path()); + /// + /// p.set_extension(""); + /// assert_eq!(Path::new("/feel/the"), p.as_path()); + /// + /// p.set_extension(""); + /// assert_eq!(Path::new("/feel/the"), p.as_path()); + /// ``` + #[inline] + pub fn set_extension(&mut self, extension: impl AsRef) -> bool { + self.inner.set_extension(extension) + } +} + +impl PathBuf { + /// Consumes a [`PathBuf`] and returns the, potentially relative, + /// underlying [`std::path::PathBuf`]. + /// + /// # Note + /// + /// Caution should be taken when using this function. Nushell keeps track of an emulated current + /// working directory, and using the [`std::path::PathBuf`] returned from this method + /// will likely use [`std::env::current_dir`] to resolve the path instead of + /// using the emulated current working directory. + /// + /// Instead, you should probably join this path onto the emulated current working directory. + /// Any [`AbsolutePath`] or [`CanonicalPath`] will also suffice. + /// + /// # Examples + /// + /// ``` + /// use nu_path::PathBuf; + /// + /// let p = PathBuf::from("test.txt"); + /// assert_eq!(std::path::PathBuf::from("test.txt"), p.into_relative_std_path_buf()); + /// ``` + #[inline] + pub fn into_relative_std_path_buf(self) -> std::path::PathBuf { + self.inner + } +} + +impl PathBuf { + /// Consumes a [`PathBuf`] and returns the underlying [`std::path::PathBuf`]. + /// + /// # Examples + /// + #[cfg_attr(not(windows), doc = "```")] + #[cfg_attr(windows, doc = "```no_run")] + /// use nu_path::AbsolutePathBuf; + /// + /// let p = AbsolutePathBuf::try_from("/test").unwrap(); + /// assert_eq!(std::path::PathBuf::from("/test"), p.into_std_path_buf()); + /// ``` + #[inline] + pub fn into_std_path_buf(self) -> std::path::PathBuf { + self.inner + } +} + +impl CanonicalPathBuf { + /// Consumes a [`CanonicalPathBuf`] and returns an [`AbsolutePathBuf`]. + /// + /// # Examples + /// + /// ```no_run + /// use nu_path::AbsolutePathBuf; + /// + /// let absolute = AbsolutePathBuf::try_from("/test").unwrap(); + /// let p = absolute.canonicalize().unwrap(); + /// assert_eq!(absolute, p.into_absolute()); + /// ``` + #[inline] + pub fn into_absolute(self) -> AbsolutePathBuf { + self.cast_into() + } +} + +impl Default for PathBuf { + #[inline] + fn default() -> Self { + Self::new() + } +} + +impl Clone for PathBuf { + #[inline] + fn clone(&self) -> Self { + Self { + _form: PhantomData, + inner: self.inner.clone(), + } + } +} + +impl fmt::Debug for PathBuf { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&**self, f) + } +} + +impl Deref for PathBuf { + type Target = Path; + + #[inline] + fn deref(&self) -> &Self::Target { + self.as_path() + } +} + +impl DerefMut for PathBuf { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + // Safety: `Path` is a repr(transparent) wrapper around `std::path::Path`. + let path: &mut std::path::Path = &mut self.inner; + let ptr = std::ptr::from_mut(path) as *mut Path; + unsafe { &mut *ptr } + } +} + +impl, To: PathForm> Borrow> for PathBuf { + #[inline] + fn borrow(&self) -> &Path { + self.cast() + } +} + +impl Borrow for PathBuf { + #[inline] + fn borrow(&self) -> &std::path::Path { + self.as_ref() + } +} + +impl Borrow for std::path::PathBuf { + #[inline] + fn borrow(&self) -> &Path { + self.as_ref() + } +} + +impl FromStr for PathBuf { + type Err = Infallible; + + #[inline] + fn from_str(s: &str) -> Result { + Ok(s.into()) + } +} + +impl FromStr for RelativePathBuf { + type Err = TryRelativeError; + + #[inline] + fn from_str(s: &str) -> Result { + s.try_into() + } +} + +impl FromStr for AbsolutePathBuf { + type Err = TryAbsoluteError; + + #[inline] + fn from_str(s: &str) -> Result { + s.try_into() + } +} + +impl> Extend

for PathBuf { + fn extend>(&mut self, iter: T) { + for path in iter { + self.push(path); + } + } +} + +impl> FromIterator

for PathBuf { + fn from_iter>(iter: T) -> Self { + let mut buf = Self::new_unchecked(std::path::PathBuf::new()); + buf.extend(iter); + buf + } +} + +impl<'a, Form: PathForm> IntoIterator for &'a PathBuf { + type Item = &'a OsStr; + + type IntoIter = std::path::Iter<'a>; + + #[inline] + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +#[inline] +fn box_to_box_unchecked(path: Box>) -> Box> { + // Safety: `Path` and `Path` differ only by PhantomData tag. + let ptr = Box::into_raw(path) as *mut Path; + unsafe { Box::from_raw(ptr) } +} + +#[inline] +fn std_box_to_box(path: Box) -> Box> { + // Safety: `Path` is a repr(transparent) wrapper around `std::path::Path`. + let ptr = Box::into_raw(path) as *mut Path; + unsafe { Box::from_raw(ptr) } +} + +#[inline] +fn std_arc_to_arc(path: Arc) -> Arc> { + // Safety: `Path` is a repr(transparent) wrapper around `std::path::Path`. + let ptr = Arc::into_raw(path) as *mut Path; + unsafe { Arc::from_raw(ptr) } +} + +#[inline] +fn std_rc_to_rc(path: Rc) -> Rc> { + // Safety: `Path` is a repr(transparent) wrapper around `std::path::Path`. + let ptr = Rc::into_raw(path) as *mut Path; + unsafe { Rc::from_raw(ptr) } +} + +/* +================================================================================ + AsRef +================================================================================ +*/ + +// Here we match all `AsRef` implementations on `std::path::Path` and `std::path::PathBuf`, +// adding casting variations where possible. + +macro_rules! impl_as_ref { + ([$($from:ty),* $(,)?] => $to:ty |$self:ident| $cast:block) => { + $( + impl AsRef<$to> for $from { + #[inline] + fn as_ref(&$self) -> &$to $cast + } + )* + }; +} + +// === To and from crate types === + +impl, To: PathForm> AsRef> for Path { + #[inline] + fn as_ref(&self) -> &Path { + self.cast() + } +} + +impl, To: PathForm> AsRef> for PathBuf { + #[inline] + fn as_ref(&self) -> &Path { + self.cast() + } +} + +impl_as_ref!( + [ + Box, Box, Box, + Cow<'_, RelativePath>, Cow<'_, AbsolutePath>, Cow<'_, CanonicalPath>, + Rc, Rc, Rc, + Arc, Arc, Arc, + ] + => Path |self| { self.cast() } +); + +impl_as_ref!( + [Box, Cow<'_, CanonicalPath>, Rc, Arc] + => AbsolutePath |self| { self.cast() } +); + +// === To and from std::path types === + +impl AsRef for Path { + #[inline] + fn as_ref(&self) -> &std::path::Path { + self.as_std_path() + } +} + +impl AsRef for PathBuf { + #[inline] + fn as_ref(&self) -> &std::path::Path { + self.as_std_path() + } +} + +impl_as_ref!( + [std::path::Path, std::path::PathBuf, std::path::Component<'_>] + => Path |self| { Path::new(self) } +); + +impl_as_ref!( + [Box, Cow<'_, std::path::Path>, Rc, Arc] + => Path |self| { Path::new(self.as_os_str()) } +); + +// === To and from string types === + +impl AsRef for Path { + #[inline] + fn as_ref(&self) -> &OsStr { + self.as_os_str() + } +} + +impl AsRef for PathBuf { + #[inline] + fn as_ref(&self) -> &OsStr { + self.as_os_str() + } +} + +impl_as_ref!([OsStr, OsString, Cow<'_, OsStr>, str, String] => Path |self| { Path::new(self) }); + +/* +================================================================================ + From +================================================================================ +*/ + +// Here we match all `From` implementations on `std::path::Path` and `std::path::PathBuf`, +// adding casting variations where possible. + +macro_rules! impl_from { + ([$($from:ty),* $(,)?] => $to:ty |$value:ident| $convert:block) => { + $( + impl From<$from> for $to { + #[inline] + fn from($value: $from) -> Self $convert + } + )* + }; + (<$form:ident> $from:ty => $to:ty |$value:ident| $convert:block) => { + impl<$form: PathForm> From<$from> for $to { + #[inline] + fn from($value: $from) -> Self $convert + } + }; +} + +macro_rules! impl_into_std { + (<$form:ident> $from:ty => [$($to:ty),* $(,)?] |$value:ident| $convert:block) => { + $( + impl<$form: IsAbsolute> From<$from> for $to { + #[inline] + fn from($value: $from) -> Self $convert + } + )* + }; +} + +// ===== Owned to Owned ===== + +// === To and from crate types === + +impl_from!([RelativePathBuf, AbsolutePathBuf, CanonicalPathBuf] => PathBuf + |buf| { buf.cast_into() } +); +impl_from!([CanonicalPathBuf] => AbsolutePathBuf |buf| { buf.cast_into() }); + +#[inline] +fn box_to_box, To: PathForm>(path: Box>) -> Box> { + box_to_box_unchecked(path) +} +impl_from!([Box, Box, Box] => Box + |path| { box_to_box(path) } +); +impl_from!([Box] => Box |path| { box_to_box(path) }); + +impl_from!( PathBuf => Box> |buf| { buf.into_boxed_path() }); +impl_from!([RelativePathBuf, AbsolutePathBuf, CanonicalPathBuf] => Box + |buf| { buf.into_boxed_path().into() } +); +impl_from!([CanonicalPathBuf] => Box |buf| { buf.into_boxed_path().into() }); + +impl_from!( Box> => PathBuf |path| { path.into_path_buf() }); +impl_from!([Box, Box, Box] => PathBuf + |path| { path.into_path_buf().into() } +); +impl_from!([Box] => AbsolutePathBuf |path| { path.into_path_buf().into() }); + +impl_from!( PathBuf => Cow<'_, Path> |buf| { Self::Owned(buf) }); +impl_from!([RelativePathBuf, AbsolutePathBuf, CanonicalPathBuf] => Cow<'_, Path> + |buf| { Self::Owned(buf.into()) } +); +impl_from!([CanonicalPathBuf] => Cow<'_, AbsolutePath> |buf| { Self::Owned(buf.into()) }); + +impl_from!( Cow<'_, Path> => PathBuf |cow| { cow.into_owned() }); +impl_from!([Cow<'_, RelativePath>, Cow<'_, AbsolutePath>, Cow<'_, CanonicalPath>] => PathBuf + |cow| { cow.into_owned().into() } +); +impl_from!([Cow<'_, CanonicalPath>] => AbsolutePathBuf |cow| { cow.into_owned().into() }); + +#[inline] +fn cow_to_box(cow: Cow<'_, From>) -> Box +where + From: ?Sized + ToOwned, + for<'a> &'a From: Into>, + From::Owned: Into>, + To: ?Sized, +{ + match cow { + Cow::Borrowed(path) => path.into(), + Cow::Owned(path) => path.into(), + } +} +impl_from!( Cow<'_, Path> => Box> |cow| { cow_to_box(cow) }); +impl_from!([Cow<'_, RelativePath>, Cow<'_, AbsolutePath>, Cow<'_, CanonicalPath>] => Box + |cow| { cow_to_box(cow) } +); +impl_from!([Cow<'_, CanonicalPath>] => Box |cow| { cow_to_box(cow) }); + +#[inline] +fn buf_to_arc, To: PathForm>(buf: PathBuf) -> Arc> { + std_arc_to_arc(buf.inner.into()) +} +impl_from!( PathBuf => Arc> |buf| { buf_to_arc(buf) }); +impl_from!([RelativePathBuf, AbsolutePathBuf, CanonicalPathBuf] => Arc + |buf| { buf_to_arc(buf) } +); +impl_from!([CanonicalPathBuf] => Arc |buf| { buf_to_arc(buf) }); + +#[inline] +fn buf_to_rc, To: PathForm>(buf: PathBuf) -> Rc> { + std_rc_to_rc(buf.inner.into()) +} +impl_from!( PathBuf => Rc> |buf| { buf_to_rc(buf) }); +impl_from!([RelativePathBuf, AbsolutePathBuf, CanonicalPathBuf] => Rc + |buf| { buf_to_rc(buf) } +); +impl_from!([CanonicalPathBuf] => Rc |buf| { buf_to_rc(buf) }); + +// === To and from std::path types === + +impl_into_std!( PathBuf => [std::path::PathBuf] |buf| { buf.inner }); +impl_into_std!( + PathBuf => [ + Box, Cow<'_, std::path::Path>, Arc, Rc + ] + |buf| { buf.inner.into() } +); +impl_into_std!( Box> => [std::path::PathBuf, Box] + |path| { path.inner.into() } +); + +impl_from!([std::path::PathBuf] => PathBuf |buf| { Self::new_unchecked(buf) }); +impl_from!([Box] => PathBuf |path| { Self::new_unchecked(path.into()) }); +impl_from!([Cow<'_, std::path::Path>] => PathBuf |cow| { Self::new_unchecked(cow.into()) }); + +impl From> for Box { + #[inline] + fn from(path: Box) -> Self { + std_box_to_box(path) + } +} +impl_from!([std::path::PathBuf] => Box |buf| { buf.into_boxed_path().into() }); +impl_from!([Cow<'_, std::path::Path>] => Box |cow| { cow_to_box(cow) }); + +// === To and from string types === + +impl_from!( PathBuf => OsString |buf| { buf.inner.into() }); +impl_from!([OsString, String] => PathBuf |s| { Self::new_unchecked(s.into()) }); + +// ===== Borrowed to Owned ===== + +// === To and from crate types === +// Here we also add casting conversions from `T: impl AsRef>` to `PathBuf`. + +impl, To: PathForm> From<&Path> for Box> { + #[inline] + fn from(path: &Path) -> Self { + std_box_to_box(path.inner.into()) + } +} + +impl<'a, Source: PathCast, To: PathForm> From<&'a Path> for Cow<'a, Path> { + #[inline] + fn from(path: &'a Path) -> Self { + path.cast().into() + } +} + +impl<'a, Source: PathCast, To: PathForm> From<&'a PathBuf> for Cow<'a, Path> { + #[inline] + fn from(buf: &'a PathBuf) -> Self { + buf.cast().into() + } +} + +impl, To: PathForm> From<&Path> for Arc> { + #[inline] + fn from(path: &Path) -> Self { + std_arc_to_arc(path.inner.into()) + } +} + +impl, To: PathForm> From<&Path> for Rc> { + #[inline] + fn from(path: &Path) -> Self { + std_rc_to_rc(path.inner.into()) + } +} + +impl> From<&T> for RelativePathBuf { + #[inline] + fn from(s: &T) -> Self { + Self::new_unchecked(s.as_ref().into()) + } +} + +impl> From<&T> for AbsolutePathBuf { + #[inline] + fn from(s: &T) -> Self { + Self::new_unchecked(s.as_ref().into()) + } +} + +impl> From<&T> for CanonicalPathBuf { + #[inline] + fn from(s: &T) -> Self { + Self::new_unchecked(s.as_ref().into()) + } +} + +// === To and from std::path types === + +impl_into_std!( + &Path => [Box, Arc, Rc] + |path| { path.inner.into() } +); + +impl<'a, Form: IsAbsolute> From<&'a Path> for Cow<'a, std::path::Path> { + #[inline] + fn from(path: &'a Path) -> Self { + path.inner.into() + } +} + +impl<'a, Form: IsAbsolute> From<&'a PathBuf> for Cow<'a, std::path::Path> { + #[inline] + fn from(buf: &'a PathBuf) -> Self { + Self::Borrowed(buf.as_ref()) + } +} + +impl_from!([&std::path::Path] => Box |path| { Path::new(path).into() }); + +// === To and from string types === + +impl> From<&T> for PathBuf { + #[inline] + fn from(s: &T) -> Self { + Self::new_unchecked(s.as_ref().into()) + } +} + +/* +================================================================================ + TryFrom +================================================================================ +*/ + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct TryRelativeError; + +impl fmt::Display for TryRelativeError { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(fmt, "path was not a relative path") + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct TryAbsoluteError; + +impl fmt::Display for TryAbsoluteError { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(fmt, "path was not an absolute path") + } +} + +// ===== Borrowed to borrowed ===== +// Here we match all `AsRef` implementations on `std::path::Path`. + +macro_rules! impl_try_from_borrowed_to_borrowed { + ([$($from:ty),* $(,)?], |$value:ident| $convert:block $(,)?) => { + $( + impl<'a> TryFrom<&'a $from> for &'a RelativePath { + type Error = TryRelativeError; + + #[inline] + fn try_from($value: &'a $from) -> Result $convert + } + + impl<'a> TryFrom<&'a $from> for &'a AbsolutePath { + type Error = TryAbsoluteError; + + #[inline] + fn try_from($value: &'a $from) -> Result $convert + } + )* + }; +} + +// === From crate types === + +impl<'a> TryFrom<&'a Path> for &'a RelativePath { + type Error = TryRelativeError; + + #[inline] + fn try_from(path: &'a Path) -> Result { + path.try_relative().map_err(|_| TryRelativeError) + } +} + +impl<'a> TryFrom<&'a Path> for &'a AbsolutePath { + type Error = TryAbsoluteError; + + #[inline] + fn try_from(path: &'a Path) -> Result { + path.try_absolute().map_err(|_| TryAbsoluteError) + } +} + +impl_try_from_borrowed_to_borrowed!([PathBuf], |buf| { Path::new(buf).try_into() }); + +// === From std::path types === + +impl_try_from_borrowed_to_borrowed!([std::path::Path], |path| { Path::new(path).try_into() }); +impl_try_from_borrowed_to_borrowed!([std::path::PathBuf], |buf| { Path::new(buf).try_into() }); +impl_try_from_borrowed_to_borrowed!([std::path::Component<'_>], |component| { + Path::new(component).try_into() +}); +impl_try_from_borrowed_to_borrowed!([std::path::Components<'_>], |components| { + Path::new(components).try_into() +}); +impl_try_from_borrowed_to_borrowed!([std::path::Iter<'_>], |iter| { Path::new(iter).try_into() }); + +// === From string types === + +impl_try_from_borrowed_to_borrowed!( + [OsStr, OsString, Cow<'_, OsStr>, str, String], + |s| { Path::new(s).try_into() }, +); + +// ===== Borrowed to Owned ===== +// Here we match all `From<&T>` implementations on `std::path::Path` and `std::path::PathBuf`. +// Note that to match `From<&T: AsRef>` on `std::path::PathBuf`, +// we add string conversions and a few others. + +macro_rules! impl_try_from_borrowed_to_owned { + ([$($from:ty),* $(,)?] => $rel:ty, $abs:ty $(,)?) => { + $( + impl TryFrom<&$from> for $rel { + type Error = TryRelativeError; + + #[inline] + fn try_from(path: &$from) -> Result { + let path: &RelativePath = path.try_into()?; + Ok(path.into()) + } + } + + impl TryFrom<&$from> for $abs { + type Error = TryAbsoluteError; + + #[inline] + fn try_from(path: &$from) -> Result { + let path: &AbsolutePath = path.try_into()?; + Ok(path.into()) + } + } + )* + }; + (<$life:lifetime> $from:ty => $rel:ty, $abs:ty $(,)?) => { + impl<$life> TryFrom<&$life $from> for $rel { + type Error = TryRelativeError; + + #[inline] + fn try_from(path: &$life $from) -> Result { + let path: &RelativePath = path.try_into()?; + Ok(path.into()) + } + } + + impl<$life> TryFrom<&$life $from> for $abs { + type Error = TryAbsoluteError; + + #[inline] + fn try_from(path: &$life $from) -> Result { + let path: &AbsolutePath = path.try_into()?; + Ok(path.into()) + } + } + }; +} + +// === From crate types === + +impl_try_from_borrowed_to_owned!([Path] => Box, Box); +impl_try_from_borrowed_to_owned!(<'a> Path => Cow<'a, RelativePath>, Cow<'a, AbsolutePath>); +impl_try_from_borrowed_to_owned!([Path] => Arc, Arc); +impl_try_from_borrowed_to_owned!([Path] => Rc, Rc); + +impl_try_from_borrowed_to_owned!([Path, PathBuf] => RelativePathBuf, AbsolutePathBuf); +impl_try_from_borrowed_to_owned!(<'a> PathBuf => Cow<'a, RelativePath>, Cow<'a, AbsolutePath>); + +// === From std::path types === + +impl_try_from_borrowed_to_owned!([std::path::Path] => Box, Box); + +impl_try_from_borrowed_to_owned!( + [std::path::Path, std::path::PathBuf, std::path::Component<'_>] + => RelativePathBuf, AbsolutePathBuf +); + +// === From string types === + +impl_try_from_borrowed_to_owned!( + [OsStr, OsString, Cow<'_, OsStr>, str, String] => RelativePathBuf, AbsolutePathBuf +); + +// ===== Owned to Owned ===== +// Here we match all `From` implementations on `std::path::Path` and `std::path::PathBuf` +// where `T` is an owned type. + +// === From crate types === + +impl TryFrom for RelativePathBuf { + type Error = PathBuf; + + #[inline] + fn try_from(buf: PathBuf) -> Result { + buf.try_into_relative() + } +} + +impl TryFrom for AbsolutePathBuf { + type Error = PathBuf; + + #[inline] + fn try_from(buf: PathBuf) -> Result { + buf.try_into_absolute() + } +} + +impl TryFrom> for RelativePathBuf { + type Error = Box; + + #[inline] + fn try_from(path: Box) -> Result { + if path.is_relative() { + Ok(Self::new_unchecked(path.inner.into())) + } else { + Err(path) + } + } +} + +impl TryFrom> for AbsolutePathBuf { + type Error = Box; + + #[inline] + fn try_from(path: Box) -> Result { + if path.is_absolute() { + Ok(Self::new_unchecked(path.inner.into())) + } else { + Err(path) + } + } +} + +impl TryFrom> for Box { + type Error = Box; + + #[inline] + fn try_from(path: Box) -> Result { + if path.is_relative() { + Ok(box_to_box_unchecked(path)) + } else { + Err(path) + } + } +} + +impl TryFrom> for Box { + type Error = Box; + + #[inline] + fn try_from(path: Box) -> Result { + if path.is_absolute() { + Ok(box_to_box_unchecked(path)) + } else { + Err(path) + } + } +} + +impl TryFrom for Box { + type Error = PathBuf; + + #[inline] + fn try_from(buf: PathBuf) -> Result { + RelativePathBuf::try_from(buf).map(Into::into) + } +} + +impl TryFrom for Box { + type Error = PathBuf; + + #[inline] + fn try_from(buf: PathBuf) -> Result { + AbsolutePathBuf::try_from(buf).map(Into::into) + } +} + +impl<'a> TryFrom> for RelativePathBuf { + type Error = Cow<'a, Path>; + + #[inline] + fn try_from(path: Cow<'a, Path>) -> Result { + match path { + Cow::Borrowed(path) => Self::try_from(path).map_err(|_| Cow::Borrowed(path)), + Cow::Owned(path) => Self::try_from(path).map_err(Cow::Owned), + } + } +} + +impl<'a> TryFrom> for AbsolutePathBuf { + type Error = Cow<'a, Path>; + + #[inline] + fn try_from(path: Cow<'a, Path>) -> Result { + match path { + Cow::Borrowed(path) => Self::try_from(path).map_err(|_| Cow::Borrowed(path)), + Cow::Owned(path) => Self::try_from(path).map_err(Cow::Owned), + } + } +} + +impl<'a> TryFrom> for Box { + type Error = Cow<'a, Path>; + + #[inline] + fn try_from(path: Cow<'a, Path>) -> Result { + match path { + Cow::Borrowed(path) => Box::try_from(path).map_err(|_| Cow::Borrowed(path)), + Cow::Owned(path) => Box::try_from(path).map_err(Cow::Owned), + } + } +} + +impl<'a> TryFrom> for Box { + type Error = Cow<'a, Path>; + + #[inline] + fn try_from(path: Cow<'a, Path>) -> Result { + match path { + Cow::Borrowed(path) => Box::try_from(path).map_err(|_| Cow::Borrowed(path)), + Cow::Owned(path) => Box::try_from(path).map_err(Cow::Owned), + } + } +} + +// === From std::path types === + +impl TryFrom for RelativePathBuf { + type Error = std::path::PathBuf; + + #[inline] + fn try_from(buf: std::path::PathBuf) -> Result { + Self::try_from(PathBuf::from(buf)).map_err(|buf| buf.inner) + } +} + +impl TryFrom for AbsolutePathBuf { + type Error = std::path::PathBuf; + + #[inline] + fn try_from(buf: std::path::PathBuf) -> Result { + Self::try_from(PathBuf::from(buf)).map_err(|buf| buf.inner) + } +} + +impl TryFrom> for RelativePathBuf { + type Error = Box; + + #[inline] + fn try_from(path: Box) -> Result { + if path.is_relative() { + Ok(Self::new_unchecked(path.into())) + } else { + Err(path) + } + } +} + +impl TryFrom> for AbsolutePathBuf { + type Error = Box; + + #[inline] + fn try_from(path: Box) -> Result { + if path.is_absolute() { + Ok(Self::new_unchecked(path.into())) + } else { + Err(path) + } + } +} + +impl TryFrom> for Box { + type Error = Box; + + #[inline] + fn try_from(path: Box) -> Result { + if path.is_relative() { + Ok(std_box_to_box(path)) + } else { + Err(path) + } + } +} + +impl TryFrom> for Box { + type Error = Box; + + #[inline] + fn try_from(path: Box) -> Result { + if path.is_absolute() { + Ok(std_box_to_box(path)) + } else { + Err(path) + } + } +} + +impl TryFrom for Box { + type Error = std::path::PathBuf; + + #[inline] + fn try_from(buf: std::path::PathBuf) -> Result { + RelativePathBuf::try_from(buf).map(Into::into) + } +} + +impl TryFrom for Box { + type Error = std::path::PathBuf; + + #[inline] + fn try_from(buf: std::path::PathBuf) -> Result { + AbsolutePathBuf::try_from(buf).map(Into::into) + } +} + +impl<'a> TryFrom> for RelativePathBuf { + type Error = Cow<'a, std::path::Path>; + + #[inline] + fn try_from(path: Cow<'a, std::path::Path>) -> Result { + match path { + Cow::Borrowed(path) => Self::try_from(path).map_err(|_| Cow::Borrowed(path)), + Cow::Owned(path) => Self::try_from(path).map_err(Cow::Owned), + } + } +} + +impl<'a> TryFrom> for AbsolutePathBuf { + type Error = Cow<'a, std::path::Path>; + + #[inline] + fn try_from(path: Cow<'a, std::path::Path>) -> Result { + match path { + Cow::Borrowed(path) => Self::try_from(path).map_err(|_| Cow::Borrowed(path)), + Cow::Owned(path) => Self::try_from(path).map_err(Cow::Owned), + } + } +} + +impl<'a> TryFrom> for Box { + type Error = Cow<'a, std::path::Path>; + + #[inline] + fn try_from(path: Cow<'a, std::path::Path>) -> Result { + match path { + Cow::Borrowed(path) => Box::try_from(path).map_err(|_| Cow::Borrowed(path)), + Cow::Owned(path) => Box::try_from(path).map_err(Cow::Owned), + } + } +} + +impl<'a> TryFrom> for Box { + type Error = Cow<'a, std::path::Path>; + + #[inline] + fn try_from(path: Cow<'a, std::path::Path>) -> Result { + match path { + Cow::Borrowed(path) => Box::try_from(path).map_err(|_| Cow::Borrowed(path)), + Cow::Owned(path) => Box::try_from(path).map_err(Cow::Owned), + } + } +} + +// === From string types === + +impl TryFrom for RelativePathBuf { + type Error = OsString; + + #[inline] + fn try_from(s: OsString) -> Result { + Self::try_from(PathBuf::from(s)).map_err(|buf| buf.into_os_string()) + } +} + +impl TryFrom for AbsolutePathBuf { + type Error = OsString; + + #[inline] + fn try_from(s: OsString) -> Result { + Self::try_from(PathBuf::from(s)).map_err(|buf| buf.into_os_string()) + } +} + +impl TryFrom for RelativePathBuf { + type Error = String; + + #[inline] + fn try_from(s: String) -> Result { + if Path::new(&s).is_relative() { + Ok(Self::new_unchecked(s.into())) + } else { + Err(s) + } + } +} + +impl TryFrom for AbsolutePathBuf { + type Error = String; + + #[inline] + fn try_from(s: String) -> Result { + if Path::new(&s).is_absolute() { + Ok(Self::new_unchecked(s.into())) + } else { + Err(s) + } + } +} + +/* +================================================================================ + PartialEq, Eq, PartialOrd, and Ord +================================================================================ +*/ + +// Here we match all `PartialEq` and `PartialOrd` implementations on `std::path::Path` +// and `std::path::PathBuf`, adding casting variations where possible. + +// === Between crate types === + +impl PartialEq for Path { + fn eq(&self, other: &Self) -> bool { + self.inner == other.inner + } +} + +impl Eq for Path {} + +impl PartialOrd for Path { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.inner.cmp(&other.inner)) + } +} + +impl Ord for Path { + fn cmp(&self, other: &Self) -> Ordering { + self.inner.cmp(&other.inner) + } +} + +impl Hash for Path { + fn hash(&self, state: &mut H) { + self.inner.hash(state); + } +} + +impl PartialEq for PathBuf { + fn eq(&self, other: &Self) -> bool { + self.inner == other.inner + } +} + +impl Eq for PathBuf {} + +impl PartialOrd for PathBuf { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.inner.cmp(&other.inner)) + } +} + +impl Ord for PathBuf { + fn cmp(&self, other: &Self) -> Ordering { + self.inner.cmp(&other.inner) + } +} + +impl Hash for PathBuf { + fn hash(&self, state: &mut H) { + self.inner.hash(state); + } +} + +macro_rules! impl_cmp { + (<$($life:lifetime),*> $lhs:ty, $rhs:ty) => { + impl<$($life,)* Form: PathForm> PartialEq<$rhs> for $lhs { + #[inline] + fn eq(&self, other: &$rhs) -> bool { + as PartialEq>::eq(self, other) + } + } + + impl<$($life,)* Form: PathForm> PartialEq<$lhs> for $rhs { + #[inline] + fn eq(&self, other: &$lhs) -> bool { + as PartialEq>::eq(self, other) + } + } + + impl<$($life,)* Form: PathForm> PartialOrd<$rhs> for $lhs { + #[inline] + fn partial_cmp(&self, other: &$rhs) -> Option { + as PartialOrd>::partial_cmp(self, other) + } + } + + impl<$($life,)* Form: PathForm> PartialOrd<$lhs> for $rhs { + #[inline] + fn partial_cmp(&self, other: &$lhs) -> Option { + as PartialOrd>::partial_cmp(self, other) + } + } + }; +} + +impl_cmp!(<> PathBuf, Path); +impl_cmp!(<'a> PathBuf, &'a Path); +impl_cmp!(<'a> Cow<'a, Path>, Path); +impl_cmp!(<'a, 'b> Cow<'a, Path>, &'b Path); +impl_cmp!(<'a> Cow<'a, Path>, PathBuf); + +macro_rules! impl_cmp_cast { + (<$($life:lifetime),*> $lhs:ty, $rhs:ty) => { + impl<$($life),*> PartialEq<$rhs> for $lhs { + #[inline] + fn eq(&self, other: &$rhs) -> bool { + ::eq(self.cast(), other.cast()) + } + } + + impl<$($life),*> PartialEq<$lhs> for $rhs { + #[inline] + fn eq(&self, other: &$lhs) -> bool { + ::eq(self.cast(), other.cast()) + } + } + + impl<$($life),*> PartialOrd<$rhs> for $lhs { + #[inline] + fn partial_cmp(&self, other: &$rhs) -> Option { + ::partial_cmp(self.cast(), other.cast()) + } + } + + impl<$($life),*> PartialOrd<$lhs> for $rhs { + #[inline] + fn partial_cmp(&self, other: &$lhs) -> Option { + ::partial_cmp(self.cast(), other.cast()) + } + } + }; +} + +impl_cmp_cast!(<> Path, RelativePath); +impl_cmp_cast!(<> Path, AbsolutePath); +impl_cmp_cast!(<> Path, CanonicalPath); +impl_cmp_cast!(<> AbsolutePath, CanonicalPath); +impl_cmp_cast!(<> PathBuf, RelativePathBuf); +impl_cmp_cast!(<> PathBuf, AbsolutePathBuf); +impl_cmp_cast!(<> PathBuf, CanonicalPathBuf); +impl_cmp_cast!(<> AbsolutePathBuf, CanonicalPathBuf); + +impl_cmp_cast!(<'a> &'a Path, RelativePath); +impl_cmp_cast!(<'a> &'a Path, AbsolutePath); +impl_cmp_cast!(<'a> &'a Path, CanonicalPath); +impl_cmp_cast!(<'a> &'a AbsolutePath, CanonicalPath); +impl_cmp_cast!(<'a> Path, &'a RelativePath); +impl_cmp_cast!(<'a> Path, &'a AbsolutePath); +impl_cmp_cast!(<'a> Path, &'a CanonicalPath); +impl_cmp_cast!(<'a> AbsolutePath, &'a CanonicalPath); + +impl_cmp_cast!(<> PathBuf, RelativePath); +impl_cmp_cast!(<> PathBuf, AbsolutePath); +impl_cmp_cast!(<> PathBuf, CanonicalPath); +impl_cmp_cast!(<> AbsolutePathBuf, CanonicalPath); +impl_cmp_cast!(<> RelativePathBuf, Path); +impl_cmp_cast!(<> AbsolutePathBuf, Path); +impl_cmp_cast!(<> CanonicalPathBuf, Path); +impl_cmp_cast!(<> CanonicalPathBuf, AbsolutePath); + +impl_cmp_cast!(<'a> PathBuf, &'a RelativePath); +impl_cmp_cast!(<'a> PathBuf, &'a AbsolutePath); +impl_cmp_cast!(<'a> PathBuf, &'a CanonicalPath); +impl_cmp_cast!(<'a> AbsolutePathBuf, &'a CanonicalPath); +impl_cmp_cast!(<'a> RelativePathBuf, &'a Path); +impl_cmp_cast!(<'a> AbsolutePathBuf, &'a Path); +impl_cmp_cast!(<'a> CanonicalPathBuf, &'a Path); +impl_cmp_cast!(<'a> CanonicalPathBuf, &'a AbsolutePath); + +impl_cmp_cast!(<'a> Cow<'a, Path>, RelativePath); +impl_cmp_cast!(<'a> Cow<'a, Path>, AbsolutePath); +impl_cmp_cast!(<'a> Cow<'a, Path>, CanonicalPath); +impl_cmp_cast!(<'a> Cow<'a, AbsolutePath>, CanonicalPath); +impl_cmp_cast!(<'a> Cow<'a, RelativePath>, Path); +impl_cmp_cast!(<'a> Cow<'a, AbsolutePath>, Path); +impl_cmp_cast!(<'a> Cow<'a, CanonicalPath>, Path); +impl_cmp_cast!(<'a> Cow<'a, CanonicalPath>, AbsolutePath); + +impl_cmp_cast!(<'a, 'b> Cow<'a, Path>, &'b RelativePath); +impl_cmp_cast!(<'a, 'b> Cow<'a, Path>, &'b AbsolutePath); +impl_cmp_cast!(<'a, 'b> Cow<'a, Path>, &'b CanonicalPath); +impl_cmp_cast!(<'a, 'b> Cow<'a, AbsolutePath>, &'b CanonicalPath); +impl_cmp_cast!(<'a, 'b> Cow<'a, RelativePath>, &'b Path); +impl_cmp_cast!(<'a, 'b> Cow<'a, AbsolutePath>, &'b Path); +impl_cmp_cast!(<'a, 'b> Cow<'a, CanonicalPath>, &'b Path); +impl_cmp_cast!(<'a, 'b> Cow<'a, CanonicalPath>, &'b AbsolutePath); + +impl_cmp_cast!(<'a> Cow<'a, Path>, RelativePathBuf); +impl_cmp_cast!(<'a> Cow<'a, Path>, AbsolutePathBuf); +impl_cmp_cast!(<'a> Cow<'a, Path>, CanonicalPathBuf); +impl_cmp_cast!(<'a> Cow<'a, AbsolutePath>, CanonicalPathBuf); +impl_cmp_cast!(<'a> Cow<'a, RelativePath>, PathBuf); +impl_cmp_cast!(<'a> Cow<'a, AbsolutePath>, PathBuf); +impl_cmp_cast!(<'a> Cow<'a, CanonicalPath>, PathBuf); +impl_cmp_cast!(<'a> Cow<'a, CanonicalPath>, AbsolutePathBuf); + +// === Between std::path types === + +macro_rules! impl_cmp_std { + (<$($life:lifetime),*> $lhs:ty, $rhs:ty) => { + impl<$($life,)* Form: PathForm> PartialEq<$rhs> for $lhs { + #[inline] + fn eq(&self, other: &$rhs) -> bool { + ::eq(self.as_ref(), other.as_any()) + } + } + + impl<$($life,)* Form: PathForm> PartialEq<$lhs> for $rhs { + #[inline] + fn eq(&self, other: &$lhs) -> bool { + ::eq(self.as_any(), other.as_ref()) + } + } + + impl<$($life,)* Form: PathForm> PartialOrd<$rhs> for $lhs { + #[inline] + fn partial_cmp(&self, other: &$rhs) -> Option { + ::partial_cmp(self.as_ref(), other.as_any()) + } + } + + impl<$($life,)* Form: PathForm> PartialOrd<$lhs> for $rhs { + #[inline] + fn partial_cmp(&self, other: &$lhs) -> Option { + ::partial_cmp(self.as_any(), other.as_ref()) + } + } + }; +} + +impl_cmp_std!(<> std::path::Path, Path); +impl_cmp_std!(<> std::path::PathBuf, Path); +impl_cmp_std!(<'a> std::path::PathBuf, &'a Path); +impl_cmp_std!(<'a> Cow<'a, std::path::Path>, Path); +impl_cmp_std!(<'a, 'b> Cow<'a, std::path::Path>, &'b Path); + +impl_cmp_std!(<> std::path::Path, PathBuf); +impl_cmp_std!(<'a> &'a std::path::Path, PathBuf); +impl_cmp_std!(<> std::path::PathBuf, PathBuf); +impl_cmp_std!(<'a> Cow<'a, std::path::Path>, PathBuf); + +// === Between string types === + +impl_cmp_std!(<> OsStr, Path); +impl_cmp_std!(<'a> OsStr, &'a Path); +impl_cmp_std!(<'a> &'a OsStr, Path); +impl_cmp_std!(<'a> Cow<'a, OsStr>, Path); +impl_cmp_std!(<'a, 'b> Cow<'b, OsStr>, &'a Path); +impl_cmp_std!(<> OsString, Path); +impl_cmp_std!(<'a> OsString, &'a Path); + +impl_cmp_std!(<> OsStr, PathBuf); +impl_cmp_std!(<'a> &'a OsStr, PathBuf); +impl_cmp_std!(<'a> Cow<'a, OsStr>, PathBuf); +impl_cmp_std!(<> OsString, PathBuf); diff --git a/crates/nu-path/src/tilde.rs b/crates/nu-path/src/tilde.rs index 60cc7d11eb..95c91addf8 100644 --- a/crates/nu-path/src/tilde.rs +++ b/crates/nu-path/src/tilde.rs @@ -77,7 +77,7 @@ fn user_home_dir(username: &str) -> PathBuf { fn user_home_dir(username: &str) -> PathBuf { use std::path::Component; - match dirs_next::home_dir() { + match dirs::home_dir() { None => { // Termux always has the same home directory #[cfg(target_os = "android")] @@ -145,7 +145,7 @@ fn expand_tilde_with_another_user_home(path: &Path) -> 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 - expand_tilde_with_home(path, dirs_next::home_dir()) + expand_tilde_with_home(path, dirs::home_dir()) } #[cfg(test)] diff --git a/crates/nu-plugin-core/Cargo.toml b/crates/nu-plugin-core/Cargo.toml index 965b09d085..48aee13276 100644 --- a/crates/nu-plugin-core/Cargo.toml +++ b/crates/nu-plugin-core/Cargo.toml @@ -5,14 +5,14 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-plugin-core edition = "2021" license = "MIT" name = "nu-plugin-core" -version = "0.94.3" +version = "0.95.1" [lib] bench = false [dependencies] -nu-protocol = { path = "../nu-protocol", version = "0.94.3" } -nu-plugin-protocol = { path = "../nu-plugin-protocol", version = "0.94.3", default-features = false } +nu-protocol = { path = "../nu-protocol", version = "0.95.1" } +nu-plugin-protocol = { path = "../nu-plugin-protocol", version = "0.95.1", default-features = false } rmp-serde = { workspace = true } serde = { workspace = true } @@ -25,4 +25,4 @@ default = ["local-socket"] local-socket = ["interprocess", "nu-plugin-protocol/local-socket"] [target.'cfg(target_os = "windows")'.dependencies] -windows = { workspace = true } +windows = { workspace = true } \ No newline at end of file diff --git a/crates/nu-plugin-core/src/interface/mod.rs b/crates/nu-plugin-core/src/interface/mod.rs index 4f287f39c0..89aa2b15e5 100644 --- a/crates/nu-plugin-core/src/interface/mod.rs +++ b/crates/nu-plugin-core/src/interface/mod.rs @@ -1,10 +1,10 @@ //! Implements the stream multiplexing interface for both the plugin side and the engine side. use nu_plugin_protocol::{ByteStreamInfo, ListStreamInfo, PipelineDataHeader, StreamMessage}; -use nu_protocol::{ByteStream, IntoSpanned, ListStream, PipelineData, Reader, ShellError}; +use nu_protocol::{ByteStream, IntoSpanned, ListStream, PipelineData, Reader, ShellError, Signals}; use std::{ io::{Read, Write}, - sync::{atomic::AtomicBool, Arc, Mutex}, + sync::Mutex, thread, }; @@ -170,7 +170,7 @@ pub trait InterfaceManager { fn read_pipeline_data( &self, header: PipelineDataHeader, - ctrlc: Option<&Arc>, + signals: &Signals, ) -> Result { self.prepare_pipeline_data(match header { PipelineDataHeader::Empty => PipelineData::Empty, @@ -178,12 +178,12 @@ pub trait InterfaceManager { PipelineDataHeader::ListStream(info) => { let handle = self.stream_manager().get_handle(); let reader = handle.read_stream(info.id, self.get_interface())?; - ListStream::new(reader, info.span, ctrlc.cloned()).into() + ListStream::new(reader, info.span, signals.clone()).into() } PipelineDataHeader::ByteStream(info) => { let handle = self.stream_manager().get_handle(); let reader = handle.read_stream(info.id, self.get_interface())?; - ByteStream::from_result_iter(reader, info.span, ctrlc.cloned(), info.type_).into() + ByteStream::from_result_iter(reader, info.span, signals.clone(), info.type_).into() } }) } diff --git a/crates/nu-plugin-core/src/interface/tests.rs b/crates/nu-plugin-core/src/interface/tests.rs index e318a2648e..456eb547b5 100644 --- a/crates/nu-plugin-core/src/interface/tests.rs +++ b/crates/nu-plugin-core/src/interface/tests.rs @@ -11,13 +11,14 @@ use nu_plugin_protocol::{ }; use nu_protocol::{ ByteStream, ByteStreamSource, ByteStreamType, DataSource, ListStream, PipelineData, - PipelineMetadata, ShellError, Span, Value, + PipelineMetadata, ShellError, Signals, Span, Value, }; use std::{path::Path, sync::Arc}; fn test_metadata() -> PipelineMetadata { PipelineMetadata { data_source: DataSource::FilePath("/test/path".into()), + content_type: None, } } @@ -128,7 +129,7 @@ fn read_pipeline_data_empty() -> Result<(), ShellError> { let header = PipelineDataHeader::Empty; assert!(matches!( - manager.read_pipeline_data(header, None)?, + manager.read_pipeline_data(header, &Signals::empty())?, PipelineData::Empty )); Ok(()) @@ -140,7 +141,7 @@ fn read_pipeline_data_value() -> Result<(), ShellError> { let value = Value::test_int(4); let header = PipelineDataHeader::Value(value.clone()); - match manager.read_pipeline_data(header, None)? { + match manager.read_pipeline_data(header, &Signals::empty())? { PipelineData::Value(read_value, ..) => assert_eq!(value, read_value), PipelineData::ListStream(..) => panic!("unexpected ListStream"), PipelineData::ByteStream(..) => panic!("unexpected ByteStream"), @@ -167,7 +168,7 @@ fn read_pipeline_data_list_stream() -> Result<(), ShellError> { span: Span::test_data(), }); - let pipe = manager.read_pipeline_data(header, None)?; + let pipe = manager.read_pipeline_data(header, &Signals::empty())?; assert!( matches!(pipe, PipelineData::ListStream(..)), "unexpected PipelineData: {pipe:?}" @@ -211,7 +212,7 @@ fn read_pipeline_data_byte_stream() -> Result<(), ShellError> { type_: ByteStreamType::Unknown, }); - let pipe = manager.read_pipeline_data(header, None)?; + let pipe = manager.read_pipeline_data(header, &Signals::empty())?; // need to consume input manager.consume_all()?; @@ -256,9 +257,9 @@ fn read_pipeline_data_prepared_properly() -> Result<(), ShellError> { id: 0, span: Span::test_data(), }); - match manager.read_pipeline_data(header, None)? { + match manager.read_pipeline_data(header, &Signals::empty())? { PipelineData::ListStream(_, meta) => match meta { - Some(PipelineMetadata { data_source }) => match data_source { + Some(PipelineMetadata { data_source, .. }) => match data_source { DataSource::FilePath(path) => { assert_eq!(Path::new("/test/path"), path); Ok(()) @@ -352,7 +353,11 @@ fn write_pipeline_data_list_stream() -> Result<(), ShellError> { // Set up pipeline data for a list stream let pipe = PipelineData::ListStream( - ListStream::new(values.clone().into_iter(), Span::test_data(), None), + ListStream::new( + values.clone().into_iter(), + Span::test_data(), + Signals::empty(), + ), None, ); @@ -405,7 +410,7 @@ fn write_pipeline_data_byte_stream() -> Result<(), ShellError> { ByteStream::read( std::io::Cursor::new(expected), span, - None, + Signals::empty(), ByteStreamType::Unknown, ), None, diff --git a/crates/nu-plugin-engine/Cargo.toml b/crates/nu-plugin-engine/Cargo.toml index 95c0298c09..a6dfa471b8 100644 --- a/crates/nu-plugin-engine/Cargo.toml +++ b/crates/nu-plugin-engine/Cargo.toml @@ -5,17 +5,18 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-plugin-engi edition = "2021" license = "MIT" name = "nu-plugin-engine" -version = "0.94.3" +version = "0.95.1" [lib] bench = false [dependencies] -nu-engine = { path = "../nu-engine", version = "0.94.3" } -nu-protocol = { path = "../nu-protocol", version = "0.94.3" } -nu-system = { path = "../nu-system", version = "0.94.3" } -nu-plugin-protocol = { path = "../nu-plugin-protocol", version = "0.94.3" } -nu-plugin-core = { path = "../nu-plugin-core", version = "0.94.3", default-features = false } +nu-engine = { path = "../nu-engine", version = "0.95.1" } +nu-protocol = { path = "../nu-protocol", version = "0.95.1" } +nu-system = { path = "../nu-system", version = "0.95.1" } +nu-plugin-protocol = { path = "../nu-plugin-protocol", version = "0.95.1" } +nu-plugin-core = { path = "../nu-plugin-core", version = "0.95.1", default-features = false } +nu-utils = { path = "../nu-utils", version = "0.95.1" } serde = { workspace = true } log = { workspace = true } diff --git a/crates/nu-plugin-engine/src/context.rs b/crates/nu-plugin-engine/src/context.rs index d5be6ad4b6..aa504a246a 100644 --- a/crates/nu-plugin-engine/src/context.rs +++ b/crates/nu-plugin-engine/src/context.rs @@ -1,29 +1,26 @@ use crate::util::MutableCow; use nu_engine::{get_eval_block_with_early_return, get_full_help, ClosureEvalOnce}; use nu_protocol::{ - ast::Call, - engine::{Closure, EngineState, Redirection, Stack}, - Config, IntoSpanned, OutDest, PipelineData, PluginIdentity, ShellError, Span, Spanned, Value, + engine::{Call, Closure, EngineState, Redirection, Stack}, + Config, IntoSpanned, OutDest, PipelineData, PluginIdentity, ShellError, Signals, Span, Spanned, + Value, }; use std::{ borrow::Cow, collections::HashMap, - sync::{ - atomic::{AtomicBool, AtomicU32}, - Arc, - }, + sync::{atomic::AtomicU32, Arc}, }; /// Object safe trait for abstracting operations required of the plugin context. pub trait PluginExecutionContext: Send + Sync { /// A span pointing to the command being executed fn span(&self) -> Span; - /// The interrupt signal, if present - fn ctrlc(&self) -> Option<&Arc>; + /// The [`Signals`] struct, if present + fn signals(&self) -> &Signals; /// The pipeline externals state, for tracking the foreground process group, if present fn pipeline_externals_state(&self) -> Option<&Arc<(AtomicU32, AtomicU32)>>; /// Get engine configuration - fn get_config(&self) -> Result; + fn get_config(&self) -> Result, ShellError>; /// Get plugin configuration fn get_plugin_config(&self) -> Result, ShellError>; /// Get an environment variable from `$env` @@ -56,7 +53,7 @@ pub struct PluginExecutionCommandContext<'a> { identity: Arc, engine_state: Cow<'a, EngineState>, stack: MutableCow<'a, Stack>, - call: Cow<'a, Call>, + call: Call<'a>, } impl<'a> PluginExecutionCommandContext<'a> { @@ -64,13 +61,13 @@ impl<'a> PluginExecutionCommandContext<'a> { identity: Arc, engine_state: &'a EngineState, stack: &'a mut Stack, - call: &'a Call, + call: &'a Call<'a>, ) -> PluginExecutionCommandContext<'a> { PluginExecutionCommandContext { identity, engine_state: Cow::Borrowed(engine_state), stack: MutableCow::Borrowed(stack), - call: Cow::Borrowed(call), + call: call.clone(), } } } @@ -80,23 +77,23 @@ impl<'a> PluginExecutionContext for PluginExecutionCommandContext<'a> { self.call.head } - fn ctrlc(&self) -> Option<&Arc> { - self.engine_state.ctrlc.as_ref() + fn signals(&self) -> &Signals { + self.engine_state.signals() } fn pipeline_externals_state(&self) -> Option<&Arc<(AtomicU32, AtomicU32)>> { Some(&self.engine_state.pipeline_externals_state) } - fn get_config(&self) -> Result { - Ok(nu_engine::get_config(&self.engine_state, &self.stack)) + fn get_config(&self) -> Result, ShellError> { + Ok(self.stack.get_config(&self.engine_state)) } fn get_plugin_config(&self) -> Result, ShellError> { // Fetch the configuration for a plugin // - // The `plugin` must match the registered name of a plugin. For - // `register nu_plugin_example` the plugin config lookup uses `"example"` + // The `plugin` must match the registered name of a plugin. For `plugin add + // nu_plugin_example` the plugin config lookup uses `"example"` Ok(self .get_config()? .plugins @@ -219,7 +216,7 @@ impl<'a> PluginExecutionContext for PluginExecutionCommandContext<'a> { identity: self.identity.clone(), engine_state: Cow::Owned(self.engine_state.clone().into_owned()), stack: self.stack.owned(), - call: Cow::Owned(self.call.clone().into_owned()), + call: self.call.to_owned(), }) } } @@ -234,15 +231,15 @@ impl PluginExecutionContext for PluginExecutionBogusContext { Span::test_data() } - fn ctrlc(&self) -> Option<&Arc> { - None + fn signals(&self) -> &Signals { + &Signals::EMPTY } fn pipeline_externals_state(&self) -> Option<&Arc<(AtomicU32, AtomicU32)>> { None } - fn get_config(&self) -> Result { + fn get_config(&self) -> Result, ShellError> { Err(ShellError::NushellFailed { msg: "get_config not implemented on bogus".into(), }) diff --git a/crates/nu-plugin-engine/src/declaration.rs b/crates/nu-plugin-engine/src/declaration.rs index d48fa39b85..745ba9a998 100644 --- a/crates/nu-plugin-engine/src/declaration.rs +++ b/crates/nu-plugin-engine/src/declaration.rs @@ -76,7 +76,7 @@ impl Command for PluginDeclaration { EvaluatedCall::try_from_call(call, engine_state, stack, eval_expression)?; // Get the engine config - let engine_config = nu_engine::get_config(engine_state, stack); + let engine_config = stack.get_config(engine_state); // Get, or start, the plugin. let plugin = self diff --git a/crates/nu-plugin-engine/src/init.rs b/crates/nu-plugin-engine/src/init.rs index 198a01cd1c..44fa4f031f 100644 --- a/crates/nu-plugin-engine/src/init.rs +++ b/crates/nu-plugin-engine/src/init.rs @@ -252,7 +252,7 @@ pub fn load_plugin_registry_item( })?; match &plugin.data { - PluginRegistryItemData::Valid { commands } => { + PluginRegistryItemData::Valid { metadata, commands } => { let plugin = add_plugin_to_working_set(working_set, &identity)?; // Ensure that the plugin is reset. We're going to load new signatures, so we want to @@ -260,6 +260,9 @@ pub fn load_plugin_registry_item( // doesn't. plugin.reset()?; + // Set the plugin metadata from the file + plugin.set_metadata(Some(metadata.clone())); + // Create the declarations from the commands for signature in commands { let decl = PluginDeclaration::new(plugin.clone(), signature.clone()); diff --git a/crates/nu-plugin-engine/src/interface/mod.rs b/crates/nu-plugin-engine/src/interface/mod.rs index adab9dc68d..d1b722dedf 100644 --- a/crates/nu-plugin-engine/src/interface/mod.rs +++ b/crates/nu-plugin-engine/src/interface/mod.rs @@ -11,12 +11,13 @@ use nu_plugin_protocol::{ PluginOutput, ProtocolInfo, StreamId, StreamMessage, }; use nu_protocol::{ - ast::Operator, CustomValue, IntoSpanned, PipelineData, PluginSignature, ShellError, Span, - Spanned, Value, + ast::Operator, CustomValue, IntoSpanned, PipelineData, PluginMetadata, PluginSignature, + ShellError, Signals, Span, Spanned, Value, }; +use nu_utils::SharedCow; use std::{ collections::{btree_map, BTreeMap}, - sync::{atomic::AtomicBool, mpsc, Arc, OnceLock}, + sync::{mpsc, Arc, OnceLock}, }; use crate::{ @@ -103,8 +104,8 @@ struct PluginCallState { /// Don't try to send the plugin call response. This is only used for `Dropped` to avoid an /// error dont_send_response: bool, - /// Interrupt signal to be used for stream iterators - ctrlc: Option>, + /// Signals to be used for stream iterators + signals: Signals, /// Channel to receive context on to be used if needed context_rx: Option>, /// Span associated with the call, if any @@ -231,14 +232,14 @@ impl PluginInterfaceManager { } } - /// Find the ctrlc signal corresponding to the given plugin call id - fn get_ctrlc(&mut self, id: PluginCallId) -> Result>, ShellError> { + /// Find the [`Signals`] struct corresponding to the given plugin call id + fn get_signals(&mut self, id: PluginCallId) -> Result { // Make sure we're up to date self.receive_plugin_call_subscriptions(); // Find the subscription and return the context self.plugin_call_states .get(&id) - .map(|state| state.ctrlc.clone()) + .map(|state| state.signals.clone()) .ok_or_else(|| ShellError::PluginFailedToDecode { msg: format!("Unknown plugin call ID: {id}"), }) @@ -517,14 +518,14 @@ impl InterfaceManager for PluginInterfaceManager { // Handle reading the pipeline data, if any let response = response .map_data(|data| { - let ctrlc = self.get_ctrlc(id)?; + let signals = self.get_signals(id)?; // Register the stream in the response if let Some(stream_id) = data.stream_id() { self.recv_stream_started(id, stream_id); } - self.read_pipeline_data(data, ctrlc.as_ref()) + self.read_pipeline_data(data, &signals) }) .unwrap_or_else(|err| { // If there's an error with initializing this stream, change it to a plugin @@ -544,8 +545,8 @@ impl InterfaceManager for PluginInterfaceManager { let call = call // Handle reading the pipeline data, if any .map_data(|input| { - let ctrlc = self.get_ctrlc(context)?; - self.read_pipeline_data(input, ctrlc.as_ref()) + let signals = self.get_signals(context)?; + self.read_pipeline_data(input, &signals) }) // Do anything extra needed for each engine call setup .and_then(|mut engine_call| { @@ -698,7 +699,9 @@ impl PluginInterface { context: Option<&dyn PluginExecutionContext>, ) -> Result { let id = self.state.plugin_call_id_sequence.next()?; - let ctrlc = context.and_then(|c| c.ctrlc().cloned()); + let signals = context + .map(|c| c.signals().clone()) + .unwrap_or_else(Signals::empty); let (tx, rx) = mpsc::channel(); let (context_tx, context_rx) = mpsc::channel(); let keep_plugin_custom_values = mpsc::channel(); @@ -716,6 +719,7 @@ impl PluginInterface { // Convert the call into one with a header and handle the stream, if necessary let (call, writer) = match call { + PluginCall::Metadata => (PluginCall::Metadata, Default::default()), PluginCall::Signature => (PluginCall::Signature, Default::default()), PluginCall::CustomValueOp(value, op) => { (PluginCall::CustomValueOp(value, op), Default::default()) @@ -745,7 +749,7 @@ impl PluginInterface { PluginCallState { sender: Some(tx).filter(|_| !dont_send_response), dont_send_response, - ctrlc, + signals, context_rx: Some(context_rx), span: call.span(), keep_plugin_custom_values, @@ -913,6 +917,17 @@ impl PluginInterface { self.receive_plugin_call_response(result.receiver, context, result.state) } + /// Get the metadata from the plugin. + pub fn get_metadata(&self) -> Result { + match self.plugin_call(PluginCall::Metadata, None)? { + PluginCallResponse::Metadata(meta) => Ok(meta), + PluginCallResponse::Error(err) => Err(err.into()), + _ => Err(ShellError::PluginFailedToDecode { + msg: "Received unexpected response to plugin Metadata call".into(), + }), + } + } + /// Get the command signatures from the plugin. pub fn get_signature(&self) -> Result, ShellError> { match self.plugin_call(PluginCall::Signature, None)? { @@ -1206,6 +1221,7 @@ impl CurrentCallState { source: &PluginSource, ) -> Result<(), ShellError> { match call { + PluginCall::Metadata => Ok(()), PluginCall::Signature => Ok(()), PluginCall::Run(CallInfo { call, .. }) => self.prepare_call_args(call, source), PluginCall::CustomValueOp(_, op) => { @@ -1245,7 +1261,7 @@ pub(crate) fn handle_engine_call( match call { EngineCall::GetConfig => { - let config = Box::new(context.get_config()?); + let config = SharedCow::from(context.get_config()?); Ok(EngineCallResponse::Config(config)) } EngineCall::GetPluginConfig => { diff --git a/crates/nu-plugin-engine/src/interface/tests.rs b/crates/nu-plugin-engine/src/interface/tests.rs index e718886b3b..2bab67f25f 100644 --- a/crates/nu-plugin-engine/src/interface/tests.rs +++ b/crates/nu-plugin-engine/src/interface/tests.rs @@ -18,7 +18,7 @@ use nu_protocol::{ ast::{Math, Operator}, engine::Closure, ByteStreamType, CustomValue, IntoInterruptiblePipelineData, IntoSpanned, PipelineData, - PluginSignature, ShellError, Span, Spanned, Value, + PluginMetadata, PluginSignature, ShellError, Signals, Span, Spanned, Value, }; use serde::{Deserialize, Serialize}; use std::{ @@ -56,7 +56,7 @@ fn manager_consume_all_exits_after_streams_and_interfaces_are_dropped() -> Resul id: 0, span: Span::test_data(), }), - None, + &Signals::empty(), )?; // and an interface... @@ -112,7 +112,7 @@ fn manager_consume_all_propagates_io_error_to_readers() -> Result<(), ShellError id: 0, span: Span::test_data(), }), - None, + &Signals::empty(), )?; manager @@ -159,7 +159,7 @@ fn manager_consume_all_propagates_message_error_to_readers() -> Result<(), Shell span: Span::test_data(), type_: ByteStreamType::Unknown, }), - None, + &Signals::empty(), )?; manager @@ -190,7 +190,7 @@ fn fake_plugin_call( PluginCallState { sender: Some(tx), dont_send_response: false, - ctrlc: None, + signals: Signals::empty(), context_rx: None, span: None, keep_plugin_custom_values: mpsc::channel(), @@ -493,7 +493,7 @@ fn manager_handle_engine_call_after_response_received() -> Result<(), ShellError PluginCallState { sender: None, dont_send_response: false, - ctrlc: None, + signals: Signals::empty(), context_rx: Some(context_rx), span: None, keep_plugin_custom_values: mpsc::channel(), @@ -559,7 +559,7 @@ fn manager_send_plugin_call_response_removes_context_only_if_no_streams_to_read( PluginCallState { sender: None, dont_send_response: false, - ctrlc: None, + signals: Signals::empty(), context_rx: None, span: None, keep_plugin_custom_values: mpsc::channel(), @@ -595,7 +595,7 @@ fn manager_consume_stream_end_removes_context_only_if_last_stream() -> Result<() PluginCallState { sender: None, dont_send_response: false, - ctrlc: None, + signals: Signals::empty(), context_rx: None, span: None, keep_plugin_custom_values: mpsc::channel(), @@ -678,7 +678,7 @@ fn manager_prepare_pipeline_data_adds_source_to_list_streams() -> Result<(), She [Value::test_custom_value(Box::new( test_plugin_custom_value(), ))] - .into_pipeline_data(Span::test_data(), None), + .into_pipeline_data(Span::test_data(), Signals::empty()), )?; let value = data @@ -852,7 +852,9 @@ fn interface_write_plugin_call_writes_run_with_stream_input() -> Result<(), Shel positional: vec![], named: vec![], }, - input: values.clone().into_pipeline_data(Span::test_data(), None), + input: values + .clone() + .into_pipeline_data(Span::test_data(), Signals::empty()), }), None, )?; @@ -1019,6 +1021,25 @@ fn start_fake_plugin_call_responder( .expect("failed to spawn thread"); } +#[test] +fn interface_get_metadata() -> Result<(), ShellError> { + let test = TestCase::new(); + let manager = test.plugin("test"); + let interface = manager.get_interface(); + + start_fake_plugin_call_responder(manager, 1, |_| { + vec![ReceivedPluginCallMessage::Response( + PluginCallResponse::Metadata(PluginMetadata::new().with_version("test")), + )] + }); + + let metadata = interface.get_metadata()?; + + assert_eq!(Some("test"), metadata.version.as_deref()); + assert!(test.has_unconsumed_write()); + Ok(()) +} + #[test] fn interface_get_signature() -> Result<(), ShellError> { let test = TestCase::new(); @@ -1129,7 +1150,9 @@ fn interface_prepare_pipeline_data_accepts_normal_streams() -> Result<(), ShellE let values = normal_values(&interface); let state = CurrentCallState::default(); let data = interface.prepare_pipeline_data( - values.clone().into_pipeline_data(Span::test_data(), None), + values + .clone() + .into_pipeline_data(Span::test_data(), Signals::empty()), &state, )?; @@ -1192,7 +1215,9 @@ fn interface_prepare_pipeline_data_rejects_bad_custom_value_in_a_stream() -> Res let values = bad_custom_values(); let state = CurrentCallState::default(); let data = interface.prepare_pipeline_data( - values.clone().into_pipeline_data(Span::test_data(), None), + values + .clone() + .into_pipeline_data(Span::test_data(), Signals::empty()), &state, )?; diff --git a/crates/nu-plugin-engine/src/persistent.rs b/crates/nu-plugin-engine/src/persistent.rs index 5f01c70ca7..6a87aa1e6b 100644 --- a/crates/nu-plugin-engine/src/persistent.rs +++ b/crates/nu-plugin-engine/src/persistent.rs @@ -7,7 +7,7 @@ use super::{PluginInterface, PluginSource}; use nu_plugin_core::CommunicationMode; use nu_protocol::{ engine::{EngineState, Stack}, - PluginGcConfig, PluginIdentity, RegisteredPlugin, ShellError, + PluginGcConfig, PluginIdentity, PluginMetadata, RegisteredPlugin, ShellError, }; use std::{ collections::HashMap, @@ -31,6 +31,8 @@ pub struct PersistentPlugin { struct MutableState { /// Reference to the plugin if running running: Option, + /// Metadata for the plugin, e.g. version. + metadata: Option, /// Plugin's preferred communication mode (if known) preferred_mode: Option, /// Garbage collector config @@ -59,6 +61,7 @@ impl PersistentPlugin { identity, mutable: Mutex::new(MutableState { running: None, + metadata: None, preferred_mode: None, gc_config, }), @@ -268,6 +271,16 @@ impl RegisteredPlugin for PersistentPlugin { self.stop_internal(true) } + fn metadata(&self) -> Option { + self.mutable.lock().ok().and_then(|m| m.metadata.clone()) + } + + fn set_metadata(&self, metadata: Option) { + if let Ok(mut mutable) = self.mutable.lock() { + mutable.metadata = metadata; + } + } + fn set_gc_config(&self, gc_config: &PluginGcConfig) { if let Ok(mut mutable) = self.mutable.lock() { // Save the new config for future calls diff --git a/crates/nu-plugin-protocol/Cargo.toml b/crates/nu-plugin-protocol/Cargo.toml index 41c8a7c535..939f164786 100644 --- a/crates/nu-plugin-protocol/Cargo.toml +++ b/crates/nu-plugin-protocol/Cargo.toml @@ -5,14 +5,14 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-plugin-prot edition = "2021" license = "MIT" name = "nu-plugin-protocol" -version = "0.94.3" +version = "0.95.1" [lib] bench = false [dependencies] -nu-protocol = { path = "../nu-protocol", version = "0.94.3", features = ["plugin"] } -nu-utils = { path = "../nu-utils", version = "0.94.3" } +nu-protocol = { path = "../nu-protocol", version = "0.95.1", features = ["plugin"] } +nu-utils = { path = "../nu-utils", version = "0.95.1" } bincode = "1.3" serde = { workspace = true, features = ["derive"] } @@ -21,4 +21,4 @@ typetag = "0.2" [features] default = ["local-socket"] -local-socket = [] +local-socket = [] \ No newline at end of file diff --git a/crates/nu-plugin-protocol/src/evaluated_call.rs b/crates/nu-plugin-protocol/src/evaluated_call.rs index 19f9049340..58f8987865 100644 --- a/crates/nu-plugin-protocol/src/evaluated_call.rs +++ b/crates/nu-plugin-protocol/src/evaluated_call.rs @@ -1,7 +1,7 @@ use nu_protocol::{ - ast::{Call, Expression}, - engine::{EngineState, Stack}, - FromValue, ShellError, Span, Spanned, Value, + ast::{self, Expression}, + engine::{Call, CallImpl, EngineState, Stack}, + ir, FromValue, ShellError, Span, Spanned, Value, }; use serde::{Deserialize, Serialize}; @@ -33,6 +33,24 @@ impl EvaluatedCall { engine_state: &EngineState, stack: &mut Stack, eval_expression_fn: fn(&EngineState, &mut Stack, &Expression) -> Result, + ) -> Result { + match &call.inner { + CallImpl::AstRef(call) => { + Self::try_from_ast_call(call, engine_state, stack, eval_expression_fn) + } + CallImpl::AstBox(call) => { + Self::try_from_ast_call(call, engine_state, stack, eval_expression_fn) + } + CallImpl::IrRef(call) => Self::try_from_ir_call(call, stack), + CallImpl::IrBox(call) => Self::try_from_ir_call(call, stack), + } + } + + fn try_from_ast_call( + call: &ast::Call, + engine_state: &EngineState, + stack: &mut Stack, + eval_expression_fn: fn(&EngineState, &mut Stack, &Expression) -> Result, ) -> Result { let positional = call.rest_iter_flattened(0, |expr| eval_expression_fn(engine_state, stack, expr))?; @@ -54,6 +72,22 @@ impl EvaluatedCall { }) } + fn try_from_ir_call(call: &ir::Call, stack: &Stack) -> Result { + let positional = call.rest_iter_flattened(stack, 0)?; + + let mut named = Vec::with_capacity(call.named_len(stack)); + named.extend( + call.named_iter(stack) + .map(|(name, value)| (name.map(|s| s.to_owned()), value.cloned())), + ); + + Ok(Self { + head: call.head, + positional, + named, + }) + } + /// Check if a flag (named parameter that does not take a value) is set /// Returns Ok(true) if flag is set or passed true value /// Returns Ok(false) if flag is not set or passed false value diff --git a/crates/nu-plugin-protocol/src/lib.rs b/crates/nu-plugin-protocol/src/lib.rs index db19ee02f6..739366d910 100644 --- a/crates/nu-plugin-protocol/src/lib.rs +++ b/crates/nu-plugin-protocol/src/lib.rs @@ -23,8 +23,9 @@ pub mod test_util; use nu_protocol::{ ast::Operator, engine::Closure, ByteStreamType, Config, LabeledError, PipelineData, - PluginSignature, ShellError, Span, Spanned, Value, + PluginMetadata, PluginSignature, ShellError, Span, Spanned, Value, }; +use nu_utils::SharedCow; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -119,6 +120,7 @@ pub struct ByteStreamInfo { /// Calls that a plugin can execute. The type parameter determines the input type. #[derive(Serialize, Deserialize, Debug, Clone)] pub enum PluginCall { + Metadata, Signature, Run(CallInfo), CustomValueOp(Spanned, CustomValueOp), @@ -132,6 +134,7 @@ impl PluginCall { f: impl FnOnce(D) -> Result, ) -> Result, ShellError> { Ok(match self { + PluginCall::Metadata => PluginCall::Metadata, PluginCall::Signature => PluginCall::Signature, PluginCall::Run(call) => PluginCall::Run(call.map_data(f)?), PluginCall::CustomValueOp(custom_value, op) => { @@ -143,6 +146,7 @@ impl PluginCall { /// The span associated with the call. pub fn span(&self) -> Option { match self { + PluginCall::Metadata => None, PluginCall::Signature => None, PluginCall::Run(CallInfo { call, .. }) => Some(call.head), PluginCall::CustomValueOp(val, _) => Some(val.span), @@ -309,6 +313,7 @@ pub enum StreamMessage { #[derive(Serialize, Deserialize, Debug, Clone)] pub enum PluginCallResponse { Error(LabeledError), + Metadata(PluginMetadata), Signature(Vec), Ordering(Option), PipelineData(D), @@ -323,6 +328,7 @@ impl PluginCallResponse { ) -> Result, ShellError> { Ok(match self { PluginCallResponse::Error(err) => PluginCallResponse::Error(err), + PluginCallResponse::Metadata(meta) => PluginCallResponse::Metadata(meta), PluginCallResponse::Signature(sigs) => PluginCallResponse::Signature(sigs), PluginCallResponse::Ordering(ordering) => PluginCallResponse::Ordering(ordering), PluginCallResponse::PipelineData(input) => PluginCallResponse::PipelineData(f(input)?), @@ -548,7 +554,7 @@ impl EngineCall { pub enum EngineCallResponse { Error(ShellError), PipelineData(D), - Config(Box), + Config(SharedCow), ValueMap(HashMap), } diff --git a/crates/nu-plugin-test-support/Cargo.toml b/crates/nu-plugin-test-support/Cargo.toml index bc390d3994..efc76c3b93 100644 --- a/crates/nu-plugin-test-support/Cargo.toml +++ b/crates/nu-plugin-test-support/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nu-plugin-test-support" -version = "0.94.3" +version = "0.95.1" edition = "2021" license = "MIT" description = "Testing support for Nushell plugins" @@ -12,17 +12,17 @@ bench = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -nu-engine = { path = "../nu-engine", version = "0.94.3", features = ["plugin"] } -nu-protocol = { path = "../nu-protocol", version = "0.94.3", features = ["plugin"] } -nu-parser = { path = "../nu-parser", version = "0.94.3", features = ["plugin"] } -nu-plugin = { path = "../nu-plugin", version = "0.94.3" } -nu-plugin-core = { path = "../nu-plugin-core", version = "0.94.3" } -nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.94.3" } -nu-plugin-protocol = { path = "../nu-plugin-protocol", version = "0.94.3" } -nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.94.3" } +nu-engine = { path = "../nu-engine", version = "0.95.1", features = ["plugin"] } +nu-protocol = { path = "../nu-protocol", version = "0.95.1", features = ["plugin"] } +nu-parser = { path = "../nu-parser", version = "0.95.1", features = ["plugin"] } +nu-plugin = { path = "../nu-plugin", version = "0.95.1" } +nu-plugin-core = { path = "../nu-plugin-core", version = "0.95.1" } +nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.95.1" } +nu-plugin-protocol = { path = "../nu-plugin-protocol", version = "0.95.1" } +nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.95.1" } nu-ansi-term = { workspace = true } similar = "2.5" [dev-dependencies] typetag = "0.2" -serde = "1.0" +serde = "1.0" \ No newline at end of file diff --git a/crates/nu-plugin-test-support/src/fake_persistent_plugin.rs b/crates/nu-plugin-test-support/src/fake_persistent_plugin.rs index 617e55c3d1..e316e85147 100644 --- a/crates/nu-plugin-test-support/src/fake_persistent_plugin.rs +++ b/crates/nu-plugin-test-support/src/fake_persistent_plugin.rs @@ -6,7 +6,7 @@ use std::{ use nu_plugin_engine::{GetPlugin, PluginInterface}; use nu_protocol::{ engine::{EngineState, Stack}, - PluginGcConfig, PluginIdentity, RegisteredPlugin, ShellError, + PluginGcConfig, PluginIdentity, PluginMetadata, RegisteredPlugin, ShellError, }; pub struct FakePersistentPlugin { @@ -42,6 +42,12 @@ impl RegisteredPlugin for FakePersistentPlugin { None } + fn metadata(&self) -> Option { + None + } + + fn set_metadata(&self, _metadata: Option) {} + fn set_gc_config(&self, _gc_config: &PluginGcConfig) { // We don't have a GC } diff --git a/crates/nu-plugin-test-support/src/lib.rs b/crates/nu-plugin-test-support/src/lib.rs index caa7cbac1a..998c31ac0d 100644 --- a/crates/nu-plugin-test-support/src/lib.rs +++ b/crates/nu-plugin-test-support/src/lib.rs @@ -8,8 +8,8 @@ //! use nu_plugin::*; //! use nu_plugin_test_support::PluginTest; //! use nu_protocol::{ -//! Example, IntoInterruptiblePipelineData, LabeledError, PipelineData, ShellError, Signature, -//! Span, Type, Value, +//! Example, IntoInterruptiblePipelineData, LabeledError, PipelineData, ShellError, Signals, +//! Signature, Span, Type, Value, //! }; //! //! struct LowercasePlugin; @@ -60,12 +60,16 @@ //! // Errors in a stream should be returned as values. //! .unwrap_or_else(|err| Value::error(err, span)) //! }, -//! None, +//! &Signals::empty(), //! )?) //! } //! } //! //! impl Plugin for LowercasePlugin { +//! fn version(&self) -> String { +//! env!("CARGO_PKG_VERSION").into() +//! } +//! //! fn commands(&self) -> Vec>> { //! vec![Box::new(Lowercase)] //! } @@ -79,7 +83,7 @@ //! //! // #[test] //! fn test_lowercase() -> Result<(), ShellError> { -//! let input = vec![Value::test_string("FooBar")].into_pipeline_data(Span::test_data(), None); +//! let input = vec![Value::test_string("FooBar")].into_pipeline_data(Span::test_data(), Signals::empty()); //! let output = PluginTest::new("lowercase", LowercasePlugin.into())? //! .eval_with("lowercase", input)? //! .into_value(Span::test_data())?; diff --git a/crates/nu-plugin-test-support/src/plugin_test.rs b/crates/nu-plugin-test-support/src/plugin_test.rs index 3d6b3eec23..5e3334c78b 100644 --- a/crates/nu-plugin-test-support/src/plugin_test.rs +++ b/crates/nu-plugin-test-support/src/plugin_test.rs @@ -11,7 +11,7 @@ use nu_protocol::{ debugger::WithoutDebug, engine::{EngineState, Stack, StateWorkingSet}, report_error_new, CustomValue, Example, IntoSpanned as _, LabeledError, PipelineData, - ShellError, Span, Value, + ShellError, Signals, Span, Value, }; use crate::{diff::diff_by_line, fake_register::fake_register}; @@ -85,13 +85,13 @@ impl PluginTest { /// /// ```rust,no_run /// # use nu_plugin_test_support::PluginTest; - /// # use nu_protocol::{ShellError, Span, Value, IntoInterruptiblePipelineData}; + /// # use nu_protocol::{IntoInterruptiblePipelineData, ShellError, Signals, Span, Value}; /// # use nu_plugin::*; /// # fn test(MyPlugin: impl Plugin + Send + 'static) -> Result<(), ShellError> { /// let result = PluginTest::new("my_plugin", MyPlugin.into())? /// .eval_with( /// "my-command", - /// vec![Value::test_int(42)].into_pipeline_data(Span::test_data(), None) + /// vec![Value::test_int(42)].into_pipeline_data(Span::test_data(), Signals::empty()) /// )? /// .into_value(Span::test_data())?; /// assert_eq!(Value::test_string("42"), result); @@ -151,7 +151,7 @@ impl PluginTest { Err(err) => Value::error(err, value.span()), } }, - None, + &Signals::empty(), )? }; @@ -171,7 +171,7 @@ impl PluginTest { Err(err) => Value::error(err, value.span()), } }, - None, + &Signals::empty(), ) } } diff --git a/crates/nu-plugin-test-support/tests/custom_value/mod.rs b/crates/nu-plugin-test-support/tests/custom_value/mod.rs index f703a92e33..888f05c1cb 100644 --- a/crates/nu-plugin-test-support/tests/custom_value/mod.rs +++ b/crates/nu-plugin-test-support/tests/custom_value/mod.rs @@ -53,6 +53,10 @@ struct IntoU32; struct IntoIntFromU32; impl Plugin for CustomU32Plugin { + fn version(&self) -> String { + "0.0.0".into() + } + fn commands(&self) -> Vec>> { vec![Box::new(IntoU32), Box::new(IntoIntFromU32)] } diff --git a/crates/nu-plugin-test-support/tests/hello/mod.rs b/crates/nu-plugin-test-support/tests/hello/mod.rs index 424940f156..a222637a76 100644 --- a/crates/nu-plugin-test-support/tests/hello/mod.rs +++ b/crates/nu-plugin-test-support/tests/hello/mod.rs @@ -8,6 +8,10 @@ struct HelloPlugin; struct Hello; impl Plugin for HelloPlugin { + fn version(&self) -> String { + "0.0.0".into() + } + fn commands(&self) -> Vec>> { vec![Box::new(Hello)] } diff --git a/crates/nu-plugin-test-support/tests/lowercase/mod.rs b/crates/nu-plugin-test-support/tests/lowercase/mod.rs index 0072a08aa2..5be127e7f5 100644 --- a/crates/nu-plugin-test-support/tests/lowercase/mod.rs +++ b/crates/nu-plugin-test-support/tests/lowercase/mod.rs @@ -1,8 +1,8 @@ use nu_plugin::*; use nu_plugin_test_support::PluginTest; use nu_protocol::{ - Example, IntoInterruptiblePipelineData, LabeledError, PipelineData, ShellError, Signature, - Span, Type, Value, + Example, IntoInterruptiblePipelineData, LabeledError, PipelineData, ShellError, Signals, + Signature, Span, Type, Value, }; struct LowercasePlugin; @@ -53,12 +53,16 @@ impl PluginCommand for Lowercase { // Errors in a stream should be returned as values. .unwrap_or_else(|err| Value::error(err, span)) }, - None, + &Signals::empty(), )?) } } impl Plugin for LowercasePlugin { + fn version(&self) -> String { + "0.0.0".into() + } + fn commands(&self) -> Vec>> { vec![Box::new(Lowercase)] } @@ -68,7 +72,8 @@ impl Plugin for LowercasePlugin { fn test_lowercase_using_eval_with() -> Result<(), ShellError> { let result = PluginTest::new("lowercase", LowercasePlugin.into())?.eval_with( "lowercase", - vec![Value::test_string("HeLlO wOrLd")].into_pipeline_data(Span::test_data(), None), + vec![Value::test_string("HeLlO wOrLd")] + .into_pipeline_data(Span::test_data(), Signals::empty()), )?; assert_eq!( diff --git a/crates/nu-plugin/Cargo.toml b/crates/nu-plugin/Cargo.toml index 68b3a7cadd..d46a3cc9cb 100644 --- a/crates/nu-plugin/Cargo.toml +++ b/crates/nu-plugin/Cargo.toml @@ -5,16 +5,17 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-plugin" edition = "2021" license = "MIT" name = "nu-plugin" -version = "0.94.3" +version = "0.95.1" [lib] bench = false [dependencies] -nu-engine = { path = "../nu-engine", version = "0.94.3" } -nu-protocol = { path = "../nu-protocol", version = "0.94.3" } -nu-plugin-protocol = { path = "../nu-plugin-protocol", version = "0.94.3" } -nu-plugin-core = { path = "../nu-plugin-core", version = "0.94.3", default-features = false } +nu-engine = { path = "../nu-engine", version = "0.95.1" } +nu-protocol = { path = "../nu-protocol", version = "0.95.1" } +nu-plugin-protocol = { path = "../nu-plugin-protocol", version = "0.95.1" } +nu-plugin-core = { path = "../nu-plugin-core", version = "0.95.1", default-features = false } +nu-utils = { path = "../nu-utils", version = "0.95.1" } log = { workspace = true } thiserror = "1.0" diff --git a/crates/nu-plugin/src/lib.rs b/crates/nu-plugin/src/lib.rs index 915c8d36fe..4c1a033a02 100644 --- a/crates/nu-plugin/src/lib.rs +++ b/crates/nu-plugin/src/lib.rs @@ -24,6 +24,10 @@ //! struct MyCommand; //! //! impl Plugin for MyPlugin { +//! fn version(&self) -> String { +//! env!("CARGO_PKG_VERSION").into() +//! } +//! //! fn commands(&self) -> Vec>> { //! vec![Box::new(MyCommand)] //! } diff --git a/crates/nu-plugin/src/plugin/command.rs b/crates/nu-plugin/src/plugin/command.rs index 5def950b0b..a90555ac40 100644 --- a/crates/nu-plugin/src/plugin/command.rs +++ b/crates/nu-plugin/src/plugin/command.rs @@ -22,7 +22,7 @@ use crate::{EngineInterface, EvaluatedCall, Plugin}; /// Basic usage: /// ``` /// # use nu_plugin::*; -/// # use nu_protocol::{Signature, PipelineData, Type, Value, LabeledError}; +/// # use nu_protocol::{LabeledError, PipelineData, Signals, Signature, Type, Value}; /// struct LowercasePlugin; /// struct Lowercase; /// @@ -55,11 +55,14 @@ use crate::{EngineInterface, EvaluatedCall, Plugin}; /// .map(|string| Value::string(string.to_lowercase(), span)) /// // Errors in a stream should be returned as values. /// .unwrap_or_else(|err| Value::error(err, span)) -/// }, None)?) +/// }, &Signals::empty())?) /// } /// } /// /// # impl Plugin for LowercasePlugin { +/// # fn version(&self) -> String { +/// # "0.0.0".into() +/// # } /// # fn commands(&self) -> Vec>> { /// # vec![Box::new(Lowercase)] /// # } @@ -195,6 +198,9 @@ pub trait PluginCommand: Sync { /// } /// /// # impl Plugin for HelloPlugin { +/// # fn version(&self) -> String { +/// # "0.0.0".into() +/// # } /// # fn commands(&self) -> Vec>> { /// # vec![Box::new(Hello)] /// # } @@ -334,7 +340,7 @@ where /// Build a [`PluginSignature`] from the signature-related methods on [`PluginCommand`]. /// -/// This is sent to the engine on `register`. +/// This is sent to the engine on `plugin add`. /// /// This is not a public API. #[doc(hidden)] diff --git a/crates/nu-plugin/src/plugin/interface/mod.rs b/crates/nu-plugin/src/plugin/interface/mod.rs index e3e9679471..60c5964f26 100644 --- a/crates/nu-plugin/src/plugin/interface/mod.rs +++ b/crates/nu-plugin/src/plugin/interface/mod.rs @@ -11,9 +11,10 @@ use nu_plugin_protocol::{ ProtocolInfo, }; use nu_protocol::{ - engine::Closure, Config, LabeledError, PipelineData, PluginSignature, ShellError, Span, - Spanned, Value, + engine::Closure, Config, LabeledError, PipelineData, PluginMetadata, PluginSignature, + ShellError, Signals, Span, Spanned, Value, }; +use nu_utils::SharedCow; use std::{ collections::{btree_map, BTreeMap, HashMap}, sync::{mpsc, Arc}, @@ -29,6 +30,9 @@ use std::{ #[derive(Debug)] #[doc(hidden)] pub enum ReceivedPluginCall { + Metadata { + engine: EngineInterface, + }, Signature { engine: EngineInterface, }, @@ -271,7 +275,9 @@ impl InterfaceManager for EngineInterfaceManager { PluginInput::Call(id, call) => { let interface = self.interface_for_context(id); // Read streams in the input - let call = match call.map_data(|input| self.read_pipeline_data(input, None)) { + let call = match call + .map_data(|input| self.read_pipeline_data(input, &Signals::empty())) + { Ok(call) => call, Err(err) => { // If there's an error with initialization of the input stream, just send @@ -280,8 +286,11 @@ impl InterfaceManager for EngineInterfaceManager { } }; match call { - // We just let the receiver handle it rather than trying to store signature here - // or something + // Ask the plugin for metadata + PluginCall::Metadata => { + self.send_plugin_call(ReceivedPluginCall::Metadata { engine: interface }) + } + // Ask the plugin for signatures PluginCall::Signature => { self.send_plugin_call(ReceivedPluginCall::Signature { engine: interface }) } @@ -314,7 +323,7 @@ impl InterfaceManager for EngineInterfaceManager { } PluginInput::EngineCallResponse(id, response) => { let response = response - .map_data(|header| self.read_pipeline_data(header, None)) + .map_data(|header| self.read_pipeline_data(header, &Signals::empty())) .unwrap_or_else(|err| { // If there's an error with initializing this stream, change it to an engine // call error response, but send it anyway @@ -416,6 +425,13 @@ impl EngineInterface { } } + /// Write a call response of plugin metadata. + pub(crate) fn write_metadata(&self, metadata: PluginMetadata) -> Result<(), ShellError> { + let response = PluginCallResponse::Metadata(metadata); + self.write(PluginOutput::CallResponse(self.context()?, response))?; + self.flush() + } + /// Write a call response of plugin signatures. /// /// Any custom values in the examples will be rendered using `to_base_value()`. @@ -510,9 +526,9 @@ impl EngineInterface { /// # Ok(()) /// # } /// ``` - pub fn get_config(&self) -> Result, ShellError> { + pub fn get_config(&self) -> Result, ShellError> { match self.engine_call(EngineCall::GetConfig)? { - EngineCallResponse::Config(config) => Ok(config), + EngineCallResponse::Config(config) => Ok(SharedCow::into_arc(config)), EngineCallResponse::Error(err) => Err(err), _ => Err(ShellError::PluginFailedToDecode { msg: "Received unexpected response for EngineCall::GetConfig".into(), diff --git a/crates/nu-plugin/src/plugin/interface/tests.rs b/crates/nu-plugin/src/plugin/interface/tests.rs index 6c3dfdf6c9..7065c6f64d 100644 --- a/crates/nu-plugin/src/plugin/interface/tests.rs +++ b/crates/nu-plugin/src/plugin/interface/tests.rs @@ -10,7 +10,7 @@ use nu_plugin_protocol::{ }; use nu_protocol::{ engine::Closure, ByteStreamType, Config, CustomValue, IntoInterruptiblePipelineData, - LabeledError, PipelineData, PluginSignature, ShellError, Span, Spanned, Value, + LabeledError, PipelineData, PluginSignature, ShellError, Signals, Span, Spanned, Value, }; use std::{ collections::HashMap, @@ -59,7 +59,7 @@ fn manager_consume_all_exits_after_streams_and_interfaces_are_dropped() -> Resul id: 0, span: Span::test_data(), }), - None, + &Signals::empty(), )?; // and an interface... @@ -115,7 +115,7 @@ fn manager_consume_all_propagates_io_error_to_readers() -> Result<(), ShellError id: 0, span: Span::test_data(), }), - None, + &Signals::empty(), )?; manager @@ -162,7 +162,7 @@ fn manager_consume_all_propagates_message_error_to_readers() -> Result<(), Shell span: Span::test_data(), type_: ByteStreamType::Unknown, }), - None, + &Signals::empty(), )?; manager @@ -322,6 +322,26 @@ fn manager_consume_goodbye_closes_plugin_call_channel() -> Result<(), ShellError Ok(()) } +#[test] +fn manager_consume_call_metadata_forwards_to_receiver_with_context() -> Result<(), ShellError> { + let mut manager = TestCase::new().engine(); + set_default_protocol_info(&mut manager)?; + + let rx = manager + .take_plugin_call_receiver() + .expect("couldn't take receiver"); + + manager.consume(PluginInput::Call(0, PluginCall::Metadata))?; + + match rx.try_recv().expect("call was not forwarded to receiver") { + ReceivedPluginCall::Metadata { engine } => { + assert_eq!(Some(0), engine.context); + Ok(()) + } + call => panic!("wrong call type: {call:?}"), + } +} + #[test] fn manager_consume_call_signature_forwards_to_receiver_with_context() -> Result<(), ShellError> { let mut manager = TestCase::new().engine(); @@ -595,7 +615,7 @@ fn manager_prepare_pipeline_data_deserializes_custom_values_in_streams() -> Resu [Value::test_custom_value(Box::new( test_plugin_custom_value(), ))] - .into_pipeline_data(Span::test_data(), None), + .into_pipeline_data(Span::test_data(), Signals::empty()), )?; let value = data @@ -627,7 +647,7 @@ fn manager_prepare_pipeline_data_embeds_deserialization_errors_in_streams() -> R let span = Span::new(20, 30); let data = manager.prepare_pipeline_data( [Value::custom(Box::new(invalid_custom_value), span)] - .into_pipeline_data(Span::test_data(), None), + .into_pipeline_data(Span::test_data(), Signals::empty()), )?; let value = data @@ -710,7 +730,7 @@ fn interface_write_response_with_stream() -> Result<(), ShellError> { interface .write_response(Ok::<_, ShellError>( [Value::test_int(3), Value::test_int(4), Value::test_int(5)] - .into_pipeline_data(Span::test_data(), None), + .into_pipeline_data(Span::test_data(), Signals::empty()), ))? .write()?; @@ -1112,7 +1132,7 @@ fn interface_prepare_pipeline_data_serializes_custom_values_in_streams() -> Resu [Value::test_custom_value(Box::new( expected_test_custom_value(), ))] - .into_pipeline_data(Span::test_data(), None), + .into_pipeline_data(Span::test_data(), Signals::empty()), &(), )?; @@ -1171,7 +1191,7 @@ fn interface_prepare_pipeline_data_embeds_serialization_errors_in_streams() -> R let span = Span::new(40, 60); let data = interface.prepare_pipeline_data( [Value::custom(Box::new(CantSerialize::BadVariant), span)] - .into_pipeline_data(Span::test_data(), None), + .into_pipeline_data(Span::test_data(), Signals::empty()), &(), )?; diff --git a/crates/nu-plugin/src/plugin/mod.rs b/crates/nu-plugin/src/plugin/mod.rs index 85283aadd0..d809318d68 100644 --- a/crates/nu-plugin/src/plugin/mod.rs +++ b/crates/nu-plugin/src/plugin/mod.rs @@ -16,7 +16,8 @@ use nu_plugin_core::{ }; use nu_plugin_protocol::{CallInfo, CustomValueOp, PluginCustomValue, PluginInput, PluginOutput}; use nu_protocol::{ - ast::Operator, CustomValue, IntoSpanned, LabeledError, PipelineData, ShellError, Spanned, Value, + ast::Operator, CustomValue, IntoSpanned, LabeledError, PipelineData, PluginMetadata, + ShellError, Spanned, Value, }; use thiserror::Error; @@ -37,7 +38,7 @@ pub(crate) const OUTPUT_BUFFER_SIZE: usize = 16384; /// The API for a Nushell plugin /// /// A plugin defines multiple commands, which are added to the engine when the user calls -/// `register`. +/// `plugin add`. /// /// The plugin must be able to be safely shared between threads, so that multiple invocations can /// be run in parallel. If interior mutability is desired, consider synchronization primitives such @@ -52,6 +53,10 @@ pub(crate) const OUTPUT_BUFFER_SIZE: usize = 16384; /// struct Hello; /// /// impl Plugin for HelloPlugin { +/// fn version(&self) -> String { +/// env!("CARGO_PKG_VERSION").into() +/// } +/// /// fn commands(&self) -> Vec>> { /// vec![Box::new(Hello)] /// } @@ -89,6 +94,23 @@ pub(crate) const OUTPUT_BUFFER_SIZE: usize = 16384; /// # } /// ``` pub trait Plugin: Sync { + /// The version of the plugin. + /// + /// The recommended implementation, which will use the version from your crate's `Cargo.toml` + /// file: + /// + /// ```no_run + /// # use nu_plugin::{Plugin, PluginCommand}; + /// # struct MyPlugin; + /// # impl Plugin for MyPlugin { + /// fn version(&self) -> String { + /// env!("CARGO_PKG_VERSION").into() + /// } + /// # fn commands(&self) -> Vec>> { vec![] } + /// # } + /// ``` + fn version(&self) -> String; + /// The commands supported by the plugin /// /// Each [`PluginCommand`] contains both the signature of the command and the functionality it @@ -216,6 +238,7 @@ pub trait Plugin: Sync { /// # struct MyPlugin; /// # impl MyPlugin { fn new() -> Self { Self }} /// # impl Plugin for MyPlugin { +/// # fn version(&self) -> String { "0.0.0".into() } /// # fn commands(&self) -> Vec>> {todo!();} /// # } /// fn main() { @@ -504,6 +527,12 @@ where } match plugin_call { + // Send metadata back to nushell so it can be stored with the plugin signatures + ReceivedPluginCall::Metadata { engine } => { + engine + .write_metadata(PluginMetadata::new().with_version(plugin.version())) + .try_to_report(&engine)?; + } // Sending the signature back to nushell to create the declaration definition ReceivedPluginCall::Signature { engine } => { let sigs = commands @@ -641,7 +670,7 @@ fn print_help(plugin: &impl Plugin, encoder: impl PluginEncoder) { } }) .and_then(|_| { - let flags = get_flags_section(None, &signature, |v| format!("{:#?}", v)); + let flags = get_flags_section(None, None, &signature, |v| format!("{:#?}", v)); write!(help, "{flags}") }) .and_then(|_| writeln!(help, "\nParameters:")) diff --git a/crates/nu-pretty-hex/Cargo.toml b/crates/nu-pretty-hex/Cargo.toml index ae5be298aa..cd3c3502a4 100644 --- a/crates/nu-pretty-hex/Cargo.toml +++ b/crates/nu-pretty-hex/Cargo.toml @@ -5,7 +5,7 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-pretty-hex" edition = "2021" license = "MIT" name = "nu-pretty-hex" -version = "0.94.3" +version = "0.95.1" [lib] doctest = false @@ -18,4 +18,4 @@ nu-ansi-term = { workspace = true } [dev-dependencies] heapless = { version = "0.8", default-features = false } -rand = "0.8" +rand = "0.8" \ No newline at end of file diff --git a/crates/nu-protocol/Cargo.toml b/crates/nu-protocol/Cargo.toml index de73ba1a2c..eaa861a073 100644 --- a/crates/nu-protocol/Cargo.toml +++ b/crates/nu-protocol/Cargo.toml @@ -5,7 +5,7 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-protocol" edition = "2021" license = "MIT" name = "nu-protocol" -version = "0.94.3" +version = "0.95.1" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -13,14 +13,16 @@ version = "0.94.3" bench = false [dependencies] -nu-utils = { path = "../nu-utils", version = "0.94.3" } -nu-path = { path = "../nu-path", version = "0.94.3" } -nu-system = { path = "../nu-system", version = "0.94.3" } +nu-utils = { path = "../nu-utils", version = "0.95.1" } +nu-path = { path = "../nu-path", version = "0.95.1" } +nu-system = { path = "../nu-system", version = "0.95.1" } +nu-derive-value = { path = "../nu-derive-value", version = "0.95.1" } brotli = { workspace = true, optional = true } byte-unit = { version = "5.1", features = [ "serde" ] } chrono = { workspace = true, features = [ "serde", "std", "unstable-locales" ], default-features = false } chrono-humanize = { workspace = true } +convert_case = { workspace = true } fancy-regex = { workspace = true } indexmap = { workspace = true } lru = { workspace = true } @@ -31,6 +33,7 @@ serde = { workspace = true, default-features = false } thiserror = "1.0" typetag = "0.2" os_pipe = { workspace = true, features = ["io_safety"] } +log = { workspace = true } [target.'cfg(unix)'.dependencies] nix = { workspace = true, default-features = false, features = ["signal"] } @@ -45,7 +48,7 @@ plugin = [ serde_json = { workspace = true } strum = "0.26" strum_macros = "0.26" -nu-test-support = { path = "../nu-test-support", version = "0.94.3" } +nu-test-support = { path = "../nu-test-support", version = "0.95.1" } pretty_assertions = { workspace = true } rstest = { workspace = true } tempfile = { workspace = true } diff --git a/crates/nu-protocol/src/alias.rs b/crates/nu-protocol/src/alias.rs index 24448225d4..701543c57b 100644 --- a/crates/nu-protocol/src/alias.rs +++ b/crates/nu-protocol/src/alias.rs @@ -1,13 +1,19 @@ use crate::{ - ast::{Call, Expression}, - engine::{Command, CommandType, EngineState, Stack}, + ast::Expression, + engine::{Call, Command, CommandType, EngineState, Stack}, PipelineData, ShellError, Signature, }; +/// Command wrapper of an alias. +/// +/// Our current aliases are implemented as wrapping commands +/// This has some limitations compared to text-substitution macro aliases but can reliably use more +/// of our machinery #[derive(Clone)] pub struct Alias { pub name: String, - pub command: Option>, // None if external call + /// Wrapped inner [`Command`]. `None` if alias of external call + pub command: Option>, pub wrapped_call: Expression, pub usage: String, pub extra_usage: String, diff --git a/crates/nu-protocol/src/ast/block.rs b/crates/nu-protocol/src/ast/block.rs index 6e3449af26..a6e640cc15 100644 --- a/crates/nu-protocol/src/ast/block.rs +++ b/crates/nu-protocol/src/ast/block.rs @@ -1,5 +1,5 @@ use super::Pipeline; -use crate::{engine::EngineState, OutDest, Signature, Span, Type, VarId}; +use crate::{engine::StateWorkingSet, ir::IrBlock, OutDest, Signature, Span, Type, VarId}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -8,6 +8,8 @@ pub struct Block { pub pipelines: Vec, pub captures: Vec, pub redirect_env: bool, + /// The block compiled to IR instructions. Not available for subexpressions. + pub ir_block: Option, pub span: Option, // None option encodes no span to avoid using test_span() } @@ -22,10 +24,10 @@ impl Block { pub fn pipe_redirection( &self, - engine_state: &EngineState, + working_set: &StateWorkingSet, ) -> (Option, Option) { if let Some(first) = self.pipelines.first() { - first.pipe_redirection(engine_state) + first.pipe_redirection(working_set) } else { (None, None) } @@ -45,6 +47,7 @@ impl Block { pipelines: vec![], captures: vec![], redirect_env: false, + ir_block: None, span: None, } } @@ -55,6 +58,7 @@ impl Block { pipelines: Vec::with_capacity(capacity), captures: vec![], redirect_env: false, + ir_block: None, span: None, } } @@ -74,6 +78,19 @@ impl Block { Type::Nothing } } + + /// Replace any `$in` variables in the initial element of pipelines within the block + pub fn replace_in_variable( + &mut self, + working_set: &mut StateWorkingSet<'_>, + new_var_id: VarId, + ) { + for pipeline in self.pipelines.iter_mut() { + if let Some(element) = pipeline.elements.first_mut() { + element.replace_in_variable(working_set, new_var_id); + } + } + } } impl From for Block @@ -86,6 +103,7 @@ where pipelines: pipelines.collect(), captures: vec![], redirect_env: false, + ir_block: None, span: None, } } diff --git a/crates/nu-protocol/src/ast/call.rs b/crates/nu-protocol/src/ast/call.rs index 6e8cc64a05..d26d0af300 100644 --- a/crates/nu-protocol/src/ast/call.rs +++ b/crates/nu-protocol/src/ast/call.rs @@ -7,12 +7,30 @@ use crate::{ ShellError, Span, Spanned, Value, }; +/// Parsed command arguments +/// +/// Primarily for internal commands #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum Argument { + /// A positional argument (that is not [`Argument::Spread`]) + /// + /// ```nushell + /// my_cmd positional + /// ``` Positional(Expression), + /// A named/flag argument that can optionally receive a [`Value`] as an [`Expression`] + /// + /// The optional second `Spanned` refers to the short-flag version if used + /// ```nushell + /// my_cmd --flag + /// my_cmd -f + /// my_cmd --flag-with-value + /// ``` Named((Spanned, Option>, Option)), - Unknown(Expression), // unknown argument used in "fall-through" signatures - Spread(Expression), // a list spread to fill in rest arguments + /// unknown argument used in "fall-through" signatures + Unknown(Expression), + /// a list spread to fill in rest arguments + Spread(Expression), } impl Argument { @@ -47,12 +65,24 @@ impl Argument { } } +/// Argument passed to an external command +/// +/// Here the parsing rules slightly differ to directly pass strings to the external process #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum ExternalArgument { + /// Expression that needs to be evaluated to turn into an external process argument Regular(Expression), + /// Occurrence of a `...` spread operator that needs to be expanded Spread(Expression), } +/// Parsed call of a `Command` +/// +/// As we also implement some internal keywords in terms of the `Command` trait, this type stores the passed arguments as [`Expression`]. +/// Some of its methods lazily evaluate those to [`Value`] while others return the underlying +/// [`Expression`]. +/// +/// For further utilities check the `nu_engine::CallExt` trait that extends [`Call`] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Call { /// identifier of the declaration to call diff --git a/crates/nu-protocol/src/ast/cell_path.rs b/crates/nu-protocol/src/ast/cell_path.rs index 3de37f3992..0880537557 100644 --- a/crates/nu-protocol/src/ast/cell_path.rs +++ b/crates/nu-protocol/src/ast/cell_path.rs @@ -3,16 +3,23 @@ use crate::Span; use serde::{Deserialize, Serialize}; use std::{cmp::Ordering, fmt::Display}; +/// One level of access of a [`CellPath`] #[derive(Debug, Clone, Serialize, Deserialize)] pub enum PathMember { + /// Accessing a member by string (i.e. columns of a table or [`Record`](crate::Record)) String { val: String, span: Span, + /// If marked as optional don't throw an error if not found but perform default handling + /// (e.g. return `Value::Nothing`) optional: bool, }, + /// Accessing a member by index (i.e. row of a table or item in a list) Int { val: usize, span: Span, + /// If marked as optional don't throw an error if not found but perform default handling + /// (e.g. return `Value::Nothing`) optional: bool, }, } @@ -143,6 +150,18 @@ impl PartialOrd for PathMember { } } +/// Represents the potentially nested access to fields/cells of a container type +/// +/// In our current implementation for table access the order of row/column is commutative. +/// This limits the number of possible rows to select in one [`CellPath`] to 1 as it could +/// otherwise be ambiguous +/// +/// ```nushell +/// col1.0 +/// 0.col1 +/// col2 +/// 42 +/// ``` #[derive(Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct CellPath { pub members: Vec, diff --git a/crates/nu-protocol/src/ast/expr.rs b/crates/nu-protocol/src/ast/expr.rs index 13d9e42985..d6386f92dc 100644 --- a/crates/nu-protocol/src/ast/expr.rs +++ b/crates/nu-protocol/src/ast/expr.rs @@ -5,8 +5,11 @@ use super::{ Call, CellPath, Expression, ExternalArgument, FullCellPath, Keyword, MatchPattern, Operator, Range, Table, ValueWithUnit, }; -use crate::{ast::ImportPattern, engine::EngineState, BlockId, OutDest, Signature, Span, VarId}; +use crate::{ + ast::ImportPattern, engine::StateWorkingSet, BlockId, OutDest, Signature, Span, VarId, +}; +/// An [`Expression`] AST node #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum Expr { Bool(bool), @@ -22,6 +25,7 @@ pub enum Expr { RowCondition(BlockId), UnaryNot(Box), BinaryOp(Box, Box, Box), //lhs, op, rhs + Collect(VarId, Box), Subexpression(BlockId), Block(BlockId), Closure(BlockId), @@ -32,8 +36,11 @@ pub enum Expr { Keyword(Box), ValueWithUnit(Box), DateTime(chrono::DateTime), + /// The boolean is `true` if the string is quoted. Filepath(String, bool), + /// The boolean is `true` if the string is quoted. Directory(String, bool), + /// The boolean is `true` if the string is quoted. GlobPattern(String, bool), String(String), RawString(String), @@ -43,6 +50,8 @@ pub enum Expr { Overlay(Option), // block ID of the overlay's origin module Signature(Box), StringInterpolation(Vec), + /// The boolean is `true` if the string is quoted. + GlobInterpolation(Vec, bool), Nothing, Garbage, } @@ -55,17 +64,19 @@ const _: () = assert!(std::mem::size_of::() <= 40); impl Expr { pub fn pipe_redirection( &self, - engine_state: &EngineState, + working_set: &StateWorkingSet, ) -> (Option, Option) { - // Usages of `$in` will be wrapped by a `collect` call by the parser, - // so we do not have to worry about that when considering - // which of the expressions below may consume pipeline output. match self { - Expr::Call(call) => engine_state.get_decl(call.decl_id).pipe_redirection(), - Expr::Subexpression(block_id) | Expr::Block(block_id) => engine_state + Expr::Call(call) => working_set.get_decl(call.decl_id).pipe_redirection(), + Expr::Collect(_, _) => { + // A collect expression always has default redirection, it's just going to collect + // stdout unless another type of redirection is specified + (None, None) + }, + Expr::Subexpression(block_id) | Expr::Block(block_id) => working_set .get_block(*block_id) - .pipe_redirection(engine_state), - Expr::FullCellPath(cell_path) => cell_path.head.expr.pipe_redirection(engine_state), + .pipe_redirection(working_set), + Expr::FullCellPath(cell_path) => cell_path.head.expr.pipe_redirection(working_set), Expr::Bool(_) | Expr::Int(_) | Expr::Float(_) @@ -84,6 +95,7 @@ impl Expr { | Expr::RawString(_) | Expr::CellPath(_) | Expr::StringInterpolation(_) + | Expr::GlobInterpolation(_, _) | Expr::Nothing => { // These expressions do not use the output of the pipeline in any meaningful way, // so we can discard the previous output by redirecting it to `Null`. @@ -119,6 +131,7 @@ impl Expr { } } +/// Expressions permitted inside a record expression/literal #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum RecordItem { /// A key: val mapping @@ -127,6 +140,7 @@ pub enum RecordItem { Spread(Span, Expression), } +/// Expressions permitted inside a list expression/literal #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum ListItem { /// A normal expression diff --git a/crates/nu-protocol/src/ast/expression.rs b/crates/nu-protocol/src/ast/expression.rs index 08ecc59aaf..4818aa2db2 100644 --- a/crates/nu-protocol/src/ast/expression.rs +++ b/crates/nu-protocol/src/ast/expression.rs @@ -6,6 +6,9 @@ use crate::{ use serde::{Deserialize, Serialize}; use std::sync::Arc; +use super::ListItem; + +/// Wrapper around [`Expr`] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Expression { pub expr: Expr, @@ -27,42 +30,9 @@ impl Expression { } } - pub fn precedence(&self) -> usize { + pub fn precedence(&self) -> u8 { match &self.expr { - Expr::Operator(operator) => { - use super::operator::*; - // Higher precedence binds tighter - - match operator { - Operator::Math(Math::Pow) => 100, - Operator::Math(Math::Multiply) - | Operator::Math(Math::Divide) - | Operator::Math(Math::Modulo) - | Operator::Math(Math::FloorDivision) => 95, - Operator::Math(Math::Plus) | Operator::Math(Math::Minus) => 90, - Operator::Bits(Bits::ShiftLeft) | Operator::Bits(Bits::ShiftRight) => 85, - Operator::Comparison(Comparison::NotRegexMatch) - | Operator::Comparison(Comparison::RegexMatch) - | Operator::Comparison(Comparison::StartsWith) - | Operator::Comparison(Comparison::EndsWith) - | Operator::Comparison(Comparison::LessThan) - | Operator::Comparison(Comparison::LessThanOrEqual) - | Operator::Comparison(Comparison::GreaterThan) - | Operator::Comparison(Comparison::GreaterThanOrEqual) - | Operator::Comparison(Comparison::Equal) - | Operator::Comparison(Comparison::NotEqual) - | Operator::Comparison(Comparison::In) - | Operator::Comparison(Comparison::NotIn) - | Operator::Math(Math::Append) => 80, - Operator::Bits(Bits::BitAnd) => 75, - Operator::Bits(Bits::BitXor) => 70, - Operator::Bits(Bits::BitOr) => 60, - Operator::Boolean(Boolean::And) => 50, - Operator::Boolean(Boolean::Xor) => 45, - Operator::Boolean(Boolean::Or) => 40, - Operator::Assignment(_) => 10, - } - } + Expr::Operator(operator) => operator.precedence(), _ => 0, } } @@ -138,37 +108,14 @@ impl Expression { left.has_in_variable(working_set) || right.has_in_variable(working_set) } Expr::UnaryNot(expr) => expr.has_in_variable(working_set), - Expr::Block(block_id) => { + Expr::Block(block_id) | Expr::Closure(block_id) => { let block = working_set.get_block(*block_id); - - if block.captures.contains(&IN_VARIABLE_ID) { - return true; - } - - if let Some(pipeline) = block.pipelines.first() { - match pipeline.elements.first() { - Some(element) => element.has_in_variable(working_set), - None => false, - } - } else { - false - } - } - Expr::Closure(block_id) => { - let block = working_set.get_block(*block_id); - - if block.captures.contains(&IN_VARIABLE_ID) { - return true; - } - - if let Some(pipeline) = block.pipelines.first() { - match pipeline.elements.first() { - Some(element) => element.has_in_variable(working_set), - None => false, - } - } else { - false - } + block.captures.contains(&IN_VARIABLE_ID) + || block + .pipelines + .iter() + .flat_map(|pipeline| pipeline.elements.first()) + .any(|element| element.has_in_variable(working_set)) } Expr::Binary(_) => false, Expr::Bool(_) => false, @@ -232,7 +179,7 @@ impl Expression { } false } - Expr::StringInterpolation(items) => { + Expr::StringInterpolation(items) | Expr::GlobInterpolation(items, _) => { for i in items { if i.has_in_variable(working_set) { return true; @@ -283,6 +230,9 @@ impl Expression { Expr::Signature(_) => false, Expr::String(_) => false, Expr::RawString(_) => false, + // A `$in` variable found within a `Collect` is local, as it's already been wrapped + // This is probably unlikely to happen anyway - the expressions are wrapped depth-first + Expr::Collect(_, _) => false, Expr::RowCondition(block_id) | Expr::Subexpression(block_id) => { let block = working_set.get_block(*block_id); @@ -441,11 +391,12 @@ impl Expression { Expr::Signature(_) => {} Expr::String(_) => {} Expr::RawString(_) => {} - Expr::StringInterpolation(items) => { + Expr::StringInterpolation(items) | Expr::GlobInterpolation(items, _) => { for i in items { i.replace_span(working_set, replaced, new_span) } } + Expr::Collect(_, expr) => expr.replace_span(working_set, replaced, new_span), Expr::RowCondition(block_id) | Expr::Subexpression(block_id) => { let mut block = (**working_set.get_block(*block_id)).clone(); @@ -475,6 +426,129 @@ impl Expression { } } + pub fn replace_in_variable(&mut self, working_set: &mut StateWorkingSet, new_var_id: VarId) { + match &mut self.expr { + Expr::Bool(_) => {} + Expr::Int(_) => {} + Expr::Float(_) => {} + Expr::Binary(_) => {} + Expr::Range(_) => {} + Expr::Var(var_id) | Expr::VarDecl(var_id) => { + if *var_id == IN_VARIABLE_ID { + *var_id = new_var_id; + } + } + Expr::Call(call) => { + for arg in call.arguments.iter_mut() { + match arg { + Argument::Positional(expr) + | Argument::Unknown(expr) + | Argument::Named((_, _, Some(expr))) + | Argument::Spread(expr) => { + expr.replace_in_variable(working_set, new_var_id) + } + Argument::Named((_, _, None)) => {} + } + } + for expr in call.parser_info.values_mut() { + expr.replace_in_variable(working_set, new_var_id) + } + } + Expr::ExternalCall(head, args) => { + head.replace_in_variable(working_set, new_var_id); + for arg in args.iter_mut() { + match arg { + ExternalArgument::Regular(expr) | ExternalArgument::Spread(expr) => { + expr.replace_in_variable(working_set, new_var_id) + } + } + } + } + Expr::Operator(_) => {} + // `$in` in `Collect` has already been handled, so we don't need to check further + Expr::Collect(_, _) => {} + Expr::Block(block_id) + | Expr::Closure(block_id) + | Expr::RowCondition(block_id) + | Expr::Subexpression(block_id) => { + let mut block = Block::clone(working_set.get_block(*block_id)); + block.replace_in_variable(working_set, new_var_id); + *working_set.get_block_mut(*block_id) = block; + } + Expr::UnaryNot(expr) => { + expr.replace_in_variable(working_set, new_var_id); + } + Expr::BinaryOp(lhs, op, rhs) => { + for expr in [lhs, op, rhs] { + expr.replace_in_variable(working_set, new_var_id); + } + } + Expr::MatchBlock(match_patterns) => { + for (_, expr) in match_patterns.iter_mut() { + expr.replace_in_variable(working_set, new_var_id); + } + } + Expr::List(items) => { + for item in items.iter_mut() { + match item { + ListItem::Item(expr) | ListItem::Spread(_, expr) => { + expr.replace_in_variable(working_set, new_var_id) + } + } + } + } + Expr::Table(table) => { + for col_expr in table.columns.iter_mut() { + col_expr.replace_in_variable(working_set, new_var_id); + } + for row in table.rows.iter_mut() { + for row_expr in row.iter_mut() { + row_expr.replace_in_variable(working_set, new_var_id); + } + } + } + Expr::Record(items) => { + for item in items.iter_mut() { + match item { + RecordItem::Pair(key, val) => { + key.replace_in_variable(working_set, new_var_id); + val.replace_in_variable(working_set, new_var_id); + } + RecordItem::Spread(_, expr) => { + expr.replace_in_variable(working_set, new_var_id) + } + } + } + } + Expr::Keyword(kw) => kw.expr.replace_in_variable(working_set, new_var_id), + Expr::ValueWithUnit(value_with_unit) => value_with_unit + .expr + .replace_in_variable(working_set, new_var_id), + Expr::DateTime(_) => {} + Expr::Filepath(_, _) => {} + Expr::Directory(_, _) => {} + Expr::GlobPattern(_, _) => {} + Expr::String(_) => {} + Expr::RawString(_) => {} + Expr::CellPath(_) => {} + Expr::FullCellPath(full_cell_path) => { + full_cell_path + .head + .replace_in_variable(working_set, new_var_id); + } + Expr::ImportPattern(_) => {} + Expr::Overlay(_) => {} + Expr::Signature(_) => {} + Expr::StringInterpolation(exprs) | Expr::GlobInterpolation(exprs, _) => { + for expr in exprs.iter_mut() { + expr.replace_in_variable(working_set, new_var_id); + } + } + Expr::Nothing => {} + Expr::Garbage => {} + } + } + pub fn new(working_set: &mut StateWorkingSet, expr: Expr, span: Span, ty: Type) -> Expression { let span_id = working_set.add_span(span); Expression { diff --git a/crates/nu-protocol/src/ast/import_pattern.rs b/crates/nu-protocol/src/ast/import_pattern.rs index 893dd9897b..ad08bfb2d1 100644 --- a/crates/nu-protocol/src/ast/import_pattern.rs +++ b/crates/nu-protocol/src/ast/import_pattern.rs @@ -3,10 +3,14 @@ use serde::{Deserialize, Serialize}; use crate::{ModuleId, Span, VarId}; use std::collections::HashSet; +/// possible patterns after the first module level in an [`ImportPattern`]. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum ImportPatternMember { + /// Wildcard import of items Glob { span: Span }, + /// single specific module or item Name { name: Vec, span: Span }, + /// list of items List { names: Vec<(Vec, Span)> }, } @@ -31,6 +35,7 @@ impl ImportPatternMember { } } +/// The first item of a `use` statement needs to specify an explicit module #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ImportPatternHead { pub name: Vec, @@ -38,6 +43,7 @@ pub struct ImportPatternHead { pub span: Span, } +/// The pattern specifying modules in a `use` statement #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ImportPattern { pub head: ImportPatternHead, diff --git a/crates/nu-protocol/src/ast/match_pattern.rs b/crates/nu-protocol/src/ast/match_pattern.rs index b8f87c3f63..ca639270aa 100644 --- a/crates/nu-protocol/src/ast/match_pattern.rs +++ b/crates/nu-protocol/src/ast/match_pattern.rs @@ -2,10 +2,11 @@ use super::Expression; use crate::{Span, VarId}; use serde::{Deserialize, Serialize}; +/// AST Node for match arm with optional match guard #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct MatchPattern { pub pattern: Pattern, - pub guard: Option, + pub guard: Option>, pub span: Span, } @@ -15,16 +16,28 @@ impl MatchPattern { } } +/// AST Node for pattern matching rules #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum Pattern { + /// Destructuring of records Record(Vec<(String, MatchPattern)>), + /// List destructuring List(Vec), - Value(Expression), + /// Matching against a literal + // TODO: it would be nice if this didn't depend on AST + // maybe const evaluation can get us to a Value instead? + Value(Box), + /// binding to a variable Variable(VarId), + /// the `pattern1 \ pattern2` or-pattern Or(Vec), - Rest(VarId), // the ..$foo pattern - IgnoreRest, // the .. pattern - IgnoreValue, // the _ pattern + /// the `..$foo` pattern + Rest(VarId), + /// the `..` pattern + IgnoreRest, + /// the `_` pattern + IgnoreValue, + /// Failed parsing of a pattern Garbage, } diff --git a/crates/nu-protocol/src/ast/mod.rs b/crates/nu-protocol/src/ast/mod.rs index 7c627997fe..cb09168805 100644 --- a/crates/nu-protocol/src/ast/mod.rs +++ b/crates/nu-protocol/src/ast/mod.rs @@ -1,3 +1,4 @@ +//! Types representing parsed Nushell code (the Abstract Syntax Tree) mod block; mod call; mod cell_path; @@ -9,7 +10,7 @@ mod match_pattern; mod operator; mod pipeline; mod range; -pub mod table; +mod table; mod unit; mod value_with_unit; diff --git a/crates/nu-protocol/src/ast/operator.rs b/crates/nu-protocol/src/ast/operator.rs index 46484761bc..713c6c0060 100644 --- a/crates/nu-protocol/src/ast/operator.rs +++ b/crates/nu-protocol/src/ast/operator.rs @@ -68,6 +68,40 @@ pub enum Operator { Assignment(Assignment), } +impl Operator { + pub fn precedence(&self) -> u8 { + match self { + Self::Math(Math::Pow) => 100, + Self::Math(Math::Multiply) + | Self::Math(Math::Divide) + | Self::Math(Math::Modulo) + | Self::Math(Math::FloorDivision) => 95, + Self::Math(Math::Plus) | Self::Math(Math::Minus) => 90, + Self::Bits(Bits::ShiftLeft) | Self::Bits(Bits::ShiftRight) => 85, + Self::Comparison(Comparison::NotRegexMatch) + | Self::Comparison(Comparison::RegexMatch) + | Self::Comparison(Comparison::StartsWith) + | Self::Comparison(Comparison::EndsWith) + | Self::Comparison(Comparison::LessThan) + | Self::Comparison(Comparison::LessThanOrEqual) + | Self::Comparison(Comparison::GreaterThan) + | Self::Comparison(Comparison::GreaterThanOrEqual) + | Self::Comparison(Comparison::Equal) + | Self::Comparison(Comparison::NotEqual) + | Self::Comparison(Comparison::In) + | Self::Comparison(Comparison::NotIn) + | Self::Math(Math::Append) => 80, + Self::Bits(Bits::BitAnd) => 75, + Self::Bits(Bits::BitXor) => 70, + Self::Bits(Bits::BitOr) => 60, + Self::Boolean(Boolean::And) => 50, + Self::Boolean(Boolean::Xor) => 45, + Self::Boolean(Boolean::Or) => 40, + Self::Assignment(_) => 10, + } + } +} + impl Display for Operator { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -95,10 +129,10 @@ impl Display for Operator { Operator::Math(Math::Multiply) => write!(f, "*"), Operator::Math(Math::Divide) => write!(f, "/"), Operator::Math(Math::Modulo) => write!(f, "mod"), - Operator::Math(Math::FloorDivision) => write!(f, "fdiv"), + Operator::Math(Math::FloorDivision) => write!(f, "//"), Operator::Math(Math::Pow) => write!(f, "**"), - Operator::Boolean(Boolean::And) => write!(f, "&&"), - Operator::Boolean(Boolean::Or) => write!(f, "||"), + Operator::Boolean(Boolean::And) => write!(f, "and"), + Operator::Boolean(Boolean::Or) => write!(f, "or"), Operator::Boolean(Boolean::Xor) => write!(f, "xor"), Operator::Bits(Bits::BitOr) => write!(f, "bit-or"), Operator::Bits(Bits::BitXor) => write!(f, "bit-xor"), diff --git a/crates/nu-protocol/src/ast/pipeline.rs b/crates/nu-protocol/src/ast/pipeline.rs index f03c016daf..0ed02c700b 100644 --- a/crates/nu-protocol/src/ast/pipeline.rs +++ b/crates/nu-protocol/src/ast/pipeline.rs @@ -1,8 +1,4 @@ -use crate::{ - ast::Expression, - engine::{EngineState, StateWorkingSet}, - OutDest, Span, -}; +use crate::{ast::Expression, engine::StateWorkingSet, OutDest, Span, VarId}; use serde::{Deserialize, Serialize}; use std::fmt::Display; @@ -66,6 +62,19 @@ impl RedirectionTarget { RedirectionTarget::Pipe { .. } => {} } } + + pub fn replace_in_variable( + &mut self, + working_set: &mut StateWorkingSet<'_>, + new_var_id: VarId, + ) { + match self { + RedirectionTarget::File { expr, .. } => { + expr.replace_in_variable(working_set, new_var_id) + } + RedirectionTarget::Pipe { .. } => {} + } + } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] @@ -79,6 +88,23 @@ pub enum PipelineRedirection { err: RedirectionTarget, }, } +impl PipelineRedirection { + pub fn replace_in_variable( + &mut self, + working_set: &mut StateWorkingSet<'_>, + new_var_id: VarId, + ) { + match self { + PipelineRedirection::Single { source: _, target } => { + target.replace_in_variable(working_set, new_var_id) + } + PipelineRedirection::Separate { out, err } => { + out.replace_in_variable(working_set, new_var_id); + err.replace_in_variable(working_set, new_var_id); + } + } + } +} #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PipelineElement { @@ -120,9 +146,20 @@ impl PipelineElement { pub fn pipe_redirection( &self, - engine_state: &EngineState, + working_set: &StateWorkingSet, ) -> (Option, Option) { - self.expr.expr.pipe_redirection(engine_state) + self.expr.expr.pipe_redirection(working_set) + } + + pub fn replace_in_variable( + &mut self, + working_set: &mut StateWorkingSet<'_>, + new_var_id: VarId, + ) { + self.expr.replace_in_variable(working_set, new_var_id); + if let Some(redirection) = &mut self.redirection { + redirection.replace_in_variable(working_set, new_var_id); + } } } @@ -166,10 +203,10 @@ impl Pipeline { pub fn pipe_redirection( &self, - engine_state: &EngineState, + working_set: &StateWorkingSet, ) -> (Option, Option) { if let Some(first) = self.elements.first() { - first.pipe_redirection(engine_state) + first.pipe_redirection(working_set) } else { (None, None) } diff --git a/crates/nu-protocol/src/config/mod.rs b/crates/nu-protocol/src/config/mod.rs index 5aa611937b..eda3d9a15e 100644 --- a/crates/nu-protocol/src/config/mod.rs +++ b/crates/nu-protocol/src/config/mod.rs @@ -1,3 +1,4 @@ +//! Module containing the internal representation of user configuration use self::completer::*; use self::helper::*; use self::hooks::*; @@ -104,7 +105,7 @@ pub struct Config { /// Configuration for plugins. /// /// Users can provide configuration for a plugin through this entry. The entry name must - /// match the registered plugin name so `register nu_plugin_example` will be able to place + /// match the registered plugin name so `plugin add nu_plugin_example` will be able to place /// its configuration under a `nu_plugin_example` column. pub plugins: HashMap, /// Configuration for plugin garbage collection. diff --git a/crates/nu-protocol/src/debugger/debugger_trait.rs b/crates/nu-protocol/src/debugger/debugger_trait.rs index 69d395fb98..88cc753548 100644 --- a/crates/nu-protocol/src/debugger/debugger_trait.rs +++ b/crates/nu-protocol/src/debugger/debugger_trait.rs @@ -16,6 +16,7 @@ use crate::{ ast::{Block, PipelineElement}, engine::EngineState, + ir::IrBlock, PipelineData, ShellError, Span, Value, }; use std::{fmt::Debug, ops::DerefMut}; @@ -35,11 +36,11 @@ pub trait DebugContext: Clone + Copy + Debug { #[allow(unused_variables)] fn leave_block(engine_state: &EngineState, block: &Block) {} - /// Called when the evaluator enters a pipeline element + /// Called when the AST evaluator enters a pipeline element #[allow(unused_variables)] fn enter_element(engine_state: &EngineState, element: &PipelineElement) {} - /// Called when the evaluator leaves a pipeline element + /// Called when the AST evaluator leaves a pipeline element #[allow(unused_variables)] fn leave_element( engine_state: &EngineState, @@ -47,6 +48,27 @@ pub trait DebugContext: Clone + Copy + Debug { result: &Result, ) { } + + /// Called before the IR evaluator runs an instruction + #[allow(unused_variables)] + fn enter_instruction( + engine_state: &EngineState, + ir_block: &IrBlock, + instruction_index: usize, + registers: &[PipelineData], + ) { + } + + /// Called after the IR evaluator runs an instruction + #[allow(unused_variables)] + fn leave_instruction( + engine_state: &EngineState, + ir_block: &IrBlock, + instruction_index: usize, + registers: &[PipelineData], + error: Option<&ShellError>, + ) { + } } /// Marker struct signalizing that evaluation should use a Debugger @@ -85,6 +107,40 @@ impl DebugContext for WithDebug { .leave_element(engine_state, element, result); } } + + fn enter_instruction( + engine_state: &EngineState, + ir_block: &IrBlock, + instruction_index: usize, + registers: &[PipelineData], + ) { + if let Ok(mut debugger) = engine_state.debugger.lock() { + debugger.deref_mut().enter_instruction( + engine_state, + ir_block, + instruction_index, + registers, + ) + } + } + + fn leave_instruction( + engine_state: &EngineState, + ir_block: &IrBlock, + instruction_index: usize, + registers: &[PipelineData], + error: Option<&ShellError>, + ) { + if let Ok(mut debugger) = engine_state.debugger.lock() { + debugger.deref_mut().leave_instruction( + engine_state, + ir_block, + instruction_index, + registers, + error, + ) + } + } } /// Marker struct signalizing that evaluation should NOT use a Debugger @@ -118,11 +174,11 @@ pub trait Debugger: Send + Debug { #[allow(unused_variables)] fn leave_block(&mut self, engine_state: &EngineState, block: &Block) {} - /// Called when the evaluator enters a pipeline element + /// Called when the AST evaluator enters a pipeline element #[allow(unused_variables)] fn enter_element(&mut self, engine_state: &EngineState, pipeline_element: &PipelineElement) {} - /// Called when the evaluator leaves a pipeline element + /// Called when the AST evaluator leaves a pipeline element #[allow(unused_variables)] fn leave_element( &mut self, @@ -132,6 +188,29 @@ pub trait Debugger: Send + Debug { ) { } + /// Called before the IR evaluator runs an instruction + #[allow(unused_variables)] + fn enter_instruction( + &mut self, + engine_state: &EngineState, + ir_block: &IrBlock, + instruction_index: usize, + registers: &[PipelineData], + ) { + } + + /// Called after the IR evaluator runs an instruction + #[allow(unused_variables)] + fn leave_instruction( + &mut self, + engine_state: &EngineState, + ir_block: &IrBlock, + instruction_index: usize, + registers: &[PipelineData], + error: Option<&ShellError>, + ) { + } + /// Create a final report as a Value /// /// Intended to be called after deactivate() diff --git a/crates/nu-protocol/src/debugger/mod.rs b/crates/nu-protocol/src/debugger/mod.rs index 03208fb3c7..86768a0a34 100644 --- a/crates/nu-protocol/src/debugger/mod.rs +++ b/crates/nu-protocol/src/debugger/mod.rs @@ -1,3 +1,4 @@ +//! Module containing the trait to instrument the engine for debugging and profiling pub mod debugger_trait; pub mod profiler; diff --git a/crates/nu-protocol/src/debugger/profiler.rs b/crates/nu-protocol/src/debugger/profiler.rs index 0fa2d4f3aa..ea81a1b814 100644 --- a/crates/nu-protocol/src/debugger/profiler.rs +++ b/crates/nu-protocol/src/debugger/profiler.rs @@ -7,10 +7,11 @@ use crate::{ ast::{Block, Expr, PipelineElement}, debugger::Debugger, engine::EngineState, + ir::IrBlock, record, PipelineData, ShellError, Span, Value, }; -use std::io::BufRead; use std::time::Instant; +use std::{borrow::Borrow, io::BufRead}; #[derive(Debug, Clone, Copy)] struct ElementId(usize); @@ -24,6 +25,7 @@ struct ElementInfo { element_span: Span, element_output: Option, expr: Option, + instruction: Option<(usize, String)>, children: Vec, } @@ -36,57 +38,53 @@ impl ElementInfo { element_span, element_output: None, expr: None, + instruction: None, children: vec![], } } } +/// Options for [`Profiler`] +#[derive(Debug, Clone)] +pub struct ProfilerOptions { + pub max_depth: i64, + pub collect_spans: bool, + pub collect_source: bool, + pub collect_expanded_source: bool, + pub collect_values: bool, + pub collect_exprs: bool, + pub collect_instructions: bool, + pub collect_lines: bool, +} + /// Basic profiler, used in `debug profile` #[derive(Debug, Clone)] pub struct Profiler { depth: i64, - max_depth: i64, - collect_spans: bool, - collect_source: bool, - collect_expanded_source: bool, - collect_values: bool, - collect_exprs: bool, - collect_lines: bool, + opts: ProfilerOptions, elements: Vec, element_stack: Vec, } impl Profiler { #[allow(clippy::too_many_arguments)] - pub fn new( - max_depth: i64, - collect_spans: bool, - collect_source: bool, - collect_expanded_source: bool, - collect_values: bool, - collect_exprs: bool, - collect_lines: bool, - span: Span, - ) -> Self { + pub fn new(opts: ProfilerOptions, span: Span) -> Self { let first = ElementInfo { start: Instant::now(), duration_sec: 0.0, depth: 0, element_span: span, - element_output: collect_values.then(|| Value::nothing(span)), - expr: collect_exprs.then(|| "call".to_string()), + element_output: opts.collect_values.then(|| Value::nothing(span)), + expr: opts.collect_exprs.then(|| "call".to_string()), + instruction: opts + .collect_instructions + .then(|| (0, "".to_string())), children: vec![], }; Profiler { depth: 0, - max_depth, - collect_spans, - collect_source, - collect_expanded_source, - collect_values, - collect_exprs, - collect_lines, + opts, elements: vec![first], element_stack: vec![ElementId(0)], } @@ -130,7 +128,7 @@ impl Debugger for Profiler { } fn enter_element(&mut self, engine_state: &EngineState, element: &PipelineElement) { - if self.depth > self.max_depth { + if self.depth > self.opts.max_depth { return; } @@ -140,6 +138,7 @@ impl Debugger for Profiler { }; let expr_opt = self + .opts .collect_exprs .then(|| expr_to_string(engine_state, &element.expr.expr)); @@ -165,13 +164,13 @@ impl Debugger for Profiler { element: &PipelineElement, result: &Result, ) { - if self.depth > self.max_depth { + if self.depth > self.opts.max_depth { return; } let element_span = element.expr.span; - let out_opt = self.collect_values.then(|| match result { + let out_opt = self.opts.collect_values.then(|| match result { Ok(pipeline_data) => match pipeline_data { PipelineData::Value(val, ..) => val.clone(), PipelineData::ListStream(..) => Value::string("list stream", element_span), @@ -192,6 +191,91 @@ impl Debugger for Profiler { self.element_stack.pop(); } + fn enter_instruction( + &mut self, + engine_state: &EngineState, + ir_block: &IrBlock, + instruction_index: usize, + _registers: &[PipelineData], + ) { + if self.depth > self.opts.max_depth { + return; + } + + let Some(parent_id) = self.last_element_id() else { + eprintln!("Profiler Error: Missing parent element ID."); + return; + }; + + let instruction = &ir_block.instructions[instruction_index]; + let span = ir_block.spans[instruction_index]; + + let instruction_opt = self.opts.collect_instructions.then(|| { + ( + instruction_index, + instruction + .display(engine_state, &ir_block.data) + .to_string(), + ) + }); + + let new_id = ElementId(self.elements.len()); + + let mut new_element = ElementInfo::new(self.depth, span); + new_element.instruction = instruction_opt; + + self.elements.push(new_element); + + let Some(parent) = self.elements.get_mut(parent_id.0) else { + eprintln!("Profiler Error: Missing parent element."); + return; + }; + + parent.children.push(new_id); + self.element_stack.push(new_id); + } + + fn leave_instruction( + &mut self, + _engine_state: &EngineState, + ir_block: &IrBlock, + instruction_index: usize, + registers: &[PipelineData], + error: Option<&ShellError>, + ) { + if self.depth > self.opts.max_depth { + return; + } + + let instruction = &ir_block.instructions[instruction_index]; + let span = ir_block.spans[instruction_index]; + + let out_opt = self + .opts + .collect_values + .then(|| { + error + .map(Err) + .or_else(|| { + instruction + .output_register() + .map(|register| Ok(®isters[register.0 as usize])) + }) + .map(|result| format_result(&result, span)) + }) + .flatten(); + + let Some(last_element) = self.last_element_mut() else { + eprintln!("Profiler Error: Missing last element."); + return; + }; + + last_element.duration_sec = last_element.start.elapsed().as_secs_f64(); + last_element.element_output = out_opt; + + self.element_stack.pop(); + } + fn report(&self, engine_state: &EngineState, profiler_span: Span) -> Result { Ok(Value::list( collect_data( @@ -258,6 +342,8 @@ fn expr_to_string(engine_state: &EngineState, expr: &Expr) -> String { Expr::Signature(_) => "signature".to_string(), Expr::String(_) | Expr::RawString(_) => "string".to_string(), Expr::StringInterpolation(_) => "string interpolation".to_string(), + Expr::GlobInterpolation(_, _) => "glob interpolation".to_string(), + Expr::Collect(_, _) => "collect".to_string(), Expr::Subexpression(_) => "subexpression".to_string(), Expr::Table(_) => "table".to_string(), Expr::UnaryNot(_) => "unary not".to_string(), @@ -267,6 +353,21 @@ fn expr_to_string(engine_state: &EngineState, expr: &Expr) -> String { } } +fn format_result( + result: &Result, impl Borrow>, + element_span: Span, +) -> Value { + match result { + Ok(pipeline_data) => match pipeline_data.borrow() { + PipelineData::Value(val, ..) => val.clone(), + PipelineData::ListStream(..) => Value::string("list stream", element_span), + PipelineData::ByteStream(..) => Value::string("byte stream", element_span), + _ => Value::nothing(element_span), + }, + Err(e) => Value::error(e.borrow().clone(), element_span), + } +} + // Find a file name and a line number (indexed from 1) of a span fn find_file_of_span(engine_state: &EngineState, span: Span) -> Option<(&str, usize)> { for file in engine_state.files() { @@ -307,7 +408,7 @@ fn collect_data( "parent_id" => Value::int(parent_id.0 as i64, profiler_span), }; - if profiler.collect_lines { + if profiler.opts.collect_lines { if let Some((fname, line_num)) = find_file_of_span(engine_state, element.element_span) { row.push("file", Value::string(fname, profiler_span)); row.push("line", Value::int(line_num as i64, profiler_span)); @@ -317,7 +418,7 @@ fn collect_data( } } - if profiler.collect_spans { + if profiler.opts.collect_spans { let span_start = i64::try_from(element.element_span.start) .map_err(|_| profiler_error("error converting span start to i64", profiler_span))?; let span_end = i64::try_from(element.element_span.end) @@ -335,12 +436,12 @@ fn collect_data( ); } - if profiler.collect_source { + if profiler.opts.collect_source { let val = String::from_utf8_lossy(engine_state.get_span_contents(element.element_span)); let val = val.trim(); let nlines = val.lines().count(); - let fragment = if profiler.collect_expanded_source { + let fragment = if profiler.opts.collect_expanded_source { val.to_string() } else { let mut first_line = val.lines().next().unwrap_or("").to_string(); @@ -359,6 +460,17 @@ fn collect_data( row.push("expr", Value::string(expr_string.clone(), profiler_span)); } + if let Some((instruction_index, instruction)) = &element.instruction { + row.push( + "pc", + (*instruction_index) + .try_into() + .map(|index| Value::int(index, profiler_span)) + .unwrap_or(Value::nothing(profiler_span)), + ); + row.push("instruction", Value::string(instruction, profiler_span)); + } + if let Some(val) = &element.element_output { row.push("output", val.clone()); } diff --git a/crates/nu-protocol/src/engine/argument.rs b/crates/nu-protocol/src/engine/argument.rs new file mode 100644 index 0000000000..043654b761 --- /dev/null +++ b/crates/nu-protocol/src/engine/argument.rs @@ -0,0 +1,124 @@ +use std::sync::Arc; + +use crate::{ast::Expression, ir::DataSlice, Span, Value}; + +/// Represents a fully evaluated argument to a call. +#[derive(Debug, Clone)] +pub enum Argument { + /// A positional argument + Positional { + span: Span, + val: Value, + ast: Option>, + }, + /// A spread argument, e.g. `...$args` + Spread { + span: Span, + vals: Value, + ast: Option>, + }, + /// A named argument with no value, e.g. `--flag` + Flag { + data: Arc<[u8]>, + name: DataSlice, + short: DataSlice, + span: Span, + }, + /// A named argument with a value, e.g. `--flag value` or `--flag=` + Named { + data: Arc<[u8]>, + name: DataSlice, + short: DataSlice, + span: Span, + val: Value, + ast: Option>, + }, + /// Information generated by the parser for use by certain keyword commands + ParserInfo { + data: Arc<[u8]>, + name: DataSlice, + // TODO: rather than `Expression`, this would probably be best served by a specific enum + // type for this purpose. + info: Box, + }, +} + +impl Argument { + /// The span encompassing the argument's usage within the call, distinct from the span of the + /// actual value of the argument. + pub fn span(&self) -> Option { + match self { + Argument::Positional { span, .. } => Some(*span), + Argument::Spread { span, .. } => Some(*span), + Argument::Flag { span, .. } => Some(*span), + Argument::Named { span, .. } => Some(*span), + // Because `ParserInfo` is generated, its span shouldn't be used + Argument::ParserInfo { .. } => None, + } + } + + /// The original AST [`Expression`] for the argument's value. This is not usually available; + /// declarations have to opt-in if they require this. + pub fn ast_expression(&self) -> Option<&Arc> { + match self { + Argument::Positional { ast, .. } => ast.as_ref(), + Argument::Spread { ast, .. } => ast.as_ref(), + Argument::Flag { .. } => None, + Argument::Named { ast, .. } => ast.as_ref(), + Argument::ParserInfo { .. } => None, + } + } +} + +/// Stores the argument context for calls in IR evaluation. +#[derive(Debug, Clone)] +pub struct ArgumentStack { + arguments: Vec, +} + +impl ArgumentStack { + /// Create a new, empty argument stack. + pub const fn new() -> Self { + ArgumentStack { arguments: vec![] } + } + + /// Returns the index of the end of the argument stack. Call and save this before adding + /// arguments. + pub fn get_base(&self) -> usize { + self.arguments.len() + } + + /// Calculates the number of arguments past the given [previously retrieved](.get_base) base + /// pointer. + pub fn get_len(&self, base: usize) -> usize { + self.arguments.len().checked_sub(base).unwrap_or_else(|| { + panic!( + "base ({}) is beyond the end of the arguments stack ({})", + base, + self.arguments.len() + ); + }) + } + + /// Push an argument onto the end of the argument stack. + pub fn push(&mut self, argument: Argument) { + self.arguments.push(argument); + } + + /// Clear all of the arguments after the given base index, to prepare for the next frame. + pub fn leave_frame(&mut self, base: usize) { + self.arguments.truncate(base); + } + + /// Get arguments for the frame based on the given [`base`](`.get_base()`) and + /// [`len`](`.get_len()`) parameters. + pub fn get_args(&self, base: usize, len: usize) -> &[Argument] { + &self.arguments[base..(base + len)] + } + + /// Move arguments for the frame based on the given [`base`](`.get_base()`) and + /// [`len`](`.get_len()`) parameters. + pub fn drain_args(&mut self, base: usize, len: usize) -> impl Iterator + '_ { + self.arguments.drain(base..(base + len)) + } +} diff --git a/crates/nu-protocol/src/engine/call.rs b/crates/nu-protocol/src/engine/call.rs new file mode 100644 index 0000000000..741e2bd87a --- /dev/null +++ b/crates/nu-protocol/src/engine/call.rs @@ -0,0 +1,223 @@ +use crate::{ + ast::{self, Expression}, + ir, DeclId, FromValue, ShellError, Span, Value, +}; + +use super::{EngineState, Stack, StateWorkingSet}; + +/// This is a HACK to help [`Command`](super::Command) support both the old AST evaluator and the +/// new IR evaluator at the same time. It should be removed once we are satisfied with the new +/// evaluator. +#[derive(Debug, Clone)] +pub struct Call<'a> { + pub head: Span, + pub decl_id: DeclId, + pub inner: CallImpl<'a>, +} + +#[derive(Debug, Clone)] +pub enum CallImpl<'a> { + AstRef(&'a ast::Call), + AstBox(Box), + IrRef(&'a ir::Call), + IrBox(Box), +} + +impl Call<'_> { + /// Returns a new AST call with the given span. This is often used by commands that need an + /// empty call to pass to a command. It's not easily possible to add anything to this. + pub fn new(span: Span) -> Self { + // this is using the boxed variant, which isn't so efficient... but this is only temporary + // anyway. + Call { + head: span, + decl_id: 0, + inner: CallImpl::AstBox(Box::new(ast::Call::new(span))), + } + } + + /// Convert the `Call` from any lifetime into `'static`, by cloning the data within onto the + /// heap. + pub fn to_owned(&self) -> Call<'static> { + Call { + head: self.head, + decl_id: self.decl_id, + inner: self.inner.to_owned(), + } + } + + /// Assert that the call is `ast::Call`, and fail with an error if it isn't. + /// + /// Provided as a stop-gap for commands that can't work with `ir::Call`, or just haven't been + /// implemented yet. Eventually these issues should be resolved and then this can be removed. + pub fn assert_ast_call(&self) -> Result<&ast::Call, ShellError> { + match &self.inner { + CallImpl::AstRef(call) => Ok(call), + CallImpl::AstBox(call) => Ok(call), + _ => Err(ShellError::NushellFailedSpanned { + msg: "Can't be used in IR context".into(), + label: "this command is not yet supported by IR evaluation".into(), + span: self.head, + }), + } + } + + /// FIXME: implementation asserts `ast::Call` and proxies to that + pub fn has_flag_const( + &self, + working_set: &StateWorkingSet, + flag_name: &str, + ) -> Result { + self.assert_ast_call()? + .has_flag_const(working_set, flag_name) + } + + /// FIXME: implementation asserts `ast::Call` and proxies to that + pub fn get_flag_const( + &self, + working_set: &StateWorkingSet, + name: &str, + ) -> Result, ShellError> { + self.assert_ast_call()?.get_flag_const(working_set, name) + } + + /// FIXME: implementation asserts `ast::Call` and proxies to that + pub fn req_const( + &self, + working_set: &StateWorkingSet, + pos: usize, + ) -> Result { + self.assert_ast_call()?.req_const(working_set, pos) + } + + /// FIXME: implementation asserts `ast::Call` and proxies to that + pub fn rest_const( + &self, + working_set: &StateWorkingSet, + starting_pos: usize, + ) -> Result, ShellError> { + self.assert_ast_call()? + .rest_const(working_set, starting_pos) + } + + /// Returns a span covering the call's arguments. + pub fn arguments_span(&self) -> Span { + match &self.inner { + CallImpl::AstRef(call) => call.arguments_span(), + CallImpl::AstBox(call) => call.arguments_span(), + CallImpl::IrRef(call) => call.arguments_span(), + CallImpl::IrBox(call) => call.arguments_span(), + } + } + + /// Returns a span covering the whole call. + pub fn span(&self) -> Span { + match &self.inner { + CallImpl::AstRef(call) => call.span(), + CallImpl::AstBox(call) => call.span(), + CallImpl::IrRef(call) => call.span(), + CallImpl::IrBox(call) => call.span(), + } + } + + /// Get a parser info argument by name. + pub fn get_parser_info<'a>(&'a self, stack: &'a Stack, name: &str) -> Option<&'a Expression> { + match &self.inner { + CallImpl::AstRef(call) => call.get_parser_info(name), + CallImpl::AstBox(call) => call.get_parser_info(name), + CallImpl::IrRef(call) => call.get_parser_info(stack, name), + CallImpl::IrBox(call) => call.get_parser_info(stack, name), + } + } + + /// Evaluator-agnostic implementation of `rest_iter_flattened()`. Evaluates or gets all of the + /// positional and spread arguments, flattens spreads, and then returns one list of values. + pub fn rest_iter_flattened( + &self, + engine_state: &EngineState, + stack: &mut Stack, + eval_expression: fn( + &EngineState, + &mut Stack, + &ast::Expression, + ) -> Result, + starting_pos: usize, + ) -> Result, ShellError> { + fn by_ast( + call: &ast::Call, + engine_state: &EngineState, + stack: &mut Stack, + eval_expression: fn( + &EngineState, + &mut Stack, + &ast::Expression, + ) -> Result, + starting_pos: usize, + ) -> Result, ShellError> { + call.rest_iter_flattened(starting_pos, |expr| { + eval_expression(engine_state, stack, expr) + }) + } + + fn by_ir( + call: &ir::Call, + stack: &Stack, + starting_pos: usize, + ) -> Result, ShellError> { + call.rest_iter_flattened(stack, starting_pos) + } + + match &self.inner { + CallImpl::AstRef(call) => { + by_ast(call, engine_state, stack, eval_expression, starting_pos) + } + CallImpl::AstBox(call) => { + by_ast(call, engine_state, stack, eval_expression, starting_pos) + } + CallImpl::IrRef(call) => by_ir(call, stack, starting_pos), + CallImpl::IrBox(call) => by_ir(call, stack, starting_pos), + } + } + + /// Get the original AST expression for a positional argument. Does not usually work for IR + /// unless the decl specified `requires_ast_for_arguments()` + pub fn positional_nth<'a>(&'a self, stack: &'a Stack, index: usize) -> Option<&'a Expression> { + match &self.inner { + CallImpl::AstRef(call) => call.positional_nth(index), + CallImpl::AstBox(call) => call.positional_nth(index), + CallImpl::IrRef(call) => call.positional_ast(stack, index).map(|arc| arc.as_ref()), + CallImpl::IrBox(call) => call.positional_ast(stack, index).map(|arc| arc.as_ref()), + } + } +} + +impl CallImpl<'_> { + pub fn to_owned(&self) -> CallImpl<'static> { + match self { + CallImpl::AstRef(call) => CallImpl::AstBox(Box::new((*call).clone())), + CallImpl::AstBox(call) => CallImpl::AstBox(call.clone()), + CallImpl::IrRef(call) => CallImpl::IrBox(Box::new((*call).clone())), + CallImpl::IrBox(call) => CallImpl::IrBox(call.clone()), + } + } +} + +impl<'a> From<&'a ast::Call> for Call<'a> { + fn from(call: &'a ast::Call) -> Self { + Call { + head: call.head, + decl_id: call.decl_id, + inner: CallImpl::AstRef(call), + } + } +} + +impl<'a> From<&'a ir::Call> for Call<'a> { + fn from(call: &'a ir::Call) -> Self { + Call { + head: call.head, + decl_id: call.decl_id, + inner: CallImpl::IrRef(call), + } + } +} diff --git a/crates/nu-protocol/src/engine/command.rs b/crates/nu-protocol/src/engine/command.rs index 043d2a66c7..48cdc4440d 100644 --- a/crates/nu-protocol/src/engine/command.rs +++ b/crates/nu-protocol/src/engine/command.rs @@ -1,5 +1,5 @@ use super::{EngineState, Stack, StateWorkingSet}; -use crate::{ast::Call, Alias, BlockId, Example, OutDest, PipelineData, ShellError, Signature}; +use crate::{engine::Call, Alias, BlockId, Example, OutDest, PipelineData, ShellError, Signature}; use std::fmt::Display; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -124,6 +124,12 @@ pub trait Command: Send + Sync + CommandClone { fn pipe_redirection(&self) -> (Option, Option) { (None, None) } + + /// Return true if the AST nodes for the arguments are required for IR evaluation. This is + /// currently inefficient so is not generally done. + fn requires_ast_for_arguments(&self) -> bool { + false + } } pub trait CommandClone { diff --git a/crates/nu-protocol/src/engine/engine_state.rs b/crates/nu-protocol/src/engine/engine_state.rs index 3960f6d9d0..fce24cebbd 100644 --- a/crates/nu-protocol/src/engine/engine_state.rs +++ b/crates/nu-protocol/src/engine/engine_state.rs @@ -8,10 +8,11 @@ use crate::{ }, eval_const::create_nu_constant, BlockId, Category, Config, DeclId, FileId, GetSpan, HistoryConfig, Module, ModuleId, OverlayId, - ShellError, Signature, Span, SpanId, Type, Value, VarId, VirtualPathId, + ShellError, Signals, Signature, Span, SpanId, Type, Value, VarId, VirtualPathId, }; use fancy_regex::Regex; use lru::LruCache; +use nu_path::AbsolutePathBuf; use std::{ collections::HashMap, num::NonZeroUsize, @@ -84,7 +85,7 @@ pub struct EngineState { pub spans: Vec, usage: Usage, pub scope: ScopeFrame, - pub ctrlc: Option>, + signals: Signals, pub env_vars: Arc, pub previous_env_vars: Arc>, pub config: Arc, @@ -144,7 +145,7 @@ impl EngineState { 0, false, ), - ctrlc: None, + signals: Signals::empty(), env_vars: Arc::new( [(DEFAULT_OVERLAY_NAME.to_string(), HashMap::new())] .into_iter() @@ -177,6 +178,18 @@ impl EngineState { } } + pub fn signals(&self) -> &Signals { + &self.signals + } + + pub fn reset_signals(&mut self) { + self.signals.reset() + } + + pub fn set_signals(&mut self, signals: Signals) { + self.signals = signals; + } + /// 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: @@ -288,28 +301,11 @@ impl EngineState { stack: &mut Stack, cwd: impl AsRef, ) -> Result<(), ShellError> { - let mut config_updated = false; - for mut scope in stack.env_vars.drain(..) { - for (overlay_name, mut env) in scope.drain() { + for (overlay_name, mut env) in Arc::make_mut(&mut scope).drain() { if let Some(env_vars) = Arc::make_mut(&mut self.env_vars).get_mut(&overlay_name) { // Updating existing overlay - for (k, v) in env.drain() { - if k == "config" { - // Don't insert the record as the "config" env var as-is. - // Instead, mutate a clone of it with into_config(), and put THAT in env_vars. - let mut new_record = v.clone(); - let (config, error) = new_record.parse_as_config(&self.config); - self.config = Arc::new(config); - config_updated = true; - env_vars.insert(k, new_record); - if let Some(e) = error { - return Err(e); - } - } else { - env_vars.insert(k, v); - } - } + env_vars.extend(env.drain()); } else { // Pushing a new overlay Arc::make_mut(&mut self.env_vars).insert(overlay_name, env); @@ -320,7 +316,10 @@ impl EngineState { // TODO: better error std::env::set_current_dir(cwd)?; - if config_updated { + if let Some(config) = stack.config.take() { + // If config was updated in the stack, replace it. + self.config = config; + // Make plugin GC config changes take effect immediately. #[cfg(feature = "plugin")] self.update_plugin_gc_configs(&self.config.plugin_gc); @@ -725,24 +724,30 @@ impl EngineState { &[0u8; 0] } - pub fn get_config(&self) -> &Config { + /// Get the global config from the engine state. + /// + /// Use [`Stack::get_config()`] instead whenever the `Stack` is available, as it takes into + /// account local changes to `$env.config`. + pub fn get_config(&self) -> &Arc { &self.config } - pub fn set_config(&mut self, conf: Config) { + pub fn set_config(&mut self, conf: impl Into>) { + let conf = conf.into(); + #[cfg(feature = "plugin")] if conf.plugin_gc != self.config.plugin_gc { // Make plugin GC config changes take effect immediately. self.update_plugin_gc_configs(&conf.plugin_gc); } - self.config = Arc::new(conf); + self.config = conf; } /// Fetch the configuration for a plugin /// - /// The `plugin` must match the registered name of a plugin. For `register nu_plugin_example` - /// the plugin name to use will be `"example"` + /// The `plugin` must match the registered name of a plugin. For `plugin add + /// nu_plugin_example` the plugin name to use will be `"example"` pub fn get_plugin_config(&self, plugin: &str) -> Option<&Value> { self.config.plugins.get(plugin) } @@ -910,58 +915,44 @@ impl EngineState { /// /// If `stack` is supplied, also considers modifications to the working /// directory on the stack that have yet to be merged into the engine state. - pub fn cwd(&self, stack: Option<&Stack>) -> Result { + pub fn cwd(&self, stack: Option<&Stack>) -> Result { // Helper function to create a simple generic error. - fn error(msg: &str, cwd: impl AsRef) -> Result { - Err(ShellError::GenericError { + fn error(msg: &str, cwd: impl AsRef) -> ShellError { + ShellError::GenericError { error: msg.into(), msg: format!("$env.PWD = {}", cwd.as_ref().display()), span: None, help: Some("Use `cd` to reset $env.PWD into a good state".into()), inner: vec![], - }) - } - - // Helper function to check if a path is a root path. - fn is_root(path: &Path) -> bool { - path.parent().is_none() - } - - // Helper function to check if a path has trailing slashes. - fn has_trailing_slash(path: &Path) -> bool { - nu_path::components(path).last() - == Some(std::path::Component::Normal(std::ffi::OsStr::new(""))) + } } // Retrieve $env.PWD from the stack or the engine state. let pwd = if let Some(stack) = stack { stack.get_env_var(self, "PWD") } else { - self.get_env_var("PWD").map(ToOwned::to_owned) + self.get_env_var("PWD").cloned() }; - if let Some(pwd) = pwd { - if let Value::String { val, .. } = pwd { - let path = PathBuf::from(val); + let pwd = pwd.ok_or_else(|| error("$env.PWD not found", ""))?; - // Technically, a root path counts as "having trailing slashes", but - // for the purpose of PWD, a root path is acceptable. - if !is_root(&path) && has_trailing_slash(&path) { - error("$env.PWD contains trailing slashes", path) - } else if !path.is_absolute() { - error("$env.PWD is not an absolute path", path) - } else if !path.exists() { - error("$env.PWD points to a non-existent directory", path) - } else if !path.is_dir() { - error("$env.PWD points to a non-directory", path) - } else { - Ok(path) - } + if let Value::String { val, .. } = pwd { + let path = AbsolutePathBuf::try_from(val) + .map_err(|path| error("$env.PWD is not an absolute path", path))?; + + // Technically, a root path counts as "having trailing slashes", but + // for the purpose of PWD, a root path is acceptable. + if path.parent().is_some() && nu_path::has_trailing_slash(path.as_ref()) { + Err(error("$env.PWD contains trailing slashes", &path)) + } else if !path.exists() { + Err(error("$env.PWD points to a non-existent directory", &path)) + } else if !path.is_dir() { + Err(error("$env.PWD points to a non-directory", &path)) } else { - error("$env.PWD is not a string", format!("{pwd:?}")) + Ok(path) } } else { - error("$env.PWD not found", "") + Err(error("$env.PWD is not a string", format!("{pwd:?}"))) } } @@ -1138,7 +1129,7 @@ mod engine_state_tests { let mut plugins = HashMap::new(); plugins.insert("example".into(), Value::string("value", Span::test_data())); - let mut config = engine_state.get_config().clone(); + let mut config = Config::clone(engine_state.get_config()); config.plugins = plugins; engine_state.set_config(config); diff --git a/crates/nu-protocol/src/engine/error_handler.rs b/crates/nu-protocol/src/engine/error_handler.rs new file mode 100644 index 0000000000..076678be20 --- /dev/null +++ b/crates/nu-protocol/src/engine/error_handler.rs @@ -0,0 +1,55 @@ +use crate::RegId; + +/// Describes an error handler stored during IR evaluation. +#[derive(Debug, Clone, Copy)] +pub struct ErrorHandler { + /// Instruction index within the block that will handle the error + pub handler_index: usize, + /// Register to put the error information into, when an error occurs + pub error_register: Option, +} + +/// Keeps track of error handlers pushed during evaluation of an IR block. +#[derive(Debug, Clone)] +pub struct ErrorHandlerStack { + handlers: Vec, +} + +impl ErrorHandlerStack { + pub const fn new() -> ErrorHandlerStack { + ErrorHandlerStack { handlers: vec![] } + } + + /// Get the current base of the stack, which establishes a frame. + pub fn get_base(&self) -> usize { + self.handlers.len() + } + + /// Push a new error handler onto the stack. + pub fn push(&mut self, handler: ErrorHandler) { + self.handlers.push(handler); + } + + /// Try to pop an error handler from the stack. Won't go below `base`, to avoid retrieving a + /// handler belonging to a parent frame. + pub fn pop(&mut self, base: usize) -> Option { + if self.handlers.len() > base { + self.handlers.pop() + } else { + None + } + } + + /// Reset the stack to the state it was in at the beginning of the frame, in preparation to + /// return control to the parent frame. + pub fn leave_frame(&mut self, base: usize) { + if self.handlers.len() >= base { + self.handlers.truncate(base); + } else { + panic!( + "ErrorHandlerStack bug: tried to leave frame at {base}, but current base is {}", + self.get_base() + ) + } + } +} diff --git a/crates/nu-protocol/src/engine/mod.rs b/crates/nu-protocol/src/engine/mod.rs index c6e71afb37..39bb5a7ae3 100644 --- a/crates/nu-protocol/src/engine/mod.rs +++ b/crates/nu-protocol/src/engine/mod.rs @@ -1,8 +1,12 @@ +//! Representation of the engine state and many of the details that implement the scoping +mod argument; mod cached_file; +mod call; mod call_info; mod capture_block; mod command; mod engine_state; +mod error_handler; mod overlay; mod pattern_match; mod stack; @@ -14,10 +18,13 @@ mod variable; pub use cached_file::CachedFile; +pub use argument::*; +pub use call::*; pub use call_info::*; pub use capture_block::*; pub use command::*; pub use engine_state::*; +pub use error_handler::*; pub use overlay::*; pub use pattern_match::*; pub use stack::*; diff --git a/crates/nu-protocol/src/engine/stack.rs b/crates/nu-protocol/src/engine/stack.rs index 19726db9c0..fbfb586762 100644 --- a/crates/nu-protocol/src/engine/stack.rs +++ b/crates/nu-protocol/src/engine/stack.rs @@ -1,9 +1,9 @@ use crate::{ engine::{ - EngineState, Redirection, StackCallArgGuard, StackCaptureGuard, StackIoGuard, StackOutDest, - DEFAULT_OVERLAY_NAME, + ArgumentStack, EngineState, ErrorHandlerStack, Redirection, StackCallArgGuard, + StackCaptureGuard, StackIoGuard, StackOutDest, DEFAULT_OVERLAY_NAME, }, - OutDest, ShellError, Span, Value, VarId, ENV_VARIABLE_ID, NU_VARIABLE_ID, + Config, OutDest, ShellError, Span, Value, VarId, ENV_VARIABLE_ID, NU_VARIABLE_ID, }; use std::{ collections::{HashMap, HashSet}, @@ -36,15 +36,23 @@ pub struct Stack { /// Variables pub vars: Vec<(VarId, Value)>, /// Environment variables arranged as a stack to be able to recover values from parent scopes - pub env_vars: Vec, + pub env_vars: Vec>, /// Tells which environment variables from engine state are hidden, per overlay. - pub env_hidden: HashMap>, + pub env_hidden: Arc>>, /// List of active overlays pub active_overlays: Vec, + /// Argument stack for IR evaluation + pub arguments: ArgumentStack, + /// Error handler stack for IR evaluation + pub error_handlers: ErrorHandlerStack, + /// Set true to always use IR mode + pub use_ir: bool, pub recursion_count: u64, pub parent_stack: Option>, /// Variables that have been deleted (this is used to hide values from parent stack lookups) pub parent_deletions: Vec, + /// Locally updated config. Use [`.get_config()`] to access correctly. + pub config: Option>, pub(crate) out_dest: StackOutDest, } @@ -66,11 +74,15 @@ impl Stack { Self { vars: Vec::new(), env_vars: Vec::new(), - env_hidden: HashMap::new(), + env_hidden: Arc::new(HashMap::new()), active_overlays: vec![DEFAULT_OVERLAY_NAME.to_string()], + arguments: ArgumentStack::new(), + error_handlers: ErrorHandlerStack::new(), + use_ir: false, recursion_count: 0, parent_stack: None, parent_deletions: vec![], + config: None, out_dest: StackOutDest::new(), } } @@ -85,9 +97,13 @@ impl Stack { env_vars: parent.env_vars.clone(), env_hidden: parent.env_hidden.clone(), active_overlays: parent.active_overlays.clone(), + arguments: ArgumentStack::new(), + error_handlers: ErrorHandlerStack::new(), + use_ir: parent.use_ir, recursion_count: parent.recursion_count, vars: vec![], parent_deletions: vec![], + config: parent.config.clone(), out_dest: parent.out_dest.clone(), parent_stack: Some(parent), } @@ -114,13 +130,14 @@ impl Stack { unique_stack.env_vars = child.env_vars; unique_stack.env_hidden = child.env_hidden; unique_stack.active_overlays = child.active_overlays; + unique_stack.config = child.config; unique_stack } pub fn with_env( &mut self, - env_vars: &[EnvVars], - env_hidden: &HashMap>, + env_vars: &[Arc], + env_hidden: &Arc>>, ) { // Do not clone the environment if it hasn't changed if self.env_vars.iter().any(|scope| !scope.is_empty()) { @@ -180,6 +197,36 @@ impl Stack { } } + /// Get the local config if set, otherwise the config from the engine state. + /// + /// This is the canonical way to get [`Config`] when [`Stack`] is available. + pub fn get_config(&self, engine_state: &EngineState) -> Arc { + self.config + .clone() + .unwrap_or_else(|| engine_state.config.clone()) + } + + /// Update the local config with the config stored in the `config` environment variable. Run + /// this after assigning to `$env.config`. + /// + /// The config will be updated with successfully parsed values even if an error occurs. + pub fn update_config(&mut self, engine_state: &EngineState) -> Result<(), ShellError> { + if let Some(mut config) = self.get_env_var(engine_state, "config") { + let existing_config = self.get_config(engine_state); + let (new_config, error) = config.parse_as_config(&existing_config); + self.config = Some(new_config.into()); + // The config value is modified by the update, so we should add it again + self.add_env_var("config".into(), config); + match error { + None => Ok(()), + Some(err) => Err(err), + } + } else { + self.config = None; + Ok(()) + } + } + pub fn add_var(&mut self, var_id: VarId, value: Value) { //self.vars.insert(var_id, value); for (id, val) in &mut self.vars { @@ -207,23 +254,24 @@ impl Stack { pub fn add_env_var(&mut self, var: String, value: Value) { if let Some(last_overlay) = self.active_overlays.last() { - if let Some(env_hidden) = self.env_hidden.get_mut(last_overlay) { + if let Some(env_hidden) = Arc::make_mut(&mut self.env_hidden).get_mut(last_overlay) { // if the env var was hidden, let's activate it again env_hidden.remove(&var); } if let Some(scope) = self.env_vars.last_mut() { + let scope = Arc::make_mut(scope); if let Some(env_vars) = scope.get_mut(last_overlay) { env_vars.insert(var, value); } else { scope.insert(last_overlay.into(), [(var, value)].into_iter().collect()); } } else { - self.env_vars.push( + self.env_vars.push(Arc::new( [(last_overlay.into(), [(var, value)].into_iter().collect())] .into_iter() .collect(), - ); + )); } } else { // TODO: Remove panic @@ -245,24 +293,27 @@ impl Stack { } pub fn captures_to_stack_preserve_out_dest(&self, captures: Vec<(VarId, Value)>) -> Stack { - // FIXME: this is probably slow let mut env_vars = self.env_vars.clone(); - env_vars.push(HashMap::new()); + env_vars.push(Arc::new(HashMap::new())); Stack { vars: captures, env_vars, env_hidden: self.env_hidden.clone(), active_overlays: self.active_overlays.clone(), + arguments: ArgumentStack::new(), + error_handlers: ErrorHandlerStack::new(), + use_ir: self.use_ir, recursion_count: self.recursion_count, parent_stack: None, parent_deletions: vec![], + config: self.config.clone(), out_dest: self.out_dest.clone(), } } pub fn gather_captures(&self, engine_state: &EngineState, captures: &[VarId]) -> Stack { - let mut vars = vec![]; + let mut vars = Vec::with_capacity(captures.len()); let fake_span = Span::new(0, 0); @@ -277,16 +328,20 @@ impl Stack { } let mut env_vars = self.env_vars.clone(); - env_vars.push(HashMap::new()); + env_vars.push(Arc::new(HashMap::new())); Stack { vars, env_vars, env_hidden: self.env_hidden.clone(), active_overlays: self.active_overlays.clone(), + arguments: ArgumentStack::new(), + error_handlers: ErrorHandlerStack::new(), + use_ir: self.use_ir, recursion_count: self.recursion_count, parent_stack: None, parent_deletions: vec![], + config: self.config.clone(), out_dest: self.out_dest.clone(), } } @@ -444,6 +499,7 @@ impl Stack { pub fn remove_env_var(&mut self, engine_state: &EngineState, name: &str) -> bool { for scope in self.env_vars.iter_mut().rev() { + let scope = Arc::make_mut(scope); for active_overlay in self.active_overlays.iter().rev() { if let Some(env_vars) = scope.get_mut(active_overlay) { if env_vars.remove(name).is_some() { @@ -456,10 +512,11 @@ impl Stack { for active_overlay in self.active_overlays.iter().rev() { if let Some(env_vars) = engine_state.env_vars.get(active_overlay) { if env_vars.get(name).is_some() { - if let Some(env_hidden) = self.env_hidden.get_mut(active_overlay) { - env_hidden.insert(name.into()); + let env_hidden = Arc::make_mut(&mut self.env_hidden); + if let Some(env_hidden_in_overlay) = env_hidden.get_mut(active_overlay) { + env_hidden_in_overlay.insert(name.into()); } else { - self.env_hidden + env_hidden .insert(active_overlay.into(), [name.into()].into_iter().collect()); } diff --git a/crates/nu-protocol/src/engine/state_working_set.rs b/crates/nu-protocol/src/engine/state_working_set.rs index af950b8321..822bf35e3a 100644 --- a/crates/nu-protocol/src/engine/state_working_set.rs +++ b/crates/nu-protocol/src/engine/state_working_set.rs @@ -4,8 +4,8 @@ use crate::{ usage::build_usage, CachedFile, Command, CommandType, EngineState, OverlayFrame, StateDelta, Variable, VirtualPath, Visibility, }, - BlockId, Category, Config, DeclId, FileId, GetSpan, Module, ModuleId, ParseError, ParseWarning, - Span, SpanId, Type, Value, VarId, VirtualPathId, + BlockId, Category, CompileError, Config, DeclId, FileId, GetSpan, Module, ModuleId, ParseError, + ParseWarning, Span, SpanId, Type, Value, VarId, VirtualPathId, }; use core::panic; use std::{ @@ -25,12 +25,12 @@ use crate::{PluginIdentity, PluginRegistryItem, RegisteredPlugin}; pub struct StateWorkingSet<'a> { pub permanent_state: &'a EngineState, pub delta: StateDelta, - pub external_commands: Vec>, pub files: FileStack, /// Whether or not predeclarations are searched when looking up a command (used with aliases) pub search_predecls: bool, pub parse_errors: Vec, pub parse_warnings: Vec, + pub compile_errors: Vec, } impl<'a> StateWorkingSet<'a> { @@ -45,11 +45,11 @@ impl<'a> StateWorkingSet<'a> { Self { delta: StateDelta::new(permanent_state), permanent_state, - external_commands: vec![], files, search_predecls: true, parse_errors: vec![], parse_warnings: vec![], + compile_errors: vec![], } } @@ -260,6 +260,12 @@ impl<'a> StateWorkingSet<'a> { } pub fn add_block(&mut self, block: Arc) -> BlockId { + log::trace!( + "block id={} added, has IR = {:?}", + self.num_blocks(), + block.ir_block.is_some() + ); + self.delta.blocks.push(block); self.num_blocks() - 1 @@ -619,9 +625,9 @@ impl<'a> StateWorkingSet<'a> { /// Returns a reference to the config stored at permanent state /// - /// At runtime, you most likely want to call nu_engine::env::get_config because this method - /// does not capture environment updates during runtime. - pub fn get_config(&self) -> &Config { + /// At runtime, you most likely want to call [`Stack::get_config()`][super::Stack::get_config()] + /// because this method does not capture environment updates during runtime. + pub fn get_config(&self) -> &Arc { &self.permanent_state.config } diff --git a/crates/nu-protocol/src/engine/usage.rs b/crates/nu-protocol/src/engine/usage.rs index a9cf2c25f1..76f50d2424 100644 --- a/crates/nu-protocol/src/engine/usage.rs +++ b/crates/nu-protocol/src/engine/usage.rs @@ -81,7 +81,9 @@ pub(super) fn build_usage(comment_lines: &[&[u8]]) -> (String, String) { usage.push_str(&comment_line); } - if let Some((brief_usage, extra_usage)) = usage.split_once("\n\n") { + if let Some((brief_usage, extra_usage)) = usage.split_once("\r\n\r\n") { + (brief_usage.to_string(), extra_usage.to_string()) + } else if let Some((brief_usage, extra_usage)) = usage.split_once("\n\n") { (brief_usage.to_string(), extra_usage.to_string()) } else { (usage, String::default()) diff --git a/crates/nu-protocol/src/errors/cli_error.rs b/crates/nu-protocol/src/errors/cli_error.rs index 003564f933..9ce6829b7b 100644 --- a/crates/nu-protocol/src/errors/cli_error.rs +++ b/crates/nu-protocol/src/errors/cli_error.rs @@ -1,3 +1,6 @@ +//! This module manages the step of turning error types into printed error messages +//! +//! Relies on the `miette` crate for pretty layout use crate::{ engine::{EngineState, StateWorkingSet}, ErrorStyle, @@ -44,6 +47,26 @@ pub fn report_error_new( report_error(&working_set, error); } +pub fn report_warning( + working_set: &StateWorkingSet, + error: &(dyn miette::Diagnostic + Send + Sync + 'static), +) { + eprintln!("Warning: {:?}", CliError(error, working_set)); + // reset vt processing, aka ansi because illbehaved externals can break it + #[cfg(windows)] + { + let _ = nu_utils::enable_vt_processing(); + } +} + +pub fn report_warning_new( + engine_state: &EngineState, + error: &(dyn miette::Diagnostic + Send + Sync + 'static), +) { + let working_set = StateWorkingSet::new(engine_state); + report_error(&working_set, error); +} + impl std::fmt::Debug for CliError<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let config = self.1.get_config(); @@ -107,4 +130,8 @@ impl<'src> miette::Diagnostic for CliError<'src> { fn related<'a>(&'a self) -> Option + 'a>> { self.0.related() } + + fn diagnostic_source(&self) -> Option<&dyn miette::Diagnostic> { + self.0.diagnostic_source() + } } diff --git a/crates/nu-protocol/src/errors/compile_error.rs b/crates/nu-protocol/src/errors/compile_error.rs new file mode 100644 index 0000000000..cc805a73ed --- /dev/null +++ b/crates/nu-protocol/src/errors/compile_error.rs @@ -0,0 +1,238 @@ +use crate::{RegId, Span}; +use miette::Diagnostic; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +/// An internal compiler error, generally means a Nushell bug rather than an issue with user error +/// since parsing and typechecking has already passed. +#[derive(Debug, Clone, Error, Diagnostic, PartialEq, Serialize, Deserialize)] +pub enum CompileError { + #[error("Register overflow.")] + #[diagnostic(code(nu::compile::register_overflow))] + RegisterOverflow { + #[label("the code being compiled is probably too large")] + block_span: Option, + }, + + #[error("Register {reg_id} was uninitialized when used, possibly reused.")] + #[diagnostic( + code(nu::compile::register_uninitialized), + help("this is a compiler bug. Please report it at https://github.com/nushell/nushell/issues/new\nfrom: {caller}"), + )] + RegisterUninitialized { reg_id: RegId, caller: String }, + + #[error("Register {reg_id} was uninitialized when used, possibly reused.")] + #[diagnostic( + code(nu::compile::register_uninitialized), + help("this is a compiler bug. Please report it at https://github.com/nushell/nushell/issues/new\nfrom: {caller}"), + )] + RegisterUninitializedWhilePushingInstruction { + reg_id: RegId, + caller: String, + instruction: String, + #[label("while adding this instruction: {instruction}")] + span: Span, + }, + + #[error("Block contains too much string data: maximum 4 GiB exceeded.")] + #[diagnostic( + code(nu::compile::data_overflow), + help("try loading the string data from a file instead") + )] + DataOverflow { + #[label("while compiling this block")] + block_span: Option, + }, + + #[error("Block contains too many files.")] + #[diagnostic( + code(nu::compile::register_overflow), + help("try using fewer file redirections") + )] + FileOverflow { + #[label("while compiling this block")] + block_span: Option, + }, + + #[error("Invalid redirect mode: File should not be specified by commands.")] + #[diagnostic( + code(nu::compile::invalid_redirect_mode), + help("this is a command bug. Please report it at https://github.com/nushell/nushell/issues/new") + )] + InvalidRedirectMode { + #[label("while compiling this expression")] + span: Span, + }, + + #[error("Encountered garbage, likely due to parse error.")] + #[diagnostic(code(nu::compile::garbage))] + Garbage { + #[label("garbage found here")] + span: Span, + }, + + #[error("Unsupported operator expression.")] + #[diagnostic(code(nu::compile::unsupported_operator_expression))] + UnsupportedOperatorExpression { + #[label("this expression is in operator position but is not an operator")] + span: Span, + }, + + #[error("Attempted access of $env by integer path.")] + #[diagnostic(code(nu::compile::access_env_by_int))] + AccessEnvByInt { + #[label("$env keys should be strings")] + span: Span, + }, + + #[error("Encountered invalid `{keyword}` keyword call.")] + #[diagnostic(code(nu::compile::invalid_keyword_call))] + InvalidKeywordCall { + keyword: String, + #[label("this call is not properly formed")] + span: Span, + }, + + #[error("Attempted to set branch target of non-branch instruction.")] + #[diagnostic( + code(nu::compile::set_branch_target_of_non_branch_instruction), + help("this is a compiler bug. Please report it at https://github.com/nushell/nushell/issues/new"), + )] + SetBranchTargetOfNonBranchInstruction { + instruction: String, + #[label("tried to modify: {instruction}")] + span: Span, + }, + + /// You're trying to run an unsupported external command. + /// + /// ## Resolution + /// + /// Make sure there's an appropriate `run-external` declaration for this external command. + #[error("External calls are not supported.")] + #[diagnostic( + code(nu::compile::run_external_not_found), + help("`run-external` was not found in scope") + )] + RunExternalNotFound { + #[label("can't be run in this context")] + span: Span, + }, + + /// Invalid assignment left-hand side + /// + /// ## Resolution + /// + /// Assignment requires that you assign to a variable or variable cell path. + #[error("Assignment operations require a variable.")] + #[diagnostic( + code(nu::compile::assignment_requires_variable), + help("try assigning to a variable or a cell path of a variable") + )] + AssignmentRequiresVar { + #[label("needs to be a variable")] + span: Span, + }, + + /// Invalid assignment left-hand side + /// + /// ## Resolution + /// + /// Assignment requires that you assign to a mutable variable or cell path. + #[error("Assignment to an immutable variable.")] + #[diagnostic( + code(nu::compile::assignment_requires_mutable_variable), + help("declare the variable with `mut`, or shadow it again with `let`") + )] + AssignmentRequiresMutableVar { + #[label("needs to be a mutable variable")] + span: Span, + }, + + /// This environment variable cannot be set manually. + /// + /// ## Resolution + /// + /// This environment variable is set automatically by Nushell and cannot not be set manually. + #[error("{envvar_name} cannot be set manually.")] + #[diagnostic( + code(nu::compile::automatic_env_var_set_manually), + help( + r#"The environment variable '{envvar_name}' is set automatically by Nushell and cannot be set manually."# + ) + )] + AutomaticEnvVarSetManually { + envvar_name: String, + #[label("cannot set '{envvar_name}' manually")] + span: Span, + }, + + /// It is not possible to replace the entire environment at once + /// + /// ## Resolution + /// + /// Setting the entire environment is not allowed. Change environment variables individually + /// instead. + #[error("Cannot replace environment.")] + #[diagnostic( + code(nu::compile::cannot_replace_env), + help("Assigning a value to '$env' is not allowed.") + )] + CannotReplaceEnv { + #[label("setting '$env' not allowed")] + span: Span, + }, + + #[error("Unexpected expression.")] + #[diagnostic(code(nu::compile::unexpected_expression))] + UnexpectedExpression { + expr_name: String, + #[label("{expr_name} is not allowed in this context")] + span: Span, + }, + + #[error("Missing required declaration: `{decl_name}`")] + #[diagnostic(code(nu::compile::missing_required_declaration))] + MissingRequiredDeclaration { + decl_name: String, + #[label("`{decl_name}` must be in scope to compile this expression")] + span: Span, + }, + + #[error("Invalid literal")] + #[diagnostic(code(nu::compile::invalid_literal))] + InvalidLiteral { + msg: String, + #[label("{msg}")] + span: Span, + }, + + #[error("{msg}")] + #[diagnostic(code(nu::compile::not_in_a_loop))] + NotInALoop { + msg: String, + #[label("can't be used outside of a loop")] + span: Option, + }, + + #[error("Incoherent loop state: the loop that ended was not the one we were expecting.")] + #[diagnostic( + code(nu::compile::incoherent_loop_state), + help("this is a compiler bug. Please report it at https://github.com/nushell/nushell/issues/new"), + )] + IncoherentLoopState { + #[label("while compiling this block")] + block_span: Option, + }, + + #[error("Undefined label `{label_id}`.")] + #[diagnostic( + code(nu::compile::undefined_label), + help("this is a compiler bug. Please report it at https://github.com/nushell/nushell/issues/new"), + )] + UndefinedLabel { + label_id: usize, + #[label("label was used while compiling this code")] + span: Option, + }, +} diff --git a/crates/nu-protocol/src/errors/mod.rs b/crates/nu-protocol/src/errors/mod.rs index 23006ab684..59369cd54f 100644 --- a/crates/nu-protocol/src/errors/mod.rs +++ b/crates/nu-protocol/src/errors/mod.rs @@ -1,10 +1,14 @@ pub mod cli_error; +mod compile_error; mod labeled_error; mod parse_error; mod parse_warning; mod shell_error; -pub use cli_error::{format_error, report_error, report_error_new}; +pub use cli_error::{ + format_error, report_error, report_error_new, report_warning, report_warning_new, +}; +pub use compile_error::CompileError; pub use labeled_error::{ErrorLabel, LabeledError}; pub use parse_error::{DidYouMean, ParseError}; pub use parse_warning::ParseWarning; diff --git a/crates/nu-protocol/src/errors/parse_error.rs b/crates/nu-protocol/src/errors/parse_error.rs index 28c865b841..0e20f0dcba 100644 --- a/crates/nu-protocol/src/errors/parse_error.rs +++ b/crates/nu-protocol/src/errors/parse_error.rs @@ -17,6 +17,13 @@ pub enum ParseError { #[diagnostic(code(nu::parser::extra_tokens), help("Try removing them."))] ExtraTokens(#[label = "extra tokens"] Span), + #[error("Invalid characters after closing delimiter")] + #[diagnostic( + code(nu::parser::extra_token_after_closing_delimiter), + help("Try removing them.") + )] + ExtraTokensAfterClosingDelimiter(#[label = "invalid characters"] Span), + #[error("Extra positional argument.")] #[diagnostic(code(nu::parser::extra_positional), help("Usage: {0}"))] ExtraPositional(String, #[label = "extra positional argument"] Span), @@ -54,7 +61,11 @@ pub enum ParseError { #[error("Command output doesn't match {0}.")] #[diagnostic(code(nu::parser::output_type_mismatch))] - OutputMismatch(Type, #[label("command doesn't output {0}")] Span), + OutputMismatch( + Type, + Type, + #[label("expected {0}, but command outputs {1}")] Span, + ), #[error("Type mismatch during operation.")] #[diagnostic(code(nu::parser::type_mismatch))] @@ -547,7 +558,7 @@ impl ParseError { ParseError::TypeMismatch(_, _, s) => *s, ParseError::TypeMismatchHelp(_, _, s, _) => *s, ParseError::InputMismatch(_, s) => *s, - ParseError::OutputMismatch(_, s) => *s, + ParseError::OutputMismatch(_, _, s) => *s, ParseError::MissingRequiredFlag(_, s) => *s, ParseError::IncompleteMathExpression(s) => *s, ParseError::UnknownState(_, s) => *s, @@ -577,6 +588,7 @@ impl ParseError { ParseError::LabeledErrorWithHelp { span: s, .. } => *s, ParseError::RedirectingBuiltinCommand(_, s, _) => *s, ParseError::UnexpectedSpreadArg(_, s) => *s, + ParseError::ExtraTokensAfterClosingDelimiter(s) => *s, } } } diff --git a/crates/nu-protocol/src/errors/parse_warning.rs b/crates/nu-protocol/src/errors/parse_warning.rs index 0213d6889f..5f34deb20c 100644 --- a/crates/nu-protocol/src/errors/parse_warning.rs +++ b/crates/nu-protocol/src/errors/parse_warning.rs @@ -6,11 +6,11 @@ use thiserror::Error; #[derive(Clone, Debug, Error, Diagnostic, Serialize, Deserialize)] pub enum ParseWarning { #[error("Deprecated: {old_command}")] - #[diagnostic(help("for more info: {url}"))] + #[diagnostic(help("for more info see {url}"))] DeprecatedWarning { old_command: String, new_suggestion: String, - #[label("`{old_command}` is deprecated and will be removed in 0.94. Please {new_suggestion} instead")] + #[label("`{old_command}` is deprecated and will be removed in a future release. Please {new_suggestion} instead.")] span: Span, url: String, }, diff --git a/crates/nu-protocol/src/errors/shell_error.rs b/crates/nu-protocol/src/errors/shell_error.rs index e201915fcf..ab01ccfa54 100644 --- a/crates/nu-protocol/src/errors/shell_error.rs +++ b/crates/nu-protocol/src/errors/shell_error.rs @@ -557,12 +557,12 @@ pub enum ShellError { /// ## Resolution /// /// Check the spelling of your column name. Did you forget to rename a column somewhere? - #[error("Cannot find column")] + #[error("Cannot find column '{col_name}'")] #[diagnostic(code(nu::shell::column_not_found))] CantFindColumn { col_name: String, #[label = "cannot find column '{col_name}'"] - span: Span, + span: Option, #[label = "value originates here"] src_span: Span, }, @@ -1187,6 +1187,13 @@ pub enum ShellError { span: Option, }, + /// Operation interrupted + #[error("Operation interrupted")] + Interrupted { + #[label("This operation was interrupted")] + span: Span, + }, + /// Operation interrupted by user #[error("Operation interrupted by user")] InterruptedByUser { @@ -1369,6 +1376,23 @@ On Windows, this would be %USERPROFILE%\AppData\Roaming"# help("Set XDG_CONFIG_HOME to an absolute path, or set it to an empty string to ignore it") )] InvalidXdgConfig { xdg: String, default: String }, + + /// An unexpected error occurred during IR evaluation. + /// + /// ## Resolution + /// + /// This is most likely a correctness issue with the IR compiler or evaluator. Please file a + /// bug with the minimum code needed to reproduce the issue, if possible. + #[error("IR evaluation error: {msg}")] + #[diagnostic( + code(nu::shell::ir_eval_error), + help("this is a bug, please report it at https://github.com/nushell/nushell/issues/new along with the code you were running if able") + )] + IrEvalError { + msg: String, + #[label = "while running this code"] + span: Option, + }, } // TODO: Implement as From trait diff --git a/crates/nu-protocol/src/eval_base.rs b/crates/nu-protocol/src/eval_base.rs index 2317149460..933342f577 100644 --- a/crates/nu-protocol/src/eval_base.rs +++ b/crates/nu-protocol/src/eval_base.rs @@ -1,3 +1,4 @@ +//! Foundational [`Eval`] trait allowing dispatch between const-eval and regular evaluation use crate::{ ast::{ eval_operator, Assignment, Bits, Boolean, Call, Comparison, Expr, Expression, @@ -6,7 +7,7 @@ use crate::{ debugger::DebugContext, Config, GetSpan, Range, Record, ShellError, Span, Value, VarId, ENV_VARIABLE_ID, }; -use std::{borrow::Cow, collections::HashMap}; +use std::{collections::HashMap, sync::Arc}; /// To share implementations for regular eval and const eval pub trait Eval { @@ -158,6 +159,9 @@ pub trait Eval { Expr::ExternalCall(head, args) => { Self::eval_external_call(state, mut_state, head, args, expr_span) } + Expr::Collect(var_id, expr) => { + Self::eval_collect::(state, mut_state, *var_id, expr) + } Expr::Subexpression(block_id) => { Self::eval_subexpression::(state, mut_state, *block_id, expr_span) } @@ -290,6 +294,15 @@ pub trait Eval { Ok(Value::string(str, expr_span)) } + Expr::GlobInterpolation(exprs, quoted) => { + let config = Self::get_config(state, mut_state); + let str = exprs + .iter() + .map(|expr| Self::eval::(state, mut_state, expr).map(|v| v.to_expanded_string(", ", &config))) + .collect::>()?; + + Ok(Value::glob(str, *quoted, expr_span)) + } Expr::Overlay(_) => Self::eval_overlay(state, expr_span), Expr::GlobPattern(pattern, quoted) => { // GlobPattern is similar to Filepath @@ -306,7 +319,7 @@ pub trait Eval { } } - fn get_config<'a>(state: Self::State<'a>, mut_state: &mut Self::MutState) -> Cow<'a, Config>; + fn get_config(state: Self::State<'_>, mut_state: &mut Self::MutState) -> Arc; fn eval_filepath( state: Self::State<'_>, @@ -346,6 +359,13 @@ pub trait Eval { span: Span, ) -> Result; + fn eval_collect( + state: Self::State<'_>, + mut_state: &mut Self::MutState, + var_id: VarId, + expr: &Expression, + ) -> Result; + fn eval_subexpression( state: Self::State<'_>, mut_state: &mut Self::MutState, diff --git a/crates/nu-protocol/src/eval_const.rs b/crates/nu-protocol/src/eval_const.rs index 08ff6fa391..cd4780773f 100644 --- a/crates/nu-protocol/src/eval_const.rs +++ b/crates/nu-protocol/src/eval_const.rs @@ -1,3 +1,7 @@ +//! Implementation of const-evaluation +//! +//! This enables you to assign `const`-constants and execute parse-time code dependent on this. +//! e.g. `source $my_const` use crate::{ ast::{Assignment, Block, Call, Expr, Expression, ExternalArgument}, debugger::{DebugContext, WithoutDebug}, @@ -7,8 +11,8 @@ use crate::{ }; use nu_system::os_info::{get_kernel_version, get_os_arch, get_os_family, get_os_name}; use std::{ - borrow::Cow, path::{Path, PathBuf}, + sync::Arc, }; /// Create a Value for `$nu`. @@ -149,6 +153,58 @@ pub(crate) fn create_nu_constant(engine_state: &EngineState, span: Span) -> Valu }, ); + record.push( + "data-dir", + if let Some(path) = nu_path::data_dir() { + let mut canon_data_path = canonicalize_path(engine_state, &path); + canon_data_path.push("nushell"); + Value::string(canon_data_path.to_string_lossy(), span) + } else { + Value::error( + ShellError::IOError { + msg: "Could not get data path".into(), + }, + span, + ) + }, + ); + + record.push( + "cache-dir", + if let Some(path) = nu_path::cache_dir() { + let mut canon_cache_path = canonicalize_path(engine_state, &path); + canon_cache_path.push("nushell"); + Value::string(canon_cache_path.to_string_lossy(), span) + } else { + Value::error( + ShellError::IOError { + msg: "Could not get cache path".into(), + }, + span, + ) + }, + ); + + // Create a system level directory for nushell scripts, modules, completions, etc + // that can be changed by setting the NU_VENDOR_AUTOLOAD_DIR env var on any platform + // before nushell is compiled OR if NU_VENDOR_AUTOLOAD_DIR is not set for non-windows + // systems, the PREFIX env var can be set before compile and used as PREFIX/nushell/vendor/autoload + record.push( + "vendor-autoload-dir", + // pseudo code + // if env var NU_VENDOR_AUTOLOAD_DIR is set, in any platform, use it + // if not, if windows, use ALLUSERPROFILE\nushell\vendor\autoload + // if not, if non-windows, if env var PREFIX is set, use PREFIX/share/nushell/vendor/autoload + // if not, use the default /usr/share/nushell/vendor/autoload + + // check to see if NU_VENDOR_AUTOLOAD_DIR env var is set, if not, use the default + if let Some(path) = get_vendor_autoload_dir(engine_state) { + Value::string(path.to_string_lossy(), span) + } else { + Value::error(ShellError::ConfigDirNotFound { span: Some(span) }, span) + }, + ); + record.push("temp-path", { let canon_temp_path = canonicalize_path(engine_state, &std::env::temp_dir()); Value::string(canon_temp_path.to_string_lossy(), span) @@ -203,6 +259,41 @@ pub(crate) fn create_nu_constant(engine_state: &EngineState, span: Span) -> Valu Value::record(record, span) } +pub fn get_vendor_autoload_dir(engine_state: &EngineState) -> Option { + // pseudo code + // if env var NU_VENDOR_AUTOLOAD_DIR is set, in any platform, use it + // if not, if windows, use ALLUSERPROFILE\nushell\vendor\autoload + // if not, if non-windows, if env var PREFIX is set, use PREFIX/share/nushell/vendor/autoload + // if not, use the default /usr/share/nushell/vendor/autoload + + // check to see if NU_VENDOR_AUTOLOAD_DIR env var is set, if not, use the default + Some( + option_env!("NU_VENDOR_AUTOLOAD_DIR") + .map(String::from) + .unwrap_or_else(|| { + if cfg!(windows) { + let all_user_profile = match engine_state.get_env_var("ALLUSERPROFILE") { + Some(v) => format!( + "{}\\nushell\\vendor\\autoload", + v.coerce_string().unwrap_or("C:\\ProgramData".into()) + ), + None => "C:\\ProgramData\\nushell\\vendor\\autoload".into(), + }; + all_user_profile + } else { + // In non-Windows environments, if NU_VENDOR_AUTOLOAD_DIR is not set + // check to see if PREFIX env var is set, and use it as PREFIX/nushell/vendor/autoload + // otherwise default to /usr/share/nushell/vendor/autoload + option_env!("PREFIX").map(String::from).map_or_else( + || "/usr/local/share/nushell/vendor/autoload".into(), + |prefix| format!("{}/share/nushell/vendor/autoload", prefix), + ) + } + }) + .into(), + ) +} + fn eval_const_call( working_set: &StateWorkingSet, call: &Call, @@ -220,7 +311,7 @@ fn eval_const_call( return Err(ShellError::NotAConstHelp { span: call.head }); } - decl.run_const(working_set, call, input) + decl.run_const(working_set, &call.into(), input) } pub fn eval_const_subexpression( @@ -273,8 +364,8 @@ impl Eval for EvalConst { type MutState = (); - fn get_config<'a>(state: Self::State<'a>, _: &mut ()) -> Cow<'a, Config> { - Cow::Borrowed(state.get_config()) + fn get_config(state: Self::State<'_>, _: &mut ()) -> Arc { + state.get_config().clone() } fn eval_filepath( @@ -331,6 +422,15 @@ impl Eval for EvalConst { Err(ShellError::NotAConstant { span }) } + fn eval_collect( + _: &StateWorkingSet, + _: &mut (), + _var_id: VarId, + expr: &Expression, + ) -> Result { + Err(ShellError::NotAConstant { span: expr.span }) + } + fn eval_subexpression( working_set: &StateWorkingSet, _: &mut (), diff --git a/crates/nu-protocol/src/id.rs b/crates/nu-protocol/src/id.rs index 73c4f52e70..829ee8f36d 100644 --- a/crates/nu-protocol/src/id.rs +++ b/crates/nu-protocol/src/id.rs @@ -7,5 +7,19 @@ pub type ModuleId = usize; pub type OverlayId = usize; pub type FileId = usize; pub type VirtualPathId = usize; + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub struct SpanId(pub usize); // more robust ID style used in the new parser + +/// An ID for an [IR](crate::ir) register. `%n` is a common shorthand for `RegId(n)`. +/// +/// Note: `%0` is allocated with the block input at the beginning of a compiled block. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[repr(transparent)] +pub struct RegId(pub u32); + +impl std::fmt::Display for RegId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "%{}", self.0) + } +} diff --git a/crates/nu-protocol/src/ir/call.rs b/crates/nu-protocol/src/ir/call.rs new file mode 100644 index 0000000000..3d16f82eb6 --- /dev/null +++ b/crates/nu-protocol/src/ir/call.rs @@ -0,0 +1,351 @@ +use std::sync::Arc; + +use crate::{ + ast::Expression, + engine::{self, Argument, Stack}, + DeclId, ShellError, Span, Spanned, Value, +}; + +use super::DataSlice; + +/// Contains the information for a call being made to a declared command. +#[derive(Debug, Clone)] +pub struct Call { + /// The declaration ID of the command to be invoked. + pub decl_id: DeclId, + /// The span encompassing the command name, before the arguments. + pub head: Span, + /// The span encompassing the command name and all arguments. + pub span: Span, + /// The base index of the arguments for this call within the + /// [argument stack](crate::engine::ArgumentStack). + pub args_base: usize, + /// The number of [`Argument`]s for the call. Note that this just counts the number of + /// `Argument` entries on the stack, and has nothing to do with the actual number of positional + /// or spread arguments. + pub args_len: usize, +} + +impl Call { + /// Build a new call with arguments. + pub fn build(decl_id: DeclId, head: Span) -> CallBuilder { + CallBuilder { + inner: Call { + decl_id, + head, + span: head, + args_base: 0, + args_len: 0, + }, + } + } + + /// Get the arguments for this call from the arguments stack. + pub fn arguments<'a>(&self, stack: &'a Stack) -> &'a [Argument] { + stack.arguments.get_args(self.args_base, self.args_len) + } + + /// The span encompassing the arguments + /// + /// If there are no arguments the span covers where the first argument would exist + /// + /// If there are one or more arguments the span encompasses the start of the first argument to + /// end of the last argument + pub fn arguments_span(&self) -> Span { + let past = self.head.past(); + Span::new(past.start, self.span.end) + } + + /// The number of named arguments, with or without values. + pub fn named_len(&self, stack: &Stack) -> usize { + self.arguments(stack) + .iter() + .filter(|arg| matches!(arg, Argument::Named { .. } | Argument::Flag { .. })) + .count() + } + + /// Iterate through named arguments, with or without values. + pub fn named_iter<'a>( + &'a self, + stack: &'a Stack, + ) -> impl Iterator, Option<&'a Value>)> + 'a { + self.arguments(stack).iter().filter_map( + |arg: &Argument| -> Option<(Spanned<&str>, Option<&Value>)> { + match arg { + Argument::Flag { + data, name, span, .. + } => Some(( + Spanned { + item: std::str::from_utf8(&data[*name]).expect("invalid arg name"), + span: *span, + }, + None, + )), + Argument::Named { + data, + name, + span, + val, + .. + } => Some(( + Spanned { + item: std::str::from_utf8(&data[*name]).expect("invalid arg name"), + span: *span, + }, + Some(val), + )), + _ => None, + } + }, + ) + } + + /// Get a named argument's value by name. Returns [`None`] for named arguments with no value as + /// well. + pub fn get_named_arg<'a>(&self, stack: &'a Stack, flag_name: &str) -> Option<&'a Value> { + // Optimized to avoid str::from_utf8() + self.arguments(stack) + .iter() + .find_map(|arg: &Argument| -> Option> { + match arg { + Argument::Flag { data, name, .. } if &data[*name] == flag_name.as_bytes() => { + Some(None) + } + Argument::Named { + data, name, val, .. + } if &data[*name] == flag_name.as_bytes() => Some(Some(val)), + _ => None, + } + }) + .flatten() + } + + /// The number of positional arguments, excluding spread arguments. + pub fn positional_len(&self, stack: &Stack) -> usize { + self.arguments(stack) + .iter() + .filter(|arg| matches!(arg, Argument::Positional { .. })) + .count() + } + + /// Iterate through positional arguments. Does not include spread arguments. + pub fn positional_iter<'a>(&self, stack: &'a Stack) -> impl Iterator { + self.arguments(stack).iter().filter_map(|arg| match arg { + Argument::Positional { val, .. } => Some(val), + _ => None, + }) + } + + /// Get a positional argument by index. Does not include spread arguments. + pub fn positional_nth<'a>(&self, stack: &'a Stack, index: usize) -> Option<&'a Value> { + self.positional_iter(stack).nth(index) + } + + /// Get the AST node for a positional argument by index. Not usually available unless the decl + /// required it. + pub fn positional_ast<'a>( + &self, + stack: &'a Stack, + index: usize, + ) -> Option<&'a Arc> { + self.arguments(stack) + .iter() + .filter_map(|arg| match arg { + Argument::Positional { ast, .. } => Some(ast), + _ => None, + }) + .nth(index) + .and_then(|option| option.as_ref()) + } + + /// Returns every argument to the rest parameter, as well as whether each argument + /// is spread or a normal positional argument (true for spread, false for normal) + pub fn rest_iter<'a>( + &self, + stack: &'a Stack, + start: usize, + ) -> impl Iterator + 'a { + self.arguments(stack) + .iter() + .filter_map(|arg| match arg { + Argument::Positional { val, .. } => Some((val, false)), + Argument::Spread { vals, .. } => Some((vals, true)), + _ => None, + }) + .skip(start) + } + + /// Returns all of the positional arguments including and after `start`, with spread arguments + /// flattened into a single `Vec`. + pub fn rest_iter_flattened( + &self, + stack: &Stack, + start: usize, + ) -> Result, ShellError> { + let mut acc = vec![]; + for (rest_val, spread) in self.rest_iter(stack, start) { + if spread { + match rest_val { + Value::List { vals, .. } => acc.extend(vals.iter().cloned()), + Value::Error { error, .. } => return Err(ShellError::clone(error)), + _ => { + return Err(ShellError::CannotSpreadAsList { + span: rest_val.span(), + }) + } + } + } else { + acc.push(rest_val.clone()); + } + } + Ok(acc) + } + + /// Get a parser info argument by name. + pub fn get_parser_info<'a>(&self, stack: &'a Stack, name: &str) -> Option<&'a Expression> { + self.arguments(stack) + .iter() + .find_map(|argument| match argument { + Argument::ParserInfo { + data, + name: name_slice, + info: expr, + } if &data[*name_slice] == name.as_bytes() => Some(expr.as_ref()), + _ => None, + }) + } + + /// Returns a span encompassing the entire call. + pub fn span(&self) -> Span { + self.span + } + + /// Resets the [`Stack`] to its state before the call was made. + pub fn leave(&self, stack: &mut Stack) { + stack.arguments.leave_frame(self.args_base); + } +} + +/// Utility struct for building a [`Call`] with arguments on the [`Stack`]. +pub struct CallBuilder { + inner: Call, +} + +impl CallBuilder { + /// Add an argument to the [`Stack`] and reference it from the [`Call`]. + pub fn add_argument(&mut self, stack: &mut Stack, argument: Argument) -> &mut Self { + if self.inner.args_len == 0 { + self.inner.args_base = stack.arguments.get_base(); + } + self.inner.args_len += 1; + if let Some(span) = argument.span() { + self.inner.span = self.inner.span.append(span); + } + stack.arguments.push(argument); + self + } + + /// Add a positional argument to the [`Stack`] and reference it from the [`Call`]. + pub fn add_positional(&mut self, stack: &mut Stack, span: Span, val: Value) -> &mut Self { + self.add_argument( + stack, + Argument::Positional { + span, + val, + ast: None, + }, + ) + } + + /// Add a spread argument to the [`Stack`] and reference it from the [`Call`]. + pub fn add_spread(&mut self, stack: &mut Stack, span: Span, vals: Value) -> &mut Self { + self.add_argument( + stack, + Argument::Spread { + span, + vals, + ast: None, + }, + ) + } + + /// Add a flag (no-value named) argument to the [`Stack`] and reference it from the [`Call`]. + pub fn add_flag( + &mut self, + stack: &mut Stack, + name: impl AsRef, + short: impl AsRef, + span: Span, + ) -> &mut Self { + let (data, name, short) = data_from_name_and_short(name.as_ref(), short.as_ref()); + self.add_argument( + stack, + Argument::Flag { + data, + name, + short, + span, + }, + ) + } + + /// Add a named argument to the [`Stack`] and reference it from the [`Call`]. + pub fn add_named( + &mut self, + stack: &mut Stack, + name: impl AsRef, + short: impl AsRef, + span: Span, + val: Value, + ) -> &mut Self { + let (data, name, short) = data_from_name_and_short(name.as_ref(), short.as_ref()); + self.add_argument( + stack, + Argument::Named { + data, + name, + short, + span, + val, + ast: None, + }, + ) + } + + /// Produce the finished [`Call`] from the builder. + /// + /// The call should be entered / run before any other calls are constructed, because the + /// argument stack will be reset when they exit. + pub fn finish(&self) -> Call { + self.inner.clone() + } + + /// Run a closure with the [`Call`] as an [`engine::Call`] reference, and then clean up the + /// arguments that were added to the [`Stack`] after. + /// + /// For convenience. Calls [`Call::leave`] after the closure ends. + pub fn with( + self, + stack: &mut Stack, + f: impl FnOnce(&mut Stack, &engine::Call<'_>) -> T, + ) -> T { + let call = engine::Call::from(&self.inner); + let result = f(stack, &call); + self.inner.leave(stack); + result + } +} + +fn data_from_name_and_short(name: &str, short: &str) -> (Arc<[u8]>, DataSlice, DataSlice) { + let data: Vec = name.bytes().chain(short.bytes()).collect(); + let data: Arc<[u8]> = data.into(); + let name = DataSlice { + start: 0, + len: name.len().try_into().expect("flag name too big"), + }; + let short = DataSlice { + start: name.start.checked_add(name.len).expect("flag name too big"), + len: short.len().try_into().expect("flag short name too big"), + }; + (data, name, short) +} diff --git a/crates/nu-protocol/src/ir/display.rs b/crates/nu-protocol/src/ir/display.rs new file mode 100644 index 0000000000..0026e15eab --- /dev/null +++ b/crates/nu-protocol/src/ir/display.rs @@ -0,0 +1,456 @@ +use std::fmt; + +use crate::{ast::Pattern, engine::EngineState, DeclId, VarId}; + +use super::{DataSlice, Instruction, IrBlock, Literal, RedirectMode}; + +pub struct FmtIrBlock<'a> { + pub(super) engine_state: &'a EngineState, + pub(super) ir_block: &'a IrBlock, +} + +impl<'a> fmt::Display for FmtIrBlock<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let plural = |count| if count == 1 { "" } else { "s" }; + writeln!( + f, + "# {} register{}, {} instruction{}, {} byte{} of data", + self.ir_block.register_count, + plural(self.ir_block.register_count as usize), + self.ir_block.instructions.len(), + plural(self.ir_block.instructions.len()), + self.ir_block.data.len(), + plural(self.ir_block.data.len()), + )?; + if self.ir_block.file_count > 0 { + writeln!( + f, + "# {} file{} used for redirection", + self.ir_block.file_count, + plural(self.ir_block.file_count as usize) + )?; + } + for (index, instruction) in self.ir_block.instructions.iter().enumerate() { + let formatted = format!( + "{:-4}: {}", + index, + FmtInstruction { + engine_state: self.engine_state, + instruction, + data: &self.ir_block.data, + } + ); + let comment = &self.ir_block.comments[index]; + if comment.is_empty() { + writeln!(f, "{formatted}")?; + } else { + writeln!(f, "{formatted:40} # {comment}")?; + } + } + Ok(()) + } +} + +pub struct FmtInstruction<'a> { + pub(super) engine_state: &'a EngineState, + pub(super) instruction: &'a Instruction, + pub(super) data: &'a [u8], +} + +impl<'a> fmt::Display for FmtInstruction<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + const WIDTH: usize = 22; + + match self.instruction { + Instruction::Unreachable => { + write!(f, "{:WIDTH$}", "unreachable") + } + Instruction::LoadLiteral { dst, lit } => { + let lit = FmtLiteral { + literal: lit, + data: self.data, + }; + write!(f, "{:WIDTH$} {dst}, {lit}", "load-literal") + } + Instruction::LoadValue { dst, val } => { + let val = val.to_debug_string(); + write!(f, "{:WIDTH$} {dst}, {val}", "load-value") + } + Instruction::Move { dst, src } => { + write!(f, "{:WIDTH$} {dst}, {src}", "move") + } + Instruction::Clone { dst, src } => { + write!(f, "{:WIDTH$} {dst}, {src}", "clone") + } + Instruction::Collect { src_dst } => { + write!(f, "{:WIDTH$} {src_dst}", "collect") + } + Instruction::Span { src_dst } => { + write!(f, "{:WIDTH$} {src_dst}", "span") + } + Instruction::Drop { src } => { + write!(f, "{:WIDTH$} {src}", "drop") + } + Instruction::Drain { src } => { + write!(f, "{:WIDTH$} {src}", "drain") + } + Instruction::LoadVariable { dst, var_id } => { + let var = FmtVar::new(self.engine_state, *var_id); + write!(f, "{:WIDTH$} {dst}, {var}", "load-variable") + } + Instruction::StoreVariable { var_id, src } => { + let var = FmtVar::new(self.engine_state, *var_id); + write!(f, "{:WIDTH$} {var}, {src}", "store-variable") + } + Instruction::DropVariable { var_id } => { + let var = FmtVar::new(self.engine_state, *var_id); + write!(f, "{:WIDTH$} {var}", "drop-variable") + } + Instruction::LoadEnv { dst, key } => { + let key = FmtData(self.data, *key); + write!(f, "{:WIDTH$} {dst}, {key}", "load-env") + } + Instruction::LoadEnvOpt { dst, key } => { + let key = FmtData(self.data, *key); + write!(f, "{:WIDTH$} {dst}, {key}", "load-env-opt") + } + Instruction::StoreEnv { key, src } => { + let key = FmtData(self.data, *key); + write!(f, "{:WIDTH$} {key}, {src}", "store-env") + } + Instruction::PushPositional { src } => { + write!(f, "{:WIDTH$} {src}", "push-positional") + } + Instruction::AppendRest { src } => { + write!(f, "{:WIDTH$} {src}", "append-rest") + } + Instruction::PushFlag { name } => { + let name = FmtData(self.data, *name); + write!(f, "{:WIDTH$} {name}", "push-flag") + } + Instruction::PushShortFlag { short } => { + let short = FmtData(self.data, *short); + write!(f, "{:WIDTH$} {short}", "push-short-flag") + } + Instruction::PushNamed { name, src } => { + let name = FmtData(self.data, *name); + write!(f, "{:WIDTH$} {name}, {src}", "push-named") + } + Instruction::PushShortNamed { short, src } => { + let short = FmtData(self.data, *short); + write!(f, "{:WIDTH$} {short}, {src}", "push-short-named") + } + Instruction::PushParserInfo { name, info } => { + let name = FmtData(self.data, *name); + write!(f, "{:WIDTH$} {name}, {info:?}", "push-parser-info") + } + Instruction::RedirectOut { mode } => { + write!(f, "{:WIDTH$} {mode}", "redirect-out") + } + Instruction::RedirectErr { mode } => { + write!(f, "{:WIDTH$} {mode}", "redirect-err") + } + Instruction::CheckErrRedirected { src } => { + write!(f, "{:WIDTH$} {src}", "check-err-redirected") + } + Instruction::OpenFile { + file_num, + path, + append, + } => { + write!( + f, + "{:WIDTH$} file({file_num}), {path}, append = {append:?}", + "open-file" + ) + } + Instruction::WriteFile { file_num, src } => { + write!(f, "{:WIDTH$} file({file_num}), {src}", "write-file") + } + Instruction::CloseFile { file_num } => { + write!(f, "{:WIDTH$} file({file_num})", "close-file") + } + Instruction::Call { decl_id, src_dst } => { + let decl = FmtDecl::new(self.engine_state, *decl_id); + write!(f, "{:WIDTH$} {decl}, {src_dst}", "call") + } + Instruction::StringAppend { src_dst, val } => { + write!(f, "{:WIDTH$} {src_dst}, {val}", "string-append") + } + Instruction::GlobFrom { src_dst, no_expand } => { + let no_expand = if *no_expand { "no-expand" } else { "expand" }; + write!(f, "{:WIDTH$} {src_dst}, {no_expand}", "glob-from",) + } + Instruction::ListPush { src_dst, item } => { + write!(f, "{:WIDTH$} {src_dst}, {item}", "list-push") + } + Instruction::ListSpread { src_dst, items } => { + write!(f, "{:WIDTH$} {src_dst}, {items}", "list-spread") + } + Instruction::RecordInsert { src_dst, key, val } => { + write!(f, "{:WIDTH$} {src_dst}, {key}, {val}", "record-insert") + } + Instruction::RecordSpread { src_dst, items } => { + write!(f, "{:WIDTH$} {src_dst}, {items}", "record-spread") + } + Instruction::Not { src_dst } => { + write!(f, "{:WIDTH$} {src_dst}", "not") + } + Instruction::BinaryOp { lhs_dst, op, rhs } => { + write!(f, "{:WIDTH$} {lhs_dst}, {op:?}, {rhs}", "binary-op") + } + Instruction::FollowCellPath { src_dst, path } => { + write!(f, "{:WIDTH$} {src_dst}, {path}", "follow-cell-path") + } + Instruction::CloneCellPath { dst, src, path } => { + write!(f, "{:WIDTH$} {dst}, {src}, {path}", "clone-cell-path") + } + Instruction::UpsertCellPath { + src_dst, + path, + new_value, + } => { + write!( + f, + "{:WIDTH$} {src_dst}, {path}, {new_value}", + "upsert-cell-path" + ) + } + Instruction::Jump { index } => { + write!(f, "{:WIDTH$} {index}", "jump") + } + Instruction::BranchIf { cond, index } => { + write!(f, "{:WIDTH$} {cond}, {index}", "branch-if") + } + Instruction::BranchIfEmpty { src, index } => { + write!(f, "{:WIDTH$} {src}, {index}", "branch-if-empty") + } + Instruction::Match { + pattern, + src, + index, + } => { + let pattern = FmtPattern { + engine_state: self.engine_state, + pattern, + }; + write!(f, "{:WIDTH$} ({pattern}), {src}, {index}", "match") + } + Instruction::CheckMatchGuard { src } => { + write!(f, "{:WIDTH$} {src}", "check-match-guard") + } + Instruction::Iterate { + dst, + stream, + end_index, + } => { + write!(f, "{:WIDTH$} {dst}, {stream}, end {end_index}", "iterate") + } + Instruction::OnError { index } => { + write!(f, "{:WIDTH$} {index}", "on-error") + } + Instruction::OnErrorInto { index, dst } => { + write!(f, "{:WIDTH$} {index}, {dst}", "on-error-into") + } + Instruction::PopErrorHandler => { + write!(f, "{:WIDTH$}", "pop-error-handler") + } + Instruction::CheckExternalFailed { dst, src } => { + write!(f, "{:WIDTH$} {dst}, {src}", "check-external-failed") + } + Instruction::ReturnEarly { src } => { + write!(f, "{:WIDTH$} {src}", "return-early") + } + Instruction::Return { src } => { + write!(f, "{:WIDTH$} {src}", "return") + } + } + } +} + +struct FmtDecl<'a>(DeclId, &'a str); + +impl<'a> FmtDecl<'a> { + fn new(engine_state: &'a EngineState, decl_id: DeclId) -> Self { + FmtDecl(decl_id, engine_state.get_decl(decl_id).name()) + } +} + +impl fmt::Display for FmtDecl<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "decl {} {:?}", self.0, self.1) + } +} + +struct FmtVar<'a>(DeclId, Option<&'a str>); + +impl<'a> FmtVar<'a> { + fn new(engine_state: &'a EngineState, var_id: VarId) -> Self { + // Search for the name of the variable + let name: Option<&str> = engine_state + .active_overlays(&[]) + .flat_map(|overlay| overlay.vars.iter()) + .find(|(_, v)| **v == var_id) + .map(|(k, _)| std::str::from_utf8(k).unwrap_or("")); + FmtVar(var_id, name) + } +} + +impl fmt::Display for FmtVar<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(name) = self.1 { + write!(f, "var {} {:?}", self.0, name) + } else { + write!(f, "var {}", self.0) + } + } +} + +impl fmt::Display for RedirectMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + RedirectMode::Pipe => write!(f, "pipe"), + RedirectMode::Capture => write!(f, "capture"), + RedirectMode::Null => write!(f, "null"), + RedirectMode::Inherit => write!(f, "inherit"), + RedirectMode::File { file_num } => write!(f, "file({file_num})"), + RedirectMode::Caller => write!(f, "caller"), + } + } +} + +struct FmtData<'a>(&'a [u8], DataSlice); + +impl<'a> fmt::Display for FmtData<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Ok(s) = std::str::from_utf8(&self.0[self.1]) { + // Write as string + write!(f, "{s:?}") + } else { + // Write as byte array + write!(f, "0x{:x?}", self.0) + } + } +} + +struct FmtLiteral<'a> { + literal: &'a Literal, + data: &'a [u8], +} + +impl<'a> fmt::Display for FmtLiteral<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.literal { + Literal::Bool(b) => write!(f, "bool({b:?})"), + Literal::Int(i) => write!(f, "int({i:?})"), + Literal::Float(fl) => write!(f, "float({fl:?})"), + Literal::Filesize(q) => write!(f, "filesize({q}b)"), + Literal::Duration(q) => write!(f, "duration({q}ns)"), + Literal::Binary(b) => write!(f, "binary({})", FmtData(self.data, *b)), + Literal::Block(id) => write!(f, "block({id})"), + Literal::Closure(id) => write!(f, "closure({id})"), + Literal::RowCondition(id) => write!(f, "row_condition({id})"), + Literal::Range { + start, + step, + end, + inclusion, + } => write!(f, "range({start}, {step}, {end}, {inclusion:?})"), + Literal::List { capacity } => write!(f, "list(capacity = {capacity})"), + Literal::Record { capacity } => write!(f, "record(capacity = {capacity})"), + Literal::Filepath { val, no_expand } => write!( + f, + "filepath({}, no_expand = {no_expand:?})", + FmtData(self.data, *val) + ), + Literal::Directory { val, no_expand } => write!( + f, + "directory({}, no_expand = {no_expand:?})", + FmtData(self.data, *val) + ), + Literal::GlobPattern { val, no_expand } => write!( + f, + "glob-pattern({}, no_expand = {no_expand:?})", + FmtData(self.data, *val) + ), + Literal::String(s) => write!(f, "string({})", FmtData(self.data, *s)), + Literal::RawString(rs) => write!(f, "raw-string({})", FmtData(self.data, *rs)), + Literal::CellPath(p) => write!(f, "cell-path({p})"), + Literal::Date(dt) => write!(f, "date({dt})"), + Literal::Nothing => write!(f, "nothing"), + } + } +} + +struct FmtPattern<'a> { + engine_state: &'a EngineState, + pattern: &'a Pattern, +} + +impl<'a> fmt::Display for FmtPattern<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.pattern { + Pattern::Record(bindings) => { + f.write_str("{")?; + for (name, pattern) in bindings { + write!( + f, + "{}: {}", + name, + FmtPattern { + engine_state: self.engine_state, + pattern: &pattern.pattern, + } + )?; + } + f.write_str("}") + } + Pattern::List(bindings) => { + f.write_str("[")?; + for pattern in bindings { + write!( + f, + "{}", + FmtPattern { + engine_state: self.engine_state, + pattern: &pattern.pattern + } + )?; + } + f.write_str("]") + } + Pattern::Value(expr) => { + let string = + String::from_utf8_lossy(self.engine_state.get_span_contents(expr.span)); + f.write_str(&string) + } + Pattern::Variable(var_id) => { + let variable = FmtVar::new(self.engine_state, *var_id); + write!(f, "{}", variable) + } + Pattern::Or(patterns) => { + for (index, pattern) in patterns.iter().enumerate() { + if index > 0 { + f.write_str(" | ")?; + } + write!( + f, + "{}", + FmtPattern { + engine_state: self.engine_state, + pattern: &pattern.pattern + } + )?; + } + Ok(()) + } + Pattern::Rest(var_id) => { + let variable = FmtVar::new(self.engine_state, *var_id); + write!(f, "..{}", variable) + } + Pattern::IgnoreRest => f.write_str(".."), + Pattern::IgnoreValue => f.write_str("_"), + Pattern::Garbage => f.write_str(""), + } + } +} diff --git a/crates/nu-protocol/src/ir/mod.rs b/crates/nu-protocol/src/ir/mod.rs new file mode 100644 index 0000000000..bdded28c0e --- /dev/null +++ b/crates/nu-protocol/src/ir/mod.rs @@ -0,0 +1,479 @@ +use std::{fmt, sync::Arc}; + +use crate::{ + ast::{CellPath, Expression, Operator, Pattern, RangeInclusion}, + engine::EngineState, + BlockId, DeclId, RegId, Span, Value, VarId, +}; + +use chrono::{DateTime, FixedOffset}; +use serde::{Deserialize, Serialize}; + +mod call; +mod display; + +pub use call::*; +pub use display::{FmtInstruction, FmtIrBlock}; + +#[derive(Clone, Serialize, Deserialize)] +pub struct IrBlock { + pub instructions: Vec, + pub spans: Vec, + #[serde(with = "serde_arc_u8_array")] + pub data: Arc<[u8]>, + pub ast: Vec>, + /// Additional information that can be added to help with debugging + pub comments: Vec>, + pub register_count: u32, + pub file_count: u32, +} + +impl fmt::Debug for IrBlock { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // the ast field is too verbose and doesn't add much + f.debug_struct("IrBlock") + .field("instructions", &self.instructions) + .field("spans", &self.spans) + .field("data", &self.data) + .field("comments", &self.comments) + .field("register_count", &self.register_count) + .field("file_count", &self.file_count) + .finish_non_exhaustive() + } +} + +impl IrBlock { + /// Returns a value that can be formatted with [`Display`](std::fmt::Display) to show a detailed + /// listing of the instructions contained within this [`IrBlock`]. + pub fn display<'a>(&'a self, engine_state: &'a EngineState) -> FmtIrBlock<'a> { + FmtIrBlock { + engine_state, + ir_block: self, + } + } +} + +/// A slice into the `data` array of a block. This is a compact and cache-friendly way to store +/// string data that a block uses. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +pub struct DataSlice { + pub start: u32, + pub len: u32, +} + +impl DataSlice { + /// A data slice that contains no data. This slice is always valid. + pub const fn empty() -> DataSlice { + DataSlice { start: 0, len: 0 } + } +} + +impl std::ops::Index for [u8] { + type Output = [u8]; + + fn index(&self, index: DataSlice) -> &Self::Output { + &self[index.start as usize..(index.start as usize + index.len as usize)] + } +} + +/// A possible reference into the abstract syntax tree for an instruction. This is not present for +/// most instructions and is just added when needed. +#[derive(Debug, Clone)] +pub struct IrAstRef(pub Arc); + +impl Serialize for IrAstRef { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.0.as_ref().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for IrAstRef { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + Expression::deserialize(deserializer).map(|expr| IrAstRef(Arc::new(expr))) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Instruction { + /// Unreachable code path (error) + Unreachable, + /// Load a literal value into the `dst` register + LoadLiteral { dst: RegId, lit: Literal }, + /// Load a clone of a boxed value into the `dst` register (e.g. from const evaluation) + LoadValue { dst: RegId, val: Box }, + /// Move a register. Value is taken from `src` (used by this instruction). + Move { dst: RegId, src: RegId }, + /// Copy a register (must be a collected value). Value is still in `src` after this instruction. + Clone { dst: RegId, src: RegId }, + /// Collect a stream in a register to a value + Collect { src_dst: RegId }, + /// Change the span of the contents of a register to the span of this instruction. + Span { src_dst: RegId }, + /// Drop the value/stream in a register, without draining + Drop { src: RegId }, + /// Drain the value/stream in a register and discard (e.g. semicolon). + /// + /// If passed a stream from an external command, sets $env.LAST_EXIT_CODE to the resulting exit + /// code, and invokes any available error handler with Empty, or if not available, returns an + /// exit-code-only stream, leaving the block. + Drain { src: RegId }, + /// Load the value of a variable into the `dst` register + LoadVariable { dst: RegId, var_id: VarId }, + /// Store the value of a variable from the `src` register + StoreVariable { var_id: VarId, src: RegId }, + /// Remove a variable from the stack, freeing up whatever resources were associated with it + DropVariable { var_id: VarId }, + /// Load the value of an environment variable into the `dst` register + LoadEnv { dst: RegId, key: DataSlice }, + /// Load the value of an environment variable into the `dst` register, or `Nothing` if it + /// doesn't exist + LoadEnvOpt { dst: RegId, key: DataSlice }, + /// Store the value of an environment variable from the `src` register + StoreEnv { key: DataSlice, src: RegId }, + /// Add a positional arg to the next (internal) call. + PushPositional { src: RegId }, + /// Add a list of args to the next (internal) call (spread/rest). + AppendRest { src: RegId }, + /// Add a named arg with no value to the next (internal) call. + PushFlag { name: DataSlice }, + /// Add a short named arg with no value to the next (internal) call. + PushShortFlag { short: DataSlice }, + /// Add a named arg with a value to the next (internal) call. + PushNamed { name: DataSlice, src: RegId }, + /// Add a short named arg with a value to the next (internal) call. + PushShortNamed { short: DataSlice, src: RegId }, + /// Add parser info to the next (internal) call. + PushParserInfo { + name: DataSlice, + info: Box, + }, + /// Set the redirection for stdout for the next call (only). + /// + /// The register for a file redirection is not consumed. + RedirectOut { mode: RedirectMode }, + /// Set the redirection for stderr for the next call (only). + /// + /// The register for a file redirection is not consumed. + RedirectErr { mode: RedirectMode }, + /// Throw an error if stderr wasn't redirected in the given stream. `src` is preserved. + CheckErrRedirected { src: RegId }, + /// Open a file for redirection, pushing it onto the file stack. + OpenFile { + file_num: u32, + path: RegId, + append: bool, + }, + /// Write data from the register to a file. This is done to finish a file redirection, in case + /// an internal command or expression was evaluated rather than an external one. + WriteFile { file_num: u32, src: RegId }, + /// Pop a file used for redirection from the file stack. + CloseFile { file_num: u32 }, + /// Make a call. The input is taken from `src_dst`, and the output is placed in `src_dst`, + /// overwriting it. The argument stack is used implicitly and cleared when the call ends. + Call { decl_id: DeclId, src_dst: RegId }, + /// Append a value onto the end of a string. Uses `to_expanded_string(", ", ...)` on the value. + /// Used for string interpolation literals. Not the same thing as the `++` operator. + StringAppend { src_dst: RegId, val: RegId }, + /// Convert a string into a glob. Used for glob interpolation and setting glob variables. If the + /// value is already a glob, it won't be modified (`no_expand` will have no effect). + GlobFrom { src_dst: RegId, no_expand: bool }, + /// Push a value onto the end of a list. Used to construct list literals. + ListPush { src_dst: RegId, item: RegId }, + /// Spread a value onto the end of a list. Used to construct list literals. + ListSpread { src_dst: RegId, items: RegId }, + /// Insert a key-value pair into a record. Used to construct record literals. Raises an error if + /// the key already existed in the record. + RecordInsert { + src_dst: RegId, + key: RegId, + val: RegId, + }, + /// Spread a record onto a record. Used to construct record literals. Any existing value for the + /// key is overwritten. + RecordSpread { src_dst: RegId, items: RegId }, + /// Negate a boolean. + Not { src_dst: RegId }, + /// Do a binary operation on `lhs_dst` (left) and `rhs` (right) and write the result to + /// `lhs_dst`. + BinaryOp { + lhs_dst: RegId, + op: Operator, + rhs: RegId, + }, + /// Follow a cell path on the value in `src_dst`, storing the result back to `src_dst` + FollowCellPath { src_dst: RegId, path: RegId }, + /// Clone the value at a cell path in `src`, storing the result to `dst`. The original value + /// remains in `src`. Must be a collected value. + CloneCellPath { dst: RegId, src: RegId, path: RegId }, + /// Update/insert a cell path to `new_value` on the value in `src_dst`, storing the modified + /// value back to `src_dst` + UpsertCellPath { + src_dst: RegId, + path: RegId, + new_value: RegId, + }, + /// Jump to an offset in this block + Jump { index: usize }, + /// Branch to an offset in this block if the value of the `cond` register is a true boolean, + /// otherwise continue execution + BranchIf { cond: RegId, index: usize }, + /// Branch to an offset in this block if the value of the `src` register is Empty or Nothing, + /// otherwise continue execution. The original value in `src` is preserved. + BranchIfEmpty { src: RegId, index: usize }, + /// Match a pattern on `src`. If the pattern matches, branch to `index` after having set any + /// variables captured by the pattern. If the pattern doesn't match, continue execution. The + /// original value is preserved in `src` through this instruction. + Match { + pattern: Box, + src: RegId, + index: usize, + }, + /// Check that a match guard is a boolean, throwing + /// [`MatchGuardNotBool`](crate::ShellError::MatchGuardNotBool) if it isn't. Preserves `src`. + CheckMatchGuard { src: RegId }, + /// Iterate on register `stream`, putting the next value in `dst` if present, or jumping to + /// `end_index` if the iterator is finished + Iterate { + dst: RegId, + stream: RegId, + end_index: usize, + }, + /// Push an error handler, without capturing the error value + OnError { index: usize }, + /// Push an error handler, capturing the error value into `dst`. If the error handler is not + /// called, the register should be freed manually. + OnErrorInto { index: usize, dst: RegId }, + /// Pop an error handler. This is not necessary when control flow is directed to the error + /// handler due to an error. + PopErrorHandler, + /// Check if an external command failed. Boolean value into `dst`. `src` is preserved, but it + /// does require waiting for the command to exit. + CheckExternalFailed { dst: RegId, src: RegId }, + /// Return early from the block, raising a `ShellError::Return` instead. + /// + /// Collecting the value is unavoidable. + ReturnEarly { src: RegId }, + /// Return from the block with the value in the register + Return { src: RegId }, +} + +impl Instruction { + /// Returns a value that can be formatted with [`Display`](std::fmt::Display) to show a detailed + /// listing of the instruction. + pub fn display<'a>( + &'a self, + engine_state: &'a EngineState, + data: &'a [u8], + ) -> FmtInstruction<'a> { + FmtInstruction { + engine_state, + instruction: self, + data, + } + } + + /// Get the output register, for instructions that produce some kind of immediate result. + pub fn output_register(&self) -> Option { + match *self { + Instruction::Unreachable => None, + Instruction::LoadLiteral { dst, .. } => Some(dst), + Instruction::LoadValue { dst, .. } => Some(dst), + Instruction::Move { dst, .. } => Some(dst), + Instruction::Clone { dst, .. } => Some(dst), + Instruction::Collect { src_dst } => Some(src_dst), + Instruction::Span { src_dst } => Some(src_dst), + Instruction::Drop { .. } => None, + Instruction::Drain { .. } => None, + Instruction::LoadVariable { dst, .. } => Some(dst), + Instruction::StoreVariable { .. } => None, + Instruction::DropVariable { .. } => None, + Instruction::LoadEnv { dst, .. } => Some(dst), + Instruction::LoadEnvOpt { dst, .. } => Some(dst), + Instruction::StoreEnv { .. } => None, + Instruction::PushPositional { .. } => None, + Instruction::AppendRest { .. } => None, + Instruction::PushFlag { .. } => None, + Instruction::PushShortFlag { .. } => None, + Instruction::PushNamed { .. } => None, + Instruction::PushShortNamed { .. } => None, + Instruction::PushParserInfo { .. } => None, + Instruction::RedirectOut { .. } => None, + Instruction::RedirectErr { .. } => None, + Instruction::CheckErrRedirected { .. } => None, + Instruction::OpenFile { .. } => None, + Instruction::WriteFile { .. } => None, + Instruction::CloseFile { .. } => None, + Instruction::Call { src_dst, .. } => Some(src_dst), + Instruction::StringAppend { src_dst, .. } => Some(src_dst), + Instruction::GlobFrom { src_dst, .. } => Some(src_dst), + Instruction::ListPush { src_dst, .. } => Some(src_dst), + Instruction::ListSpread { src_dst, .. } => Some(src_dst), + Instruction::RecordInsert { src_dst, .. } => Some(src_dst), + Instruction::RecordSpread { src_dst, .. } => Some(src_dst), + Instruction::Not { src_dst } => Some(src_dst), + Instruction::BinaryOp { lhs_dst, .. } => Some(lhs_dst), + Instruction::FollowCellPath { src_dst, .. } => Some(src_dst), + Instruction::CloneCellPath { dst, .. } => Some(dst), + Instruction::UpsertCellPath { src_dst, .. } => Some(src_dst), + Instruction::Jump { .. } => None, + Instruction::BranchIf { .. } => None, + Instruction::BranchIfEmpty { .. } => None, + Instruction::Match { .. } => None, + Instruction::CheckMatchGuard { .. } => None, + Instruction::Iterate { dst, .. } => Some(dst), + Instruction::OnError { .. } => None, + Instruction::OnErrorInto { .. } => None, + Instruction::PopErrorHandler => None, + Instruction::CheckExternalFailed { dst, .. } => Some(dst), + Instruction::ReturnEarly { .. } => None, + Instruction::Return { .. } => None, + } + } + + /// Returns the branch target index of the instruction if this is a branching instruction. + pub fn branch_target(&self) -> Option { + match self { + Instruction::Jump { index } => Some(*index), + Instruction::BranchIf { cond: _, index } => Some(*index), + Instruction::BranchIfEmpty { src: _, index } => Some(*index), + Instruction::Match { + pattern: _, + src: _, + index, + } => Some(*index), + + Instruction::Iterate { + dst: _, + stream: _, + end_index, + } => Some(*end_index), + Instruction::OnError { index } => Some(*index), + Instruction::OnErrorInto { index, dst: _ } => Some(*index), + _ => None, + } + } + + /// Sets the branch target of the instruction if this is a branching instruction. + /// + /// Returns `Err(target_index)` if it isn't a branching instruction. + pub fn set_branch_target(&mut self, target_index: usize) -> Result<(), usize> { + match self { + Instruction::Jump { index } => *index = target_index, + Instruction::BranchIf { cond: _, index } => *index = target_index, + Instruction::BranchIfEmpty { src: _, index } => *index = target_index, + Instruction::Match { + pattern: _, + src: _, + index, + } => *index = target_index, + + Instruction::Iterate { + dst: _, + stream: _, + end_index, + } => *end_index = target_index, + Instruction::OnError { index } => *index = target_index, + Instruction::OnErrorInto { index, dst: _ } => *index = target_index, + _ => return Err(target_index), + } + Ok(()) + } +} + +// This is to document/enforce the size of `Instruction` in bytes. +// We should try to avoid increasing the size of `Instruction`, +// and PRs that do so will have to change the number below so that it's noted in review. +const _: () = assert!(std::mem::size_of::() <= 24); + +/// A literal value that can be embedded in an instruction. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Literal { + Bool(bool), + Int(i64), + Float(f64), + Filesize(i64), + Duration(i64), + Binary(DataSlice), + Block(BlockId), + Closure(BlockId), + RowCondition(BlockId), + Range { + start: RegId, + step: RegId, + end: RegId, + inclusion: RangeInclusion, + }, + List { + capacity: usize, + }, + Record { + capacity: usize, + }, + Filepath { + val: DataSlice, + no_expand: bool, + }, + Directory { + val: DataSlice, + no_expand: bool, + }, + GlobPattern { + val: DataSlice, + no_expand: bool, + }, + String(DataSlice), + RawString(DataSlice), + CellPath(Box), + Date(Box>), + Nothing, +} + +/// A redirection mode for the next call. See [`OutDest`](crate::OutDest). +/// +/// This is generated by: +/// +/// 1. Explicit redirection in a [`PipelineElement`](crate::ast::PipelineElement), or +/// 2. The [`pipe_redirection()`](crate::engine::Command::pipe_redirection) of the command being +/// piped into. +/// +/// Not setting it uses the default, determined by [`Stack`](crate::engine::Stack). +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub enum RedirectMode { + Pipe, + Capture, + Null, + Inherit, + /// Use the given numbered file. + File { + file_num: u32, + }, + /// Use the redirection mode requested by the caller, for a pre-return call. + Caller, +} + +/// Just a hack to allow `Arc<[u8]>` to be serialized and deserialized +mod serde_arc_u8_array { + use serde::{Deserialize, Serialize}; + use std::sync::Arc; + + pub fn serialize(data: &Arc<[u8]>, ser: S) -> Result + where + S: serde::Serializer, + { + data.as_ref().serialize(ser) + } + + pub fn deserialize<'de, D>(de: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + let data: Vec = Deserialize::deserialize(de)?; + Ok(data.into()) + } +} diff --git a/crates/nu-protocol/src/lib.rs b/crates/nu-protocol/src/lib.rs index d09186cf46..e143a19819 100644 --- a/crates/nu-protocol/src/lib.rs +++ b/crates/nu-protocol/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] mod alias; pub mod ast; pub mod config; @@ -9,8 +10,10 @@ pub mod eval_base; pub mod eval_const; mod example; mod id; +pub mod ir; mod lev_distance; mod module; +pub mod parser_path; mod pipeline; #[cfg(feature = "plugin")] mod plugin; @@ -39,3 +42,5 @@ pub use span::*; pub use syntax_shape::*; pub use ty::*; pub use value::*; + +pub use nu_derive_value::*; diff --git a/crates/nu-protocol/src/module.rs b/crates/nu-protocol/src/module.rs index ebb6f5621e..6b4aec0c60 100644 --- a/crates/nu-protocol/src/module.rs +++ b/crates/nu-protocol/src/module.rs @@ -1,8 +1,9 @@ use crate::{ - ast::ImportPatternMember, engine::StateWorkingSet, BlockId, DeclId, ModuleId, ParseError, Span, - Value, VarId, + ast::ImportPatternMember, engine::StateWorkingSet, BlockId, DeclId, FileId, ModuleId, + ParseError, Span, Value, VarId, }; +use crate::parser_path::ParserPath; use indexmap::IndexMap; pub struct ResolvedImportPattern { @@ -35,6 +36,8 @@ pub struct Module { pub env_block: Option, // `export-env { ... }` block pub main: Option, // `export def main` pub span: Option, + pub imported_modules: Vec, // use other_module.nu + pub file: Option<(ParserPath, FileId)>, } impl Module { @@ -47,6 +50,8 @@ impl Module { env_block: None, main: None, span: None, + imported_modules: vec![], + file: None, } } @@ -59,6 +64,8 @@ impl Module { env_block: None, main: None, span: Some(span), + imported_modules: vec![], + file: None, } } @@ -82,6 +89,12 @@ impl Module { self.env_block = Some(block_id); } + pub fn track_imported_modules(&mut self, module_id: &[ModuleId]) { + for m in module_id { + self.imported_modules.push(*m) + } + } + pub fn has_decl(&self, name: &[u8]) -> bool { if name == self.name && self.main.is_some() { return true; @@ -90,6 +103,9 @@ impl Module { self.decls.contains_key(name) } + /// Resolve `members` from given module, which is indicated by `self_id` to import. + /// + /// When resolving, all modules are recorded in `imported_modules`. pub fn resolve_import_pattern( &self, working_set: &StateWorkingSet, @@ -97,7 +113,9 @@ impl Module { members: &[ImportPatternMember], name_override: Option<&[u8]>, // name under the module was stored (doesn't have to be the same as self.name) backup_span: Span, + imported_modules: &mut Vec, ) -> (ResolvedImportPattern, Vec) { + imported_modules.push(self_id); let final_name = name_override.unwrap_or(&self.name).to_vec(); let (head, rest) = if let Some((head, rest)) = members.split_first() { @@ -112,8 +130,14 @@ impl Module { let submodule = working_set.get_module(*id); let span = submodule.span.or(self.span).unwrap_or(backup_span); - let (sub_results, sub_errors) = - submodule.resolve_import_pattern(working_set, *id, &[], None, span); + let (sub_results, sub_errors) = submodule.resolve_import_pattern( + working_set, + *id, + &[], + None, + span, + imported_modules, + ); errors.extend(sub_errors); for (sub_name, sub_decl_id) in sub_results.decls { @@ -212,6 +236,7 @@ impl Module { rest, None, self.span.unwrap_or(backup_span), + imported_modules, ) } else { ( @@ -234,6 +259,7 @@ impl Module { &[], None, self.span.unwrap_or(backup_span), + imported_modules, ); decls.extend(sub_results.decls); @@ -287,6 +313,7 @@ impl Module { rest, None, self.span.unwrap_or(backup_span), + imported_modules, ); decls.extend(sub_results.decls); diff --git a/crates/nu-parser/src/parser_path.rs b/crates/nu-protocol/src/parser_path.rs similarity index 98% rename from crates/nu-parser/src/parser_path.rs rename to crates/nu-protocol/src/parser_path.rs index 2d0fbce2a2..4935d9fbe2 100644 --- a/crates/nu-parser/src/parser_path.rs +++ b/crates/nu-protocol/src/parser_path.rs @@ -1,4 +1,4 @@ -use nu_protocol::engine::{StateWorkingSet, VirtualPath}; +use crate::engine::{StateWorkingSet, VirtualPath}; use std::{ ffi::OsStr, path::{Path, PathBuf}, diff --git a/crates/nu-protocol/src/pipeline/byte_stream.rs b/crates/nu-protocol/src/pipeline/byte_stream.rs index babd195a9e..bd384e88c2 100644 --- a/crates/nu-protocol/src/pipeline/byte_stream.rs +++ b/crates/nu-protocol/src/pipeline/byte_stream.rs @@ -1,9 +1,9 @@ -use serde::{Deserialize, Serialize}; - +//! Module managing the streaming of raw bytes between pipeline elements use crate::{ process::{ChildPipe, ChildProcess, ExitStatus}, - ErrSpan, IntoSpanned, OutDest, PipelineData, ShellError, Span, Type, Value, + ErrSpan, IntoSpanned, OutDest, PipelineData, ShellError, Signals, Span, Type, Value, }; +use serde::{Deserialize, Serialize}; #[cfg(unix)] use std::os::fd::OwnedFd; #[cfg(windows)] @@ -13,10 +13,6 @@ use std::{ fs::File, io::{self, BufRead, BufReader, Cursor, ErrorKind, Read, Write}, process::Stdio, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, thread, }; @@ -163,14 +159,14 @@ impl From for Type { /// Try not to use this method if possible. Rather, please use [`reader`](ByteStream::reader) /// (or [`lines`](ByteStream::lines) if it matches the situation). /// -/// Additionally, there are few methods to collect a [`Bytestream`] into memory: +/// Additionally, there are few methods to collect a [`ByteStream`] into memory: /// - [`into_bytes`](ByteStream::into_bytes): collects all bytes into a [`Vec`]. /// - [`into_string`](ByteStream::into_string): collects all bytes into a [`String`], erroring if utf-8 decoding failed. /// - [`into_value`](ByteStream::into_value): collects all bytes into a value typed appropriately /// for the [type](.type_()) of this stream. If the type is [`Unknown`](ByteStreamType::Unknown), /// it will produce a string value if the data is valid UTF-8, or a binary value otherwise. /// -/// There are also a few other methods to consume all the data of a [`Bytestream`]: +/// There are also a few other methods to consume all the data of a [`ByteStream`]: /// - [`drain`](ByteStream::drain): consumes all bytes and outputs nothing. /// - [`write_to`](ByteStream::write_to): writes all bytes to the given [`Write`] destination. /// - [`print`](ByteStream::print): a convenience wrapper around [`write_to`](ByteStream::write_to). @@ -182,7 +178,7 @@ impl From for Type { pub struct ByteStream { stream: ByteStreamSource, span: Span, - ctrlc: Option>, + signals: Signals, type_: ByteStreamType, known_size: Option, } @@ -192,13 +188,13 @@ impl ByteStream { pub fn new( stream: ByteStreamSource, span: Span, - interrupt: Option>, + signals: Signals, type_: ByteStreamType, ) -> Self { Self { stream, span, - ctrlc: interrupt, + signals, type_, known_size: None, } @@ -208,33 +204,33 @@ impl ByteStream { pub fn read( reader: impl Read + Send + 'static, span: Span, - interrupt: Option>, + signals: Signals, type_: ByteStreamType, ) -> Self { Self::new( ByteStreamSource::Read(Box::new(reader)), span, - interrupt, + signals, type_, ) } /// Create a [`ByteStream`] from a string. The type of the stream is always `String`. - pub fn read_string(string: String, span: Span, interrupt: Option>) -> Self { + pub fn read_string(string: String, span: Span, signals: Signals) -> Self { let len = string.len(); ByteStream::read( Cursor::new(string.into_bytes()), span, - interrupt, + signals, ByteStreamType::String, ) .with_known_size(Some(len as u64)) } /// Create a [`ByteStream`] from a byte vector. The type of the stream is always `Binary`. - pub fn read_binary(bytes: Vec, span: Span, interrupt: Option>) -> Self { + pub fn read_binary(bytes: Vec, span: Span, signals: Signals) -> Self { let len = bytes.len(); - ByteStream::read(Cursor::new(bytes), span, interrupt, ByteStreamType::Binary) + ByteStream::read(Cursor::new(bytes), span, signals, ByteStreamType::Binary) .with_known_size(Some(len as u64)) } @@ -242,11 +238,11 @@ impl ByteStream { /// /// The type is implicitly `Unknown`, as it's not typically known whether files will /// return text or binary. - pub fn file(file: File, span: Span, interrupt: Option>) -> Self { + pub fn file(file: File, span: Span, signals: Signals) -> Self { Self::new( ByteStreamSource::File(file), span, - interrupt, + signals, ByteStreamType::Unknown, ) } @@ -259,7 +255,7 @@ impl ByteStream { Self::new( ByteStreamSource::Child(Box::new(child)), span, - None, + Signals::empty(), ByteStreamType::Unknown, ) } @@ -271,14 +267,19 @@ impl ByteStream { pub fn stdin(span: Span) -> Result { let stdin = os_pipe::dup_stdin().err_span(span)?; let source = ByteStreamSource::File(convert_file(stdin)); - Ok(Self::new(source, span, None, ByteStreamType::Unknown)) + Ok(Self::new( + source, + span, + Signals::empty(), + ByteStreamType::Unknown, + )) } /// Create a [`ByteStream`] from a generator function that writes data to the given buffer /// when called, and returns `Ok(false)` on end of stream. pub fn from_fn( span: Span, - interrupt: Option>, + signals: Signals, type_: ByteStreamType, generator: impl FnMut(&mut Vec) -> Result + Send + 'static, ) -> Self { @@ -288,7 +289,7 @@ impl ByteStream { generator, }, span, - interrupt, + signals, type_, ) } @@ -301,12 +302,7 @@ impl ByteStream { /// Create a new [`ByteStream`] from an [`Iterator`] of bytes slices. /// /// The returned [`ByteStream`] will have a [`ByteStreamSource`] of `Read`. - pub fn from_iter( - iter: I, - span: Span, - interrupt: Option>, - type_: ByteStreamType, - ) -> Self + pub fn from_iter(iter: I, span: Span, signals: Signals, type_: ByteStreamType) -> Self where I: IntoIterator, I::IntoIter: Send + 'static, @@ -314,7 +310,7 @@ impl ByteStream { { let iter = iter.into_iter(); let cursor = Some(Cursor::new(I::Item::default())); - Self::read(ReadIterator { iter, cursor }, span, interrupt, type_) + Self::read(ReadIterator { iter, cursor }, span, signals, type_) } /// Create a new [`ByteStream`] from an [`Iterator`] of [`Result`] bytes slices. @@ -323,7 +319,7 @@ impl ByteStream { pub fn from_result_iter( iter: I, span: Span, - interrupt: Option>, + signals: Signals, type_: ByteStreamType, ) -> Self where @@ -333,7 +329,7 @@ impl ByteStream { { let iter = iter.into_iter(); let cursor = Some(Cursor::new(T::default())); - Self::read(ReadResultIterator { iter, cursor }, span, interrupt, type_) + Self::read(ReadResultIterator { iter, cursor }, span, signals, type_) } /// Set the known size, in number of bytes, of the [`ByteStream`]. @@ -357,6 +353,12 @@ impl ByteStream { self.span } + /// Changes the [`Span`] associated with the [`ByteStream`]. + pub fn with_span(mut self, span: Span) -> Self { + self.span = span; + self + } + /// Returns the [`ByteStreamType`] associated with the [`ByteStream`]. pub fn type_(&self) -> ByteStreamType { self.type_ @@ -378,7 +380,7 @@ impl ByteStream { Some(Reader { reader: BufReader::new(reader), span: self.span, - ctrlc: self.ctrlc, + signals: self.signals, }) } @@ -394,7 +396,7 @@ impl ByteStream { Some(Lines { reader: BufReader::new(reader), span: self.span, - ctrlc: self.ctrlc, + signals: self.signals, }) } @@ -415,7 +417,7 @@ impl ByteStream { /// then the stream is considered empty and `None` will be returned. pub fn chunks(self) -> Option { let reader = self.stream.reader()?; - Some(Chunks::new(reader, self.span, self.ctrlc, self.type_)) + Some(Chunks::new(reader, self.span, self.signals, self.type_)) } /// Convert the [`ByteStream`] into its inner [`ByteStreamSource`]. @@ -552,7 +554,7 @@ impl ByteStream { pub fn drain(self) -> Result, ShellError> { match self.stream { ByteStreamSource::Read(read) => { - copy_with_interrupt(read, io::sink(), self.span, self.ctrlc.as_deref())?; + copy_with_signals(read, io::sink(), self.span, &self.signals)?; Ok(None) } ByteStreamSource::File(_) => Ok(None), @@ -578,14 +580,14 @@ impl ByteStream { /// then the [`ExitStatus`] of the [`ChildProcess`] is returned. pub fn write_to(self, dest: impl Write) -> Result, ShellError> { let span = self.span; - let ctrlc = self.ctrlc.as_deref(); + let signals = &self.signals; match self.stream { ByteStreamSource::Read(read) => { - copy_with_interrupt(read, dest, span, ctrlc)?; + copy_with_signals(read, dest, span, signals)?; Ok(None) } ByteStreamSource::File(file) => { - copy_with_interrupt(file, dest, span, ctrlc)?; + copy_with_signals(file, dest, span, signals)?; Ok(None) } ByteStreamSource::Child(mut child) => { @@ -597,10 +599,10 @@ impl ByteStream { if let Some(stdout) = child.stdout.take() { match stdout { ChildPipe::Pipe(pipe) => { - copy_with_interrupt(pipe, dest, span, ctrlc)?; + copy_with_signals(pipe, dest, span, signals)?; } ChildPipe::Tee(tee) => { - copy_with_interrupt(tee, dest, span, ctrlc)?; + copy_with_signals(tee, dest, span, signals)?; } } } @@ -615,21 +617,21 @@ impl ByteStream { stderr: &OutDest, ) -> Result, ShellError> { let span = self.span; - let ctrlc = self.ctrlc.as_deref(); + let signals = &self.signals; match self.stream { ByteStreamSource::Read(read) => { - write_to_out_dest(read, stdout, true, span, ctrlc)?; + write_to_out_dest(read, stdout, true, span, signals)?; Ok(None) } ByteStreamSource::File(file) => { match stdout { OutDest::Pipe | OutDest::Capture | OutDest::Null => {} OutDest::Inherit => { - copy_with_interrupt(file, io::stdout(), span, ctrlc)?; + copy_with_signals(file, io::stdout(), span, signals)?; } OutDest::File(f) => { - copy_with_interrupt(file, f.as_ref(), span, ctrlc)?; + copy_with_signals(file, f.as_ref(), span, signals)?; } } Ok(None) @@ -643,20 +645,20 @@ impl ByteStream { .name("stderr writer".into()) .spawn_scoped(s, || match err { ChildPipe::Pipe(pipe) => { - write_to_out_dest(pipe, stderr, false, span, ctrlc) + write_to_out_dest(pipe, stderr, false, span, signals) } ChildPipe::Tee(tee) => { - write_to_out_dest(tee, stderr, false, span, ctrlc) + write_to_out_dest(tee, stderr, false, span, signals) } }) .err_span(span); match out { ChildPipe::Pipe(pipe) => { - write_to_out_dest(pipe, stdout, true, span, ctrlc) + write_to_out_dest(pipe, stdout, true, span, signals) } ChildPipe::Tee(tee) => { - write_to_out_dest(tee, stdout, true, span, ctrlc) + write_to_out_dest(tee, stdout, true, span, signals) } }?; @@ -672,11 +674,11 @@ impl ByteStream { } (Some(out), None) => { // single output stream, we can consume directly - write_to_out_dest(out, stdout, true, span, ctrlc)?; + write_to_out_dest(out, stdout, true, span, signals)?; } (None, Some(err)) => { // single output stream, we can consume directly - write_to_out_dest(err, stderr, false, span, ctrlc)?; + write_to_out_dest(err, stderr, false, span, signals)?; } (None, None) => {} } @@ -749,7 +751,7 @@ where pub struct Reader { reader: BufReader, span: Span, - ctrlc: Option>, + signals: Signals, } impl Reader { @@ -760,14 +762,8 @@ impl Reader { impl Read for Reader { fn read(&mut self, buf: &mut [u8]) -> io::Result { - if nu_utils::ctrl_c::was_pressed(&self.ctrlc) { - Err(ShellError::InterruptedByUser { - span: Some(self.span), - } - .into()) - } else { - self.reader.read(buf) - } + self.signals.check(self.span)?; + self.reader.read(buf) } } @@ -784,7 +780,7 @@ impl BufRead for Reader { pub struct Lines { reader: BufReader, span: Span, - ctrlc: Option>, + signals: Signals, } impl Lines { @@ -797,7 +793,7 @@ impl Iterator for Lines { type Item = Result; fn next(&mut self) -> Option { - if nu_utils::ctrl_c::was_pressed(&self.ctrlc) { + if self.signals.interrupted() { None } else { let mut buf = Vec::new(); @@ -826,23 +822,18 @@ pub struct Chunks { pos: u64, error: bool, span: Span, - ctrlc: Option>, + signals: Signals, type_: ByteStreamType, } impl Chunks { - fn new( - reader: SourceReader, - span: Span, - ctrlc: Option>, - type_: ByteStreamType, - ) -> Self { + fn new(reader: SourceReader, span: Span, signals: Signals, type_: ByteStreamType) -> Self { Self { reader: BufReader::new(reader), pos: 0, error: false, span, - ctrlc, + signals, type_, } } @@ -922,7 +913,7 @@ impl Iterator for Chunks { type Item = Result; fn next(&mut self) -> Option { - if self.error || nu_utils::ctrl_c::was_pressed(&self.ctrlc) { + if self.error || self.signals.interrupted() { None } else { match self.type_ { @@ -988,14 +979,14 @@ fn write_to_out_dest( stream: &OutDest, stdout: bool, span: Span, - ctrlc: Option<&AtomicBool>, + signals: &Signals, ) -> Result<(), ShellError> { match stream { OutDest::Pipe | OutDest::Capture => return Ok(()), - OutDest::Null => copy_with_interrupt(read, io::sink(), span, ctrlc), - OutDest::Inherit if stdout => copy_with_interrupt(read, io::stdout(), span, ctrlc), - OutDest::Inherit => copy_with_interrupt(read, io::stderr(), span, ctrlc), - OutDest::File(file) => copy_with_interrupt(read, file.as_ref(), span, ctrlc), + OutDest::Null => copy_with_signals(read, io::sink(), span, signals), + OutDest::Inherit if stdout => copy_with_signals(read, io::stdout(), span, signals), + OutDest::Inherit => copy_with_signals(read, io::stderr(), span, signals), + OutDest::File(file) => copy_with_signals(read, file.as_ref(), span, signals), }?; Ok(()) } @@ -1012,28 +1003,13 @@ pub(crate) fn convert_file>(file: impl Into) - const DEFAULT_BUF_SIZE: usize = 8192; -pub fn copy_with_interrupt( +pub fn copy_with_signals( mut reader: impl Read, mut writer: impl Write, span: Span, - interrupt: Option<&AtomicBool>, + signals: &Signals, ) -> Result { - if let Some(interrupt) = interrupt { - // #[cfg(any(target_os = "linux", target_os = "android"))] - // { - // return crate::sys::kernel_copy::copy_spec(reader, writer); - // } - match generic_copy(&mut reader, &mut writer, span, interrupt) { - Ok(len) => { - writer.flush().err_span(span)?; - Ok(len) - } - Err(err) => { - let _ = writer.flush(); - Err(err) - } - } - } else { + if signals.is_empty() { match io::copy(&mut reader, &mut writer) { Ok(n) => { writer.flush().err_span(span)?; @@ -1044,6 +1020,21 @@ pub fn copy_with_interrupt( Err(err.into_spanned(span).into()) } } + } else { + // #[cfg(any(target_os = "linux", target_os = "android"))] + // { + // return crate::sys::kernel_copy::copy_spec(reader, writer); + // } + match generic_copy(&mut reader, &mut writer, span, signals) { + Ok(len) => { + writer.flush().err_span(span)?; + Ok(len) + } + Err(err) => { + let _ = writer.flush(); + Err(err) + } + } } } @@ -1052,14 +1043,12 @@ fn generic_copy( mut reader: impl Read, mut writer: impl Write, span: Span, - interrupt: &AtomicBool, + signals: &Signals, ) -> Result { let buf = &mut [0; DEFAULT_BUF_SIZE]; let mut len = 0; loop { - if interrupt.load(Ordering::Relaxed) { - return Err(ShellError::InterruptedByUser { span: Some(span) }); - } + signals.check(span)?; let n = match reader.read(buf) { Ok(0) => break, Ok(n) => n, @@ -1134,7 +1123,7 @@ mod tests { Chunks::new( SourceReader::Read(Box::new(reader)), Span::test_data(), - None, + Signals::empty(), type_, ) } diff --git a/crates/nu-protocol/src/pipeline/list_stream.rs b/crates/nu-protocol/src/pipeline/list_stream.rs index 117c264219..55ae4bfee0 100644 --- a/crates/nu-protocol/src/pipeline/list_stream.rs +++ b/crates/nu-protocol/src/pipeline/list_stream.rs @@ -1,8 +1,9 @@ -use crate::{Config, PipelineData, ShellError, Span, Value}; -use std::{ - fmt::Debug, - sync::{atomic::AtomicBool, Arc}, -}; +//! Module managing the streaming of individual [`Value`]s as a [`ListStream`] between pipeline +//! elements +//! +//! For more general infos regarding our pipelining model refer to [`PipelineData`] +use crate::{Config, PipelineData, ShellError, Signals, Span, Value}; +use std::fmt::Debug; pub type ValueIterator = Box + Send + 'static>; @@ -21,10 +22,10 @@ impl ListStream { pub fn new( iter: impl Iterator + Send + 'static, span: Span, - interrupt: Option>, + signals: Signals, ) -> Self { Self { - stream: Box::new(Interrupt::new(iter, interrupt)), + stream: Box::new(InterruptIter::new(iter, signals)), span, } } @@ -34,11 +35,22 @@ impl ListStream { self.span } + /// Changes the [`Span`] associated with this [`ListStream`]. + pub fn with_span(mut self, span: Span) -> Self { + self.span = span; + self + } + /// Convert a [`ListStream`] into its inner [`Value`] `Iterator`. pub fn into_inner(self) -> ValueIterator { self.stream } + /// Take a single value from the inner `Iterator`, modifying the stream. + pub fn next_value(&mut self) -> Option { + self.stream.next() + } + /// Converts each value in a [`ListStream`] into a string and then joins the strings together /// using the given separator. pub fn into_string(self, separator: &str, config: &Config) -> String { @@ -69,10 +81,10 @@ impl ListStream { /// E.g., `take`, `filter`, `step_by`, and more. /// /// ``` - /// use nu_protocol::{ListStream, Span, Value}; + /// use nu_protocol::{ListStream, Signals, Span, Value}; /// /// let span = Span::unknown(); - /// let stream = ListStream::new(std::iter::repeat(Value::int(0, span)), span, None); + /// let stream = ListStream::new(std::iter::repeat(Value::int(0, span)), span, Signals::empty()); /// let new_stream = stream.modify(|iter| iter.take(100)); /// ``` pub fn modify(self, f: impl FnOnce(ValueIterator) -> I) -> Self @@ -128,22 +140,22 @@ impl Iterator for IntoIter { } } -struct Interrupt { +struct InterruptIter { iter: I, - interrupt: Option>, + signals: Signals, } -impl Interrupt { - fn new(iter: I, interrupt: Option>) -> Self { - Self { iter, interrupt } +impl InterruptIter { + fn new(iter: I, signals: Signals) -> Self { + Self { iter, signals } } } -impl Iterator for Interrupt { +impl Iterator for InterruptIter { type Item = ::Item; fn next(&mut self) -> Option { - if nu_utils::ctrl_c::was_pressed(&self.interrupt) { + if self.signals.interrupted() { None } else { self.iter.next() diff --git a/crates/nu-protocol/src/pipeline/metadata.rs b/crates/nu-protocol/src/pipeline/metadata.rs index 08aa0fe964..1ed9222f46 100644 --- a/crates/nu-protocol/src/pipeline/metadata.rs +++ b/crates/nu-protocol/src/pipeline/metadata.rs @@ -1,18 +1,37 @@ use std::path::PathBuf; /// Metadata that is valid for the whole [`PipelineData`](crate::PipelineData) -#[derive(Debug, Clone)] +#[derive(Debug, Default, Clone)] pub struct PipelineMetadata { pub data_source: DataSource, + pub content_type: Option, +} + +impl PipelineMetadata { + pub fn with_data_source(self, data_source: DataSource) -> Self { + Self { + data_source, + ..self + } + } + + pub fn with_content_type(self, content_type: Option) -> Self { + Self { + content_type, + ..self + } + } } /// Describes where the particular [`PipelineMetadata`] originates. /// /// This can either be a particular family of commands (useful so downstream commands can adjust /// the presentation e.g. `Ls`) or the opened file to protect against overwrite-attempts properly. -#[derive(Debug, Clone)] +#[derive(Debug, Default, Clone)] pub enum DataSource { Ls, HtmlThemes, FilePath(PathBuf), + #[default] + None, } diff --git a/crates/nu-protocol/src/pipeline/mod.rs b/crates/nu-protocol/src/pipeline/mod.rs index a018a084ed..525806425d 100644 --- a/crates/nu-protocol/src/pipeline/mod.rs +++ b/crates/nu-protocol/src/pipeline/mod.rs @@ -3,9 +3,11 @@ pub mod list_stream; mod metadata; mod out_dest; mod pipeline_data; +mod signals; pub use byte_stream::*; pub use list_stream::*; pub use metadata::*; pub use out_dest::*; pub use pipeline_data::*; +pub use signals::*; diff --git a/crates/nu-protocol/src/pipeline/pipeline_data.rs b/crates/nu-protocol/src/pipeline/pipeline_data.rs index 03fbc64d21..0d1c0b3092 100644 --- a/crates/nu-protocol/src/pipeline/pipeline_data.rs +++ b/crates/nu-protocol/src/pipeline/pipeline_data.rs @@ -3,13 +3,10 @@ use crate::{ engine::{EngineState, Stack}, process::{ChildPipe, ChildProcess, ExitStatus}, ByteStream, ByteStreamType, Config, ErrSpan, ListStream, OutDest, PipelineMetadata, Range, - ShellError, Span, Type, Value, + ShellError, Signals, Span, Type, Value, }; use nu_utils::{stderr_write_all_and_flush, stdout_write_all_and_flush}; -use std::{ - io::{Cursor, Read, Write}, - sync::{atomic::AtomicBool, Arc}, -}; +use std::io::{Cursor, Read, Write}; const LINE_ENDING_PATTERN: &[char] = &['\r', '\n']; @@ -99,6 +96,24 @@ impl PipelineData { } } + /// Change the span of the [`PipelineData`]. + /// + /// Returns `Value(Nothing)` with the given span if it was [`PipelineData::Empty`]. + pub fn with_span(self, span: Span) -> Self { + match self { + PipelineData::Empty => PipelineData::Value(Value::nothing(span), None), + PipelineData::Value(value, metadata) => { + PipelineData::Value(value.with_span(span), metadata) + } + PipelineData::ListStream(stream, metadata) => { + PipelineData::ListStream(stream.with_span(span), metadata) + } + PipelineData::ByteStream(stream, metadata) => { + PipelineData::ByteStream(stream.with_span(span), metadata) + } + } + } + /// Get a type that is representative of the `PipelineData`. /// /// The type returned here makes no effort to collect a stream, so it may be a different type @@ -132,7 +147,8 @@ impl PipelineData { /// without consuming input and without writing anything. /// /// For the other [`OutDest`]s, the given `PipelineData` will be completely consumed - /// and `PipelineData::Empty` will be returned. + /// and `PipelineData::Empty` will be returned, unless the data is from an external stream, + /// in which case an external stream containing only that exit code will be returned. pub fn write_to_out_dests( self, engine_state: &EngineState, @@ -140,7 +156,11 @@ impl PipelineData { ) -> Result { match (self, stack.stdout()) { (PipelineData::ByteStream(stream, ..), stdout) => { - stream.write_to_out_dests(stdout, stack.stderr())?; + if let Some(exit_status) = stream.write_to_out_dests(stdout, stack.stderr())? { + return Ok(PipelineData::new_external_stream_with_only_exit_code( + exit_status.code(), + )); + } } (data, OutDest::Pipe | OutDest::Capture) => return Ok(data), (PipelineData::Empty, ..) => {} @@ -196,19 +216,23 @@ impl PipelineData { let val_span = value.span(); match value { Value::List { vals, .. } => PipelineIteratorInner::ListStream( - ListStream::new(vals.into_iter(), val_span, None).into_iter(), + ListStream::new(vals.into_iter(), val_span, Signals::empty()).into_iter(), ), Value::Binary { val, .. } => PipelineIteratorInner::ListStream( ListStream::new( val.into_iter().map(move |x| Value::int(x as i64, val_span)), val_span, - None, + Signals::empty(), ) .into_iter(), ), Value::Range { val, .. } => PipelineIteratorInner::ListStream( - ListStream::new(val.into_range_iter(val_span, None), val_span, None) - .into_iter(), + ListStream::new( + val.into_range_iter(val_span, Signals::empty()), + val_span, + Signals::empty(), + ) + .into_iter(), ), // Propagate errors by explicitly matching them before the final case. Value::Error { error, .. } => return Err(*error), @@ -301,11 +325,7 @@ impl PipelineData { } /// 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 + pub fn map(self, mut f: F, signals: &Signals) -> Result where Self: Sized, F: FnMut(Value) -> Value + 'static + Send, @@ -314,13 +334,14 @@ impl PipelineData { PipelineData::Value(value, metadata) => { let span = value.span(); let pipeline = match value { - Value::List { vals, .. } => { - vals.into_iter().map(f).into_pipeline_data(span, ctrlc) - } - Value::Range { val, .. } => val - .into_range_iter(span, ctrlc.clone()) + Value::List { vals, .. } => vals + .into_iter() .map(f) - .into_pipeline_data(span, ctrlc), + .into_pipeline_data(span, signals.clone()), + Value::Range { val, .. } => val + .into_range_iter(span, Signals::empty()) + .map(f) + .into_pipeline_data(span, signals.clone()), value => match f(value) { Value::Error { error, .. } => return Err(*error), v => v.into_pipeline_data(), @@ -339,11 +360,7 @@ impl PipelineData { } /// Simplified flatmapper. For full iterator support use `.into_iter()` instead - pub fn flat_map( - self, - mut f: F, - ctrlc: Option>, - ) -> Result + pub fn flat_map(self, mut f: F, signals: &Signals) -> Result where Self: Sized, U: IntoIterator + 'static, @@ -355,14 +372,17 @@ impl PipelineData { PipelineData::Value(value, metadata) => { let span = value.span(); let pipeline = match value { - Value::List { vals, .. } => { - vals.into_iter().flat_map(f).into_pipeline_data(span, ctrlc) - } - Value::Range { val, .. } => val - .into_range_iter(span, ctrlc.clone()) + Value::List { vals, .. } => vals + .into_iter() .flat_map(f) - .into_pipeline_data(span, ctrlc), - value => f(value).into_iter().into_pipeline_data(span, ctrlc), + .into_pipeline_data(span, signals.clone()), + Value::Range { val, .. } => val + .into_range_iter(span, Signals::empty()) + .flat_map(f) + .into_pipeline_data(span, signals.clone()), + value => f(value) + .into_iter() + .into_pipeline_data(span, signals.clone()), }; Ok(pipeline.set_metadata(metadata)) } @@ -380,18 +400,16 @@ impl PipelineData { } Err(err) => f(Value::binary(err.into_bytes(), span)), }; - Ok(iter - .into_iter() - .into_pipeline_data_with_metadata(span, ctrlc, metadata)) + Ok(iter.into_iter().into_pipeline_data_with_metadata( + span, + signals.clone(), + metadata, + )) } } } - pub fn filter( - self, - mut f: F, - ctrlc: Option>, - ) -> Result + pub fn filter(self, mut f: F, signals: &Signals) -> Result where Self: Sized, F: FnMut(&Value) -> bool + 'static + Send, @@ -401,13 +419,14 @@ impl PipelineData { PipelineData::Value(value, metadata) => { let span = value.span(); let pipeline = match value { - Value::List { vals, .. } => { - vals.into_iter().filter(f).into_pipeline_data(span, ctrlc) - } - Value::Range { val, .. } => val - .into_range_iter(span, ctrlc.clone()) + Value::List { vals, .. } => vals + .into_iter() .filter(f) - .into_pipeline_data(span, ctrlc), + .into_pipeline_data(span, signals.clone()), + Value::Range { val, .. } => val + .into_range_iter(span, Signals::empty()) + .filter(f) + .into_pipeline_data(span, signals.clone()), value => { if f(&value) { value.into_pipeline_data() @@ -453,7 +472,10 @@ impl PipelineData { /// Currently this will consume an external command to completion. pub fn check_external_failed(self) -> Result<(Self, bool), ShellError> { if let PipelineData::ByteStream(stream, metadata) = self { + // Preserve stream attributes let span = stream.span(); + let type_ = stream.type_(); + let known_size = stream.known_size(); match stream.into_child() { Ok(mut child) => { // Only check children without stdout. This means that nothing @@ -485,10 +507,12 @@ impl PipelineData { child.stderr = Some(ChildPipe::Tee(Box::new(Cursor::new(stderr)))); } child.set_exit_code(code); - let stream = ByteStream::child(child, span); + let stream = ByteStream::child(child, span).with_type(type_); Ok((PipelineData::ByteStream(stream, metadata), code != 0)) } else { - let stream = ByteStream::child(child, span); + let stream = ByteStream::child(child, span) + .with_type(type_) + .with_known_size(known_size); Ok((PipelineData::ByteStream(stream, metadata), false)) } } @@ -533,7 +557,8 @@ impl PipelineData { } } } - let range_values: Vec = val.into_range_iter(span, None).collect(); + let range_values: Vec = + val.into_range_iter(span, Signals::empty()).collect(); Ok(PipelineData::Value(Value::list(range_values, span), None)) } x => Ok(PipelineData::Value(x, metadata)), @@ -568,7 +593,7 @@ impl PipelineData { self.write_all_and_flush(engine_state, no_newline, to_stderr) } else { let call = Call::new(Span::new(0, 0)); - let table = command.run(engine_state, stack, &call, self)?; + let table = command.run(engine_state, stack, &(&call).into(), self)?; table.write_all_and_flush(engine_state, no_newline, to_stderr) } } else { @@ -610,6 +635,34 @@ impl PipelineData { Ok(None) } } + + pub fn unsupported_input_error( + self, + expected_type: impl Into, + span: Span, + ) -> ShellError { + match self { + PipelineData::Empty => ShellError::PipelineEmpty { dst_span: span }, + PipelineData::Value(value, ..) => ShellError::OnlySupportsThisInputType { + exp_input_type: expected_type.into(), + wrong_type: value.get_type().get_non_specified_string(), + dst_span: span, + src_span: value.span(), + }, + PipelineData::ListStream(stream, ..) => ShellError::OnlySupportsThisInputType { + exp_input_type: expected_type.into(), + wrong_type: "list (stream)".into(), + dst_span: span, + src_span: stream.span(), + }, + PipelineData::ByteStream(stream, ..) => ShellError::OnlySupportsThisInputType { + exp_input_type: expected_type.into(), + wrong_type: stream.type_().describe().into(), + dst_span: span, + src_span: stream.span(), + }, + } + } } enum PipelineIteratorInner { @@ -633,10 +686,15 @@ impl IntoIterator for PipelineData { let span = value.span(); match value { Value::List { vals, .. } => PipelineIteratorInner::ListStream( - ListStream::new(vals.into_iter(), span, None).into_iter(), + ListStream::new(vals.into_iter(), span, Signals::empty()).into_iter(), ), Value::Range { val, .. } => PipelineIteratorInner::ListStream( - ListStream::new(val.into_range_iter(span, None), span, None).into_iter(), + ListStream::new( + val.into_range_iter(span, Signals::empty()), + span, + Signals::empty(), + ) + .into_iter(), ), x => PipelineIteratorInner::Value(x), } @@ -698,11 +756,11 @@ where } pub trait IntoInterruptiblePipelineData { - fn into_pipeline_data(self, span: Span, ctrlc: Option>) -> PipelineData; + fn into_pipeline_data(self, span: Span, signals: Signals) -> PipelineData; fn into_pipeline_data_with_metadata( self, span: Span, - ctrlc: Option>, + signals: Signals, metadata: impl Into>, ) -> PipelineData; } @@ -713,18 +771,18 @@ where I::IntoIter: Send + 'static, ::Item: Into, { - fn into_pipeline_data(self, span: Span, ctrlc: Option>) -> PipelineData { - ListStream::new(self.into_iter().map(Into::into), span, ctrlc).into() + fn into_pipeline_data(self, span: Span, signals: Signals) -> PipelineData { + ListStream::new(self.into_iter().map(Into::into), span, signals).into() } fn into_pipeline_data_with_metadata( self, span: Span, - ctrlc: Option>, + signals: Signals, metadata: impl Into>, ) -> PipelineData { PipelineData::ListStream( - ListStream::new(self.into_iter().map(Into::into), span, ctrlc), + ListStream::new(self.into_iter().map(Into::into), span, signals), metadata.into(), ) } diff --git a/crates/nu-protocol/src/pipeline/signals.rs b/crates/nu-protocol/src/pipeline/signals.rs new file mode 100644 index 0000000000..06ce583c82 --- /dev/null +++ b/crates/nu-protocol/src/pipeline/signals.rs @@ -0,0 +1,76 @@ +use crate::{ShellError, Span}; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; + +/// Used to check for signals to suspend or terminate the execution of Nushell code. +/// +/// For now, this struct only supports interruption (ctrl+c or SIGINT). +#[derive(Debug, Clone)] +pub struct Signals { + signals: Option>, +} + +impl Signals { + /// A [`Signals`] that is not hooked up to any event/signals source. + /// + /// So, this [`Signals`] will never be interrupted. + pub const EMPTY: Self = Signals { signals: None }; + + /// Create a new [`Signals`] with `ctrlc` as the interrupt source. + /// + /// Once `ctrlc` is set to `true`, [`check`](Self::check) will error + /// and [`interrupted`](Self::interrupted) will return `true`. + pub fn new(ctrlc: Arc) -> Self { + Self { + signals: Some(ctrlc), + } + } + + /// Create a [`Signals`] that is not hooked up to any event/signals source. + /// + /// So, the returned [`Signals`] will never be interrupted. + /// + /// This should only be used in test code, or if the stream/iterator being created + /// already has an underlying [`Signals`]. + pub const fn empty() -> Self { + Self::EMPTY + } + + /// Returns an `Err` if an interrupt has been triggered. + /// + /// Otherwise, returns `Ok`. + #[inline] + pub fn check(&self, span: Span) -> Result<(), ShellError> { + #[inline] + #[cold] + fn interrupt_error(span: Span) -> Result<(), ShellError> { + Err(ShellError::Interrupted { span }) + } + + if self.interrupted() { + interrupt_error(span) + } else { + Ok(()) + } + } + + /// Returns whether an interrupt has been triggered. + #[inline] + pub fn interrupted(&self) -> bool { + self.signals + .as_deref() + .is_some_and(|b| b.load(Ordering::Relaxed)) + } + + pub(crate) fn is_empty(&self) -> bool { + self.signals.is_none() + } + + pub(crate) fn reset(&self) { + if let Some(signals) = &self.signals { + signals.store(false, Ordering::Relaxed); + } + } +} diff --git a/crates/nu-protocol/src/plugin/metadata.rs b/crates/nu-protocol/src/plugin/metadata.rs new file mode 100644 index 0000000000..d2fab7a89b --- /dev/null +++ b/crates/nu-protocol/src/plugin/metadata.rs @@ -0,0 +1,38 @@ +use serde::{Deserialize, Serialize}; + +/// Metadata about the installed plugin. This is cached in the registry file along with the +/// signatures. None of the metadata fields are required, and more may be added in the future. +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +#[non_exhaustive] +pub struct PluginMetadata { + /// The version of the plugin itself, as self-reported. + pub version: Option, +} + +impl PluginMetadata { + /// Create empty metadata. + pub const fn new() -> PluginMetadata { + PluginMetadata { version: None } + } + + /// Set the version of the plugin on the metadata. A suggested way to construct this is: + /// + /// ```no_run + /// # use nu_protocol::PluginMetadata; + /// # fn example() -> PluginMetadata { + /// PluginMetadata::new().with_version(env!("CARGO_PKG_VERSION")) + /// # } + /// ``` + /// + /// which will use the version of your plugin's crate from its `Cargo.toml` file. + pub fn with_version(mut self, version: impl Into) -> Self { + self.version = Some(version.into()); + self + } +} + +impl Default for PluginMetadata { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/nu-protocol/src/plugin/mod.rs b/crates/nu-protocol/src/plugin/mod.rs index b266f8ebac..1678c9c40a 100644 --- a/crates/nu-protocol/src/plugin/mod.rs +++ b/crates/nu-protocol/src/plugin/mod.rs @@ -1,9 +1,11 @@ mod identity; +mod metadata; mod registered; mod registry_file; mod signature; pub use identity::*; +pub use metadata::*; pub use registered::*; pub use registry_file::*; pub use signature::*; diff --git a/crates/nu-protocol/src/plugin/registered.rs b/crates/nu-protocol/src/plugin/registered.rs index 46d65b41d1..56d7f4079a 100644 --- a/crates/nu-protocol/src/plugin/registered.rs +++ b/crates/nu-protocol/src/plugin/registered.rs @@ -1,6 +1,6 @@ use std::{any::Any, sync::Arc}; -use crate::{PluginGcConfig, PluginIdentity, ShellError}; +use crate::{PluginGcConfig, PluginIdentity, PluginMetadata, ShellError}; /// Trait for plugins registered in the [`EngineState`](crate::engine::EngineState). pub trait RegisteredPlugin: Send + Sync { @@ -13,6 +13,12 @@ pub trait RegisteredPlugin: Send + Sync { /// Process ID of the plugin executable, if running. fn pid(&self) -> Option; + /// Get metadata for the plugin, if set. + fn metadata(&self) -> Option; + + /// Set metadata for the plugin. + fn set_metadata(&self, metadata: Option); + /// Set garbage collection config for the plugin. fn set_gc_config(&self, gc_config: &PluginGcConfig); @@ -20,7 +26,7 @@ pub trait RegisteredPlugin: Send + Sync { fn stop(&self) -> Result<(), ShellError>; /// Stop the plugin and reset any state so that we don't make any assumptions about the plugin - /// next time it launches. This is used on `register`. + /// next time it launches. This is used on `plugin add`. fn reset(&self) -> Result<(), ShellError>; /// Cast the pointer to an [`Any`] so that its concrete type can be retrieved. diff --git a/crates/nu-protocol/src/plugin/registry_file/mod.rs b/crates/nu-protocol/src/plugin/registry_file/mod.rs index dcaba26b90..17adec1f4e 100644 --- a/crates/nu-protocol/src/plugin/registry_file/mod.rs +++ b/crates/nu-protocol/src/plugin/registry_file/mod.rs @@ -5,7 +5,7 @@ use std::{ use serde::{Deserialize, Serialize}; -use crate::{PluginIdentity, PluginSignature, ShellError, Span}; +use crate::{PluginIdentity, PluginMetadata, PluginSignature, ShellError, Span}; // This has a big impact on performance const BUFFER_SIZE: usize = 65536; @@ -121,9 +121,10 @@ pub struct PluginRegistryItem { } impl PluginRegistryItem { - /// Create a [`PluginRegistryItem`] from an identity and signatures. + /// Create a [`PluginRegistryItem`] from an identity, metadata, and signatures. pub fn new( identity: &PluginIdentity, + metadata: PluginMetadata, mut commands: Vec, ) -> PluginRegistryItem { // Sort the commands for consistency @@ -133,7 +134,7 @@ impl PluginRegistryItem { name: identity.name().to_owned(), filename: identity.filename().to_owned(), shell: identity.shell().map(|p| p.to_owned()), - data: PluginRegistryItemData::Valid { commands }, + data: PluginRegistryItemData::Valid { metadata, commands }, } } } @@ -144,6 +145,9 @@ impl PluginRegistryItem { #[serde(untagged)] pub enum PluginRegistryItemData { Valid { + /// Metadata for the plugin, including its version. + #[serde(default)] + metadata: PluginMetadata, /// Signatures and examples for each command provided by the plugin. commands: Vec, }, diff --git a/crates/nu-protocol/src/plugin/registry_file/tests.rs b/crates/nu-protocol/src/plugin/registry_file/tests.rs index 0d34ecca1c..e264e4463d 100644 --- a/crates/nu-protocol/src/plugin/registry_file/tests.rs +++ b/crates/nu-protocol/src/plugin/registry_file/tests.rs @@ -1,6 +1,7 @@ use super::{PluginRegistryFile, PluginRegistryItem, PluginRegistryItemData}; use crate::{ - Category, PluginExample, PluginSignature, ShellError, Signature, SyntaxShape, Type, Value, + Category, PluginExample, PluginMetadata, PluginSignature, ShellError, Signature, SyntaxShape, + Type, Value, }; use pretty_assertions::assert_eq; use std::io::Cursor; @@ -11,6 +12,9 @@ fn foo_plugin() -> PluginRegistryItem { filename: "/path/to/nu_plugin_foo".into(), shell: None, data: PluginRegistryItemData::Valid { + metadata: PluginMetadata { + version: Some("0.1.0".into()), + }, commands: vec![PluginSignature { sig: Signature::new("foo") .input_output_type(Type::Int, Type::List(Box::new(Type::Int))) @@ -36,6 +40,9 @@ fn bar_plugin() -> PluginRegistryItem { filename: "/path/to/nu_plugin_bar".into(), shell: None, data: PluginRegistryItemData::Valid { + metadata: PluginMetadata { + version: Some("0.2.0".into()), + }, commands: vec![PluginSignature { sig: Signature::new("bar") .usage("overwrites files with random data") diff --git a/crates/nu-protocol/src/process/mod.rs b/crates/nu-protocol/src/process/mod.rs index 2fcf65f56e..6e10fdd620 100644 --- a/crates/nu-protocol/src/process/mod.rs +++ b/crates/nu-protocol/src/process/mod.rs @@ -1,3 +1,4 @@ +//! Handling of external subprocesses mod child; mod exit_status; diff --git a/crates/nu-protocol/src/signature.rs b/crates/nu-protocol/src/signature.rs index 70e94b35f1..f8b83063d7 100644 --- a/crates/nu-protocol/src/signature.rs +++ b/crates/nu-protocol/src/signature.rs @@ -1,11 +1,11 @@ use crate::{ - ast::Call, - engine::{Command, CommandType, EngineState, Stack}, + engine::{Call, Command, CommandType, EngineState, Stack}, BlockId, PipelineData, ShellError, SyntaxShape, Type, Value, VarId, }; use serde::{Deserialize, Serialize}; use std::fmt::Write; +/// The signature definition of a named flag that either accepts a value or acts as a toggle flag #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Flag { pub long: String, @@ -19,6 +19,7 @@ pub struct Flag { pub default_value: Option, } +/// The signature definition for a positional argument #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct PositionalArg { pub name: String, @@ -30,6 +31,7 @@ pub struct PositionalArg { pub default_value: Option, } +/// Command categories #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum Category { Bits, @@ -103,6 +105,7 @@ impl std::fmt::Display for Category { } } +/// Signature information of a [`Command`] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Signature { pub name: String, @@ -486,15 +489,14 @@ impl Signature { (name, s) } - pub fn get_positional(&self, position: usize) -> Option { + pub fn get_positional(&self, position: usize) -> Option<&PositionalArg> { if position < self.required_positional.len() { - self.required_positional.get(position).cloned() + self.required_positional.get(position) } 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() + self.rest_positional.as_ref() } } diff --git a/crates/nu-protocol/src/span.rs b/crates/nu-protocol/src/span.rs index 0d280eaa9d..cb6d96421c 100644 --- a/crates/nu-protocol/src/span.rs +++ b/crates/nu-protocol/src/span.rs @@ -1,3 +1,4 @@ +//! [`Span`] to point to sections of source code and the [`Spanned`] wrapper type use crate::SpanId; use miette::SourceSpan; use serde::{Deserialize, Serialize}; @@ -53,6 +54,22 @@ impl Spanned { } } +impl Spanned> { + /// Move the `Result` to the outside, resulting in a spanned `Ok` or unspanned `Err`. + pub fn transpose(self) -> Result, E> { + match self { + Spanned { + item: Ok(item), + span, + } => Ok(Spanned { item, span }), + Spanned { + item: Err(err), + span: _, + } => Err(err), + } + } +} + /// Helper trait to create [`Spanned`] more ergonomically. pub trait IntoSpanned: Sized { /// Wrap items together with a span into [`Spanned`]. diff --git a/crates/nu-protocol/src/ty.rs b/crates/nu-protocol/src/ty.rs index d5ea8c1554..7c004d81de 100644 --- a/crates/nu-protocol/src/ty.rs +++ b/crates/nu-protocol/src/ty.rs @@ -36,6 +36,10 @@ pub enum Type { } impl Type { + pub fn list(inner: Type) -> Self { + Self::List(Box::new(inner)) + } + pub fn record() -> Self { Self::Record([].into()) } diff --git a/crates/nu-protocol/src/value/from_value.rs b/crates/nu-protocol/src/value/from_value.rs index 97fb95d482..1c3bc7dfd4 100644 --- a/crates/nu-protocol/src/value/from_value.rs +++ b/crates/nu-protocol/src/value/from_value.rs @@ -1,37 +1,188 @@ use crate::{ ast::{CellPath, PathMember}, engine::Closure, - NuGlob, Range, Record, ShellError, Spanned, Value, + NuGlob, Range, Record, ShellError, Spanned, Type, Value, }; use chrono::{DateTime, FixedOffset}; -use std::path::PathBuf; +use std::{ + any, + cmp::Ordering, + collections::{HashMap, VecDeque}, + path::PathBuf, + str::FromStr, +}; +/// A trait for loading a value from a [`Value`]. +/// +/// # Derivable +/// This trait can be used with `#[derive]`. +/// When derived on structs with named fields, it expects a [`Value::Record`] where each field of +/// the struct maps to a corresponding field in the record. +/// For structs with unnamed fields, it expects a [`Value::List`], and the fields are populated in +/// the order they appear in the list. +/// Unit structs expect a [`Value::Nothing`], as they contain no data. +/// Attempting to convert from a non-matching `Value` type will result in an error. +/// +/// Only enums with no fields may derive this trait. +/// The expected value representation will be the name of the variant as a [`Value::String`]. +/// By default, variant names will be expected in ["snake_case"](convert_case::Case::Snake). +/// You can customize the case conversion using `#[nu_value(rename_all = "kebab-case")]` on the enum. +/// All deterministic and useful case conversions provided by [`convert_case::Case`] are supported +/// by specifying the case name followed by "case". +/// Also all values for +/// [`#[serde(rename_all = "...")]`](https://serde.rs/container-attrs.html#rename_all) are valid +/// here. +/// +/// ``` +/// # use nu_protocol::{FromValue, Value, ShellError}; +/// #[derive(FromValue, Debug, PartialEq)] +/// #[nu_value(rename_all = "COBOL-CASE")] +/// enum Bird { +/// MountainEagle, +/// ForestOwl, +/// RiverDuck, +/// } +/// +/// assert_eq!( +/// Bird::from_value(Value::test_string("RIVER-DUCK")).unwrap(), +/// Bird::RiverDuck +/// ); +/// ``` pub trait FromValue: Sized { + // TODO: instead of ShellError, maybe we could have a FromValueError that implements Into + /// Loads a value from a [`Value`]. + /// + /// This method retrieves a value similarly to how strings are parsed using [`FromStr`]. + /// The operation might fail if the `Value` contains unexpected types or structures. fn from_value(v: Value) -> Result; -} -impl FromValue for Value { - fn from_value(v: Value) -> Result { - Ok(v) + /// Expected `Value` type. + /// + /// This is used to print out errors of what type of value is expected for conversion. + /// Even if not used in [`from_value`](FromValue::from_value) this should still be implemented + /// so that other implementations like `Option` or `Vec` can make use of it. + /// It is advised to call this method in `from_value` to ensure that expected type in the error + /// is consistent. + /// + /// Unlike the default implementation, derived implementations explicitly reveal the concrete + /// type, such as [`Type::Record`] or [`Type::List`], instead of an opaque type. + fn expected_type() -> Type { + Type::Custom( + any::type_name::() + .split(':') + .last() + .expect("str::split returns an iterator with at least one element") + .to_string() + .into_boxed_str(), + ) } } -impl FromValue for Spanned { +// Primitive Types + +impl FromValue for [T; N] +where + T: FromValue, +{ fn from_value(v: Value) -> Result { let span = v.span(); - match v { - Value::Int { val, .. } => Ok(Spanned { item: val, span }), - Value::Filesize { val, .. } => Ok(Spanned { item: val, span }), - Value::Duration { val, .. } => Ok(Spanned { item: val, span }), + let v_ty = v.get_type(); + let vec = Vec::::from_value(v)?; + vec.try_into() + .map_err(|err_vec: Vec| ShellError::CantConvert { + to_type: Self::expected_type().to_string(), + from_type: v_ty.to_string(), + span, + help: Some(match err_vec.len().cmp(&N) { + Ordering::Less => format!( + "input list too short ({}), expected length of {N}, add missing values", + err_vec.len() + ), + Ordering::Equal => { + unreachable!("conversion would have worked if the length would be the same") + } + Ordering::Greater => format!( + "input list too long ({}), expected length of {N}, remove trailing values", + err_vec.len() + ), + }), + }) + } + fn expected_type() -> Type { + Type::Custom(format!("list<{};{N}>", T::expected_type()).into_boxed_str()) + } +} + +impl FromValue for bool { + fn from_value(v: Value) -> Result { + match v { + Value::Bool { val, .. } => Ok(val), v => Err(ShellError::CantConvert { - to_type: "int".into(), + to_type: Self::expected_type().to_string(), from_type: v.get_type().to_string(), span: v.span(), help: None, }), } } + + fn expected_type() -> Type { + Type::Bool + } +} + +impl FromValue for char { + fn from_value(v: Value) -> Result { + let span = v.span(); + let v_ty = v.get_type(); + match v { + Value::String { ref val, .. } => match char::from_str(val) { + Ok(c) => Ok(c), + Err(_) => Err(ShellError::CantConvert { + to_type: Self::expected_type().to_string(), + from_type: v_ty.to_string(), + span, + help: Some("make the string only one char long".to_string()), + }), + }, + _ => Err(ShellError::CantConvert { + to_type: Self::expected_type().to_string(), + from_type: v_ty.to_string(), + span, + help: None, + }), + } + } + + fn expected_type() -> Type { + Type::String + } +} + +impl FromValue for f32 { + fn from_value(v: Value) -> Result { + f64::from_value(v).map(|float| float as f32) + } +} + +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 { + to_type: Self::expected_type().to_string(), + from_type: v.get_type().to_string(), + span: v.span(), + help: None, + }), + } + } + + fn expected_type() -> Type { + Type::Float + } } impl FromValue for i64 { @@ -42,128 +193,207 @@ impl FromValue for i64 { Value::Duration { val, .. } => Ok(val), v => Err(ShellError::CantConvert { - to_type: "int".into(), + to_type: Self::expected_type().to_string(), from_type: v.get_type().to_string(), span: v.span(), help: None, }), } } + + fn expected_type() -> Type { + Type::Int + } } -impl FromValue for Spanned { - fn from_value(v: Value) -> Result { - let span = v.span(); - match v { - Value::Int { val, .. } => Ok(Spanned { - item: val as f64, - span, - }), - Value::Float { val, .. } => Ok(Spanned { item: val, span }), +macro_rules! impl_from_value_for_int { + ($type:ty) => { + impl FromValue for $type { + fn from_value(v: Value) -> Result { + let span = v.span(); + let int = i64::from_value(v)?; + const MIN: i64 = <$type>::MIN as i64; + const MAX: i64 = <$type>::MAX as i64; + #[allow(overlapping_range_endpoints)] // calculating MIN-1 is not possible for i64::MIN + #[allow(unreachable_patterns)] // isize might max out i64 number range + <$type>::try_from(int).map_err(|_| match int { + MIN..=MAX => unreachable!( + "int should be within the valid range for {}", + stringify!($type) + ), + i64::MIN..=MIN => ShellError::GenericError { + error: "Integer too small".to_string(), + msg: format!("{int} is smaller than {}", <$type>::MIN), + span: Some(span), + help: None, + inner: vec![], + }, + MAX..=i64::MAX => ShellError::GenericError { + error: "Integer too large".to_string(), + msg: format!("{int} is larger than {}", <$type>::MAX), + span: Some(span), + help: None, + inner: vec![], + }, + }) + } + fn expected_type() -> Type { + i64::expected_type() + } + } + }; +} + +impl_from_value_for_int!(i8); +impl_from_value_for_int!(i16); +impl_from_value_for_int!(i32); +impl_from_value_for_int!(isize); + +macro_rules! impl_from_value_for_uint { + ($type:ty, $max:expr) => { + impl FromValue for $type { + fn from_value(v: Value) -> Result { + let span = v.span(); + const MAX: i64 = $max; + match v { + Value::Int { val, .. } + | Value::Filesize { val, .. } + | Value::Duration { val, .. } => { + match val { + i64::MIN..=-1 => Err(ShellError::NeedsPositiveValue { span }), + 0..=MAX => Ok(val as $type), + #[allow(unreachable_patterns)] // u64 will max out the i64 number range + n => Err(ShellError::GenericError { + error: "Integer too large".to_string(), + msg: format!("{n} is larger than {MAX}"), + span: Some(span), + help: None, + inner: vec![], + }), + } + } + v => Err(ShellError::CantConvert { + to_type: Self::expected_type().to_string(), + from_type: v.get_type().to_string(), + span: v.span(), + help: None, + }), + } + } + + fn expected_type() -> Type { + Type::Custom("non-negative int".to_string().into_boxed_str()) + } + } + }; +} + +// Sadly we cannot implement FromValue for u8 without losing the impl of Vec, +// Rust would find two possible implementations then, Vec and Vec, +// and wouldn't compile. +// The blanket implementation for Vec is probably more useful than +// implementing FromValue for u8. + +impl_from_value_for_uint!(u16, u16::MAX as i64); +impl_from_value_for_uint!(u32, u32::MAX as i64); +impl_from_value_for_uint!(u64, i64::MAX); // u64::Max would be -1 as i64 +#[cfg(target_pointer_width = "64")] +impl_from_value_for_uint!(usize, i64::MAX); +#[cfg(target_pointer_width = "32")] +impl_from_value_for_uint!(usize, usize::MAX as i64); + +impl FromValue for () { + fn from_value(v: Value) -> Result { + match v { + Value::Nothing { .. } => Ok(()), v => Err(ShellError::CantConvert { - to_type: "float".into(), + to_type: Self::expected_type().to_string(), from_type: v.get_type().to_string(), span: v.span(), help: None, }), } } + + fn expected_type() -> Type { + Type::Nothing + } } -impl FromValue for f64 { +macro_rules! tuple_from_value { + ($template:literal, $($t:ident:$n:tt),+) => { + impl<$($t),+> FromValue for ($($t,)+) where $($t: FromValue,)+ { + fn from_value(v: Value) -> Result { + let span = v.span(); + match v { + Value::List { vals, .. } => { + let mut deque = VecDeque::from(vals); + + Ok(($( + { + let v = deque.pop_front().ok_or_else(|| ShellError::CantFindColumn { + col_name: $n.to_string(), + span: None, + src_span: span + })?; + $t::from_value(v)? + }, + )*)) + }, + v => Err(ShellError::CantConvert { + to_type: Self::expected_type().to_string(), + from_type: v.get_type().to_string(), + span: v.span(), + help: None, + }), + } + } + + fn expected_type() -> Type { + Type::Custom( + format!( + $template, + $($t::expected_type()),* + ) + .into_boxed_str(), + ) + } + } + }; +} + +// Tuples in std are implemented for up to 12 elements, so we do it here too. +tuple_from_value!("[{}]", T0:0); +tuple_from_value!("[{}, {}]", T0:0, T1:1); +tuple_from_value!("[{}, {}, {}]", T0:0, T1:1, T2:2); +tuple_from_value!("[{}, {}, {}, {}]", T0:0, T1:1, T2:2, T3:3); +tuple_from_value!("[{}, {}, {}, {}, {}]", T0:0, T1:1, T2:2, T3:3, T4:4); +tuple_from_value!("[{}, {}, {}, {}, {}, {}]", T0:0, T1:1, T2:2, T3:3, T4:4, T5:5); +tuple_from_value!("[{}, {}, {}, {}, {}, {}, {}]", T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6); +tuple_from_value!("[{}, {}, {}, {}, {}, {}, {}, {}]", T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7); +tuple_from_value!("[{}, {}, {}, {}, {}, {}, {}, {}, {}]", T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7, T8:8); +tuple_from_value!("[{}, {}, {}, {}, {}, {}, {}, {}, {}, {}]", T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7, T8:8, T9:9); +tuple_from_value!("[{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}]", T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7, T8:8, T9:9, T10:10); +tuple_from_value!("[{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}]", T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7, T8:8, T9:9, T10:10, T11:11); + +// Other std Types + +impl FromValue for PathBuf { fn from_value(v: Value) -> Result { match v { - Value::Float { val, .. } => Ok(val), - Value::Int { val, .. } => Ok(val as f64), + Value::String { val, .. } => Ok(val.into()), v => Err(ShellError::CantConvert { - to_type: "float".into(), + to_type: Self::expected_type().to_string(), from_type: v.get_type().to_string(), span: v.span(), help: None, }), } } -} -impl FromValue for Spanned { - fn from_value(v: Value) -> Result { - let span = v.span(); - match v { - Value::Int { val, .. } => { - if val.is_negative() { - Err(ShellError::NeedsPositiveValue { span }) - } else { - Ok(Spanned { - item: val as usize, - span, - }) - } - } - Value::Filesize { val, .. } => { - if val.is_negative() { - Err(ShellError::NeedsPositiveValue { span }) - } else { - Ok(Spanned { - item: val as usize, - span, - }) - } - } - Value::Duration { val, .. } => { - if val.is_negative() { - Err(ShellError::NeedsPositiveValue { span }) - } else { - Ok(Spanned { - item: val as usize, - span, - }) - } - } - - v => Err(ShellError::CantConvert { - to_type: "non-negative int".into(), - from_type: v.get_type().to_string(), - span: v.span(), - help: None, - }), - } - } -} - -impl FromValue for usize { - fn from_value(v: Value) -> Result { - let span = v.span(); - match v { - Value::Int { val, .. } => { - if val.is_negative() { - Err(ShellError::NeedsPositiveValue { span }) - } else { - Ok(val as usize) - } - } - Value::Filesize { val, .. } => { - if val.is_negative() { - Err(ShellError::NeedsPositiveValue { span }) - } else { - Ok(val as usize) - } - } - Value::Duration { val, .. } => { - if val.is_negative() { - Err(ShellError::NeedsPositiveValue { span }) - } else { - Ok(val as usize) - } - } - - v => Err(ShellError::CantConvert { - to_type: "non-negative int".into(), - from_type: v.get_type().to_string(), - span: v.span(), - help: None, - }), - } + fn expected_type() -> Type { + Type::String } } @@ -174,176 +404,111 @@ impl FromValue for String { Value::CellPath { val, .. } => Ok(val.to_string()), Value::String { val, .. } => Ok(val), v => Err(ShellError::CantConvert { - to_type: "string".into(), + to_type: Self::expected_type().to_string(), from_type: v.get_type().to_string(), span: v.span(), help: None, }), } } -} -impl FromValue for Spanned { - fn from_value(v: Value) -> Result { - let span = v.span(); - Ok(Spanned { - item: match v { - Value::CellPath { val, .. } => val.to_string(), - Value::String { val, .. } => val, - v => { - return Err(ShellError::CantConvert { - to_type: "string".into(), - from_type: v.get_type().to_string(), - span: v.span(), - help: None, - }) - } - }, - span, - }) + fn expected_type() -> Type { + Type::String } } -impl FromValue for NuGlob { +// This impl is different from Vec as it reads from Value::Binary and +// Value::String instead of Value::List. +// This also denies implementing FromValue for u8. +impl FromValue for Vec { fn from_value(v: Value) -> Result { - // FIXME: we may want to fail a little nicer here match v { - Value::CellPath { val, .. } => Ok(NuGlob::Expand(val.to_string())), - Value::String { val, .. } => Ok(NuGlob::DoNotExpand(val)), - Value::Glob { - val, - no_expand: quoted, - .. - } => { - if quoted { - Ok(NuGlob::DoNotExpand(val)) - } else { - Ok(NuGlob::Expand(val)) - } - } + Value::Binary { val, .. } => Ok(val), + Value::String { val, .. } => Ok(val.into_bytes()), v => Err(ShellError::CantConvert { - to_type: "string".into(), + to_type: Self::expected_type().to_string(), from_type: v.get_type().to_string(), span: v.span(), help: None, }), } } -} -impl FromValue for Spanned { - fn from_value(v: Value) -> Result { - let span = v.span(); - Ok(Spanned { - item: match v { - Value::CellPath { val, .. } => NuGlob::Expand(val.to_string()), - Value::String { val, .. } => NuGlob::DoNotExpand(val), - Value::Glob { - val, - no_expand: quoted, - .. - } => { - if quoted { - NuGlob::DoNotExpand(val) - } else { - NuGlob::Expand(val) - } - } - v => { - return Err(ShellError::CantConvert { - to_type: "string".into(), - from_type: v.get_type().to_string(), - span: v.span(), - help: None, - }) - } - }, - span, - }) + fn expected_type() -> Type { + Type::Binary } } -impl FromValue for Vec { +// Blanket std Implementations + +impl FromValue for Option +where + T: FromValue, +{ fn from_value(v: Value) -> Result { - // FIXME: we may want to fail a little nicer here match v { - Value::List { vals, .. } => vals - .into_iter() - .map(|val| match val { - Value::String { val, .. } => Ok(val), - c => Err(ShellError::CantConvert { - to_type: "string".into(), - from_type: c.get_type().to_string(), - span: c.span(), - help: None, - }), - }) - .collect::, ShellError>>(), - v => Err(ShellError::CantConvert { - to_type: "string".into(), - from_type: v.get_type().to_string(), - span: v.span(), - help: None, - }), + Value::Nothing { .. } => Ok(None), + v => T::from_value(v).map(Option::Some), } } + + fn expected_type() -> Type { + T::expected_type() + } } -impl FromValue for Vec> { +impl FromValue for HashMap +where + V: FromValue, +{ fn from_value(v: Value) -> Result { - // FIXME: we may want to fail a little nicer here - match v { - Value::List { vals, .. } => vals - .into_iter() - .map(|val| { - let val_span = val.span(); - match val { - Value::String { val, .. } => Ok(Spanned { - item: val, - span: val_span, - }), - c => Err(ShellError::CantConvert { - to_type: "string".into(), - from_type: c.get_type().to_string(), - span: c.span(), - help: None, - }), - } - }) - .collect::>, ShellError>>(), - v => Err(ShellError::CantConvert { - to_type: "string".into(), - from_type: v.get_type().to_string(), - span: v.span(), - help: None, - }), - } + let record = v.into_record()?; + let items: Result, ShellError> = record + .into_iter() + .map(|(k, v)| Ok((k, V::from_value(v)?))) + .collect(); + Ok(HashMap::from_iter(items?)) + } + + fn expected_type() -> Type { + Type::Record(vec![].into_boxed_slice()) } } -impl FromValue for Vec { +impl FromValue for Vec +where + T: FromValue, +{ fn from_value(v: Value) -> Result { match v { Value::List { vals, .. } => vals .into_iter() - .map(|val| match val { - Value::Bool { val, .. } => Ok(val), - c => Err(ShellError::CantConvert { - to_type: "bool".into(), - from_type: c.get_type().to_string(), - span: c.span(), - help: None, - }), - }) - .collect::, ShellError>>(), + .map(|v| T::from_value(v)) + .collect::, ShellError>>(), v => Err(ShellError::CantConvert { - to_type: "bool".into(), + to_type: Self::expected_type().to_string(), from_type: v.get_type().to_string(), span: v.span(), help: None, }), } } + + fn expected_type() -> Type { + Type::List(Box::new(T::expected_type())) + } +} + +// Nu Types + +impl FromValue for Value { + fn from_value(v: Value) -> Result { + Ok(v) + } + + fn expected_type() -> Type { + Type::Any + } } impl FromValue for CellPath { @@ -372,36 +537,25 @@ impl FromValue for CellPath { } } x => Err(ShellError::CantConvert { - to_type: "cell path".into(), + to_type: Self::expected_type().to_string(), from_type: x.get_type().to_string(), span, help: None, }), } } -} -impl FromValue for bool { - fn from_value(v: Value) -> Result { - match v { - Value::Bool { val, .. } => Ok(val), - v => Err(ShellError::CantConvert { - to_type: "bool".into(), - from_type: v.get_type().to_string(), - span: v.span(), - help: None, - }), - } + fn expected_type() -> Type { + Type::CellPath } } -impl FromValue for Spanned { +impl FromValue for Closure { fn from_value(v: Value) -> Result { - let span = v.span(); match v { - Value::Bool { val, .. } => Ok(Spanned { item: val, span }), + Value::Closure { val, .. } => Ok(*val), v => Err(ShellError::CantConvert { - to_type: "bool".into(), + to_type: Self::expected_type().to_string(), from_type: v.get_type().to_string(), span: v.span(), help: None, @@ -415,28 +569,48 @@ impl FromValue for DateTime { match v { Value::Date { val, .. } => Ok(val), v => Err(ShellError::CantConvert { - to_type: "date".into(), + to_type: Self::expected_type().to_string(), from_type: v.get_type().to_string(), span: v.span(), help: None, }), } } + + fn expected_type() -> Type { + Type::Date + } } -impl FromValue for Spanned> { +impl FromValue for NuGlob { fn from_value(v: Value) -> Result { - let span = v.span(); + // FIXME: we may want to fail a little nicer here match v { - Value::Date { val, .. } => Ok(Spanned { item: val, span }), + Value::CellPath { val, .. } => Ok(NuGlob::Expand(val.to_string())), + Value::String { val, .. } => Ok(NuGlob::DoNotExpand(val)), + Value::Glob { + val, + no_expand: quoted, + .. + } => { + if quoted { + Ok(NuGlob::DoNotExpand(val)) + } else { + Ok(NuGlob::Expand(val)) + } + } v => Err(ShellError::CantConvert { - to_type: "date".into(), + to_type: Self::expected_type().to_string(), from_type: v.get_type().to_string(), span: v.span(), help: None, }), } } + + fn expected_type() -> Type { + Type::String + } } impl FromValue for Range { @@ -444,94 +618,16 @@ impl FromValue for Range { match v { Value::Range { val, .. } => Ok(*val), v => Err(ShellError::CantConvert { - to_type: "range".into(), + to_type: Self::expected_type().to_string(), from_type: v.get_type().to_string(), span: v.span(), help: None, }), } } -} -impl FromValue for Spanned { - fn from_value(v: Value) -> Result { - let span = v.span(); - match v { - Value::Range { val, .. } => Ok(Spanned { item: *val, span }), - v => Err(ShellError::CantConvert { - to_type: "range".into(), - from_type: v.get_type().to_string(), - span: v.span(), - help: None, - }), - } - } -} - -impl FromValue for Vec { - fn from_value(v: Value) -> Result { - match v { - Value::Binary { val, .. } => Ok(val), - Value::String { val, .. } => Ok(val.into_bytes()), - v => Err(ShellError::CantConvert { - to_type: "binary data".into(), - from_type: v.get_type().to_string(), - span: v.span(), - help: None, - }), - } - } -} - -impl FromValue for Spanned> { - fn from_value(v: Value) -> Result { - let span = v.span(); - match v { - Value::Binary { val, .. } => Ok(Spanned { item: val, span }), - Value::String { val, .. } => Ok(Spanned { - item: val.into_bytes(), - span, - }), - v => Err(ShellError::CantConvert { - to_type: "binary data".into(), - from_type: v.get_type().to_string(), - span: v.span(), - help: None, - }), - } - } -} - -impl FromValue for Spanned { - fn from_value(v: Value) -> Result { - let span = v.span(); - match v { - Value::String { val, .. } => Ok(Spanned { - item: val.into(), - span, - }), - v => Err(ShellError::CantConvert { - to_type: "range".into(), - from_type: v.get_type().to_string(), - span: v.span(), - help: None, - }), - } - } -} - -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), - v => Err(ShellError::CantConvert { - to_type: "Vector of values".into(), - from_type: v.get_type().to_string(), - span: v.span(), - help: None, - }), - } + fn expected_type() -> Type { + Type::Range } } @@ -540,7 +636,7 @@ impl FromValue for Record { match v { Value::Record { val, .. } => Ok(val.into_owned()), v => Err(ShellError::CantConvert { - to_type: "Record".into(), + to_type: Self::expected_type().to_string(), from_type: v.get_type().to_string(), span: v.span(), help: None, @@ -549,31 +645,39 @@ impl FromValue for Record { } } -impl FromValue for Closure { - fn from_value(v: Value) -> Result { - match v { - Value::Closure { val, .. } => Ok(*val), - v => Err(ShellError::CantConvert { - to_type: "Closure".into(), - from_type: v.get_type().to_string(), - span: v.span(), - help: None, - }), - } - } -} +// Blanket Nu Implementations -impl FromValue for Spanned { +impl FromValue for Spanned +where + T: FromValue, +{ fn from_value(v: Value) -> Result { let span = v.span(); - match v { - Value::Closure { val, .. } => Ok(Spanned { item: *val, span }), - v => Err(ShellError::CantConvert { - to_type: "Closure".into(), - from_type: v.get_type().to_string(), - span: v.span(), - help: None, - }), - } + Ok(Spanned { + item: T::from_value(v)?, + span, + }) + } + + fn expected_type() -> Type { + T::expected_type() + } +} + +#[cfg(test)] +mod tests { + use crate::{engine::Closure, FromValue, Record, Type}; + + #[test] + fn expected_type_default_impl() { + assert_eq!( + Record::expected_type(), + Type::Custom("Record".to_string().into_boxed_str()) + ); + + assert_eq!( + Closure::expected_type(), + Type::Custom("Closure".to_string().into_boxed_str()) + ); } } diff --git a/crates/nu-protocol/src/value/into_value.rs b/crates/nu-protocol/src/value/into_value.rs new file mode 100644 index 0000000000..c27b19b651 --- /dev/null +++ b/crates/nu-protocol/src/value/into_value.rs @@ -0,0 +1,196 @@ +use std::collections::HashMap; + +use crate::{Record, ShellError, Span, Value}; + +/// A trait for converting a value into a [`Value`]. +/// +/// This conversion is infallible, for fallible conversions use [`TryIntoValue`]. +/// +/// # Derivable +/// This trait can be used with `#[derive]`. +/// When derived on structs with named fields, the resulting value representation will use +/// [`Value::Record`], where each field of the record corresponds to a field of the struct. +/// For structs with unnamed fields, the value representation will be [`Value::List`], with all +/// fields inserted into a list. +/// Unit structs will be represented as [`Value::Nothing`] since they contain no data. +/// +/// Only enums with no fields may derive this trait. +/// The resulting value representation will be the name of the variant as a [`Value::String`]. +/// By default, variant names will be converted to ["snake_case"](convert_case::Case::Snake). +/// You can customize the case conversion using `#[nu_value(rename_all = "kebab-case")]` on the enum. +/// All deterministic and useful case conversions provided by [`convert_case::Case`] are supported +/// by specifying the case name followed by "case". +/// Also all values for +/// [`#[serde(rename_all = "...")]`](https://serde.rs/container-attrs.html#rename_all) are valid +/// here. +/// +/// ``` +/// # use nu_protocol::{IntoValue, Value, Span}; +/// #[derive(IntoValue)] +/// #[nu_value(rename_all = "COBOL-CASE")] +/// enum Bird { +/// MountainEagle, +/// ForestOwl, +/// RiverDuck, +/// } +/// +/// assert_eq!( +/// Bird::RiverDuck.into_value(Span::unknown()), +/// Value::test_string("RIVER-DUCK") +/// ); +/// ``` +pub trait IntoValue: Sized { + /// Converts the given value to a [`Value`]. + fn into_value(self, span: Span) -> Value; +} + +// Primitive Types + +impl IntoValue for [T; N] +where + T: IntoValue, +{ + fn into_value(self, span: Span) -> Value { + Vec::from(self).into_value(span) + } +} + +macro_rules! primitive_into_value { + ($type:ty, $method:ident) => { + primitive_into_value!($type => $type, $method); + }; + + ($type:ty => $as_type:ty, $method:ident) => { + impl IntoValue for $type { + fn into_value(self, span: Span) -> Value { + Value::$method(<$as_type>::from(self), span) + } + } + }; +} + +primitive_into_value!(bool, bool); +primitive_into_value!(char, string); +primitive_into_value!(f32 => f64, float); +primitive_into_value!(f64, float); +primitive_into_value!(i8 => i64, int); +primitive_into_value!(i16 => i64, int); +primitive_into_value!(i32 => i64, int); +primitive_into_value!(i64, int); +primitive_into_value!(u8 => i64, int); +primitive_into_value!(u16 => i64, int); +primitive_into_value!(u32 => i64, int); +// u64 and usize may be truncated as Value only supports i64. + +impl IntoValue for isize { + fn into_value(self, span: Span) -> Value { + Value::int(self as i64, span) + } +} + +impl IntoValue for () { + fn into_value(self, span: Span) -> Value { + Value::nothing(span) + } +} + +macro_rules! tuple_into_value { + ($($t:ident:$n:tt),+) => { + impl<$($t),+> IntoValue for ($($t,)+) where $($t: IntoValue,)+ { + fn into_value(self, span: Span) -> Value { + let vals = vec![$(self.$n.into_value(span)),+]; + Value::list(vals, span) + } + } + } +} + +// Tuples in std are implemented for up to 12 elements, so we do it here too. +tuple_into_value!(T0:0); +tuple_into_value!(T0:0, T1:1); +tuple_into_value!(T0:0, T1:1, T2:2); +tuple_into_value!(T0:0, T1:1, T2:2, T3:3); +tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4); +tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4, T5:5); +tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6); +tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7); +tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7, T8:8); +tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7, T8:8, T9:9); +tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7, T8:8, T9:9, T10:10); +tuple_into_value!(T0:0, T1:1, T2:2, T3:3, T4:4, T5:5, T6:6, T7:7, T8:8, T9:9, T10:10, T11:11); + +// Other std Types + +impl IntoValue for String { + fn into_value(self, span: Span) -> Value { + Value::string(self, span) + } +} + +impl IntoValue for Vec +where + T: IntoValue, +{ + fn into_value(self, span: Span) -> Value { + Value::list(self.into_iter().map(|v| v.into_value(span)).collect(), span) + } +} + +impl IntoValue for Option +where + T: IntoValue, +{ + fn into_value(self, span: Span) -> Value { + match self { + Some(v) => v.into_value(span), + None => Value::nothing(span), + } + } +} + +impl IntoValue for HashMap +where + V: IntoValue, +{ + fn into_value(self, span: Span) -> Value { + let mut record = Record::new(); + for (k, v) in self.into_iter() { + // Using `push` is fine as a hashmaps have unique keys. + // To ensure this uniqueness, we only allow hashmaps with strings as + // keys and not keys which implement `Into` or `ToString`. + record.push(k, v.into_value(span)); + } + Value::record(record, span) + } +} + +// Nu Types + +impl IntoValue for Value { + fn into_value(self, span: Span) -> Value { + self.with_span(span) + } +} + +// TODO: use this type for all the `into_value` methods that types implement but return a Result +/// A trait for trying to convert a value into a `Value`. +/// +/// Types like streams may fail while collecting the `Value`, +/// for these types it is useful to implement a fallible variant. +/// +/// This conversion is fallible, for infallible conversions use [`IntoValue`]. +/// All types that implement `IntoValue` will automatically implement this trait. +pub trait TryIntoValue: Sized { + // TODO: instead of ShellError, maybe we could have a IntoValueError that implements Into + /// Tries to convert the given value into a `Value`. + fn try_into_value(self, span: Span) -> Result; +} + +impl TryIntoValue for T +where + T: IntoValue, +{ + fn try_into_value(self, span: Span) -> Result { + Ok(self.into_value(span)) + } +} diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index f924218b79..fc618d6341 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -4,7 +4,10 @@ mod filesize; mod from; mod from_value; mod glob; +mod into_value; mod range; +#[cfg(test)] +mod test_derive; pub mod record; pub use custom_value::CustomValue; @@ -12,6 +15,7 @@ pub use duration::*; pub use filesize::*; pub use from_value::FromValue; pub use glob::*; +pub use into_value::{IntoValue, TryIntoValue}; pub use range::{FloatRange, IntRange, Range}; pub use record::Record; @@ -19,7 +23,7 @@ use crate::{ ast::{Bits, Boolean, CellPath, Comparison, Math, Operator, PathMember}, did_you_mean, engine::{Closure, EngineState}, - Config, ShellError, Span, Type, + Config, ShellError, Signals, Span, Type, }; use chrono::{DateTime, Datelike, FixedOffset, Locale, TimeZone}; use chrono_humanize::HumanTime; @@ -1013,8 +1017,9 @@ impl Value { } } Value::Range { ref val, .. } => { - if let Some(item) = - val.into_range_iter(current.span(), None).nth(*count) + if let Some(item) = val + .into_range_iter(current.span(), Signals::empty()) + .nth(*count) { current = item; } else if *optional { @@ -1089,7 +1094,7 @@ impl Value { } else { return Err(ShellError::CantFindColumn { col_name: column_name.clone(), - span: *origin_span, + span: Some(*origin_span), src_span: span, }); } @@ -1126,7 +1131,7 @@ impl Value { } else { Err(ShellError::CantFindColumn { col_name: column_name.clone(), - span: *origin_span, + span: Some(*origin_span), src_span: val_span, }) } @@ -1136,7 +1141,7 @@ impl Value { } _ => Err(ShellError::CantFindColumn { col_name: column_name.clone(), - span: *origin_span, + span: Some(*origin_span), src_span: val_span, }), } @@ -1237,7 +1242,7 @@ impl Value { v => { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: *span, + span: Some(*span), src_span: v.span(), }); } @@ -1262,7 +1267,7 @@ impl Value { v => { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: *span, + span: Some(*span), src_span: v.span(), }); } @@ -1342,7 +1347,7 @@ impl Value { } else { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: *span, + span: Some(*span), src_span: v_span, }); } @@ -1351,7 +1356,7 @@ impl Value { v => { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: *span, + span: Some(*span), src_span: v.span(), }); } @@ -1364,7 +1369,7 @@ impl Value { } else { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: *span, + span: Some(*span), src_span: v_span, }); } @@ -1373,7 +1378,7 @@ impl Value { v => { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: *span, + span: Some(*span), src_span: v.span(), }); } @@ -1427,7 +1432,7 @@ impl Value { if record.to_mut().remove(col_name).is_none() && !optional { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: *span, + span: Some(*span), src_span: v_span, }); } @@ -1435,7 +1440,7 @@ impl Value { v => { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: *span, + span: Some(*span), src_span: v.span(), }); } @@ -1447,7 +1452,7 @@ impl Value { if record.to_mut().remove(col_name).is_none() && !optional { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: *span, + span: Some(*span), src_span: v_span, }); } @@ -1455,7 +1460,7 @@ impl Value { } v => Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: *span, + span: Some(*span), src_span: v.span(), }), }, @@ -1504,7 +1509,7 @@ impl Value { } else if !optional { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: *span, + span: Some(*span), src_span: v_span, }); } @@ -1512,7 +1517,7 @@ impl Value { v => { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: *span, + span: Some(*span), src_span: v.span(), }); } @@ -1526,7 +1531,7 @@ impl Value { } else if !optional { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: *span, + span: Some(*span), src_span: v_span, }); } @@ -1534,7 +1539,7 @@ impl Value { } v => Err(ShellError::CantFindColumn { col_name: col_name.clone(), - span: *span, + span: Some(*span), src_span: v.span(), }), }, diff --git a/crates/nu-protocol/src/value/range.rs b/crates/nu-protocol/src/value/range.rs index 5b1fa32ec7..6ec7ce16ad 100644 --- a/crates/nu-protocol/src/value/range.rs +++ b/crates/nu-protocol/src/value/range.rs @@ -1,25 +1,13 @@ //! A Range is an iterator over integers or floats. +use crate::{ast::RangeInclusion, ShellError, Signals, Span, Value}; use serde::{Deserialize, Serialize}; -use std::{ - cmp::Ordering, - fmt::Display, - sync::{atomic::AtomicBool, Arc}, -}; - -use crate::{ast::RangeInclusion, ShellError, Span, Value}; +use std::{cmp::Ordering, fmt::Display}; mod int_range { - use std::{ - cmp::Ordering, - fmt::Display, - ops::Bound, - sync::{atomic::AtomicBool, Arc}, - }; - + use crate::{ast::RangeInclusion, ShellError, Signals, Span, Value}; use serde::{Deserialize, Serialize}; - - use crate::{ast::RangeInclusion, ShellError, Span, Value}; + use std::{cmp::Ordering, fmt::Display, ops::Bound}; #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct IntRange { @@ -123,12 +111,12 @@ mod int_range { } } - pub fn into_range_iter(self, ctrlc: Option>) -> Iter { + pub fn into_range_iter(self, signals: Signals) -> Iter { Iter { current: Some(self.start), step: self.step, end: self.end, - ctrlc, + signals, } } } @@ -202,7 +190,7 @@ mod int_range { current: Option, step: i64, end: Bound, - ctrlc: Option>, + signals: Signals, } impl Iterator for Iter { @@ -218,7 +206,7 @@ mod int_range { (_, Bound::Unbounded) => true, // will stop once integer overflows }; - if not_end && !nu_utils::ctrl_c::was_pressed(&self.ctrlc) { + if not_end && !self.signals.interrupted() { self.current = current.checked_add(self.step); Some(current) } else { @@ -233,16 +221,9 @@ mod int_range { } mod float_range { - use std::{ - cmp::Ordering, - fmt::Display, - ops::Bound, - sync::{atomic::AtomicBool, Arc}, - }; - + use crate::{ast::RangeInclusion, IntRange, Range, ShellError, Signals, Span, Value}; use serde::{Deserialize, Serialize}; - - use crate::{ast::RangeInclusion, IntRange, Range, ShellError, Span, Value}; + use std::{cmp::Ordering, fmt::Display, ops::Bound}; #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct FloatRange { @@ -365,13 +346,13 @@ mod float_range { } } - pub fn into_range_iter(self, ctrlc: Option>) -> Iter { + pub fn into_range_iter(self, signals: Signals) -> Iter { Iter { start: self.start, step: self.step, end: self.end, iter: Some(0), - ctrlc, + signals, } } } @@ -477,7 +458,7 @@ mod float_range { step: f64, end: Bound, iter: Option, - ctrlc: Option>, + signals: Signals, } impl Iterator for Iter { @@ -495,7 +476,7 @@ mod float_range { (_, Bound::Unbounded) => current.is_finite(), }; - if not_end && !nu_utils::ctrl_c::was_pressed(&self.ctrlc) { + if not_end && !self.signals.interrupted() { self.iter = iter.checked_add(1); Some(current) } else { @@ -549,10 +530,10 @@ impl Range { } } - pub fn into_range_iter(self, span: Span, ctrlc: Option>) -> Iter { + pub fn into_range_iter(self, span: Span, signals: Signals) -> Iter { match self { - Range::IntRange(range) => Iter::IntIter(range.into_range_iter(ctrlc), span), - Range::FloatRange(range) => Iter::FloatIter(range.into_range_iter(ctrlc), span), + Range::IntRange(range) => Iter::IntIter(range.into_range_iter(signals), span), + Range::FloatRange(range) => Iter::FloatIter(range.into_range_iter(signals), span), } } } diff --git a/crates/nu-protocol/src/value/record.rs b/crates/nu-protocol/src/value/record.rs index 8b61e61f7f..f4e963737f 100644 --- a/crates/nu-protocol/src/value/record.rs +++ b/crates/nu-protocol/src/value/record.rs @@ -1,3 +1,4 @@ +//! Our insertion ordered map-type [`Record`] use std::{iter::FusedIterator, ops::RangeBounds}; use crate::{ShellError, Span, Value}; diff --git a/crates/nu-protocol/src/value/test_derive.rs b/crates/nu-protocol/src/value/test_derive.rs new file mode 100644 index 0000000000..56bccabd2a --- /dev/null +++ b/crates/nu-protocol/src/value/test_derive.rs @@ -0,0 +1,442 @@ +use crate::{record, FromValue, IntoValue, Record, Span, Value}; +use std::collections::HashMap; + +// Make nu_protocol available in this namespace, consumers of this crate will +// have this without such an export. +// The derive macro fully qualifies paths to "nu_protocol". +use crate as nu_protocol; + +trait IntoTestValue { + fn into_test_value(self) -> Value; +} + +impl IntoTestValue for T +where + T: IntoValue, +{ + fn into_test_value(self) -> Value { + self.into_value(Span::test_data()) + } +} + +#[derive(IntoValue, FromValue, Debug, PartialEq)] +struct NamedFieldsStruct +where + T: IntoValue + FromValue, +{ + array: [u16; 4], + bool: bool, + char: char, + f32: f32, + f64: f64, + i8: i8, + i16: i16, + i32: i32, + i64: i64, + isize: isize, + u16: u16, + u32: u32, + unit: (), + tuple: (u32, bool), + some: Option, + none: Option, + vec: Vec, + string: String, + hashmap: HashMap, + nested: Nestee, +} + +#[derive(IntoValue, FromValue, Debug, PartialEq)] +struct Nestee { + u32: u32, + some: Option, + none: Option, +} + +impl NamedFieldsStruct { + fn make() -> Self { + Self { + array: [1, 2, 3, 4], + bool: true, + char: 'a', + f32: std::f32::consts::PI, + f64: std::f64::consts::E, + i8: 127, + i16: -32768, + i32: 2147483647, + i64: -9223372036854775808, + isize: 2, + u16: 65535, + u32: 4294967295, + unit: (), + tuple: (1, true), + some: Some(123), + none: None, + vec: vec![10, 20, 30], + string: "string".to_string(), + hashmap: HashMap::from_iter([("a".to_string(), 10), ("b".to_string(), 20)]), + nested: Nestee { + u32: 3, + some: Some(42), + none: None, + }, + } + } + + fn value() -> Value { + Value::test_record(record! { + "array" => Value::test_list(vec![ + Value::test_int(1), + Value::test_int(2), + Value::test_int(3), + Value::test_int(4) + ]), + "bool" => Value::test_bool(true), + "char" => Value::test_string('a'), + "f32" => Value::test_float(std::f32::consts::PI.into()), + "f64" => Value::test_float(std::f64::consts::E), + "i8" => Value::test_int(127), + "i16" => Value::test_int(-32768), + "i32" => Value::test_int(2147483647), + "i64" => Value::test_int(-9223372036854775808), + "isize" => Value::test_int(2), + "u16" => Value::test_int(65535), + "u32" => Value::test_int(4294967295), + "unit" => Value::test_nothing(), + "tuple" => Value::test_list(vec![ + Value::test_int(1), + Value::test_bool(true) + ]), + "some" => Value::test_int(123), + "none" => Value::test_nothing(), + "vec" => Value::test_list(vec![ + Value::test_int(10), + Value::test_int(20), + Value::test_int(30) + ]), + "string" => Value::test_string("string"), + "hashmap" => Value::test_record(record! { + "a" => Value::test_int(10), + "b" => Value::test_int(20) + }), + "nested" => Value::test_record(record! { + "u32" => Value::test_int(3), + "some" => Value::test_int(42), + "none" => Value::test_nothing(), + }) + }) + } +} + +#[test] +fn named_fields_struct_into_value() { + let expected = NamedFieldsStruct::value(); + let actual = NamedFieldsStruct::make().into_test_value(); + assert_eq!(expected, actual); +} + +#[test] +fn named_fields_struct_from_value() { + let expected = NamedFieldsStruct::make(); + let actual = NamedFieldsStruct::from_value(NamedFieldsStruct::value()).unwrap(); + assert_eq!(expected, actual); +} + +#[test] +fn named_fields_struct_roundtrip() { + let expected = NamedFieldsStruct::make(); + let actual = + NamedFieldsStruct::from_value(NamedFieldsStruct::make().into_test_value()).unwrap(); + assert_eq!(expected, actual); + + let expected = NamedFieldsStruct::value(); + let actual = NamedFieldsStruct::::from_value(NamedFieldsStruct::value()) + .unwrap() + .into_test_value(); + assert_eq!(expected, actual); +} + +#[test] +fn named_fields_struct_missing_value() { + let value = Value::test_record(Record::new()); + let res: Result, _> = NamedFieldsStruct::from_value(value); + assert!(res.is_err()); +} + +#[test] +fn named_fields_struct_incorrect_type() { + // Should work for every type that is not a record. + let value = Value::test_nothing(); + let res: Result, _> = NamedFieldsStruct::from_value(value); + assert!(res.is_err()); +} + +#[derive(IntoValue, FromValue, Debug, PartialEq, Default)] +struct ALotOfOptions { + required: bool, + float: Option, + int: Option, + value: Option, + nested: Option, +} + +#[test] +fn missing_options() { + let value = Value::test_record(Record::new()); + let res: Result = ALotOfOptions::from_value(value); + assert!(res.is_err()); + + let value = Value::test_record(record! {"required" => Value::test_bool(true)}); + let expected = ALotOfOptions { + required: true, + ..Default::default() + }; + let actual = ALotOfOptions::from_value(value).unwrap(); + assert_eq!(expected, actual); + + let value = Value::test_record(record! { + "required" => Value::test_bool(true), + "float" => Value::test_float(std::f64::consts::PI), + }); + let expected = ALotOfOptions { + required: true, + float: Some(std::f64::consts::PI), + ..Default::default() + }; + let actual = ALotOfOptions::from_value(value).unwrap(); + assert_eq!(expected, actual); + + let value = Value::test_record(record! { + "required" => Value::test_bool(true), + "int" => Value::test_int(12), + "nested" => Value::test_record(record! { + "u32" => Value::test_int(34), + }), + }); + let expected = ALotOfOptions { + required: true, + int: Some(12), + nested: Some(Nestee { + u32: 34, + some: None, + none: None, + }), + ..Default::default() + }; + let actual = ALotOfOptions::from_value(value).unwrap(); + assert_eq!(expected, actual); +} + +#[derive(IntoValue, FromValue, Debug, PartialEq)] +struct UnnamedFieldsStruct(u32, String, T) +where + T: IntoValue + FromValue; + +impl UnnamedFieldsStruct { + fn make() -> Self { + UnnamedFieldsStruct(420, "Hello, tuple!".to_string(), 33.33) + } + + fn value() -> Value { + Value::test_list(vec![ + Value::test_int(420), + Value::test_string("Hello, tuple!"), + Value::test_float(33.33), + ]) + } +} + +#[test] +fn unnamed_fields_struct_into_value() { + let expected = UnnamedFieldsStruct::value(); + let actual = UnnamedFieldsStruct::make().into_test_value(); + assert_eq!(expected, actual); +} + +#[test] +fn unnamed_fields_struct_from_value() { + let expected = UnnamedFieldsStruct::make(); + let value = UnnamedFieldsStruct::value(); + let actual = UnnamedFieldsStruct::from_value(value).unwrap(); + assert_eq!(expected, actual); +} + +#[test] +fn unnamed_fields_struct_roundtrip() { + let expected = UnnamedFieldsStruct::make(); + let actual = + UnnamedFieldsStruct::from_value(UnnamedFieldsStruct::make().into_test_value()).unwrap(); + assert_eq!(expected, actual); + + let expected = UnnamedFieldsStruct::value(); + let actual = UnnamedFieldsStruct::::from_value(UnnamedFieldsStruct::value()) + .unwrap() + .into_test_value(); + assert_eq!(expected, actual); +} + +#[test] +fn unnamed_fields_struct_missing_value() { + let value = Value::test_list(vec![]); + let res: Result, _> = UnnamedFieldsStruct::from_value(value); + assert!(res.is_err()); +} + +#[test] +fn unnamed_fields_struct_incorrect_type() { + // Should work for every type that is not a record. + let value = Value::test_nothing(); + let res: Result, _> = UnnamedFieldsStruct::from_value(value); + assert!(res.is_err()); +} + +#[derive(IntoValue, FromValue, Debug, PartialEq)] +struct UnitStruct; + +#[test] +fn unit_struct_into_value() { + let expected = Value::test_nothing(); + let actual = UnitStruct.into_test_value(); + assert_eq!(expected, actual); +} + +#[test] +fn unit_struct_from_value() { + let expected = UnitStruct; + let actual = UnitStruct::from_value(Value::test_nothing()).unwrap(); + assert_eq!(expected, actual); +} + +#[test] +fn unit_struct_roundtrip() { + let expected = UnitStruct; + let actual = UnitStruct::from_value(UnitStruct.into_test_value()).unwrap(); + assert_eq!(expected, actual); + + let expected = Value::test_nothing(); + let actual = UnitStruct::from_value(Value::test_nothing()) + .unwrap() + .into_test_value(); + assert_eq!(expected, actual); +} + +#[derive(IntoValue, FromValue, Debug, PartialEq)] +enum Enum { + AlphaOne, + BetaTwo, + CharlieThree, +} + +impl Enum { + fn make() -> [Self; 3] { + [Enum::AlphaOne, Enum::BetaTwo, Enum::CharlieThree] + } + + fn value() -> Value { + Value::test_list(vec![ + Value::test_string("alpha_one"), + Value::test_string("beta_two"), + Value::test_string("charlie_three"), + ]) + } +} + +#[test] +fn enum_into_value() { + let expected = Enum::value(); + let actual = Enum::make().into_test_value(); + assert_eq!(expected, actual); +} + +#[test] +fn enum_from_value() { + let expected = Enum::make(); + let actual = <[Enum; 3]>::from_value(Enum::value()).unwrap(); + assert_eq!(expected, actual); +} + +#[test] +fn enum_roundtrip() { + let expected = Enum::make(); + let actual = <[Enum; 3]>::from_value(Enum::make().into_test_value()).unwrap(); + assert_eq!(expected, actual); + + let expected = Enum::value(); + let actual = <[Enum; 3]>::from_value(Enum::value()) + .unwrap() + .into_test_value(); + assert_eq!(expected, actual); +} + +#[test] +fn enum_unknown_variant() { + let value = Value::test_string("delta_four"); + let res = Enum::from_value(value); + assert!(res.is_err()); +} + +#[test] +fn enum_incorrect_type() { + // Should work for every type that is not a record. + let value = Value::test_nothing(); + let res = Enum::from_value(value); + assert!(res.is_err()); +} + +// Generate the `Enum` from before but with all possible `rename_all` variants. +macro_rules! enum_rename_all { + ($($ident:ident: $case:literal => [$a1:literal, $b2:literal, $c3:literal]),*) => { + $( + #[derive(Debug, PartialEq, IntoValue, FromValue)] + #[nu_value(rename_all = $case)] + enum $ident { + AlphaOne, + BetaTwo, + CharlieThree + } + + impl $ident { + fn make() -> [Self; 3] { + [Self::AlphaOne, Self::BetaTwo, Self::CharlieThree] + } + + fn value() -> Value { + Value::test_list(vec![ + Value::test_string($a1), + Value::test_string($b2), + Value::test_string($c3), + ]) + } + } + )* + + #[test] + fn enum_rename_all_into_value() {$({ + let expected = $ident::value(); + let actual = $ident::make().into_test_value(); + assert_eq!(expected, actual); + })*} + + #[test] + fn enum_rename_all_from_value() {$({ + let expected = $ident::make(); + let actual = <[$ident; 3]>::from_value($ident::value()).unwrap(); + assert_eq!(expected, actual); + })*} + } +} + +enum_rename_all! { + Upper: "UPPER CASE" => ["ALPHA ONE", "BETA TWO", "CHARLIE THREE"], + Lower: "lower case" => ["alpha one", "beta two", "charlie three"], + Title: "Title Case" => ["Alpha One", "Beta Two", "Charlie Three"], + Camel: "camelCase" => ["alphaOne", "betaTwo", "charlieThree"], + Pascal: "PascalCase" => ["AlphaOne", "BetaTwo", "CharlieThree"], + Snake: "snake_case" => ["alpha_one", "beta_two", "charlie_three"], + UpperSnake: "UPPER_SNAKE_CASE" => ["ALPHA_ONE", "BETA_TWO", "CHARLIE_THREE"], + Kebab: "kebab-case" => ["alpha-one", "beta-two", "charlie-three"], + Cobol: "COBOL-CASE" => ["ALPHA-ONE", "BETA-TWO", "CHARLIE-THREE"], + Train: "Train-Case" => ["Alpha-One", "Beta-Two", "Charlie-Three"], + Flat: "flatcase" => ["alphaone", "betatwo", "charliethree"], + UpperFlat: "UPPERFLATCASE" => ["ALPHAONE", "BETATWO", "CHARLIETHREE"] +} diff --git a/crates/nu-protocol/tests/test_signature.rs b/crates/nu-protocol/tests/test_signature.rs index e22029bab9..8faf772c38 100644 --- a/crates/nu-protocol/tests/test_signature.rs +++ b/crates/nu-protocol/tests/test_signature.rs @@ -39,7 +39,7 @@ fn test_signature_chained() { assert_eq!( signature.get_positional(0), - Some(PositionalArg { + Some(&PositionalArg { name: "required".to_string(), desc: "required description".to_string(), shape: SyntaxShape::String, @@ -49,7 +49,7 @@ fn test_signature_chained() { ); assert_eq!( signature.get_positional(1), - Some(PositionalArg { + Some(&PositionalArg { name: "optional".to_string(), desc: "optional description".to_string(), shape: SyntaxShape::String, @@ -59,7 +59,7 @@ fn test_signature_chained() { ); assert_eq!( signature.get_positional(2), - Some(PositionalArg { + Some(&PositionalArg { name: "rest".to_string(), desc: "rest description".to_string(), shape: SyntaxShape::String, diff --git a/crates/nu-std/CONTRIBUTING.md b/crates/nu-std/CONTRIBUTING.md index 1677a31de1..e866cc9206 100644 --- a/crates/nu-std/CONTRIBUTING.md +++ b/crates/nu-std/CONTRIBUTING.md @@ -204,7 +204,7 @@ More design guidelines: ### Useful Commands - Run all unit tests for the standard library: ```nushell - cargo run -- -c 'use std testing; testing run-tests --path crates/nu-std' + cargo run -- -c 'use crates/nu-std/testing.nu; testing run-tests --path crates/nu-std' ``` > **Note** > this uses the debug version of NU interpreter from the same repo, which is @@ -216,7 +216,7 @@ More design guidelines: - Run all tests for a specific test module, e.g, `crates/nu-std/tests/test_foo.nu` ```nushell - cargo run -- -c 'use std testing; testing run-tests --path crates/nu-std --module test_foo' + cargo run -- -c 'use crates/nu-std/testing.nu; testing run-tests --path crates/nu-std --module test_foo' ``` - Run a custom command with additional logging (assuming you have instrumented the command with `log `, as we recommend.) diff --git a/crates/nu-std/Cargo.toml b/crates/nu-std/Cargo.toml index 660d418097..9ee4113e96 100644 --- a/crates/nu-std/Cargo.toml +++ b/crates/nu-std/Cargo.toml @@ -5,12 +5,12 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-std" edition = "2021" license = "MIT" name = "nu-std" -version = "0.94.3" +version = "0.95.1" [dependencies] -nu-parser = { version = "0.94.3", path = "../nu-parser" } -nu-protocol = { version = "0.94.3", path = "../nu-protocol" } -nu-engine = { version = "0.94.3", path = "../nu-engine" } +nu-parser = { version = "0.95.1", path = "../nu-parser" } +nu-protocol = { version = "0.95.1", path = "../nu-protocol" } +nu-engine = { version = "0.95.1", path = "../nu-engine" } miette = { workspace = true, features = ["fancy-no-backtrace"] } -log = "0.4" +log = "0.4" \ No newline at end of file diff --git a/crates/nu-std/README.md b/crates/nu-std/README.md index d6e4d6e9f6..23bd684a50 100644 --- a/crates/nu-std/README.md +++ b/crates/nu-std/README.md @@ -7,7 +7,7 @@ The standard library is a pure-`nushell` collection of custom commands which provide interactive utilities and building blocks for users writing casual scripts or complex applications. To see what's here: -``` +```text > use std > scope commands | select name usage | where name =~ "std " #┬───────────name────────────┬──────────────────────usage────────────────────── diff --git a/crates/nu-std/src/lib.rs b/crates/nu-std/src/lib.rs index a20e3fc5da..ff6910e4ba 100644 --- a/crates/nu-std/src/lib.rs +++ b/crates/nu-std/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] use log::trace; use nu_engine::eval_block; use nu_parser::parse; diff --git a/crates/nu-std/std/help.nu b/crates/nu-std/std/help.nu index df6c4c6ca6..16a20504cb 100644 --- a/crates/nu-std/std/help.nu +++ b/crates/nu-std/std/help.nu @@ -658,7 +658,7 @@ def build-command-page [command: record] { $" > ($example.example | nu-highlight)" (if not ($example.result | is-empty) { $example.result - | table + | table -e | to text | if ($example.result | describe) == "binary" { str join } else { lines } | each {|line| @@ -771,6 +771,11 @@ You can also learn more at (ansi default_italic)(ansi light_cyan_underline)https let modules = (try { modules $target_item --find $find }) if not ($modules | is-empty) { return $modules } + + if ($find | is-not-empty) { + print -e $"No help results found mentioning: ($find)" + return [] + } let span = (metadata $item | get span) error make { diff --git a/crates/nu-std/std/mod.nu b/crates/nu-std/std/mod.nu index a20bdb4b78..4d4f2bb0f1 100644 --- a/crates/nu-std/std/mod.nu +++ b/crates/nu-std/std/mod.nu @@ -66,7 +66,7 @@ export def --env "path add" [ "record" => { $p | get --ignore-errors $nu.os-info.name }, } - $p | path expand + $p | path expand --no-symlink } if null in $paths or ($paths | is-empty) { @@ -80,7 +80,6 @@ export def --env "path add" [ $env | get $path_name | split row (char esep) - | path expand | if $append { append $paths } else { prepend $paths } )} @@ -161,10 +160,7 @@ export def bench [ } } -# print a banner for nushell, with information about the project -# -# Example: -# an example can be found in [this asciinema recording](https://asciinema.org/a/566513) +# Print a banner for nushell with information about the project export def banner [] { let dt = (datetime-diff (date now) 2019-05-10T09:59:12-07:00) $"(ansi green) __ ,(ansi reset) diff --git a/crates/nu-std/tests/test_asserts.nu b/crates/nu-std/tests/test_asserts.nu index b155022473..2447dfda1e 100644 --- a/crates/nu-std/tests/test_asserts.nu +++ b/crates/nu-std/tests/test_asserts.nu @@ -38,8 +38,8 @@ def assert_error [] { assert error $failing_code let good_code = {|| } - let assert_error_raised = (try { do assert $good_code; false } catch { true }) - assert $assert_error_raised "The assert error should raise an error if there is no error in the executed code." + let assert_error_raised = (try { assert error $good_code; false } catch { true }) + assert $assert_error_raised "The assert error should be false if there is no error in the executed code." } #[test] diff --git a/crates/nu-std/tests/test_help.nu b/crates/nu-std/tests/test_help.nu index 1608950886..b82e5072ef 100644 --- a/crates/nu-std/tests/test_help.nu +++ b/crates/nu-std/tests/test_help.nu @@ -7,3 +7,8 @@ def show_help_on_commands [] { assert ("item not found" not-in $help_result) } +#[test] +def show_help_on_error_make [] { + let help_result = (help error make) + assert ("Error: nu::shell::eval_block_with_input" not-in $help_result) +} diff --git a/crates/nu-std/tests/test_std.nu b/crates/nu-std/tests/test_std.nu index 45633719a0..6ac7c6b9e0 100644 --- a/crates/nu-std/tests/test_std.nu +++ b/crates/nu-std/tests/test_std.nu @@ -38,12 +38,38 @@ def path_add [] { std path add $target_paths assert equal (get_path) ([($target_paths | get $nu.os-info.name)] | path expand) - load-env {$path_name: [$"/foo(char esep)/bar"]} + load-env {$path_name: [$"(["/foo", "/bar"] | path expand | str join (char esep))"]} std path add "~/foo" assert equal (get_path) (["~/foo", "/foo", "/bar"] | path expand) } } +#[test] +def path_add_expand [] { + use std assert + + # random paths to avoid collision, especially if left dangling on failure + let real_dir = $nu.temp-path | path join $"real-dir-(random chars)" + let link_dir = $nu.temp-path | path join $"link-dir-(random chars)" + mkdir $real_dir + let path_name = if $nu.os-info.family == 'windows' { + mklink /D $link_dir $real_dir + "Path" + } else { + ln -s $real_dir $link_dir | ignore + "PATH" + } + + with-env {$path_name: []} { + def get_path [] { $env | get $path_name } + + std path add $link_dir + assert equal (get_path) ([$link_dir]) + } + + rm $real_dir $link_dir +} + #[test] def banner [] { std assert ((std banner | lines | length) == 15) diff --git a/crates/nu-system/Cargo.toml b/crates/nu-system/Cargo.toml index 3e888dba49..19ab1259fb 100644 --- a/crates/nu-system/Cargo.toml +++ b/crates/nu-system/Cargo.toml @@ -3,7 +3,7 @@ authors = ["The Nushell Project Developers", "procs creators"] description = "Nushell system querying" repository = "https://github.com/nushell/nushell/tree/main/crates/nu-system" name = "nu-system" -version = "0.94.3" +version = "0.95.1" edition = "2021" license = "MIT" @@ -45,4 +45,4 @@ windows = { workspace = true, features = [ "Win32_System_SystemInformation", "Win32_System_Threading", "Win32_UI_Shell", -]} +]} \ No newline at end of file diff --git a/crates/nu-system/README.md b/crates/nu-system/README.md new file mode 100644 index 0000000000..d847bf4e0f --- /dev/null +++ b/crates/nu-system/README.md @@ -0,0 +1,7 @@ +Operating system specific bindings used by Nushell. + +Currently primarily wrappers around processes and ways to gather process info from the system + +## Internal Nushell crate + +This crate implements components of Nushell and is not designed to support plugin authors or other users directly. diff --git a/crates/nu-system/src/freebsd.rs b/crates/nu-system/src/freebsd.rs index ff0f67dd10..4a556762e4 100644 --- a/crates/nu-system/src/freebsd.rs +++ b/crates/nu-system/src/freebsd.rs @@ -1,6 +1,7 @@ use itertools::{EitherOrBoth, Itertools}; use libc::{ - kinfo_proc, sysctl, CTL_HW, CTL_KERN, KERN_PROC, KERN_PROC_ALL, KERN_PROC_ARGS, TDF_IDLETD, + c_char, kinfo_proc, sysctl, CTL_HW, CTL_KERN, KERN_PROC, KERN_PROC_ALL, KERN_PROC_ARGS, + TDF_IDLETD, }; use std::{ ffi::CStr, @@ -16,7 +17,7 @@ pub struct ProcessInfo { pub ppid: i32, pub name: String, pub argv: Vec, - pub stat: i8, + pub stat: c_char, pub percent_cpu: f64, pub mem_resident: u64, // in bytes pub mem_virtual: u64, // in bytes diff --git a/crates/nu-system/src/lib.rs b/crates/nu-system/src/lib.rs index 2fc97d2e7e..3633d62990 100644 --- a/crates/nu-system/src/lib.rs +++ b/crates/nu-system/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] mod foreground; #[cfg(target_os = "freebsd")] diff --git a/crates/nu-table/Cargo.toml b/crates/nu-table/Cargo.toml index af7cf19f36..9f00c4d1fd 100644 --- a/crates/nu-table/Cargo.toml +++ b/crates/nu-table/Cargo.toml @@ -5,20 +5,20 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-table" edition = "2021" license = "MIT" name = "nu-table" -version = "0.94.3" +version = "0.95.1" [lib] bench = false [dependencies] -nu-protocol = { path = "../nu-protocol", version = "0.94.3" } -nu-utils = { path = "../nu-utils", version = "0.94.3" } -nu-engine = { path = "../nu-engine", version = "0.94.3" } -nu-color-config = { path = "../nu-color-config", version = "0.94.3" } +nu-protocol = { path = "../nu-protocol", version = "0.95.1" } +nu-utils = { path = "../nu-utils", version = "0.95.1" } +nu-engine = { path = "../nu-engine", version = "0.95.1" } +nu-color-config = { path = "../nu-color-config", version = "0.95.1" } nu-ansi-term = { workspace = true } once_cell = { workspace = true } fancy-regex = { workspace = true } tabled = { workspace = true, features = ["color"], default-features = false } [dev-dependencies] -# nu-test-support = { path="../nu-test-support", version = "0.94.3" } +# nu-test-support = { path="../nu-test-support", version = "0.95.1" } \ No newline at end of file diff --git a/crates/nu-table/README.md b/crates/nu-table/README.md new file mode 100644 index 0000000000..945d515e62 --- /dev/null +++ b/crates/nu-table/README.md @@ -0,0 +1,7 @@ +The layout logic for Nushell's table viewer. + +See also the separate `table` command implementation + +## Internal Nushell crate + +This crate implements components of Nushell and is not designed to support plugin authors or other users directly. diff --git a/crates/nu-table/src/lib.rs b/crates/nu-table/src/lib.rs index 66cc2a3a82..182585f193 100644 --- a/crates/nu-table/src/lib.rs +++ b/crates/nu-table/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] mod table; mod table_theme; mod types; diff --git a/crates/nu-table/src/table.rs b/crates/nu-table/src/table.rs index faf47d84cd..bc23e48248 100644 --- a/crates/nu-table/src/table.rs +++ b/crates/nu-table/src/table.rs @@ -12,13 +12,17 @@ use tabled::{ config::{AlignmentHorizontal, ColoredConfig, Entity, EntityMap, Position}, dimension::CompleteDimensionVecRecords, records::{ - vec_records::{CellInfo, VecRecords}, + vec_records::{Cell, CellInfo, VecRecords}, ExactRecords, PeekableRecords, Records, Resizable, }, }, settings::{ - formatting::AlignmentStrategy, object::Segment, peaker::Peaker, themes::ColumnNames, Color, - Modify, Padding, Settings, TableOption, Width, + formatting::AlignmentStrategy, + object::{Columns, Segment}, + peaker::Peaker, + themes::ColumnNames, + width::Truncate, + Color, Modify, Padding, Settings, TableOption, Width, }, Table, }; @@ -216,7 +220,7 @@ fn build_table( } let pad = indent.0 + indent.1; - let widths = maybe_truncate_columns(&mut data, &cfg.theme, termwidth, pad); + let widths = maybe_truncate_columns(&mut data, &cfg, termwidth, pad); if widths.is_empty() { return None; } @@ -251,7 +255,8 @@ fn draw_table( align_table(&mut table, alignments, with_index, with_header, with_footer); colorize_table(&mut table, styles, with_index, with_header, with_footer); - let width_ctrl = TableWidthCtrl::new(widths, cfg, termwidth); + let pad = indent.0 + indent.1; + let width_ctrl = TableWidthCtrl::new(widths, cfg, termwidth, pad); if with_header && border_header { set_border_head(&mut table, with_footer, width_ctrl); @@ -259,9 +264,7 @@ fn draw_table( table.with(width_ctrl); } - let tablewidth = table.total_width(); - - table_to_string(table, tablewidth, termwidth) + table_to_string(table, termwidth) } fn set_indent(table: &mut Table, left: usize, right: usize) { @@ -271,25 +274,58 @@ fn set_indent(table: &mut Table, left: usize, right: usize) { fn set_border_head(table: &mut Table, with_footer: bool, wctrl: TableWidthCtrl) { if with_footer { let count_rows = table.count_rows(); + let last_row_index = count_rows - 1; + + // note: funnily last and row must be equal at this point but we do not rely on it just in case. + + let mut first_row = GetRow(0, Vec::new()); + let mut head_settings = GetRowSettings(0, AlignmentHorizontal::Left, None); + let mut last_row = GetRow(last_row_index, Vec::new()); + + table.with(&mut first_row); + table.with(&mut head_settings); + table.with(&mut last_row); + table.with( Settings::default() .with(wctrl) .with(StripColorFromRow(0)) .with(StripColorFromRow(count_rows - 1)) - .with(HeaderMove((0, 0), true)) - .with(HeaderMove((count_rows - 1 - 1, count_rows - 1), false)), + .with(MoveRowNext::new(0, 0)) + .with(MoveRowPrev::new(last_row_index - 1, last_row_index)) + .with(SetLineHeaders::new( + 0, + first_row.1, + head_settings.1, + head_settings.2.clone(), + )) + .with(SetLineHeaders::new( + last_row_index - 1, + last_row.1, + head_settings.1, + head_settings.2, + )), ); } else { + let mut row = GetRow(0, Vec::new()); + let mut row_opts = GetRowSettings(0, AlignmentHorizontal::Left, None); + + table.with(&mut row); + table.with(&mut row_opts); + table.with( Settings::default() .with(wctrl) .with(StripColorFromRow(0)) - .with(HeaderMove((0, 0), true)), + .with(MoveRowNext::new(0, 0)) + .with(SetLineHeaders::new(0, row.1, row_opts.1, row_opts.2)), ); } } -fn table_to_string(table: Table, total_width: usize, termwidth: usize) -> Option { +fn table_to_string(table: Table, termwidth: usize) -> Option { + let total_width = table.total_width(); + if total_width > termwidth { None } else { @@ -301,12 +337,18 @@ fn table_to_string(table: Table, total_width: usize, termwidth: usize) -> Option struct TableWidthCtrl { width: Vec, cfg: NuTableConfig, - max: usize, + width_max: usize, + pad: usize, } impl TableWidthCtrl { - fn new(width: Vec, cfg: NuTableConfig, max: usize) -> Self { - Self { width, cfg, max } + fn new(width: Vec, cfg: NuTableConfig, max: usize, pad: usize) -> Self { + Self { + width, + cfg, + width_max: max, + pad, + } } } @@ -318,24 +360,52 @@ impl TableOption, ColoredConfig> for dim: &mut CompleteDimensionVecRecords<'_>, ) { let total_width = get_total_width2(&self.width, cfg); - if total_width > self.max { - TableTrim { - max: self.max, - strategy: self.cfg.trim, - width: self.width, - } + + if total_width > self.width_max { + let has_header = self.cfg.with_header && rec.count_rows() > 1; + let trim_as_head = has_header && self.cfg.header_on_border; + + TableTrim::new( + self.width, + self.width_max, + self.cfg.trim, + trim_as_head, + self.pad, + ) .change(rec, cfg, dim); - } else if self.cfg.expand && self.max > total_width { - Settings::new(SetDimensions(self.width), Width::increase(self.max)) + } else if self.cfg.expand && self.width_max > total_width { + Settings::new(SetDimensions(self.width), Width::increase(self.width_max)) .change(rec, cfg, dim) + } else { + SetDimensions(self.width).change(rec, cfg, dim); } } } struct TableTrim { width: Vec, + width_max: usize, strategy: TrimStrategy, - max: usize, + trim_as_head: bool, + pad: usize, +} + +impl TableTrim { + fn new( + width: Vec, + width_max: usize, + strategy: TrimStrategy, + trim_as_head: bool, + pad: usize, + ) -> Self { + Self { + width, + strategy, + pad, + width_max, + trim_as_head, + } + } } impl TableOption, ColoredConfig> for TableTrim { @@ -345,9 +415,16 @@ impl TableOption, ColoredConfig> for cfg: &mut ColoredConfig, dims: &mut CompleteDimensionVecRecords<'_>, ) { + // we already must have been estimated that it's safe to do. + // and all dims will be suffitient + if self.trim_as_head { + trim_as_header(recs, cfg, dims, self); + return; + } + match self.strategy { TrimStrategy::Wrap { try_to_keep_words } => { - let mut wrap = Width::wrap(self.max).priority::(); + let mut wrap = Width::wrap(self.width_max).priority::(); if try_to_keep_words { wrap = wrap.keep_words(); } @@ -355,7 +432,7 @@ impl TableOption, ColoredConfig> for Settings::new(SetDimensions(self.width), wrap).change(recs, cfg, dims); } TrimStrategy::Truncate { suffix } => { - let mut truncate = Width::truncate(self.max).priority::(); + let mut truncate = Width::truncate(self.width_max).priority::(); if let Some(suffix) = suffix { truncate = truncate.suffix(suffix).suffix_try_color(true); } @@ -366,6 +443,67 @@ impl TableOption, ColoredConfig> for } } +fn trim_as_header( + recs: &mut VecRecords>, + cfg: &mut ColoredConfig, + dims: &mut CompleteDimensionVecRecords, + trim: TableTrim, +) { + if recs.is_empty() { + return; + } + + let headers = recs[0].to_owned(); + let headers_widths = headers + .iter() + .map(CellInfo::width) + .map(|v| v + trim.pad) + .collect::>(); + let min_width_use = get_total_width2(&headers_widths, cfg); + let mut free_width = trim.width_max.saturating_sub(min_width_use); + + // even though it's safe to trim columns by header there might be left unused space + // so we do use it if possible prioritizing left columns + + for (i, head_width) in headers_widths.into_iter().enumerate() { + let head_width = head_width - trim.pad; + let column_width = trim.width[i] - trim.pad; // safe to assume width is bigger then paddding + + let mut use_width = head_width; + if free_width > 0 { + // it's safe to assume that column_width is always bigger or equal to head_width + debug_assert!(column_width >= head_width); + + let additional_width = min(free_width, column_width - head_width); + free_width -= additional_width; + use_width += additional_width; + } + + match &trim.strategy { + TrimStrategy::Wrap { try_to_keep_words } => { + let mut wrap = Width::wrap(use_width); + if *try_to_keep_words { + wrap = wrap.keep_words(); + } + + Modify::new(Columns::single(i)) + .with(wrap) + .change(recs, cfg, dims); + } + TrimStrategy::Truncate { suffix } => { + let mut truncate = Width::truncate(use_width); + if let Some(suffix) = suffix { + truncate = truncate.suffix(suffix).suffix_try_color(true); + } + + Modify::new(Columns::single(i)) + .with(truncate) + .change(recs, cfg, dims); + } + } + } +} + fn align_table( table: &mut Table, alignments: Alignments, @@ -459,19 +597,25 @@ fn load_theme( fn maybe_truncate_columns( data: &mut NuRecords, - theme: &TableTheme, + cfg: &NuTableConfig, termwidth: usize, pad: usize, ) -> Vec { const TERMWIDTH_THRESHOLD: usize = 120; - let truncate = if termwidth > TERMWIDTH_THRESHOLD { + let preserve_content = termwidth > TERMWIDTH_THRESHOLD; + let has_header = cfg.with_header && data.count_rows() > 1; + let is_header_on_border = has_header && cfg.header_on_border; + + let truncate = if is_header_on_border { + truncate_columns_by_head + } else if preserve_content { truncate_columns_by_columns } else { truncate_columns_by_content }; - truncate(data, theme, pad, termwidth) + truncate(data, &cfg.theme, pad, termwidth) } // VERSION where we are showing AS LITTLE COLUMNS AS POSSIBLE but WITH AS MUCH CONTENT AS POSSIBLE. @@ -626,6 +770,83 @@ fn truncate_columns_by_columns( widths } +// VERSION where we are showing AS LITTLE COLUMNS AS POSSIBLE but WITH AS MUCH CONTENT AS POSSIBLE. +fn truncate_columns_by_head( + data: &mut NuRecords, + theme: &TableTheme, + pad: usize, + termwidth: usize, +) -> Vec { + const TRAILING_COLUMN_WIDTH: usize = 5; + + let config = get_config(theme, false, None); + let mut widths = build_width(&*data, pad); + let total_width = get_total_width2(&widths, &config); + if total_width <= termwidth { + return widths; + } + + if data.is_empty() { + return widths; + } + + let head = &data[0]; + + let borders = config.get_borders(); + let has_vertical = borders.has_vertical(); + + let mut width = borders.has_left() as usize + borders.has_right() as usize; + let mut truncate_pos = 0; + for (i, column_header) in head.iter().enumerate() { + let column_header_width = Cell::width(column_header); + width += column_header_width + pad; + + if i > 0 { + width += has_vertical as usize; + } + + if width >= termwidth { + width -= column_header_width + (i > 0 && has_vertical) as usize + pad; + break; + } + + truncate_pos += 1; + } + + // we don't need any truncation then (is it possible?) + if truncate_pos == head.len() { + return widths; + } + + if truncate_pos == 0 { + return vec![]; + } + + truncate_columns(data, truncate_pos); + widths.truncate(truncate_pos); + + // Append columns with a trailing column + + let min_width = width; + + let diff = termwidth - min_width; + let can_trailing_column_be_pushed = diff > TRAILING_COLUMN_WIDTH + has_vertical as usize; + + if !can_trailing_column_be_pushed { + if data.count_columns() == 1 { + return vec![]; + } + + truncate_columns(data, data.count_columns() - 1); + widths.pop(); + } + + push_empty_column(data); + widths.push(3 + pad); + + widths +} + /// The same as [`tabled::peaker::PriorityMax`] but prioritizes left columns first in case of equal width. #[derive(Debug, Default, Clone)] pub struct PriorityMax; @@ -713,9 +934,8 @@ impl TableOption, ColoredConfig> for SetDi } // it assumes no spans is used. +// todo: Could be replaced by Dimension impl usage fn build_width(records: &NuRecords, pad: usize) -> Vec { - use tabled::grid::records::vec_records::Cell; - let count_columns = records.count_columns(); let mut widths = vec![0; count_columns]; for columns in records.iter_rows() { @@ -728,50 +948,156 @@ fn build_width(records: &NuRecords, pad: usize) -> Vec { widths } -struct HeaderMove((usize, usize), bool); +struct GetRow(usize, Vec); -impl TableOption, ColoredConfig> for HeaderMove { +impl TableOption, ColoredConfig> for &mut GetRow { + fn change( + self, + recs: &mut NuRecords, + _: &mut ColoredConfig, + _: &mut CompleteDimensionVecRecords<'_>, + ) { + let row = self.0; + self.1 = recs[row].iter().map(|c| c.as_ref().to_owned()).collect(); + } +} + +struct GetRowSettings(usize, AlignmentHorizontal, Option); + +impl TableOption, ColoredConfig> + for &mut GetRowSettings +{ + fn change( + self, + _: &mut NuRecords, + cfg: &mut ColoredConfig, + _: &mut CompleteDimensionVecRecords<'_>, + ) { + let row = self.0; + self.1 = *cfg.get_alignment_horizontal(Entity::Row(row)); + self.2 = cfg + .get_colors() + .get_color((row, 0)) + .cloned() + .map(Color::from); + } +} + +struct SetLineHeaders { + line: usize, + columns: Vec, + alignment: AlignmentHorizontal, + color: Option, +} + +impl SetLineHeaders { + fn new( + line: usize, + columns: Vec, + alignment: AlignmentHorizontal, + color: Option, + ) -> Self { + Self { + line, + columns, + alignment, + color, + } + } +} + +impl TableOption, ColoredConfig> for SetLineHeaders { fn change( self, recs: &mut NuRecords, cfg: &mut ColoredConfig, dims: &mut CompleteDimensionVecRecords<'_>, ) { - let (row, line) = self.0; - if self.1 { - move_header_on_next(recs, cfg, dims, row, line); - } else { - move_header_on_prev(recs, cfg, dims, row, line); - } + let mut columns = self.columns; + match dims.get_widths() { + Some(widths) => { + columns = columns + .into_iter() + .zip(widths.iter().map(|w| w.checked_sub(2).unwrap_or(*w))) // exclude padding; which is generally 2 + .map(|(s, width)| Truncate::truncate_text(&s, width).into_owned()) + .collect(); + } + None => { + // we don't have widths cached; which means that NO width adjustments were done + // which means we are OK to leave columns as they are. + // + // but we actually always have to have widths at this point + } + }; + + set_column_names( + recs, + cfg, + dims, + columns, + self.line, + self.alignment, + self.color, + ) } } -fn move_header_on_next( - recs: &mut NuRecords, - cfg: &mut ColoredConfig, - dims: &mut CompleteDimensionVecRecords<'_>, +struct MoveRowNext { row: usize, line: usize, -) { +} + +impl MoveRowNext { + fn new(row: usize, line: usize) -> Self { + Self { row, line } + } +} + +struct MoveRowPrev { + row: usize, + line: usize, +} + +impl MoveRowPrev { + fn new(row: usize, line: usize) -> Self { + Self { row, line } + } +} + +impl TableOption, ColoredConfig> for MoveRowNext { + fn change( + self, + recs: &mut NuRecords, + cfg: &mut ColoredConfig, + _: &mut CompleteDimensionVecRecords<'_>, + ) { + row_shift_next(recs, cfg, self.row, self.line); + } +} + +impl TableOption, ColoredConfig> for MoveRowPrev { + fn change( + self, + recs: &mut NuRecords, + cfg: &mut ColoredConfig, + _: &mut CompleteDimensionVecRecords<'_>, + ) { + row_shift_prev(recs, cfg, self.row, self.line); + } +} + +fn row_shift_next(recs: &mut NuRecords, cfg: &mut ColoredConfig, row: usize, line: usize) { let count_rows = recs.count_rows(); let count_columns = recs.count_columns(); let has_line = cfg.has_horizontal(line, count_rows); let has_next_line = cfg.has_horizontal(line + 1, count_rows); - let align = *cfg.get_alignment_horizontal(Entity::Row(row)); - let color = cfg - .get_colors() - .get_color((row, 0)) - .cloned() - .map(Color::from); - if !has_line && !has_next_line { return; } if !has_line { - let head = remove_row(recs, row); + let _ = remove_row(recs, row); let count_rows = recs.count_rows(); - set_column_names(recs, cfg, dims, head, line, align, color); shift_alignments_down(cfg, row, count_rows, count_columns); shift_colors_down(cfg, row, count_rows, count_columns); shift_lines_up(cfg, count_rows, &[line + 1]); @@ -779,47 +1105,31 @@ fn move_header_on_next( return; } - let head = remove_row(recs, row); + let _ = remove_row(recs, row); let count_rows = recs.count_rows(); - set_column_names(recs, cfg, dims, head, line, align, color); shift_alignments_down(cfg, row, count_rows, count_columns); shift_colors_down(cfg, row, count_rows, count_columns); remove_lines(cfg, count_rows, &[line + 1]); shift_lines_up(cfg, count_rows, &[count_rows]); } -fn move_header_on_prev( - recs: &mut NuRecords, - cfg: &mut ColoredConfig, - dims: &mut CompleteDimensionVecRecords<'_>, - row: usize, - line: usize, -) { +fn row_shift_prev(recs: &mut NuRecords, cfg: &mut ColoredConfig, row: usize, line: usize) { let count_rows = recs.count_rows(); let count_columns = recs.count_columns(); let has_line = cfg.has_horizontal(line, count_rows); let has_prev_line = cfg.has_horizontal(line - 1, count_rows); - let align = *cfg.get_alignment_horizontal(Entity::Row(row)); - let color = cfg - .get_colors() - .get_color((row, 0)) - .cloned() - .map(Color::from); - if !has_line && !has_prev_line { return; } if !has_line { - let head = remove_row(recs, row); + let _ = remove_row(recs, row); // shift_lines_down(table, &[line - 1]); - set_column_names(recs, cfg, dims, head, line - 1, align, color); return; } - let head = remove_row(recs, row); + let _ = remove_row(recs, row); let count_rows = count_rows - 1; - set_column_names(recs, cfg, dims, head, line - 1, align, color); shift_alignments_down(cfg, row, count_rows, count_columns); shift_colors_down(cfg, row, count_rows, count_columns); remove_lines(cfg, count_rows, &[line - 1]); diff --git a/crates/nu-table/src/types/expanded.rs b/crates/nu-table/src/types/expanded.rs index 9472cc2846..668e5ea25c 100644 --- a/crates/nu-table/src/types/expanded.rs +++ b/crates/nu-table/src/types/expanded.rs @@ -102,9 +102,7 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult { } for (row, item) in input.iter().enumerate() { - if nu_utils::ctrl_c::was_pressed(&cfg.opts.ctrlc) { - return Ok(None); - } + cfg.opts.signals.check(cfg.opts.span)?; if let Value::Error { error, .. } = item { return Err(*error.clone()); @@ -143,9 +141,7 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult { } for (row, item) in input.iter().enumerate() { - if nu_utils::ctrl_c::was_pressed(&cfg.opts.ctrlc) { - return Ok(None); - } + cfg.opts.signals.check(cfg.opts.span)?; if let Value::Error { error, .. } = item { return Err(*error.clone()); @@ -230,9 +226,7 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult { } for (row, item) in input.iter().enumerate() { - if nu_utils::ctrl_c::was_pressed(&cfg.opts.ctrlc) { - return Ok(None); - } + cfg.opts.signals.check(cfg.opts.span)?; if let Value::Error { error, .. } = item { return Err(*error.clone()); @@ -353,9 +347,7 @@ fn expanded_table_kv(record: &Record, cfg: Cfg<'_>) -> StringResult { let mut data = Vec::with_capacity(record.len()); for (key, value) in record { - if nu_utils::ctrl_c::was_pressed(&cfg.opts.ctrlc) { - return Ok(None); - } + cfg.opts.signals.check(cfg.opts.span)?; let (value, is_expanded) = match expand_table_value(value, value_width, &cfg)? { Some(val) => val, diff --git a/crates/nu-table/src/types/general.rs b/crates/nu-table/src/types/general.rs index 4a6cd302e8..66048267d7 100644 --- a/crates/nu-table/src/types/general.rs +++ b/crates/nu-table/src/types/general.rs @@ -43,9 +43,7 @@ fn create_table(input: &[Value], opts: TableOpts<'_>) -> Result, fn kv_table(record: &Record, opts: TableOpts<'_>) -> StringResult { let mut data = vec![Vec::with_capacity(2); record.len()]; for ((column, value), row) in record.iter().zip(data.iter_mut()) { - if nu_utils::ctrl_c::was_pressed(&opts.ctrlc) { - return Ok(None); - } + opts.signals.check(opts.span)?; let value = nu_value_to_string_colored(value, opts.config, opts.style_computer); @@ -123,9 +121,7 @@ fn to_table_with_header( } for (row, item) in input.iter().enumerate() { - if nu_utils::ctrl_c::was_pressed(&opts.ctrlc) { - return Ok(None); - } + opts.signals.check(opts.span)?; if let Value::Error { error, .. } = item { return Err(*error.clone()); @@ -158,9 +154,7 @@ fn to_table_with_no_header( table.set_index_style(get_index_style(opts.style_computer)); for (row, item) in input.iter().enumerate() { - if nu_utils::ctrl_c::was_pressed(&opts.ctrlc) { - return Ok(None); - } + opts.signals.check(opts.span)?; if let Value::Error { error, .. } = item { return Err(*error.clone()); diff --git a/crates/nu-table/src/types/mod.rs b/crates/nu-table/src/types/mod.rs index 10289c3b81..163e4b0c81 100644 --- a/crates/nu-table/src/types/mod.rs +++ b/crates/nu-table/src/types/mod.rs @@ -8,8 +8,7 @@ pub use general::JustTable; use crate::{common::INDEX_COLUMN_NAME, NuTable}; use nu_color_config::StyleComputer; -use nu_protocol::{Config, Span, TableIndexMode, TableMode}; -use std::sync::{atomic::AtomicBool, Arc}; +use nu_protocol::{Config, Signals, Span, TableIndexMode, TableMode}; pub struct TableOutput { pub table: NuTable, @@ -29,7 +28,7 @@ impl TableOutput { #[derive(Debug, Clone)] pub struct TableOpts<'a> { - ctrlc: Option>, + signals: &'a Signals, config: &'a Config, style_computer: &'a StyleComputer<'a>, span: Span, @@ -45,7 +44,7 @@ impl<'a> TableOpts<'a> { pub fn new( config: &'a Config, style_computer: &'a StyleComputer<'a>, - ctrlc: Option>, + signals: &'a Signals, span: Span, width: usize, indent: (usize, usize), @@ -54,7 +53,7 @@ impl<'a> TableOpts<'a> { index_remove: bool, ) -> Self { Self { - ctrlc, + signals, config, style_computer, span, diff --git a/crates/nu-term-grid/Cargo.toml b/crates/nu-term-grid/Cargo.toml index 47f6724b53..0229f1f0f0 100644 --- a/crates/nu-term-grid/Cargo.toml +++ b/crates/nu-term-grid/Cargo.toml @@ -5,12 +5,12 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-term-grid" edition = "2021" license = "MIT" name = "nu-term-grid" -version = "0.94.3" +version = "0.95.1" [lib] bench = false [dependencies] -nu-utils = { path = "../nu-utils", version = "0.94.3" } +nu-utils = { path = "../nu-utils", version = "0.95.1" } -unicode-width = { workspace = true } +unicode-width = { workspace = true } \ No newline at end of file diff --git a/crates/nu-term-grid/README.md b/crates/nu-term-grid/README.md new file mode 100644 index 0000000000..5bdd32077d --- /dev/null +++ b/crates/nu-term-grid/README.md @@ -0,0 +1,5 @@ +Implementation of the layout engine for the `grid` command. + +## Internal Nushell crate + +This crate implements components of Nushell and is not designed to support plugin authors or other users directly. diff --git a/crates/nu-term-grid/src/lib.rs b/crates/nu-term-grid/src/lib.rs index 79b146593f..ca8ccd4e23 100644 --- a/crates/nu-term-grid/src/lib.rs +++ b/crates/nu-term-grid/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] pub mod grid; pub use grid::Grid; diff --git a/crates/nu-test-support/Cargo.toml b/crates/nu-test-support/Cargo.toml index 86a1bf35e7..1b5f5df7ba 100644 --- a/crates/nu-test-support/Cargo.toml +++ b/crates/nu-test-support/Cargo.toml @@ -5,17 +5,17 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-test-suppor edition = "2021" license = "MIT" name = "nu-test-support" -version = "0.94.3" +version = "0.95.1" [lib] doctest = false bench = false [dependencies] -nu-path = { path = "../nu-path", version = "0.94.3" } -nu-glob = { path = "../nu-glob", version = "0.94.3" } -nu-utils = { path = "../nu-utils", version = "0.94.3" } +nu-path = { path = "../nu-path", version = "0.95.1" } +nu-glob = { path = "../nu-glob", version = "0.95.1" } +nu-utils = { path = "../nu-utils", version = "0.95.1" } num-format = { workspace = true } which = { workspace = true } -tempfile = { workspace = true } +tempfile = { workspace = true } \ No newline at end of file diff --git a/crates/nu-test-support/README.md b/crates/nu-test-support/README.md new file mode 100644 index 0000000000..f384b15067 --- /dev/null +++ b/crates/nu-test-support/README.md @@ -0,0 +1,7 @@ +This crate provides utilities for testing of Nushell + +Plugin authors should instead refer to `nu-plugin-test-support` + +## Internal Nushell crate + +This crate implements components of Nushell and is not designed to support plugin authors or other users directly. diff --git a/crates/nu-test-support/src/fs.rs b/crates/nu-test-support/src/fs.rs index 00321e6590..230e5eafe3 100644 --- a/crates/nu-test-support/src/fs.rs +++ b/crates/nu-test-support/src/fs.rs @@ -1,137 +1,5 @@ -use std::fmt::Display; +use nu_path::{AbsolutePath, AbsolutePathBuf, Path}; use std::io::Read; -use std::ops::Div; -use std::path::{Path, PathBuf}; - -pub struct AbsoluteFile { - inner: PathBuf, -} - -impl AbsoluteFile { - pub fn new(path: impl AsRef) -> AbsoluteFile { - let path = path.as_ref(); - - if !path.is_absolute() { - panic!( - "AbsoluteFile::new must take an absolute path :: {}", - path.display() - ) - } else if path.is_dir() { - // At the moment, this is not an invariant, but rather a way to catch bugs - // in tests. - panic!( - "AbsoluteFile::new must not take a directory :: {}", - path.display() - ) - } else { - AbsoluteFile { - inner: path.to_path_buf(), - } - } - } - - pub fn dir(&self) -> AbsolutePath { - AbsolutePath::new(if let Some(parent) = self.inner.parent() { - parent - } else { - unreachable!("Internal error: could not get parent in dir") - }) - } -} - -impl From for PathBuf { - fn from(file: AbsoluteFile) -> Self { - file.inner - } -} - -pub struct AbsolutePath { - pub inner: PathBuf, -} - -impl AbsolutePath { - pub fn new(path: impl AsRef) -> AbsolutePath { - let path = path.as_ref(); - - if path.is_absolute() { - AbsolutePath { - inner: path.to_path_buf(), - } - } else { - panic!("AbsolutePath::new must take an absolute path") - } - } -} - -impl Div<&str> for &AbsolutePath { - type Output = AbsolutePath; - - fn div(self, rhs: &str) -> Self::Output { - let parts = rhs.split('/'); - let mut result = self.inner.clone(); - - for part in parts { - result = result.join(part); - } - - AbsolutePath::new(result) - } -} - -impl AsRef for AbsolutePath { - fn as_ref(&self) -> &Path { - self.inner.as_path() - } -} - -pub struct RelativePath { - inner: PathBuf, -} - -impl RelativePath { - pub fn new(path: impl Into) -> RelativePath { - let path = path.into(); - - if path.is_relative() { - RelativePath { inner: path } - } else { - panic!("RelativePath::new must take a relative path") - } - } -} - -impl> Div for &RelativePath { - type Output = RelativePath; - - fn div(self, rhs: T) -> Self::Output { - let parts = rhs.as_ref().split('/'); - let mut result = self.inner.clone(); - - for part in parts { - result = result.join(part); - } - - RelativePath::new(result) - } -} - -impl Display for AbsoluteFile { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.inner.display()) - } -} - -impl Display for AbsolutePath { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.inner.display()) - } -} - -impl Display for RelativePath { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.inner.display()) - } -} pub enum Stub<'a> { FileWithContent(&'a str, &'a str), @@ -140,7 +8,7 @@ pub enum Stub<'a> { FileWithPermission(&'a str, bool), } -pub fn file_contents(full_path: impl AsRef) -> String { +pub fn file_contents(full_path: impl AsRef) -> String { let mut file = std::fs::File::open(full_path.as_ref()).expect("can not open file"); let mut contents = String::new(); file.read_to_string(&mut contents) @@ -148,7 +16,7 @@ pub fn file_contents(full_path: impl AsRef) -> String { contents } -pub fn file_contents_binary(full_path: impl AsRef) -> Vec { +pub fn file_contents_binary(full_path: impl AsRef) -> Vec { let mut file = std::fs::File::open(full_path.as_ref()).expect("can not open file"); let mut contents = Vec::new(); file.read_to_end(&mut contents).expect("can not read file"); @@ -167,56 +35,32 @@ pub fn line_ending() -> String { } } -pub fn delete_file_at(full_path: impl AsRef) { - let full_path = full_path.as_ref(); - - if full_path.exists() { - std::fs::remove_file(full_path).expect("can not delete file"); - } +pub fn files_exist_at(files: Vec>, path: impl AsRef) -> bool { + let path = path.as_ref(); + files.iter().all(|f| path.join(f.as_ref()).exists()) } -pub fn create_file_at(full_path: impl AsRef) -> Result<(), std::io::Error> { - let full_path = full_path.as_ref(); - - if full_path.parent().is_some() { - panic!("path exists"); - } - - std::fs::write(full_path, b"fake data") -} - -pub fn copy_file_to(source: &str, destination: &str) { - std::fs::copy(source, destination).expect("can not copy file"); -} - -pub fn files_exist_at(files: Vec>, path: impl AsRef) -> bool { - files.iter().all(|f| { - let mut loc = PathBuf::from(path.as_ref()); - loc.push(f); - loc.exists() - }) -} - -pub fn delete_directory_at(full_path: &str) { - std::fs::remove_dir_all(PathBuf::from(full_path)).expect("can not remove directory"); -} - -pub fn executable_path() -> PathBuf { +pub fn executable_path() -> AbsolutePathBuf { let mut path = binaries(); path.push("nu"); path } -pub fn installed_nu_path() -> PathBuf { +pub fn installed_nu_path() -> AbsolutePathBuf { let path = std::env::var_os(crate::NATIVE_PATH_ENV_VAR); - which::which_in("nu", path, ".").unwrap_or_else(|_| executable_path()) + if let Ok(path) = which::which_in("nu", path, ".") { + AbsolutePathBuf::try_from(path).expect("installed nushell path is absolute") + } else { + executable_path() + } } -pub fn root() -> PathBuf { +pub fn root() -> AbsolutePathBuf { let manifest_dir = if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") { - PathBuf::from(manifest_dir) + AbsolutePathBuf::try_from(manifest_dir).expect("CARGO_MANIFEST_DIR is not an absolute path") } else { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) + AbsolutePathBuf::try_from(env!("CARGO_MANIFEST_DIR")) + .expect("CARGO_MANIFEST_DIR is not an absolute path") }; let test_path = manifest_dir.join("Cargo.lock"); @@ -228,11 +72,11 @@ pub fn root() -> PathBuf { .expect("Couldn't find the debug binaries directory") .parent() .expect("Couldn't find the debug binaries directory") - .to_path_buf() + .into() } } -pub fn binaries() -> PathBuf { +pub fn binaries() -> AbsolutePathBuf { let build_target = std::env::var("CARGO_BUILD_TARGET").unwrap_or_default(); let profile = if let Ok(env_profile) = std::env::var("NUSHELL_CARGO_PROFILE") { @@ -244,27 +88,27 @@ pub fn binaries() -> PathBuf { }; std::env::var("CARGO_TARGET_DIR") - .map(PathBuf::from) - .unwrap_or_else(|_| root().join("target")) + .ok() + .and_then(|p| AbsolutePathBuf::try_from(p).ok()) + .unwrap_or_else(|| root().join("target")) .join(build_target) .join(profile) } -pub fn fixtures() -> PathBuf { - root().join("tests").join("fixtures") +pub fn fixtures() -> AbsolutePathBuf { + let mut path = root(); + path.push("tests"); + path.push("fixtures"); + path } -pub fn assets() -> PathBuf { - root().join("tests/assets") +pub fn assets() -> AbsolutePathBuf { + let mut path = root(); + path.push("tests"); + path.push("assets"); + path } -pub fn in_directory(str: impl AsRef) -> String { - let path = str.as_ref(); - let path = if path.is_relative() { - root().join(path) - } else { - path.to_path_buf() - }; - - path.display().to_string() +pub fn in_directory(path: impl AsRef) -> AbsolutePathBuf { + root().join(path) } diff --git a/crates/nu-test-support/src/lib.rs b/crates/nu-test-support/src/lib.rs index 5319830920..c6cadbcbd8 100644 --- a/crates/nu-test-support/src/lib.rs +++ b/crates/nu-test-support/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] pub mod commands; pub mod fs; pub mod locale_override; diff --git a/crates/nu-test-support/src/macros.rs b/crates/nu-test-support/src/macros.rs index c18dd5ac81..6fd3a8c307 100644 --- a/crates/nu-test-support/src/macros.rs +++ b/crates/nu-test-support/src/macros.rs @@ -234,34 +234,29 @@ macro_rules! nu_with_plugins { } use crate::{Outcome, NATIVE_PATH_ENV_VAR}; -use std::ffi::OsStr; +use nu_path::{AbsolutePath, AbsolutePathBuf, Path}; use std::{ - path::Path, + ffi::OsStr, process::{Command, Stdio}, }; use tempfile::tempdir; #[derive(Default)] pub struct NuOpts { - pub cwd: Option, + pub cwd: Option, pub locale: Option, + pub envs: Option>, pub collapse_output: Option, + pub use_ir: Option, } pub fn nu_run_test(opts: NuOpts, commands: impl AsRef, with_std: bool) -> Outcome { - 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| { - panic!( - "Couldn't canonicalize dummy binaries path {}: {:?}", - test_bins.display(), - e - ) - }); + let test_bins = crate::fs::binaries() + .canonicalize() + .expect("Could not canonicalize dummy binaries path"); let mut paths = crate::shell_os_paths(); - paths.insert(0, test_bins); + paths.insert(0, test_bins.into()); let commands = commands.as_ref().lines().collect::>().join("; "); @@ -270,7 +265,7 @@ pub fn nu_run_test(opts: NuOpts, commands: impl AsRef, with_std: bool) -> O Err(_) => panic!("Couldn't join paths for PATH var."), }; - let target_cwd = opts.cwd.unwrap_or(".".to_string()); + let target_cwd = opts.cwd.unwrap_or_else(crate::fs::root); let locale = opts.locale.unwrap_or("en_US.UTF-8".to_string()); let executable_path = crate::fs::executable_path(); @@ -278,16 +273,32 @@ pub fn nu_run_test(opts: NuOpts, commands: impl AsRef, with_std: bool) -> O command .env(nu_utils::locale::LOCALE_OVERRIDE_ENV_VAR, locale) .env(NATIVE_PATH_ENV_VAR, paths_joined); + + if let Some(envs) = opts.envs { + command.envs(envs); + } + // Ensure that the user's config doesn't interfere with the tests command.arg("--no-config-file"); if !with_std { command.arg("--no-std-lib"); } + // Use plain errors to help make error text matching more consistent + command.args(["--error-style", "plain"]); command .arg(format!("-c {}", escape_quote_string(&commands))) .stdout(Stdio::piped()) .stderr(Stdio::piped()); + // Explicitly set NU_USE_IR + if let Some(use_ir) = opts.use_ir { + if use_ir { + command.env("NU_USE_IR", "1"); + } else { + command.env_remove("NU_USE_IR"); + } + } + // Uncomment to debug the command being run: // println!("=== command\n{command:?}\n"); @@ -365,10 +376,13 @@ where if !executable_path.exists() { executable_path = crate::fs::installed_nu_path(); } + let process = match setup_command(&executable_path, &target_cwd) .envs(envs) .arg("--commands") .arg(command) + // Use plain errors to help make error text matching more consistent + .args(["--error-style", "plain"]) .arg("--config") .arg(temp_config_file) .arg("--env-config") @@ -429,7 +443,7 @@ fn collapse_output(out: &str) -> String { out.replace('\n', "") } -fn setup_command(executable_path: &Path, target_cwd: &str) -> Command { +fn setup_command(executable_path: &AbsolutePath, target_cwd: &AbsolutePath) -> Command { let mut command = Command::new(executable_path); command diff --git a/crates/nu-test-support/src/playground.rs b/crates/nu-test-support/src/playground.rs index 375f192983..4d33dcebe5 100644 --- a/crates/nu-test-support/src/playground.rs +++ b/crates/nu-test-support/src/playground.rs @@ -6,5 +6,5 @@ mod play; mod tests; pub use director::Director; -pub use nu_process::{Executable, NuProcess, NuResult, Outcome}; +pub use nu_process::{Executable, NuProcess, Outcome}; pub use play::{Dirs, EnvironmentVariable, Playground}; diff --git a/crates/nu-test-support/src/playground/director.rs b/crates/nu-test-support/src/playground/director.rs index 3d04155c25..a361342495 100644 --- a/crates/nu-test-support/src/playground/director.rs +++ b/crates/nu-test-support/src/playground/director.rs @@ -83,7 +83,7 @@ impl Director { } impl Executable for Director { - fn execute(&mut self) -> NuResult { + fn execute(&mut self) -> Result { use std::process::Stdio; match self.executable() { diff --git a/crates/nu-test-support/src/playground/nu_process.rs b/crates/nu-test-support/src/playground/nu_process.rs index 0031a173e8..2eeac703c6 100644 --- a/crates/nu-test-support/src/playground/nu_process.rs +++ b/crates/nu-test-support/src/playground/nu_process.rs @@ -1,12 +1,13 @@ use super::EnvironmentVariable; use crate::fs::{binaries as test_bins_path, executable_path}; -use std::ffi::{OsStr, OsString}; -use std::fmt; -use std::path::Path; -use std::process::{Command, ExitStatus}; +use std::{ + ffi::{OsStr, OsString}, + fmt, + process::{Command, ExitStatus}, +}; pub trait Executable { - fn execute(&mut self) -> NuResult; + fn execute(&mut self) -> Result; } #[derive(Clone, Debug)] @@ -24,8 +25,6 @@ impl Outcome { } } -pub type NuResult = Result; - #[derive(Debug)] pub struct NuError { pub desc: String, @@ -69,14 +68,10 @@ impl NuProcess { self } - pub fn get_cwd(&self) -> Option<&Path> { - self.cwd.as_ref().map(Path::new) - } - pub fn construct(&self) -> Command { let mut command = Command::new(executable_path()); - if let Some(cwd) = self.get_cwd() { + if let Some(cwd) = &self.cwd { command.current_dir(cwd); } diff --git a/crates/nu-test-support/src/playground/play.rs b/crates/nu-test-support/src/playground/play.rs index 902997200e..12295c2690 100644 --- a/crates/nu-test-support/src/playground/play.rs +++ b/crates/nu-test-support/src/playground/play.rs @@ -1,8 +1,9 @@ use super::Director; -use crate::fs; -use crate::fs::Stub; +use crate::fs::{self, Stub}; use nu_glob::glob; -use std::path::{Path, PathBuf}; +#[cfg(not(target_arch = "wasm32"))] +use nu_path::Path; +use nu_path::{AbsolutePath, AbsolutePathBuf}; use std::str; use tempfile::{tempdir, TempDir}; @@ -22,46 +23,46 @@ impl EnvironmentVariable { } pub struct Playground<'a> { - root: TempDir, + _root: TempDir, tests: String, - cwd: PathBuf, - config: Option, + cwd: AbsolutePathBuf, + config: Option, environment_vars: Vec, dirs: &'a Dirs, } -#[derive(Default, Clone)] +#[derive(Clone)] pub struct Dirs { - pub root: PathBuf, - pub test: PathBuf, - pub fixtures: PathBuf, + pub root: AbsolutePathBuf, + pub test: AbsolutePathBuf, + pub fixtures: AbsolutePathBuf, } impl Dirs { - pub fn formats(&self) -> PathBuf { + pub fn formats(&self) -> AbsolutePathBuf { self.fixtures.join("formats") } - pub fn root(&self) -> &Path { - self.root.as_path() + pub fn root(&self) -> &AbsolutePath { + &self.root } - pub fn test(&self) -> &Path { - self.test.as_path() + pub fn test(&self) -> &AbsolutePath { + &self.test } } impl<'a> Playground<'a> { - pub fn root(&self) -> &Path { - self.root.path() + pub fn root(&self) -> &AbsolutePath { + &self.dirs.root } - pub fn cwd(&self) -> &Path { + pub fn cwd(&self) -> &AbsolutePath { &self.cwd } pub fn back_to_playground(&mut self) -> &mut Self { - self.cwd = PathBuf::from(self.root()).join(self.tests.clone()); + self.cwd = self.root().join(&self.tests); self } @@ -70,68 +71,46 @@ impl<'a> Playground<'a> { } pub fn setup(topic: &str, block: impl FnOnce(Dirs, &mut Playground)) { - let root = tempdir().expect("Couldn't create a tempdir"); - let nuplay_dir = root.path().join(topic); + let temp = tempdir().expect("Could not create a tempdir"); - if PathBuf::from(&nuplay_dir).exists() { - std::fs::remove_dir_all(PathBuf::from(&nuplay_dir)).expect("can not remove directory"); + let root = AbsolutePathBuf::try_from(temp.path()) + .expect("Tempdir is not an absolute path") + .canonicalize() + .expect("Could not canonicalize tempdir"); + + let test = root.join(topic); + if test.exists() { + std::fs::remove_dir_all(&test).expect("Could not remove directory"); } + std::fs::create_dir(&test).expect("Could not create directory"); + let test = test + .canonicalize() + .expect("Could not canonicalize test path"); - std::fs::create_dir(PathBuf::from(&nuplay_dir)).expect("can not create directory"); - - let fixtures = fs::fixtures(); - 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| { - panic!( - "Couldn't canonicalize fixtures path {}: {:?}", - fixtures.display(), - e - ) - }); - - let mut playground = Playground { - root, - tests: topic.to_string(), - cwd: nuplay_dir, - config: None, - environment_vars: Vec::default(), - dirs: &Dirs::default(), - }; - - let playground_root = playground.root.path(); - - 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| { - panic!( - "Couldn't canonicalize tests root path {}: {:?}", - playground_root.display(), - e - ) - }); + let fixtures = fs::fixtures() + .canonicalize() + .expect("Could not canonicalize fixtures path"); let dirs = Dirs { - root, - test, - fixtures, + root: root.into(), + test: test.as_path().into(), + fixtures: fixtures.into(), }; - playground.dirs = &dirs; + let mut playground = Playground { + _root: temp, + tests: topic.to_string(), + cwd: test.into(), + config: None, + environment_vars: Vec::default(), + dirs: &dirs, + }; block(dirs.clone(), &mut playground); } - pub fn with_config(&mut self, source_file: impl AsRef) -> &mut Self { - self.config = Some(source_file.as_ref().to_path_buf()); + pub fn with_config(&mut self, source_file: AbsolutePathBuf) -> &mut Self { + self.config = Some(source_file); self } @@ -205,7 +184,6 @@ impl<'a> Playground<'a> { files .iter() .map(|f| { - let mut path = PathBuf::from(&self.cwd); let mut permission_set = false; let mut write_able = true; let (file_name, contents) = match *f { @@ -227,7 +205,7 @@ impl<'a> Playground<'a> { } }; - path.push(file_name); + let path = self.cwd.join(file_name); std::fs::write(&path, contents.as_bytes()).expect("can not create file"); if permission_set { @@ -252,7 +230,7 @@ impl<'a> Playground<'a> { self } - pub fn glob_vec(pattern: &str) -> Vec { + pub fn glob_vec(pattern: &str) -> Vec { let glob = glob(pattern); glob.expect("invalid pattern") diff --git a/crates/nu-test-support/src/playground/tests.rs b/crates/nu-test-support/src/playground/tests.rs index cc4daa597b..7c2be0237e 100644 --- a/crates/nu-test-support/src/playground/tests.rs +++ b/crates/nu-test-support/src/playground/tests.rs @@ -1,11 +1,4 @@ use crate::playground::Playground; -use std::path::{Path, PathBuf}; - -fn path(p: &Path) -> PathBuf { - let cwd = std::env::current_dir().expect("Could not get current working directory."); - nu_path::canonicalize_with(p, cwd) - .unwrap_or_else(|e| panic!("Couldn't canonicalize path {}: {:?}", p.display(), e)) -} #[test] fn current_working_directory_in_sandbox_directory_created() { @@ -13,7 +6,7 @@ fn current_working_directory_in_sandbox_directory_created() { let original_cwd = dirs.test(); nu.within("some_directory_within"); - assert_eq!(path(nu.cwd()), original_cwd.join("some_directory_within")); + assert_eq!(nu.cwd(), original_cwd.join("some_directory_within")); }) } @@ -25,6 +18,6 @@ fn current_working_directory_back_to_root_from_anywhere() { nu.within("some_directory_within"); nu.back_to_playground(); - assert_eq!(path(nu.cwd()), *original_cwd); + assert_eq!(nu.cwd(), original_cwd); }) } diff --git a/crates/nu-utils/Cargo.toml b/crates/nu-utils/Cargo.toml index 0677f655d0..60b4cb21aa 100644 --- a/crates/nu-utils/Cargo.toml +++ b/crates/nu-utils/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" license = "MIT" name = "nu-utils" repository = "https://github.com/nushell/nushell/tree/main/crates/nu-utils" -version = "0.94.3" +version = "0.95.1" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [[bin]] @@ -29,4 +29,4 @@ unicase = "2.7.0" crossterm_winapi = "0.9" [target.'cfg(unix)'.dependencies] -nix = { workspace = true, default-features = false, features = ["user", "fs"] } +nix = { workspace = true, default-features = false, features = ["user", "fs"] } \ No newline at end of file diff --git a/crates/nu-utils/README.md b/crates/nu-utils/README.md new file mode 100644 index 0000000000..de50a8ae93 --- /dev/null +++ b/crates/nu-utils/README.md @@ -0,0 +1,6 @@ +Collection of small utilities that are shared across Nushell crates. + +This crate should compile early in the crate graph and thus not depend on major dependencies or core-nushell crates itself. +## Internal Nushell crate + +This crate implements components of Nushell and is not designed to support plugin authors or other users directly. diff --git a/crates/nu-utils/src/ctrl_c.rs b/crates/nu-utils/src/ctrl_c.rs deleted file mode 100644 index 72719139ad..0000000000 --- a/crates/nu-utils/src/ctrl_c.rs +++ /dev/null @@ -1,13 +0,0 @@ -use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, -}; - -/// Returns true if Nu has received a SIGINT signal / ctrl+c event -pub fn was_pressed(ctrlc: &Option>) -> bool { - if let Some(ctrlc) = ctrlc { - ctrlc.load(Ordering::SeqCst) - } else { - false - } -} diff --git a/crates/nu-utils/src/lib.rs b/crates/nu-utils/src/lib.rs index f1a915290b..0c021f1dc9 100644 --- a/crates/nu-utils/src/lib.rs +++ b/crates/nu-utils/src/lib.rs @@ -1,5 +1,5 @@ +#![doc = include_str!("../README.md")] mod casing; -pub mod ctrl_c; mod deansi; pub mod emoji; pub mod filesystem; diff --git a/crates/nu-utils/src/sample_config/default_config.nu b/crates/nu-utils/src/sample_config/default_config.nu index 34418feb16..3c3e57fa95 100644 --- a/crates/nu-utils/src/sample_config/default_config.nu +++ b/crates/nu-utils/src/sample_config/default_config.nu @@ -1,6 +1,6 @@ # Nushell Config File # -# version = "0.94.3" +# version = "0.95.1" # For more information on defining custom themes, see # https://www.nushell.sh/book/coloring_and_theming.html @@ -47,7 +47,8 @@ let dark_theme = { shape_flag: blue_bold shape_float: purple_bold # shapes are used to change the cli syntax highlighting - shape_garbage: { fg: white bg: red attr: b} + shape_garbage: { fg: white bg: red attr: b } + shape_glob_interpolation: cyan_bold shape_globpattern: cyan_bold shape_int: purple_bold shape_internalcall: cyan_bold @@ -113,7 +114,8 @@ let light_theme = { shape_flag: blue_bold shape_float: purple_bold # shapes are used to change the cli syntax highlighting - shape_garbage: { fg: white bg: red attr: b} + shape_garbage: { fg: white bg: red attr: b } + shape_glob_interpolation: cyan_bold shape_globpattern: cyan_bold shape_int: purple_bold shape_internalcall: cyan_bold @@ -225,9 +227,9 @@ $env.config = { color_config: $dark_theme # if you want a more interesting theme, you can replace the empty record with `$dark_theme`, `$light_theme` or another custom record use_grid_icons: true - footer_mode: "25" # always, never, number_of_rows, auto + footer_mode: 25 # always, never, number_of_rows, auto float_precision: 2 # the precision for displaying floats in tables - buffer_editor: "" # command that will be used to edit the current line buffer with ctrl+o, if unset fallback to $env.EDITOR and $env.VISUAL + buffer_editor: null # command that will be used to edit the current line buffer with ctrl+o, if unset fallback to $env.EDITOR and $env.VISUAL use_ansi_coloring: true bracketed_paste: true # enable bracketed paste, currently useless on windows edit_mode: emacs # emacs, vi diff --git a/crates/nu-utils/src/sample_config/default_env.nu b/crates/nu-utils/src/sample_config/default_env.nu index 57e255ed20..a686f6f99a 100644 --- a/crates/nu-utils/src/sample_config/default_env.nu +++ b/crates/nu-utils/src/sample_config/default_env.nu @@ -1,6 +1,6 @@ # Nushell Environment Config File # -# version = "0.94.3" +# version = "0.95.1" def create_left_prompt [] { let dir = match (do --ignore-shell-errors { $env.PWD | path relative-to $nu.home-path }) { @@ -77,6 +77,7 @@ $env.ENV_CONVERSIONS = { # The default for this is $nu.default-config-dir/scripts $env.NU_LIB_DIRS = [ ($nu.default-config-dir | path join 'scripts') # add /scripts + ($nu.data-dir | path join 'completions') # default home for nushell completions ] # Directories to search for plugin binaries when calling register @@ -97,4 +98,4 @@ $env.NU_PLUGIN_DIRS = [ # $env.PATH = ($env.PATH | uniq) # To load from a custom file you can use: -# source ($nu.default-config-dir | path join 'custom.nu') +# source ($nu.default-config-dir | path join 'custom.nu') \ No newline at end of file diff --git a/crates/nu-utils/src/utils.rs b/crates/nu-utils/src/utils.rs index 7a368f5cf7..8baf674075 100644 --- a/crates/nu-utils/src/utils.rs +++ b/crates/nu-utils/src/utils.rs @@ -1,4 +1,3 @@ -use log::info; use lscolors::LsColors; use std::io::{Result, Write}; @@ -393,31 +392,27 @@ pub fn get_ls_colors(lscolors_env_string: Option) -> LsColors { } // Log some performance metrics (green text with yellow timings) -pub fn perf( - msg: &str, - dur: std::time::Instant, - file: &str, - line: u32, - column: u32, - use_color: bool, -) { - if use_color { - info!( - "perf: {}:{}:{} \x1b[32m{}\x1b[0m took \x1b[33m{:?}\x1b[0m", - file, - line, - column, - msg, - dur.elapsed(), - ); - } else { - info!( - "perf: {}:{}:{} {} took {:?}", - file, - line, - column, - msg, - dur.elapsed(), - ); - } +#[macro_export] +macro_rules! perf { + ($msg:expr, $dur:expr, $use_color:expr) => { + if $use_color { + log::info!( + "perf: {}:{}:{} \x1b[32m{}\x1b[0m took \x1b[33m{:?}\x1b[0m", + file!(), + line!(), + column!(), + $msg, + $dur.elapsed(), + ); + } else { + log::info!( + "perf: {}:{}:{} {} took {:?}", + file!(), + line!(), + column!(), + $msg, + $dur.elapsed(), + ); + } + }; } diff --git a/crates/nu_plugin_custom_values/Cargo.toml b/crates/nu_plugin_custom_values/Cargo.toml index 2791cdaa6f..e5f8951429 100644 --- a/crates/nu_plugin_custom_values/Cargo.toml +++ b/crates/nu_plugin_custom_values/Cargo.toml @@ -10,10 +10,10 @@ name = "nu_plugin_custom_values" bench = false [dependencies] -nu-plugin = { path = "../nu-plugin", version = "0.94.3" } -nu-protocol = { path = "../nu-protocol", version = "0.94.3", features = ["plugin"] } +nu-plugin = { path = "../nu-plugin", version = "0.95.1" } +nu-protocol = { path = "../nu-protocol", version = "0.95.1", features = ["plugin"] } serde = { workspace = true, default-features = false } typetag = "0.2" [dev-dependencies] -nu-plugin-test-support = { path = "../nu-plugin-test-support", version = "0.94.3" } +nu-plugin-test-support = { path = "../nu-plugin-test-support", version = "0.95.1" } \ No newline at end of file diff --git a/crates/nu_plugin_custom_values/src/cool_custom_value.rs b/crates/nu_plugin_custom_values/src/cool_custom_value.rs index d838aa3f9e..ea0cf8ec92 100644 --- a/crates/nu_plugin_custom_values/src/cool_custom_value.rs +++ b/crates/nu_plugin_custom_values/src/cool_custom_value.rs @@ -87,7 +87,7 @@ impl CustomValue for CoolCustomValue { } else { Err(ShellError::CantFindColumn { col_name: column_name, - span: path_span, + span: Some(path_span), src_span: self_span, }) } diff --git a/crates/nu_plugin_custom_values/src/main.rs b/crates/nu_plugin_custom_values/src/main.rs index 9ab69135fb..65cc0b500f 100644 --- a/crates/nu_plugin_custom_values/src/main.rs +++ b/crates/nu_plugin_custom_values/src/main.rs @@ -42,6 +42,10 @@ impl CustomValuePlugin { } impl Plugin for CustomValuePlugin { + fn version(&self) -> String { + env!("CARGO_PKG_VERSION").into() + } + fn commands(&self) -> Vec>> { vec![ Box::new(Generate), diff --git a/crates/nu_plugin_example/Cargo.toml b/crates/nu_plugin_example/Cargo.toml index b2fd13abd9..08ba3e1566 100644 --- a/crates/nu_plugin_example/Cargo.toml +++ b/crates/nu_plugin_example/Cargo.toml @@ -5,7 +5,7 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu_plugin_exam edition = "2021" license = "MIT" name = "nu_plugin_example" -version = "0.94.3" +version = "0.95.1" [[bin]] name = "nu_plugin_example" @@ -15,9 +15,9 @@ bench = false bench = false [dependencies] -nu-plugin = { path = "../nu-plugin", version = "0.94.3" } -nu-protocol = { path = "../nu-protocol", version = "0.94.3", features = ["plugin"] } +nu-plugin = { path = "../nu-plugin", version = "0.95.1" } +nu-protocol = { path = "../nu-protocol", version = "0.95.1", features = ["plugin"] } [dev-dependencies] -nu-plugin-test-support = { path = "../nu-plugin-test-support", version = "0.94.3" } -nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.94.3" } +nu-plugin-test-support = { path = "../nu-plugin-test-support", version = "0.95.1" } +nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.95.1" } \ No newline at end of file diff --git a/crates/nu_plugin_example/README.md b/crates/nu_plugin_example/README.md index d4bc6dbe29..98283e9958 100644 --- a/crates/nu_plugin_example/README.md +++ b/crates/nu_plugin_example/README.md @@ -7,10 +7,12 @@ in order to create a binary that can be registered into nushell declaration list This subcommand demonstrates sending configuration from the nushell `$env.config` to a plugin. -To register from after building `nushell` run: +To make use of the plugin after building `nushell` run: ```nushell -register target/debug/nu_plugin_example +plugin add target/debug/nu_plugin_example +# or then either restart your current nushell session or run: +plugin use target/debug/nu_plugin_example ``` The configuration for the plugin lives in `$env.config.plugins.example`: diff --git a/crates/nu_plugin_example/src/commands/collect_bytes.rs b/crates/nu_plugin_example/src/commands/collect_bytes.rs index 398a1de4b1..e716493785 100644 --- a/crates/nu_plugin_example/src/commands/collect_bytes.rs +++ b/crates/nu_plugin_example/src/commands/collect_bytes.rs @@ -1,7 +1,7 @@ use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand}; use nu_protocol::{ - ByteStream, ByteStreamType, Category, Example, LabeledError, PipelineData, Signature, Type, - Value, + ByteStream, ByteStreamType, Category, Example, LabeledError, PipelineData, Signals, Signature, + Type, Value, }; use crate::ExamplePlugin; @@ -52,7 +52,7 @@ impl PluginCommand for CollectBytes { ByteStream::from_result_iter( input.into_iter().map(Value::coerce_into_binary), call.head, - None, + Signals::empty(), ByteStreamType::Unknown, ), None, diff --git a/crates/nu_plugin_example/src/commands/config.rs b/crates/nu_plugin_example/src/commands/config.rs index f549bd324f..7716e27537 100644 --- a/crates/nu_plugin_example/src/commands/config.rs +++ b/crates/nu_plugin_example/src/commands/config.rs @@ -1,10 +1,37 @@ +use std::path::PathBuf; + use nu_plugin::{EngineInterface, EvaluatedCall, SimplePluginCommand}; -use nu_protocol::{Category, LabeledError, Signature, Type, Value}; +use nu_protocol::{Category, FromValue, LabeledError, Signature, Spanned, Type, Value}; use crate::ExamplePlugin; pub struct Config; +/// Example config struct. +/// +/// Using the `FromValue` derive macro, structs can be easily loaded from [`Value`]s, +/// similar to serde's `Deserialize` macro. +/// This is handy for plugin configs or piped data. +/// All fields must implement [`FromValue`]. +/// For [`Option`] fields, they can be omitted in the config. +/// +/// This example shows that nested and spanned data work too, so you can describe nested +/// structures and get spans of values wrapped in [`Spanned`]. +/// Since this config uses only `Option`s, no field is required in the config. +#[allow(dead_code)] +#[derive(Debug, FromValue)] +struct PluginConfig { + path: Option>, + nested: Option, +} + +#[allow(dead_code)] +#[derive(Debug, FromValue)] +struct SubConfig { + bool: bool, + string: String, +} + impl SimplePluginCommand for Config { type Plugin = ExamplePlugin; @@ -39,7 +66,11 @@ impl SimplePluginCommand for Config { ) -> Result { let config = engine.get_plugin_config()?; match config { - Some(config) => Ok(config.clone()), + Some(value) => { + let config = PluginConfig::from_value(value.clone())?; + println!("got config {config:?}"); + Ok(value) + } None => Err(LabeledError::new("No config sent").with_label( "configuration for this plugin was not found in `$env.config.plugins.example`", call.head, diff --git a/crates/nu_plugin_example/src/commands/generate.rs b/crates/nu_plugin_example/src/commands/generate.rs index 9938518f4c..c039cc23d5 100644 --- a/crates/nu_plugin_example/src/commands/generate.rs +++ b/crates/nu_plugin_example/src/commands/generate.rs @@ -1,7 +1,7 @@ use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand}; use nu_protocol::{ - Category, Example, IntoInterruptiblePipelineData, LabeledError, PipelineData, Signature, - SyntaxShape, Type, Value, + Category, Example, IntoInterruptiblePipelineData, LabeledError, PipelineData, Signals, + Signature, SyntaxShape, Type, Value, }; use crate::ExamplePlugin; @@ -85,7 +85,7 @@ impl PluginCommand for Generate { }) .map(|result| result.unwrap_or_else(|err| Value::error(err, head))) }) - .into_pipeline_data(head, None)) + .into_pipeline_data(head, Signals::empty())) } } diff --git a/crates/nu_plugin_example/src/commands/seq.rs b/crates/nu_plugin_example/src/commands/seq.rs index 5652b3ef9a..88254394c1 100644 --- a/crates/nu_plugin_example/src/commands/seq.rs +++ b/crates/nu_plugin_example/src/commands/seq.rs @@ -1,6 +1,7 @@ use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand}; use nu_protocol::{ - Category, Example, LabeledError, ListStream, PipelineData, Signature, SyntaxShape, Type, Value, + Category, Example, LabeledError, ListStream, PipelineData, Signals, Signature, SyntaxShape, + Type, Value, }; use crate::ExamplePlugin; @@ -54,7 +55,7 @@ impl PluginCommand for Seq { let first: i64 = call.req(0)?; let last: i64 = call.req(1)?; let iter = (first..=last).map(move |number| Value::int(number, head)); - Ok(ListStream::new(iter, head, None).into()) + Ok(ListStream::new(iter, head, Signals::empty()).into()) } } diff --git a/crates/nu_plugin_example/src/commands/two.rs b/crates/nu_plugin_example/src/commands/two.rs index fcf2bf75ff..e02465e8b7 100644 --- a/crates/nu_plugin_example/src/commands/two.rs +++ b/crates/nu_plugin_example/src/commands/two.rs @@ -1,5 +1,5 @@ use nu_plugin::{EngineInterface, EvaluatedCall, SimplePluginCommand}; -use nu_protocol::{record, Category, LabeledError, Signature, SyntaxShape, Value}; +use nu_protocol::{Category, IntoValue, LabeledError, Signature, SyntaxShape, Value}; use crate::ExamplePlugin; @@ -38,14 +38,22 @@ impl SimplePluginCommand for Two { ) -> Result { plugin.print_values(2, call, input)?; + // Use the IntoValue derive macro and trait to easily design output data. + #[derive(IntoValue)] + struct Output { + one: i64, + two: i64, + three: i64, + } + let vals = (0..10i64) .map(|i| { - let record = record! { - "one" => Value::int(i, call.head), - "two" => Value::int(2 * i, call.head), - "three" => Value::int(3 * i, call.head), - }; - Value::record(record, call.head) + Output { + one: i, + two: 2 * i, + three: 3 * i, + } + .into_value(call.head) }) .collect(); diff --git a/crates/nu_plugin_example/src/lib.rs b/crates/nu_plugin_example/src/lib.rs index 182bc85121..acbebf9b39 100644 --- a/crates/nu_plugin_example/src/lib.rs +++ b/crates/nu_plugin_example/src/lib.rs @@ -7,6 +7,10 @@ pub use commands::*; pub use example::ExamplePlugin; impl Plugin for ExamplePlugin { + fn version(&self) -> String { + env!("CARGO_PKG_VERSION").into() + } + fn commands(&self) -> Vec>> { // This is a list of all of the commands you would like Nu to register when your plugin is // loaded. diff --git a/crates/nu_plugin_formats/Cargo.toml b/crates/nu_plugin_formats/Cargo.toml index a649a5bc5a..29e8953f24 100644 --- a/crates/nu_plugin_formats/Cargo.toml +++ b/crates/nu_plugin_formats/Cargo.toml @@ -5,12 +5,12 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu_plugin_form edition = "2021" license = "MIT" name = "nu_plugin_formats" -version = "0.94.3" +version = "0.95.1" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -nu-plugin = { path = "../nu-plugin", version = "0.94.3" } -nu-protocol = { path = "../nu-protocol", version = "0.94.3", features = ["plugin"] } +nu-plugin = { path = "../nu-plugin", version = "0.95.1" } +nu-protocol = { path = "../nu-protocol", version = "0.95.1", features = ["plugin"] } indexmap = { workspace = true } eml-parser = "0.1" @@ -18,4 +18,4 @@ ical = "0.11" rust-ini = "0.21.0" [dev-dependencies] -nu-plugin-test-support = { path = "../nu-plugin-test-support", version = "0.94.3" } +nu-plugin-test-support = { path = "../nu-plugin-test-support", version = "0.95.1" } \ No newline at end of file diff --git a/crates/nu_plugin_formats/README.md b/crates/nu_plugin_formats/README.md index 2b5fc2d829..a7ee046f99 100644 --- a/crates/nu_plugin_formats/README.md +++ b/crates/nu_plugin_formats/README.md @@ -12,7 +12,7 @@ A nushell plugin to convert data to nushell tables. # Usage 1. compile the binary: `cargo build` -2. register plugin(assume it's compiled in ./target/debug/): +2. plugin add plugin(assume it's compiled in ./target/debug/): ``` -register ./target/debug/nu_plugin_formats +plugin add ./target/debug/nu_plugin_formats ``` diff --git a/crates/nu_plugin_formats/src/lib.rs b/crates/nu_plugin_formats/src/lib.rs index 748d29cd21..2ae24a4971 100644 --- a/crates/nu_plugin_formats/src/lib.rs +++ b/crates/nu_plugin_formats/src/lib.rs @@ -10,6 +10,10 @@ pub use from::vcf::FromVcf; pub struct FromCmds; impl Plugin for FromCmds { + fn version(&self) -> String { + env!("CARGO_PKG_VERSION").into() + } + fn commands(&self) -> Vec>> { vec![ Box::new(FromEml), diff --git a/crates/nu_plugin_gstat/Cargo.toml b/crates/nu_plugin_gstat/Cargo.toml index 01e17fdb54..da1c13a88e 100644 --- a/crates/nu_plugin_gstat/Cargo.toml +++ b/crates/nu_plugin_gstat/Cargo.toml @@ -5,7 +5,7 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu_plugin_gsta edition = "2021" license = "MIT" name = "nu_plugin_gstat" -version = "0.94.3" +version = "0.95.1" [lib] doctest = false @@ -16,7 +16,7 @@ name = "nu_plugin_gstat" bench = false [dependencies] -nu-plugin = { path = "../nu-plugin", version = "0.94.3" } -nu-protocol = { path = "../nu-protocol", version = "0.94.3" } +nu-plugin = { path = "../nu-plugin", version = "0.95.1" } +nu-protocol = { path = "../nu-protocol", version = "0.95.1" } -git2 = "0.18" +git2 = "0.19" \ No newline at end of file diff --git a/crates/nu_plugin_gstat/README.md b/crates/nu_plugin_gstat/README.md index 88db33cbf8..7b9ab158ec 100644 --- a/crates/nu_plugin_gstat/README.md +++ b/crates/nu_plugin_gstat/README.md @@ -8,6 +8,6 @@ To install: > cargo install --path . ``` -To register (from inside Nushell): +To add the plugin (from inside Nushell): ``` -> register +> plugin add diff --git a/crates/nu_plugin_gstat/src/nu/mod.rs b/crates/nu_plugin_gstat/src/nu/mod.rs index 223e5a49b5..f44894085d 100644 --- a/crates/nu_plugin_gstat/src/nu/mod.rs +++ b/crates/nu_plugin_gstat/src/nu/mod.rs @@ -5,6 +5,10 @@ use nu_protocol::{Category, LabeledError, Signature, Spanned, SyntaxShape, Value pub struct GStatPlugin; impl Plugin for GStatPlugin { + fn version(&self) -> String { + env!("CARGO_PKG_VERSION").into() + } + fn commands(&self) -> Vec>> { vec![Box::new(GStat)] } diff --git a/crates/nu_plugin_inc/Cargo.toml b/crates/nu_plugin_inc/Cargo.toml index 3701ad49bd..0479a47843 100644 --- a/crates/nu_plugin_inc/Cargo.toml +++ b/crates/nu_plugin_inc/Cargo.toml @@ -5,7 +5,7 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu_plugin_inc" edition = "2021" license = "MIT" name = "nu_plugin_inc" -version = "0.94.3" +version = "0.95.1" [lib] doctest = false @@ -16,7 +16,7 @@ name = "nu_plugin_inc" bench = false [dependencies] -nu-plugin = { path = "../nu-plugin", version = "0.94.3" } -nu-protocol = { path = "../nu-protocol", version = "0.94.3", features = ["plugin"] } +nu-plugin = { path = "../nu-plugin", version = "0.95.1" } +nu-protocol = { path = "../nu-protocol", version = "0.95.1", features = ["plugin"] } -semver = "1.0" +semver = "1.0" \ No newline at end of file diff --git a/crates/nu_plugin_inc/README.md b/crates/nu_plugin_inc/README.md index e989aba0b9..d97443a189 100644 --- a/crates/nu_plugin_inc/README.md +++ b/crates/nu_plugin_inc/README.md @@ -8,6 +8,6 @@ To install: cargo install --path . ``` -To register (from inside Nushell): +To add the plugin (from inside Nushell): ``` -register +> plugin add diff --git a/crates/nu_plugin_inc/src/nu/mod.rs b/crates/nu_plugin_inc/src/nu/mod.rs index 148f1a7002..b7d9c8960f 100644 --- a/crates/nu_plugin_inc/src/nu/mod.rs +++ b/crates/nu_plugin_inc/src/nu/mod.rs @@ -5,6 +5,10 @@ use nu_protocol::{ast::CellPath, LabeledError, Signature, SyntaxShape, Value}; pub struct IncPlugin; impl Plugin for IncPlugin { + fn version(&self) -> String { + env!("CARGO_PKG_VERSION").into() + } + fn commands(&self) -> Vec>> { vec![Box::new(Inc::new())] } diff --git a/crates/nu_plugin_nu_example/nu_plugin_nu_example.nu b/crates/nu_plugin_nu_example/nu_plugin_nu_example.nu index 028e9735fd..d19cd36c14 100755 --- a/crates/nu_plugin_nu_example/nu_plugin_nu_example.nu +++ b/crates/nu_plugin_nu_example/nu_plugin_nu_example.nu @@ -6,7 +6,8 @@ # it also allows us to test the plugin interface with something manually implemented in a scripting # language without adding any extra dependencies to our tests. -const NUSHELL_VERSION = "0.94.3" +const NUSHELL_VERSION = "0.95.1" +const PLUGIN_VERSION = "0.1.0" # bump if you change commands! def main [--stdio] { if ($stdio) { @@ -229,6 +230,13 @@ def handle_input []: any -> nothing { } { Call: [$id, $plugin_call] } => { match $plugin_call { + "Metadata" => { + write_response $id { + Metadata: { + version: $PLUGIN_VERSION + } + } + } "Signature" => { write_response $id { Signature: $SIGNATURES } } @@ -257,4 +265,4 @@ def start_plugin [] { }) | each { from json | handle_input } | ignore -} +} \ No newline at end of file diff --git a/crates/nu_plugin_polars/Cargo.toml b/crates/nu_plugin_polars/Cargo.toml index 98288f3ebf..99c88e47d6 100644 --- a/crates/nu_plugin_polars/Cargo.toml +++ b/crates/nu_plugin_polars/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" license = "MIT" name = "nu_plugin_polars" repository = "https://github.com/nushell/nushell/tree/main/crates/nu_plugin_polars" -version = "0.94.3" +version = "0.95.1" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -17,9 +17,10 @@ bench = false bench = false [dependencies] -nu-protocol = { path = "../nu-protocol", version = "0.94.3" } -nu-plugin = { path = "../nu-plugin", version = "0.94.3" } -nu-path = { path = "../nu-path", version = "0.94.3" } +nu-protocol = { path = "../nu-protocol", version = "0.95.1" } +nu-plugin = { path = "../nu-plugin", version = "0.95.1" } +nu-path = { path = "../nu-path", version = "0.95.1" } +nu-utils = { path = "../nu-utils", version = "0.95.1" } # Potential dependencies for extras chrono = { workspace = true, features = ["std", "unstable-locales"], default-features = false } @@ -30,13 +31,15 @@ mimalloc = { version = "0.1.42" } num = {version = "0.4"} serde = { version = "1.0", features = ["derive"] } sqlparser = { version = "0.47"} -polars-io = { version = "0.40", features = ["avro"]} -polars-arrow = { version = "0.40"} -polars-ops = { version = "0.40"} -polars-plan = { version = "0.40", features = ["regex"]} -polars-utils = { version = "0.40"} +polars-io = { version = "0.41", features = ["avro"]} +polars-arrow = { version = "0.41"} +polars-ops = { version = "0.41", features = ["pivot"]} +polars-plan = { version = "0.41", features = ["regex"]} +polars-utils = { version = "0.41"} typetag = "0.2" -uuid = { version = "1.7", features = ["v4", "serde"] } +env_logger = "0.11.3" +log.workspace = true +uuid = { version = "1.10", features = ["v4", "serde"] } [dependencies.polars] features = [ @@ -70,12 +73,12 @@ features = [ "to_dummies", ] optional = false -version = "0.40" +version = "0.41" [dev-dependencies] -nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.94.3" } -nu-engine = { path = "../nu-engine", version = "0.94.3" } -nu-parser = { path = "../nu-parser", version = "0.94.3" } -nu-command = { path = "../nu-command", version = "0.94.3" } -nu-plugin-test-support = { path = "../nu-plugin-test-support", version = "0.94.3" } +nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.95.1" } +nu-engine = { path = "../nu-engine", version = "0.95.1" } +nu-parser = { path = "../nu-parser", version = "0.95.1" } +nu-command = { path = "../nu-command", version = "0.95.1" } +nu-plugin-test-support = { path = "../nu-plugin-test-support", version = "0.95.1" } tempfile.workspace = true diff --git a/crates/nu_plugin_polars/src/cache/list.rs b/crates/nu_plugin_polars/src/cache/list.rs index da434901e4..88fc1769ba 100644 --- a/crates/nu_plugin_polars/src/cache/list.rs +++ b/crates/nu_plugin_polars/src/cache/list.rs @@ -16,7 +16,7 @@ impl PluginCommand for ListDF { } fn usage(&self) -> &str { - "Lists stored dataframes." + "Lists stored polars objects." } fn signature(&self) -> Signature { @@ -119,6 +119,20 @@ impl PluginCommand for ListDF { }, call.head, ))), + PolarsPluginObject::NuPolarsTestData(_, _) => Ok(Some(Value::record( + record! { + "key" => Value::string(key.to_string(), call.head), + "columns" => Value::nothing(call.head), + "rows" => Value::nothing(call.head), + "type" => Value::string("When", call.head), + "estimated_size" => Value::nothing(call.head), + "span_contents" => Value::string(span_contents.to_string(), call.head), + "span_start" => Value::int(call.head.start as i64, call.head), + "span_end" => Value::int(call.head.end as i64, call.head), + "reference_count" => Value::int(value.reference_count as i64, call.head), + }, + call.head, + ))) } })?; let vals = vals.into_iter().flatten().collect(); diff --git a/crates/nu_plugin_polars/src/cache/mod.rs b/crates/nu_plugin_polars/src/cache/mod.rs index 8862f5bb51..0fbf49f46f 100644 --- a/crates/nu_plugin_polars/src/cache/mod.rs +++ b/crates/nu_plugin_polars/src/cache/mod.rs @@ -13,7 +13,9 @@ use nu_plugin::{EngineInterface, PluginCommand}; use nu_protocol::{LabeledError, ShellError, Span}; use uuid::Uuid; -use crate::{plugin_debug, values::PolarsPluginObject, PolarsPlugin}; +use crate::{values::PolarsPluginObject, EngineWrapper, PolarsPlugin}; + +use log::debug; #[derive(Debug, Clone)] pub struct CacheValue { @@ -47,7 +49,7 @@ impl Cache { /// * `force` - Delete even if there are multiple references pub fn remove( &self, - maybe_engine: Option<&EngineInterface>, + engine: impl EngineWrapper, key: &Uuid, force: bool, ) -> Result, ShellError> { @@ -60,22 +62,19 @@ impl Cache { let removed = if force || reference_count.unwrap_or_default() < 1 { let removed = lock.remove(key); - plugin_debug!("PolarsPlugin: removing {key} from cache: {removed:?}"); + debug!("PolarsPlugin: removing {key} from cache: {removed:?}"); removed } else { - plugin_debug!("PolarsPlugin: decrementing reference count for {key}"); + debug!("PolarsPlugin: decrementing reference count for {key}"); None }; - // Once there are no more entries in the cache - // we can turn plugin gc back on - match maybe_engine { - Some(engine) if lock.is_empty() => { - plugin_debug!("PolarsPlugin: Cache is empty enabling GC"); - engine.set_gc_disabled(false).map_err(LabeledError::from)?; - } - _ => (), - }; + if lock.is_empty() { + // Once there are no more entries in the cache + // we can turn plugin gc back on + debug!("PolarsPlugin: Cache is empty enabling GC"); + engine.set_gc_disabled(false).map_err(LabeledError::from)?; + } drop(lock); Ok(removed) } @@ -84,23 +83,18 @@ impl Cache { /// The maybe_engine parameter is required outside of testing pub fn insert( &self, - maybe_engine: Option<&EngineInterface>, + engine: impl EngineWrapper, uuid: Uuid, value: PolarsPluginObject, span: Span, ) -> Result, ShellError> { let mut lock = self.lock()?; - plugin_debug!("PolarsPlugin: Inserting {uuid} into cache: {value:?}"); + debug!("PolarsPlugin: Inserting {uuid} into cache: {value:?}"); // turn off plugin gc the first time an entry is added to the cache // as we don't want the plugin to be garbage collected if there // is any live data - match maybe_engine { - Some(engine) if lock.is_empty() => { - plugin_debug!("PolarsPlugin: Cache has values disabling GC"); - engine.set_gc_disabled(true).map_err(LabeledError::from)?; - } - _ => (), - }; + debug!("PolarsPlugin: Cache has values disabling GC"); + engine.set_gc_disabled(true).map_err(LabeledError::from)?; let cache_value = CacheValue { uuid, value, @@ -154,7 +148,7 @@ pub trait Cacheable: Sized + Clone { span: Span, ) -> Result { plugin.cache.insert( - Some(engine), + engine, self.cache_id().to_owned(), self.to_cache_value()?, span, @@ -178,3 +172,83 @@ pub(crate) fn cache_commands() -> Vec>, + } + + impl MockEngineWrapper { + fn new(gc_enabled: bool) -> Self { + Self { + gc_enabled: Rc::new(RefCell::new(gc_enabled)), + } + } + + fn gc_enabled(&self) -> bool { + *self.gc_enabled.borrow() + } + } + + impl EngineWrapper for &MockEngineWrapper { + fn get_env_var(&self, _key: &str) -> Option { + todo!() + } + + fn use_color(&self) -> bool { + todo!() + } + + fn set_gc_disabled(&self, disabled: bool) -> Result<(), ShellError> { + let _ = self.gc_enabled.replace(!disabled); + Ok(()) + } + } + + #[test] + pub fn test_remove_plugin_cache_enable() { + let mock_engine = MockEngineWrapper::new(false); + + let cache = Cache::default(); + let mut lock = cache.cache.lock().expect("should be able to acquire lock"); + let key0 = Uuid::new_v4(); + lock.insert( + key0, + CacheValue { + uuid: Uuid::new_v4(), + value: PolarsPluginObject::NuPolarsTestData(Uuid::new_v4(), "object_0".into()), + created: Local::now().into(), + span: Span::unknown(), + reference_count: 1, + }, + ); + + let key1 = Uuid::new_v4(); + lock.insert( + key1, + CacheValue { + uuid: Uuid::new_v4(), + value: PolarsPluginObject::NuPolarsTestData(Uuid::new_v4(), "object_1".into()), + created: Local::now().into(), + span: Span::unknown(), + reference_count: 1, + }, + ); + drop(lock); + + let _ = cache + .remove(&mock_engine, &key0, false) + .expect("should be able to remove key0"); + assert!(!mock_engine.gc_enabled()); + + let _ = cache + .remove(&mock_engine, &key1, false) + .expect("should be able to remove key1"); + assert!(mock_engine.gc_enabled()); + } +} diff --git a/crates/nu_plugin_polars/src/cache/rm.rs b/crates/nu_plugin_polars/src/cache/rm.rs index 5918209f32..9113f39cec 100644 --- a/crates/nu_plugin_polars/src/cache/rm.rs +++ b/crates/nu_plugin_polars/src/cache/rm.rs @@ -63,7 +63,7 @@ fn remove_cache_entry( let key = as_uuid(key, span)?; let msg = plugin .cache - .remove(Some(engine), &key, true)? + .remove(engine, &key, true)? .map(|_| format!("Removed: {key}")) .unwrap_or_else(|| format!("No value found for key: {key}")); Ok(Value::string(msg, span)) diff --git a/crates/nu_plugin_polars/src/dataframe/eager/melt.rs b/crates/nu_plugin_polars/src/dataframe/eager/melt.rs deleted file mode 100644 index b69389ed24..0000000000 --- a/crates/nu_plugin_polars/src/dataframe/eager/melt.rs +++ /dev/null @@ -1,253 +0,0 @@ -use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand}; -use nu_protocol::{ - Category, Example, LabeledError, PipelineData, ShellError, Signature, Span, Spanned, - SyntaxShape, Type, Value, -}; - -use crate::{ - dataframe::values::utils::convert_columns_string, values::CustomValueSupport, PolarsPlugin, -}; - -use super::super::values::{Column, NuDataFrame}; - -#[derive(Clone)] -pub struct MeltDF; - -impl PluginCommand for MeltDF { - type Plugin = PolarsPlugin; - - fn name(&self) -> &str { - "polars 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(vec![]), - "column names for melting", - Some('c'), - ) - .required_named( - "values", - SyntaxShape::Table(vec![]), - "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'), - ) - .input_output_type( - Type::Custom("dataframe".into()), - Type::Custom("dataframe".into()), - ) - .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]] | polars into-df | polars 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"), - ], - ), - ], None) - .expect("simple df for test should not fail") - .into_value(Span::test_data()), - ), - }] - } - - fn run( - &self, - plugin: &Self::Plugin, - engine: &EngineInterface, - call: &EvaluatedCall, - input: PipelineData, - ) -> Result { - command(plugin, engine, call, input).map_err(LabeledError::from) - } -} - -fn command( - plugin: &PolarsPlugin, - engine: &EngineInterface, - call: &EvaluatedCall, - input: PipelineData, -) -> Result { - let id_col: Vec = call.get_flag("columns")?.expect("required value"); - let val_col: Vec = call.get_flag("values")?.expect("required value"); - - let value_name: Option> = call.get_flag("value-name")?; - let variable_name: Option> = call.get_flag("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_coerce(plugin, 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::GenericError { - error: "Error calculating melt".into(), - msg: e.to_string(), - span: Some(call.head), - help: None, - inner: vec![], - })?; - - if let Some(name) = &variable_name { - res.rename("variable", &name.item) - .map_err(|e| ShellError::GenericError { - error: "Error renaming column".into(), - msg: e.to_string(), - span: Some(name.span), - help: None, - inner: vec![], - })?; - } - - if let Some(name) = &value_name { - res.rename("value", &name.item) - .map_err(|e| ShellError::GenericError { - error: "Error renaming column".into(), - msg: e.to_string(), - span: Some(name.span), - help: None, - inner: vec![], - })?; - } - - let res = NuDataFrame::new(false, res); - res.to_pipeline_data(plugin, engine, call.head) -} - -fn check_column_datatypes>( - df: &polars::prelude::DataFrame, - cols: &[T], - col_span: Span, -) -> Result<(), ShellError> { - if cols.is_empty() { - return Err(ShellError::GenericError { - error: "Merge error".into(), - msg: "empty column list".into(), - span: Some(col_span), - help: None, - inner: vec![], - }); - } - - // 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::GenericError { - error: "Error selecting columns".into(), - msg: e.to_string(), - span: Some(col_span), - help: None, - inner: vec![], - })?; - - let r_series = df - .column(w[1].as_ref()) - .map_err(|e| ShellError::GenericError { - error: "Error selecting columns".into(), - msg: e.to_string(), - span: Some(col_span), - help: None, - inner: vec![], - })?; - - if l_series.dtype() != r_series.dtype() { - return Err(ShellError::GenericError { - error: "Merge error".into(), - msg: "found different column types in list".into(), - span: Some(col_span), - help: Some(format!( - "datatypes {} and {} are incompatible", - l_series.dtype(), - r_series.dtype() - )), - inner: vec![], - }); - } - } - } - - Ok(()) -} - -#[cfg(test)] -mod test { - use crate::test::test_polars_plugin_command; - - use super::*; - - #[test] - fn test_examples() -> Result<(), ShellError> { - test_polars_plugin_command(&MeltDF) - } -} diff --git a/crates/nu_plugin_polars/src/dataframe/eager/mod.rs b/crates/nu_plugin_polars/src/dataframe/eager/mod.rs index dc50ba7cd2..6aa37d1ed1 100644 --- a/crates/nu_plugin_polars/src/dataframe/eager/mod.rs +++ b/crates/nu_plugin_polars/src/dataframe/eager/mod.rs @@ -9,8 +9,8 @@ mod filter_with; mod first; mod get; mod last; -mod melt; mod open; +mod pivot; mod query_df; mod rename; mod sample; @@ -28,6 +28,7 @@ mod to_df; mod to_json_lines; mod to_nu; mod to_parquet; +mod unpivot; mod with_column; use crate::PolarsPlugin; @@ -44,7 +45,6 @@ pub use filter_with::FilterWith; pub use first::FirstDF; pub use get::GetDF; pub use last::LastDF; -pub use melt::MeltDF; use nu_plugin::PluginCommand; pub use query_df::QueryDf; pub use rename::RenameDF; @@ -62,6 +62,7 @@ pub use to_df::ToDataFrame; pub use to_json_lines::ToJsonLines; pub use to_nu::ToNu; pub use to_parquet::ToParquet; +pub use unpivot::UnpivotDF; pub use with_column::WithColumn; pub(crate) fn eager_commands() -> Vec>> { @@ -76,7 +77,8 @@ pub(crate) fn eager_commands() -> Vec Result { - let infer_schema: Option = call.get_flag("infer-schema")?; + let infer_schema: NonZeroUsize = call + .get_flag("infer-schema")? + .and_then(NonZeroUsize::new) + .unwrap_or( + NonZeroUsize::new(DEFAULT_INFER_SCHEMA) + .expect("The default infer-schema should be non zero"), + ); let maybe_schema = call .get_flag("schema")? .map(|schema| NuSchema::try_from(&schema)) .transpose()?; - let file = File::open(file_path).map_err(|e| ShellError::GenericError { - error: "Error opening file".into(), - msg: e.to_string(), - span: Some(file_span), - help: None, - inner: vec![], - })?; - let buf_reader = BufReader::new(file); - let reader = JsonReader::new(buf_reader) - .with_json_format(JsonFormat::JsonLines) - .infer_schema_len(infer_schema); + if call.has_flag("lazy")? { + let start_time = std::time::Instant::now(); - let reader = match maybe_schema { - Some(schema) => reader.with_schema(schema.into()), - None => reader, - }; + let df = LazyJsonLineReader::new(file_path) + .with_infer_schema_length(Some(infer_schema)) + .with_schema(maybe_schema.map(|s| s.into())) + .finish() + .map_err(|e| ShellError::GenericError { + error: format!("Json lines reader error: {e}"), + msg: "".into(), + span: Some(call.head), + help: None, + inner: vec![], + })?; - let df: NuDataFrame = reader - .finish() - .map_err(|e| ShellError::GenericError { - error: "Json lines reader error".into(), - msg: format!("{e:?}"), - span: Some(call.head), + perf!( + "Lazy json lines dataframe open", + start_time, + engine.use_color() + ); + + let df = NuLazyFrame::new(false, df); + df.cache_and_to_value(plugin, engine, call.head) + } else { + let file = File::open(file_path).map_err(|e| ShellError::GenericError { + error: "Error opening file".into(), + msg: e.to_string(), + span: Some(file_span), help: None, inner: vec![], - })? - .into(); + })?; + let buf_reader = BufReader::new(file); + let reader = JsonReader::new(buf_reader) + .with_json_format(JsonFormat::JsonLines) + .infer_schema_len(Some(infer_schema)); - df.cache_and_to_value(plugin, engine, call.head) + let reader = match maybe_schema { + Some(schema) => reader.with_schema(schema.into()), + None => reader, + }; + + let start_time = std::time::Instant::now(); + + let df: NuDataFrame = reader + .finish() + .map_err(|e| ShellError::GenericError { + error: "Json lines reader error".into(), + msg: format!("{e:?}"), + span: Some(call.head), + help: None, + inner: vec![], + })? + .into(); + + perf!( + "Eager json lines dataframe open", + start_time, + engine.use_color() + ); + + df.cache_and_to_value(plugin, engine, call.head) + } } fn from_csv( @@ -416,7 +462,9 @@ fn from_csv( ) -> Result { let delimiter: Option> = call.get_flag("delimiter")?; let no_header: bool = call.has_flag("no-header")?; - let infer_schema: Option = call.get_flag("infer-schema")?; + let infer_schema: usize = call + .get_flag("infer-schema")? + .unwrap_or(DEFAULT_INFER_SCHEMA); let skip_rows: Option = call.get_flag("skip-rows")?; let columns: Option> = call.get_flag("columns")?; @@ -456,16 +504,14 @@ fn from_csv( None => csv_reader, }; - let csv_reader = match infer_schema { - None => csv_reader, - Some(r) => csv_reader.with_infer_schema_length(Some(r)), - }; + let csv_reader = csv_reader.with_infer_schema_length(Some(infer_schema)); let csv_reader = match skip_rows { None => csv_reader, Some(r) => csv_reader.with_skip_rows(r), }; + let start_time = std::time::Instant::now(); let df: NuLazyFrame = csv_reader .finish() .map_err(|e| ShellError::GenericError { @@ -477,14 +523,17 @@ fn from_csv( })? .into(); + perf!("Lazy CSV dataframe open", start_time, engine.use_color()); + df.cache_and_to_value(plugin, engine, call.head) } else { + let start_time = std::time::Instant::now(); let df = CsvReadOptions::default() .with_has_header(!no_header) - .with_infer_schema_length(infer_schema) + .with_infer_schema_length(Some(infer_schema)) .with_skip_rows(skip_rows.unwrap_or_default()) .with_schema(maybe_schema.map(|s| s.into())) - .with_columns(columns.map(Arc::new)) + .with_columns(columns.map(|v| Arc::from(v.into_boxed_slice()))) .map_parse_options(|options| { options .with_separator( @@ -511,6 +560,9 @@ fn from_csv( help: None, inner: vec![], })?; + + perf!("Eager CSV dataframe open", start_time, engine.use_color()); + let df = NuDataFrame::new(false, df); df.cache_and_to_value(plugin, engine, call.head) } diff --git a/crates/nu_plugin_polars/src/dataframe/eager/pivot.rs b/crates/nu_plugin_polars/src/dataframe/eager/pivot.rs new file mode 100644 index 0000000000..b72210d011 --- /dev/null +++ b/crates/nu_plugin_polars/src/dataframe/eager/pivot.rs @@ -0,0 +1,265 @@ +use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand}; +use nu_protocol::{ + Category, Example, LabeledError, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, + Value, +}; + +use polars_ops::pivot::{pivot, PivotAgg}; + +use crate::{ + dataframe::values::utils::convert_columns_string, + values::{Column, CustomValueSupport, PolarsPluginObject}, + PolarsPlugin, +}; + +use super::super::values::NuDataFrame; + +#[derive(Clone)] +pub struct PivotDF; + +impl PluginCommand for PivotDF { + type Plugin = PolarsPlugin; + + fn name(&self) -> &str { + "polars pivot" + } + + fn usage(&self) -> &str { + "Pivot a DataFrame from wide to long format." + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required_named( + "on", + SyntaxShape::List(Box::new(SyntaxShape::String)), + "column names for pivoting", + Some('o'), + ) + .required_named( + "index", + SyntaxShape::List(Box::new(SyntaxShape::String)), + "column names for indexes", + Some('i'), + ) + .required_named( + "values", + SyntaxShape::List(Box::new(SyntaxShape::String)), + "column names used as value columns", + Some('v'), + ) + .named( + "aggregate", + SyntaxShape::String, + "Aggregation to apply when pivoting. The following are supported: first, sum, min, max, mean, median, count, last", + Some('a'), + ) + .switch( + "sort", + "Sort columns", + Some('s'), + ) + .switch( + "streamable", + "Whether or not to use the polars streaming engine. Only valid for lazy dataframes", + Some('t'), + ) + .input_output_type( + Type::Custom("dataframe".into()), + Type::Custom("dataframe".into()), + ) + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "[[name subject test_1 test_2]; [Cady maths 98 100] [Cady physics 99 100] [Karen maths 61 60] [Karen physics 58 60]] | polars into-df | polars pivot --on [subject] --index [name] --values [test_1]", + description: "Perform a pivot in order to show individuals test score by subject", + result: Some( + NuDataFrame::try_from_columns( + vec![ + Column::new( + "name".to_string(), + vec![Value::string("Cady", Span::test_data()), Value::string("Karen", Span::test_data())], + ), + Column::new( + "maths".to_string(), + vec![Value::int(98, Span::test_data()), Value::int(61, Span::test_data())], + ), + Column::new( + "physics".to_string(), + vec![Value::int(99, Span::test_data()), Value::int(58, Span::test_data())], + ), + ], + None, + ) + .expect("simple df for test should not fail") + .into_value(Span::unknown()) + ) + } + ] + } + + fn run( + &self, + plugin: &Self::Plugin, + engine: &EngineInterface, + call: &EvaluatedCall, + input: PipelineData, + ) -> Result { + match PolarsPluginObject::try_from_pipeline(plugin, input, call.head)? { + PolarsPluginObject::NuDataFrame(df) => command_eager(plugin, engine, call, df), + PolarsPluginObject::NuLazyFrame(lazy) => { + command_eager(plugin, engine, call, lazy.collect(call.head)?) + } + _ => Err(ShellError::GenericError { + error: "Must be a dataframe or lazy dataframe".into(), + msg: "".into(), + span: Some(call.head), + help: None, + inner: vec![], + }), + } + .map_err(LabeledError::from) + } +} + +fn command_eager( + plugin: &PolarsPlugin, + engine: &EngineInterface, + call: &EvaluatedCall, + df: NuDataFrame, +) -> Result { + let on_col: Vec = call.get_flag("on")?.expect("required value"); + let index_col: Vec = call.get_flag("index")?.expect("required value"); + let val_col: Vec = call.get_flag("values")?.expect("required value"); + + let (on_col_string, id_col_span) = convert_columns_string(on_col, call.head)?; + let (index_col_string, index_col_span) = convert_columns_string(index_col, call.head)?; + let (val_col_string, val_col_span) = convert_columns_string(val_col, call.head)?; + + check_column_datatypes(df.as_ref(), &on_col_string, id_col_span)?; + check_column_datatypes(df.as_ref(), &index_col_string, index_col_span)?; + check_column_datatypes(df.as_ref(), &val_col_string, val_col_span)?; + + let aggregate: Option = call + .get_flag::("aggregate")? + .map(pivot_agg_for_str) + .transpose()?; + + let sort = call.has_flag("sort")?; + + let polars_df = df.to_polars(); + // todo add other args + let pivoted = pivot( + &polars_df, + &on_col_string, + Some(&index_col_string), + Some(&val_col_string), + sort, + aggregate, + None, + ) + .map_err(|e| ShellError::GenericError { + error: format!("Pivot error: {e}"), + msg: "".into(), + span: Some(call.head), + help: None, + inner: vec![], + })?; + + let res = NuDataFrame::new(false, pivoted); + res.to_pipeline_data(plugin, engine, call.head) +} + +fn check_column_datatypes>( + df: &polars::prelude::DataFrame, + cols: &[T], + col_span: Span, +) -> Result<(), ShellError> { + if cols.is_empty() { + return Err(ShellError::GenericError { + error: "Merge error".into(), + msg: "empty column list".into(), + span: Some(col_span), + help: None, + inner: vec![], + }); + } + + // 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::GenericError { + error: "Error selecting columns".into(), + msg: e.to_string(), + span: Some(col_span), + help: None, + inner: vec![], + })?; + + let r_series = df + .column(w[1].as_ref()) + .map_err(|e| ShellError::GenericError { + error: "Error selecting columns".into(), + msg: e.to_string(), + span: Some(col_span), + help: None, + inner: vec![], + })?; + + if l_series.dtype() != r_series.dtype() { + return Err(ShellError::GenericError { + error: "Merge error".into(), + msg: "found different column types in list".into(), + span: Some(col_span), + help: Some(format!( + "datatypes {} and {} are incompatible", + l_series.dtype(), + r_series.dtype() + )), + inner: vec![], + }); + } + } + } + + Ok(()) +} + +fn pivot_agg_for_str(agg: impl AsRef) -> Result { + match agg.as_ref() { + "first" => Ok(PivotAgg::First), + "sum" => Ok(PivotAgg::Sum), + "min" => Ok(PivotAgg::Min), + "max" => Ok(PivotAgg::Max), + "mean" => Ok(PivotAgg::Mean), + "median" => Ok(PivotAgg::Median), + "count" => Ok(PivotAgg::Count), + "last" => Ok(PivotAgg::Last), + s => Err(ShellError::GenericError { + error: format!("{s} is not a valid aggregation"), + msg: "".into(), + span: None, + help: Some( + "Use one of the following: first, sum, min, max, mean, median, count, last".into(), + ), + inner: vec![], + }), + } +} + +#[cfg(test)] +mod test { + use crate::test::test_polars_plugin_command; + + use super::*; + + #[test] + fn test_examples() -> Result<(), ShellError> { + test_polars_plugin_command(&PivotDF) + } +} diff --git a/crates/nu_plugin_polars/src/dataframe/eager/schema.rs b/crates/nu_plugin_polars/src/dataframe/eager/schema.rs index b55d8ee5e2..e1350289a7 100644 --- a/crates/nu_plugin_polars/src/dataframe/eager/schema.rs +++ b/crates/nu_plugin_polars/src/dataframe/eager/schema.rs @@ -70,7 +70,7 @@ fn command( let value: Value = schema.into(); Ok(PipelineData::Value(value, None)) } - PolarsPluginObject::NuLazyFrame(lazy) => { + PolarsPluginObject::NuLazyFrame(mut lazy) => { let schema = lazy.schema()?; let value: Value = schema.into(); Ok(PipelineData::Value(value, None)) diff --git a/crates/nu_plugin_polars/src/dataframe/eager/to_nu.rs b/crates/nu_plugin_polars/src/dataframe/eager/to_nu.rs index 8e3cdffa24..72aff7a728 100644 --- a/crates/nu_plugin_polars/src/dataframe/eager/to_nu.rs +++ b/crates/nu_plugin_polars/src/dataframe/eager/to_nu.rs @@ -35,6 +35,7 @@ impl PluginCommand for ToNu { Some('n'), ) .switch("tail", "shows tail rows", Some('t')) + .switch("index", "add an index column", Some('i')) .input_output_types(vec![ (Type::Custom("expression".into()), Type::Any), (Type::Custom("dataframe".into()), Type::table()), @@ -62,18 +63,18 @@ impl PluginCommand for ToNu { vec![ Example { description: "Shows head rows from dataframe", - example: "[[a b]; [1 2] [3 4]] | polars into-df | polars into-nu", + example: "[[a b]; [1 2] [3 4]] | polars into-df | polars into-nu --index", result: Some(Value::list(vec![rec_1, rec_2], Span::test_data())), }, Example { description: "Shows tail rows from dataframe", example: - "[[a b]; [1 2] [5 6] [3 4]] | polars into-df | polars into-nu --tail --rows 1", + "[[a b]; [1 2] [5 6] [3 4]] | polars into-df | polars into-nu --tail --rows 1 --index", result: Some(Value::list(vec![rec_3], Span::test_data())), }, Example { description: "Convert a col expression into a nushell value", - example: "polars col a | polars into-nu", + example: "polars col a | polars into-nu --index", result: Some(Value::test_record(record! { "expr" => Value::test_string("column"), "value" => Value::test_string("a"), @@ -106,17 +107,18 @@ fn dataframe_command( ) -> Result { let rows: Option = call.get_flag("rows")?; let tail: bool = call.has_flag("tail")?; + let index: bool = call.has_flag("index")?; let df = NuDataFrame::try_from_value_coerce(plugin, &input, call.head)?; let values = if tail { - df.tail(rows, call.head)? + df.tail(rows, index, call.head)? } else { // if rows is specified, return those rows, otherwise return everything if rows.is_some() { - df.head(rows, call.head)? + df.head(rows, index, call.head)? } else { - df.head(Some(df.height()), call.head)? + df.head(Some(df.height()), index, call.head)? } }; diff --git a/crates/nu_plugin_polars/src/dataframe/eager/unpivot.rs b/crates/nu_plugin_polars/src/dataframe/eager/unpivot.rs new file mode 100644 index 0000000000..dafdc65ab4 --- /dev/null +++ b/crates/nu_plugin_polars/src/dataframe/eager/unpivot.rs @@ -0,0 +1,346 @@ +use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand}; +use nu_protocol::{ + Category, Example, LabeledError, PipelineData, ShellError, Signature, Span, Spanned, + SyntaxShape, Type, Value, +}; +use polars::frame::explode::UnpivotArgs; + +use crate::{ + dataframe::values::utils::convert_columns_string, + values::{CustomValueSupport, NuLazyFrame, PolarsPluginObject}, + PolarsPlugin, +}; + +use super::super::values::{Column, NuDataFrame}; + +#[derive(Clone)] +pub struct UnpivotDF; + +impl PluginCommand for UnpivotDF { + type Plugin = PolarsPlugin; + + fn name(&self) -> &str { + "polars unpivot" + } + + fn usage(&self) -> &str { + "Unpivot a DataFrame from wide to long format." + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required_named( + "index", + SyntaxShape::Table(vec![]), + "column names for unpivoting", + Some('i'), + ) + .required_named( + "on", + SyntaxShape::Table(vec![]), + "column names used as value columns", + Some('o'), + ) + .named( + "variable-name", + SyntaxShape::String, + "optional name for variable column", + Some('r'), + ) + .named( + "value-name", + SyntaxShape::String, + "optional name for value column", + Some('l'), + ) + .input_output_type( + Type::Custom("dataframe".into()), + Type::Custom("dataframe".into()), + ) + .switch( + "streamable", + "Whether or not to use the polars streaming engine. Only valid for lazy dataframes", + Some('t'), + ) + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "unpivot on an eager dataframe", + example: + "[[a b c d]; [x 1 4 a] [y 2 5 b] [z 3 6 c]] | polars into-df | polars unpivot -i [b c] -o [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"), + ], + ), + ], None) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + }, + Example { + description: "unpivot on a lazy dataframe", + example: + "[[a b c d]; [x 1 4 a] [y 2 5 b] [z 3 6 c]] | polars into-lazy | polars unpivot -i [b c] -o [a d] | polars collect", + 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"), + ], + ), + ], None) + .expect("simple df for test should not fail") + .into_value(Span::test_data()), + ), + } + ] + } + + fn run( + &self, + plugin: &Self::Plugin, + engine: &EngineInterface, + call: &EvaluatedCall, + input: PipelineData, + ) -> Result { + match PolarsPluginObject::try_from_pipeline(plugin, input, call.head)? { + PolarsPluginObject::NuDataFrame(df) => command_eager(plugin, engine, call, df), + PolarsPluginObject::NuLazyFrame(lazy) => command_lazy(plugin, engine, call, lazy), + _ => Err(ShellError::GenericError { + error: "Must be a dataframe or lazy dataframe".into(), + msg: "".into(), + span: Some(call.head), + help: None, + inner: vec![], + }), + } + .map_err(LabeledError::from) + } +} + +fn command_eager( + plugin: &PolarsPlugin, + engine: &EngineInterface, + call: &EvaluatedCall, + df: NuDataFrame, +) -> Result { + let index_col: Vec = call.get_flag("index")?.expect("required value"); + let on_col: Vec = call.get_flag("on")?.expect("required value"); + + let value_name: Option> = call.get_flag("value-name")?; + let variable_name: Option> = call.get_flag("variable-name")?; + + let (index_col_string, index_col_span) = convert_columns_string(index_col, call.head)?; + let (on_col_string, on_col_span) = convert_columns_string(on_col, call.head)?; + + check_column_datatypes(df.as_ref(), &index_col_string, index_col_span)?; + check_column_datatypes(df.as_ref(), &on_col_string, on_col_span)?; + + let streamable = call.has_flag("streamable")?; + + let args = UnpivotArgs { + on: on_col_string.iter().map(Into::into).collect(), + index: index_col_string.iter().map(Into::into).collect(), + variable_name: variable_name.map(|s| s.item.into()), + value_name: value_name.map(|s| s.item.into()), + streamable, + }; + + let res = df + .as_ref() + .unpivot2(args) + .map_err(|e| ShellError::GenericError { + error: "Error calculating unpivot".into(), + msg: e.to_string(), + span: Some(call.head), + help: None, + inner: vec![], + })?; + + let res = NuDataFrame::new(false, res); + res.to_pipeline_data(plugin, engine, call.head) +} + +fn command_lazy( + plugin: &PolarsPlugin, + engine: &EngineInterface, + call: &EvaluatedCall, + df: NuLazyFrame, +) -> Result { + let index_col: Vec = call.get_flag("index")?.expect("required value"); + let on_col: Vec = call.get_flag("on")?.expect("required value"); + + let (index_col_string, _index_col_span) = convert_columns_string(index_col, call.head)?; + let (on_col_string, _on_col_span) = convert_columns_string(on_col, call.head)?; + + let value_name: Option = call.get_flag("value-name")?; + let variable_name: Option = call.get_flag("variable-name")?; + + let streamable = call.has_flag("streamable")?; + + let unpivot_args = UnpivotArgs { + on: on_col_string.iter().map(Into::into).collect(), + index: index_col_string.iter().map(Into::into).collect(), + value_name: value_name.map(Into::into), + variable_name: variable_name.map(Into::into), + streamable, + }; + + let polars_df = df.to_polars().unpivot(unpivot_args); + + let res = NuLazyFrame::new(false, polars_df); + res.to_pipeline_data(plugin, engine, call.head) +} + +fn check_column_datatypes>( + df: &polars::prelude::DataFrame, + cols: &[T], + col_span: Span, +) -> Result<(), ShellError> { + if cols.is_empty() { + return Err(ShellError::GenericError { + error: "Merge error".into(), + msg: "empty column list".into(), + span: Some(col_span), + help: None, + inner: vec![], + }); + } + + // 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::GenericError { + error: "Error selecting columns".into(), + msg: e.to_string(), + span: Some(col_span), + help: None, + inner: vec![], + })?; + + let r_series = df + .column(w[1].as_ref()) + .map_err(|e| ShellError::GenericError { + error: "Error selecting columns".into(), + msg: e.to_string(), + span: Some(col_span), + help: None, + inner: vec![], + })?; + + if l_series.dtype() != r_series.dtype() { + return Err(ShellError::GenericError { + error: "Merge error".into(), + msg: "found different column types in list".into(), + span: Some(col_span), + help: Some(format!( + "datatypes {} and {} are incompatible", + l_series.dtype(), + r_series.dtype() + )), + inner: vec![], + }); + } + } + } + + Ok(()) +} + +#[cfg(test)] +mod test { + use crate::test::test_polars_plugin_command; + + use super::*; + + #[test] + fn test_examples() -> Result<(), ShellError> { + test_polars_plugin_command(&UnpivotDF) + } +} diff --git a/crates/nu_plugin_polars/src/dataframe/lazy/aggregate.rs b/crates/nu_plugin_polars/src/dataframe/lazy/aggregate.rs index d2fd92ec87..add72faf88 100644 --- a/crates/nu_plugin_polars/src/dataframe/lazy/aggregate.rs +++ b/crates/nu_plugin_polars/src/dataframe/lazy/aggregate.rs @@ -196,7 +196,8 @@ fn get_col_name(expr: &Expr) -> Option { | Expr::Nth(_) | Expr::SubPlan(_, _) | Expr::IndexColumn(_) - | Expr::Selector(_) => None, + | Expr::Selector(_) + | Expr::Field(_) => None, } } diff --git a/crates/nu_plugin_polars/src/dataframe/lazy/groupby.rs b/crates/nu_plugin_polars/src/dataframe/lazy/groupby.rs index 9edf8f5e60..b7bd1017a3 100644 --- a/crates/nu_plugin_polars/src/dataframe/lazy/groupby.rs +++ b/crates/nu_plugin_polars/src/dataframe/lazy/groupby.rs @@ -148,11 +148,11 @@ fn command( plugin: &PolarsPlugin, engine: &EngineInterface, call: &EvaluatedCall, - lazy: NuLazyFrame, + mut lazy: NuLazyFrame, expressions: Vec, ) -> Result { let group_by = lazy.to_polars().group_by(expressions); - let group_by = NuLazyGroupBy::new(group_by, lazy.from_eager, lazy.schema()?); + let group_by = NuLazyGroupBy::new(group_by, lazy.from_eager, lazy.schema().clone()?); group_by.to_pipeline_data(plugin, engine, call.head) } diff --git a/crates/nu_plugin_polars/src/dataframe/lazy/join.rs b/crates/nu_plugin_polars/src/dataframe/lazy/join.rs index 67f5aee9ba..01fb2ac24d 100644 --- a/crates/nu_plugin_polars/src/dataframe/lazy/join.rs +++ b/crates/nu_plugin_polars/src/dataframe/lazy/join.rs @@ -35,7 +35,7 @@ impl PluginCommand for LazyJoin { Some('i'), ) .switch("left", "left join between lazyframes", Some('l')) - .switch("outer", "outer join between lazyframes", Some('o')) + .switch("full", "full join between lazyframes", Some('f')) .switch("cross", "cross join between lazyframes", Some('c')) .named( "suffix", @@ -183,13 +183,13 @@ impl PluginCommand for LazyJoin { input: PipelineData, ) -> Result { let left = call.has_flag("left")?; - let outer = call.has_flag("outer")?; + let full = call.has_flag("full")?; let cross = call.has_flag("cross")?; let how = if left { JoinType::Left - } else if outer { - JoinType::Outer + } else if full { + JoinType::Full } else if cross { JoinType::Cross } else { diff --git a/crates/nu_plugin_polars/src/dataframe/lazy/sort_by_expr.rs b/crates/nu_plugin_polars/src/dataframe/lazy/sort_by_expr.rs index b282bd6d04..20251a0ea0 100644 --- a/crates/nu_plugin_polars/src/dataframe/lazy/sort_by_expr.rs +++ b/crates/nu_plugin_polars/src/dataframe/lazy/sort_by_expr.rs @@ -140,7 +140,7 @@ impl PluginCommand for LazySortBy { let sort_options = SortMultipleOptions { descending: reverse, - nulls_last, + nulls_last: vec![nulls_last], multithreaded: true, maintain_order, }; diff --git a/crates/nu_plugin_polars/src/dataframe/series/indexes/set_with_idx.rs b/crates/nu_plugin_polars/src/dataframe/series/indexes/set_with_idx.rs index 6b1578ad0e..436ba92f96 100644 --- a/crates/nu_plugin_polars/src/dataframe/series/indexes/set_with_idx.rs +++ b/crates/nu_plugin_polars/src/dataframe/series/indexes/set_with_idx.rs @@ -7,7 +7,10 @@ use nu_protocol::{ Category, Example, LabeledError, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value, }; -use polars::prelude::{ChunkSet, DataType, IntoSeries}; +use polars::{ + chunked_array::cast::CastOptions, + prelude::{ChunkSet, DataType, IntoSeries}, +}; #[derive(Clone)] pub struct SetWithIndex; @@ -96,7 +99,7 @@ fn command( let casted = match indices.dtype() { DataType::UInt32 | DataType::UInt64 | DataType::Int32 | DataType::Int64 => indices .as_ref() - .cast(&DataType::UInt32) + .cast(&DataType::UInt32, CastOptions::default()) .map_err(|e| ShellError::GenericError { error: "Error casting indices".into(), msg: e.to_string(), diff --git a/crates/nu_plugin_polars/src/dataframe/series/value_counts.rs b/crates/nu_plugin_polars/src/dataframe/series/value_counts.rs index 36d8718ff8..16471e7b9f 100644 --- a/crates/nu_plugin_polars/src/dataframe/series/value_counts.rs +++ b/crates/nu_plugin_polars/src/dataframe/series/value_counts.rs @@ -4,7 +4,8 @@ use super::super::values::{Column, NuDataFrame}; use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand}; use nu_protocol::{ - Category, Example, LabeledError, PipelineData, ShellError, Signature, Span, Type, Value, + Category, Example, LabeledError, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, + Value, }; use polars::prelude::SeriesMethods; @@ -25,6 +26,24 @@ impl PluginCommand for ValueCount { fn signature(&self) -> Signature { Signature::build(self.name()) + .named( + "column", + SyntaxShape::String, + "Provide a custom name for the coutn column", + Some('c'), + ) + .switch("sort", "Whether or not values should be sorted", Some('s')) + .switch( + "parallel", + "Use multiple threads when processing", + Some('p'), + ) + .named( + "normalize", + SyntaxShape::String, + "Normalize the counts", + Some('n'), + ) .input_output_type( Type::Custom("dataframe".into()), Type::Custom("dataframe".into()), @@ -73,11 +92,15 @@ fn command( call: &EvaluatedCall, input: PipelineData, ) -> Result { + let column = call.get_flag("column")?.unwrap_or("count".to_string()); + let parallel = call.has_flag("parallel")?; + let sort = call.has_flag("sort")?; + let normalize = call.has_flag("normalize")?; let df = NuDataFrame::try_from_pipeline_coerce(plugin, input, call.head)?; let series = df.as_series(call.head)?; let res = series - .value_counts(false, false) + .value_counts(sort, parallel, column, normalize) .map_err(|e| ShellError::GenericError { error: "Error calculating value counts values".into(), msg: e.to_string(), diff --git a/crates/nu_plugin_polars/src/dataframe/values/mod.rs b/crates/nu_plugin_polars/src/dataframe/values/mod.rs index d52ee25203..ec59304eab 100644 --- a/crates/nu_plugin_polars/src/dataframe/values/mod.rs +++ b/crates/nu_plugin_polars/src/dataframe/values/mod.rs @@ -27,6 +27,7 @@ pub enum PolarsPluginType { NuExpression, NuLazyGroupBy, NuWhen, + NuPolarsTestData, } impl fmt::Display for PolarsPluginType { @@ -37,6 +38,7 @@ impl fmt::Display for PolarsPluginType { Self::NuExpression => write!(f, "NuExpression"), Self::NuLazyGroupBy => write!(f, "NuLazyGroupBy"), Self::NuWhen => write!(f, "NuWhen"), + Self::NuPolarsTestData => write!(f, "NuPolarsTestData"), } } } @@ -48,6 +50,7 @@ pub enum PolarsPluginObject { NuExpression(NuExpression), NuLazyGroupBy(NuLazyGroupBy), NuWhen(NuWhen), + NuPolarsTestData(Uuid, String), } impl PolarsPluginObject { @@ -95,6 +98,7 @@ impl PolarsPluginObject { Self::NuExpression(_) => PolarsPluginType::NuExpression, Self::NuLazyGroupBy(_) => PolarsPluginType::NuLazyGroupBy, Self::NuWhen(_) => PolarsPluginType::NuWhen, + Self::NuPolarsTestData(_, _) => PolarsPluginType::NuPolarsTestData, } } @@ -105,6 +109,7 @@ impl PolarsPluginObject { PolarsPluginObject::NuExpression(e) => e.id, PolarsPluginObject::NuLazyGroupBy(lg) => lg.id, PolarsPluginObject::NuWhen(w) => w.id, + PolarsPluginObject::NuPolarsTestData(id, _) => *id, } } @@ -115,6 +120,9 @@ impl PolarsPluginObject { PolarsPluginObject::NuExpression(e) => e.into_value(span), PolarsPluginObject::NuLazyGroupBy(lg) => lg.into_value(span), PolarsPluginObject::NuWhen(w) => w.into_value(span), + PolarsPluginObject::NuPolarsTestData(id, s) => { + Value::string(format!("{id}:{s}"), Span::test_data()) + } } } } diff --git a/crates/nu_plugin_polars/src/dataframe/values/nu_dataframe/between_values.rs b/crates/nu_plugin_polars/src/dataframe/values/nu_dataframe/between_values.rs index a47197bde8..c92e657fb9 100644 --- a/crates/nu_plugin_polars/src/dataframe/values/nu_dataframe/between_values.rs +++ b/crates/nu_plugin_polars/src/dataframe/values/nu_dataframe/between_values.rs @@ -41,19 +41,37 @@ pub(super) fn compute_between_series( let operation_span = Span::merge(left.span(), right.span()); match operator.item { Operator::Math(Math::Plus) => { - let mut res = lhs + rhs; + let mut res = (lhs + rhs).map_err(|e| ShellError::GenericError { + error: format!("Addition error: {e}"), + msg: "".into(), + span: Some(operation_span), + help: None, + inner: vec![], + })?; let name = format!("sum_{}_{}", lhs.name(), rhs.name()); res.rename(&name); NuDataFrame::try_from_series(res, operation_span) } Operator::Math(Math::Minus) => { - let mut res = lhs - rhs; + let mut res = (lhs - rhs).map_err(|e| ShellError::GenericError { + error: format!("Subtraction error: {e}"), + msg: "".into(), + span: Some(operation_span), + help: None, + inner: vec![], + })?; let name = format!("sub_{}_{}", lhs.name(), rhs.name()); res.rename(&name); NuDataFrame::try_from_series(res, operation_span) } Operator::Math(Math::Multiply) => { - let mut res = lhs * rhs; + let mut res = (lhs * rhs).map_err(|e| ShellError::GenericError { + error: format!("Multiplication error: {e}"), + msg: "".into(), + span: Some(operation_span), + help: None, + inner: vec![], + })?; let name = format!("mul_{}_{}", lhs.name(), rhs.name()); res.rename(&name); NuDataFrame::try_from_series(res, operation_span) diff --git a/crates/nu_plugin_polars/src/dataframe/values/nu_dataframe/mod.rs b/crates/nu_plugin_polars/src/dataframe/values/nu_dataframe/mod.rs index 1c322579dd..89a8c862e7 100644 --- a/crates/nu_plugin_polars/src/dataframe/values/nu_dataframe/mod.rs +++ b/crates/nu_plugin_polars/src/dataframe/values/nu_dataframe/mod.rs @@ -326,22 +326,22 @@ impl NuDataFrame { } // Print is made out a head and if the dataframe is too large, then a tail - pub fn print(&self, span: Span) -> Result, ShellError> { + pub fn print(&self, include_index: bool, span: Span) -> Result, ShellError> { let df = &self.df; let size: usize = 20; if df.height() > size { let sample_size = size / 2; - let mut values = self.head(Some(sample_size), span)?; + let mut values = self.head(Some(sample_size), include_index, span)?; conversion::add_separator(&mut values, df, self.has_index(), span); let remaining = df.height() - sample_size; let tail_size = remaining.min(sample_size); - let mut tail_values = self.tail(Some(tail_size), span)?; + let mut tail_values = self.tail(Some(tail_size), include_index, span)?; values.append(&mut tail_values); Ok(values) } else { - Ok(self.head(Some(size), span)?) + Ok(self.head(Some(size), include_index, span)?) } } @@ -349,26 +349,38 @@ impl NuDataFrame { self.df.height() } - pub fn head(&self, rows: Option, span: Span) -> Result, ShellError> { + pub fn head( + &self, + rows: Option, + include_index: bool, + span: Span, + ) -> Result, ShellError> { let to_row = rows.unwrap_or(5); - let values = self.to_rows(0, to_row, span)?; + let values = self.to_rows(0, to_row, include_index, span)?; Ok(values) } - pub fn tail(&self, rows: Option, span: Span) -> Result, ShellError> { + pub fn tail( + &self, + rows: Option, + include_index: bool, + span: Span, + ) -> Result, ShellError> { let df = &self.df; 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)?; + let values = self.to_rows(from_row, to_row, include_index, span)?; Ok(values) } + /// Converts the dataframe to a nushell list of values pub fn to_rows( &self, from_row: usize, to_row: usize, + include_index: bool, span: Span, ) -> Result, ShellError> { let df = &self.df; @@ -400,7 +412,7 @@ impl NuDataFrame { .map(|i| { let mut record = Record::new(); - if !has_index { + if !has_index && include_index { record.push("index", Value::int((i + from_row) as i64, span)); } @@ -602,7 +614,7 @@ impl CustomValueSupport for NuDataFrame { } fn base_value(self, span: Span) -> Result { - let vals = self.print(span)?; + let vals = self.print(true, span)?; Ok(Value::list(vals, span)) } diff --git a/crates/nu_plugin_polars/src/dataframe/values/nu_expression/custom_value.rs b/crates/nu_plugin_polars/src/dataframe/values/nu_expression/custom_value.rs index ea751b0255..a9371f03f2 100644 --- a/crates/nu_plugin_polars/src/dataframe/values/nu_expression/custom_value.rs +++ b/crates/nu_plugin_polars/src/dataframe/values/nu_expression/custom_value.rs @@ -63,45 +63,16 @@ fn compute_with_value( op: Span, right: &Value, ) -> Result { - let rhs_span = right.span(); - match right { - Value::Custom { val: rhs, .. } => { - let rhs = rhs.as_any().downcast_ref::().ok_or_else(|| { - ShellError::TypeMismatch { - err_message: "Right hand side not a dataframe expression".into(), - span: rhs_span, - } - })?; - - match rhs.as_ref() { - polars::prelude::Expr::Literal(..) => with_operator( - (plugin, engine), - operator, - left, - rhs, - lhs_span, - right.span(), - op, - ), - _ => Err(ShellError::TypeMismatch { - err_message: "Only literal expressions or number".into(), - span: right.span(), - }), - } - } - _ => { - let rhs = NuExpression::try_from_value(plugin, right)?; - with_operator( - (plugin, engine), - operator, - left, - &rhs, - lhs_span, - right.span(), - op, - ) - } - } + let rhs = NuExpression::try_from_value(plugin, right)?; + with_operator( + (plugin, engine), + operator, + left, + &rhs, + lhs_span, + right.span(), + op, + ) } fn with_operator( diff --git a/crates/nu_plugin_polars/src/dataframe/values/nu_expression/mod.rs b/crates/nu_plugin_polars/src/dataframe/values/nu_expression/mod.rs index cead8c4a11..af00ca94a5 100644 --- a/crates/nu_plugin_polars/src/dataframe/values/nu_expression/mod.rs +++ b/crates/nu_plugin_polars/src/dataframe/values/nu_expression/mod.rs @@ -1,7 +1,10 @@ mod custom_value; use nu_protocol::{record, ShellError, Span, Value}; -use polars::prelude::{col, AggExpr, Expr, Literal}; +use polars::{ + chunked_array::cast::CastOptions, + prelude::{col, AggExpr, Expr, Literal}, +}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use uuid::Uuid; @@ -269,15 +272,23 @@ pub fn expr_to_value(expr: &Expr, span: Span) -> Result { Expr::Cast { expr, data_type, - strict, - } => Ok(Value::record( - record! { - "expr" => expr_to_value(expr.as_ref(), span)?, - "dtype" => Value::string(format!("{data_type:?}"), span), - "strict" => Value::bool(*strict, span), - }, - span, - )), + options, + } => { + let cast_option_str = match options { + CastOptions::Strict => "STRICT", + CastOptions::NonStrict => "NON_STRICT", + CastOptions::Overflowing => "OVERFLOWING", + }; + + Ok(Value::record( + record! { + "expr" => expr_to_value(expr.as_ref(), span)?, + "dtype" => Value::string(format!("{data_type:?}"), span), + "cast_options" => Value::string(cast_option_str, span) + }, + span, + )) + } Expr::Gather { expr, idx, @@ -388,6 +399,7 @@ pub fn expr_to_value(expr: &Expr, span: Span) -> Result { Expr::Window { function, partition_by, + order_by, options, } => { let partition_by: Result, ShellError> = partition_by @@ -399,6 +411,23 @@ pub fn expr_to_value(expr: &Expr, span: Span) -> Result { record! { "function" => expr_to_value(function, span)?, "partition_by" => Value::list(partition_by?, span), + "order_by" => { + if let Some((order_expr, sort_options)) = order_by { + Value::record(record! { + "expr" => expr_to_value(order_expr.as_ref(), span)?, + "sort_options" => { + Value::record(record!( + "descending" => Value::bool(sort_options.descending, span), + "nulls_last"=> Value::bool(sort_options.nulls_last, span), + "multithreaded"=> Value::bool(sort_options.multithreaded, span), + "maintain_order"=> Value::bool(sort_options.maintain_order, span), + ), span) + } + }, span) + } else { + Value::nothing(span) + } + }, "options" => Value::string(format!("{options:?}"), span), }, span, @@ -424,6 +453,18 @@ pub fn expr_to_value(expr: &Expr, span: Span) -> Result { msg_span: span, input_span: Span::unknown(), }), + Expr::Field(column_name) => { + let fields: Vec = column_name + .iter() + .map(|s| Value::string(s.to_string(), span)) + .collect(); + Ok(Value::record( + record!( + "fields" => Value::list(fields, span) + ), + span, + )) + } } } diff --git a/crates/nu_plugin_polars/src/dataframe/values/nu_lazyframe/mod.rs b/crates/nu_plugin_polars/src/dataframe/values/nu_lazyframe/mod.rs index 4dc231d706..e89f14c316 100644 --- a/crates/nu_plugin_polars/src/dataframe/values/nu_lazyframe/mod.rs +++ b/crates/nu_plugin_polars/src/dataframe/values/nu_lazyframe/mod.rs @@ -77,14 +77,17 @@ impl NuLazyFrame { Self::new(self.from_eager, new_frame) } - pub fn schema(&self) -> Result { - let internal_schema = self.lazy.schema().map_err(|e| ShellError::GenericError { - error: "Error getting schema from lazy frame".into(), - msg: e.to_string(), - span: None, - help: None, - inner: vec![], - })?; + pub fn schema(&mut self) -> Result { + let internal_schema = + Arc::make_mut(&mut self.lazy) + .schema() + .map_err(|e| ShellError::GenericError { + error: "Error getting schema from lazy frame".into(), + msg: e.to_string(), + span: None, + help: None, + inner: vec![], + })?; Ok(internal_schema.into()) } diff --git a/crates/nu_plugin_polars/src/lib.rs b/crates/nu_plugin_polars/src/lib.rs index 3baca54ad9..e4d627e4db 100644 --- a/crates/nu_plugin_polars/src/lib.rs +++ b/crates/nu_plugin_polars/src/lib.rs @@ -3,28 +3,48 @@ use std::cmp::Ordering; use cache::cache_commands; pub use cache::{Cache, Cacheable}; use dataframe::{stub::PolarsCmd, values::CustomValueType}; +use log::debug; use nu_plugin::{EngineInterface, Plugin, PluginCommand}; mod cache; pub mod dataframe; pub use dataframe::*; -use nu_protocol::{ast::Operator, CustomValue, LabeledError, Spanned, Value}; +use nu_protocol::{ast::Operator, CustomValue, LabeledError, ShellError, Span, Spanned, Value}; use crate::{ eager::eager_commands, expressions::expr_commands, lazy::lazy_commands, series::series_commands, values::PolarsPluginCustomValue, }; -#[macro_export] -macro_rules! plugin_debug { - ($($arg:tt)*) => {{ - if std::env::var("POLARS_PLUGIN_DEBUG") +pub trait EngineWrapper { + fn get_env_var(&self, key: &str) -> Option; + fn use_color(&self) -> bool; + fn set_gc_disabled(&self, disabled: bool) -> Result<(), ShellError>; +} + +impl EngineWrapper for &EngineInterface { + fn get_env_var(&self, key: &str) -> Option { + EngineInterface::get_env_var(self, key) .ok() - .filter(|x| x == "1" || x == "true") - .is_some() { - eprintln!($($arg)*); - } - }}; + .flatten() + .map(|x| match x { + Value::String { val, .. } => val, + _ => "".to_string(), + }) + } + + fn use_color(&self) -> bool { + self.get_config() + .ok() + .and_then(|config| config.color_config.get("use_color").cloned()) + .unwrap_or(Value::bool(false, Span::unknown())) + .is_true() + } + + fn set_gc_disabled(&self, disabled: bool) -> Result<(), ShellError> { + debug!("set_gc_disabled called with {disabled}"); + EngineInterface::set_gc_disabled(self, disabled) + } } #[derive(Default)] @@ -35,6 +55,10 @@ pub struct PolarsPlugin { } impl Plugin for PolarsPlugin { + fn version(&self) -> String { + env!("CARGO_PKG_VERSION").into() + } + fn commands(&self) -> Vec>> { let mut commands: Vec>> = vec![Box::new(PolarsCmd)]; commands.append(&mut eager_commands()); @@ -50,9 +74,10 @@ impl Plugin for PolarsPlugin { engine: &EngineInterface, custom_value: Box, ) -> Result<(), LabeledError> { + debug!("custom_value_dropped called {:?}", custom_value); if !self.disable_cache_drop { let id = CustomValueType::try_from_custom_value(custom_value)?.id(); - let _ = self.cache.remove(Some(engine), &id, false); + let _ = self.cache.remove(engine, &id, false); } Ok(()) } @@ -193,6 +218,22 @@ pub mod test { } } + struct TestEngineWrapper; + + impl EngineWrapper for TestEngineWrapper { + fn get_env_var(&self, key: &str) -> Option { + std::env::var(key).ok() + } + + fn use_color(&self) -> bool { + false + } + + fn set_gc_disabled(&self, _disabled: bool) -> Result<(), ShellError> { + Ok(()) + } + } + pub fn test_polars_plugin_command(command: &impl PluginCommand) -> Result<(), ShellError> { test_polars_plugin_command_with_decls(command, vec![]) } @@ -212,7 +253,7 @@ pub mod test { let id = obj.id(); plugin .cache - .insert(None, id, obj, Span::test_data()) + .insert(TestEngineWrapper {}, id, obj, Span::test_data()) .unwrap(); } } diff --git a/crates/nu_plugin_polars/src/main.rs b/crates/nu_plugin_polars/src/main.rs index 4a757701ad..a930c5caab 100644 --- a/crates/nu_plugin_polars/src/main.rs +++ b/crates/nu_plugin_polars/src/main.rs @@ -5,5 +5,6 @@ use nu_plugin_polars::PolarsPlugin; static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; fn main() { + env_logger::init(); serve_plugin(&PolarsPlugin::default(), MsgPackSerializer {}) } diff --git a/crates/nu_plugin_python/nu_plugin_python_example.py b/crates/nu_plugin_python/nu_plugin_python_example.py index 3db89a0afc..993e619366 100755 --- a/crates/nu_plugin_python/nu_plugin_python_example.py +++ b/crates/nu_plugin_python/nu_plugin_python_example.py @@ -7,7 +7,7 @@ # decode and encode information that is read and written to stdin and stdout # # To register the plugin use: -# register +# plugin add # # Be careful with the spans. Miette will crash if a span is outside the # size of the contents vector. We strongly suggest using the span found in the @@ -27,7 +27,8 @@ import sys import json -NUSHELL_VERSION = "0.94.3" +NUSHELL_VERSION = "0.95.1" +PLUGIN_VERSION = "0.1.0" # bump if you change commands! def signatures(): @@ -228,7 +229,13 @@ def handle_input(input): exit(0) elif "Call" in input: [id, plugin_call] = input["Call"] - if plugin_call == "Signature": + if plugin_call == "Metadata": + write_response(id, { + "Metadata": { + "version": PLUGIN_VERSION, + } + }) + elif plugin_call == "Signature": write_response(id, signatures()) elif "Run" in plugin_call: process_call(id, plugin_call["Run"]) diff --git a/crates/nu_plugin_query/Cargo.toml b/crates/nu_plugin_query/Cargo.toml index 20a2a2729d..551610e390 100644 --- a/crates/nu_plugin_query/Cargo.toml +++ b/crates/nu_plugin_query/Cargo.toml @@ -5,7 +5,7 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu_plugin_quer edition = "2021" license = "MIT" name = "nu_plugin_query" -version = "0.94.3" +version = "0.95.1" [lib] doctest = false @@ -16,10 +16,13 @@ name = "nu_plugin_query" bench = false [dependencies] -nu-plugin = { path = "../nu-plugin", version = "0.94.3" } -nu-protocol = { path = "../nu-protocol", version = "0.94.3" } +nu-plugin = { path = "../nu-plugin", version = "0.95.1" } +nu-protocol = { path = "../nu-protocol", version = "0.95.1" } gjson = "0.8" scraper = { default-features = false, version = "0.19" } sxd-document = "0.3" sxd-xpath = "0.4" +webpage = { version = "2.0.1", features = ["serde"] } +serde_json.workspace = true +serde.workspace = true diff --git a/crates/nu_plugin_query/README.md b/crates/nu_plugin_query/README.md index a3abd18082..621a004806 100644 --- a/crates/nu_plugin_query/README.md +++ b/crates/nu_plugin_query/README.md @@ -8,6 +8,6 @@ To install: > cargo install --path . ``` -To register (from inside Nushell): +To add the plugin (from inside Nushell): ``` -> register +> plugin add diff --git a/crates/nu_plugin_query/src/lib.rs b/crates/nu_plugin_query/src/lib.rs index 8027c67493..555144d9c7 100644 --- a/crates/nu_plugin_query/src/lib.rs +++ b/crates/nu_plugin_query/src/lib.rs @@ -1,6 +1,7 @@ mod query; mod query_json; mod query_web; +mod query_webpage_info; mod query_xml; mod web_tables; diff --git a/crates/nu_plugin_query/src/query.rs b/crates/nu_plugin_query/src/query.rs index 1e143068c4..273fc858c6 100644 --- a/crates/nu_plugin_query/src/query.rs +++ b/crates/nu_plugin_query/src/query.rs @@ -1,4 +1,7 @@ -use crate::{query_json::QueryJson, query_web::QueryWeb, query_xml::QueryXml}; +use crate::{ + query_json::QueryJson, query_web::QueryWeb, query_webpage_info::QueryWebpageInfo, + query_xml::QueryXml, +}; use nu_plugin::{EvaluatedCall, Plugin, PluginCommand, SimplePluginCommand}; use nu_protocol::{Category, LabeledError, Signature, Value}; @@ -16,12 +19,17 @@ impl Query { } impl Plugin for Query { + fn version(&self) -> String { + env!("CARGO_PKG_VERSION").into() + } + fn commands(&self) -> Vec>> { vec![ Box::new(QueryCommand), Box::new(QueryJson), Box::new(QueryXml), Box::new(QueryWeb), + Box::new(QueryWebpageInfo), ] } } diff --git a/crates/nu_plugin_query/src/query_web.rs b/crates/nu_plugin_query/src/query_web.rs index b78dccbc63..163b320e16 100644 --- a/crates/nu_plugin_query/src/query_web.rs +++ b/crates/nu_plugin_query/src/query_web.rs @@ -24,10 +24,20 @@ impl SimplePluginCommand for QueryWeb { .switch("as-html", "return the query output as html", Some('m')) .named( "attribute", - SyntaxShape::String, + SyntaxShape::Any, "downselect based on the given attribute", Some('a'), ) + // TODO: use detailed shape when https://github.com/nushell/nushell/issues/13253 is resolved + // .named( + // "attribute", + // SyntaxShape::OneOf(vec![ + // SyntaxShape::List(Box::new(SyntaxShape::String)), + // SyntaxShape::String, + // ]), + // "downselect based on the given attribute", + // Some('a'), + // ) .named( "as-table", SyntaxShape::List(Box::new(SyntaxShape::String)), @@ -79,6 +89,11 @@ pub fn web_examples() -> Vec> { example: "http get https://example.org | query web --query a --attribute href", description: "Retrieve a specific html attribute instead of the default text", result: None, + }, + Example { + example: r#"http get https://www.rust-lang.org | query web --query 'meta[property^="og:"]' --attribute [ property content ]"#, + description: r#"Retrieve the OpenGraph properties (``) from a web page"#, + result: None, } ] } @@ -86,7 +101,7 @@ pub fn web_examples() -> Vec> { pub struct Selector { pub query: String, pub as_html: bool, - pub attribute: String, + pub attribute: Value, pub as_table: Value, pub inspect: bool, } @@ -96,7 +111,7 @@ impl Selector { Selector { query: String::new(), as_html: false, - attribute: String::new(), + attribute: Value::string("".to_string(), Span::unknown()), as_table: Value::string("".to_string(), Span::unknown()), inspect: false, } @@ -113,7 +128,9 @@ pub fn parse_selector_params(call: &EvaluatedCall, input: &Value) -> Result> = call.get_flag("query")?; let as_html = call.has_flag("as-html")?; - let attribute = call.get_flag("attribute")?.unwrap_or_default(); + let attribute = call + .get_flag("attribute")? + .unwrap_or_else(|| Value::nothing(head)); let as_table: Value = call .get_flag("as-table")? .unwrap_or_else(|| Value::nothing(head)); @@ -160,11 +177,19 @@ fn begin_selector_query(input_html: String, selector: Selector, span: Span) -> V selector.inspect, span, ) + } else if let Value::List { .. } = selector.attribute { + execute_selector_query_with_attributes( + input_html.as_str(), + selector.query.as_str(), + &selector.attribute, + selector.inspect, + span, + ) } else { execute_selector_query_with_attribute( input_html.as_str(), selector.query.as_str(), - selector.attribute.as_str(), + selector.attribute.as_str().unwrap_or(""), selector.inspect, span, ) @@ -317,6 +342,40 @@ fn execute_selector_query_with_attribute( Value::list(vals, span) } +fn execute_selector_query_with_attributes( + input_string: &str, + query_string: &str, + attributes: &Value, + inspect: bool, + span: Span, +) -> Value { + let doc = Html::parse_fragment(input_string); + + let mut attrs: Vec = Vec::new(); + if let Value::List { vals, .. } = &attributes { + for x in vals { + if let Value::String { val, .. } = x { + attrs.push(val.to_string()) + } + } + } + + let vals: Vec = doc + .select(&css(query_string, inspect)) + .map(|selection| { + let mut record = Record::new(); + for attr in &attrs { + record.push( + attr.to_string(), + Value::string(selection.value().attr(attr).unwrap_or("").to_string(), span), + ); + } + Value::record(record, span) + }) + .collect(); + Value::list(vals, span) +} + fn execute_selector_query( input_string: &str, query_string: &str, @@ -369,6 +428,10 @@ mod tests { "#; const NESTED_TEXT: &str = r#"

Hello there, World

"#; + const MULTIPLE_ATTRIBUTES: &str = r#" + Example + Example + "#; #[test] fn test_first_child_is_not_empty() { @@ -424,4 +487,48 @@ mod tests { vec![vec!["Hello there, ".to_string(), "World".to_string()]], ); } + + #[test] + fn test_multiple_attributes() { + let item = execute_selector_query_with_attributes( + MULTIPLE_ATTRIBUTES, + "a", + &Value::list( + vec![ + Value::string("href".to_string(), Span::unknown()), + Value::string("target".to_string(), Span::unknown()), + ], + Span::unknown(), + ), + false, + Span::test_data(), + ); + let out = item + .into_list() + .unwrap() + .into_iter() + .map(|matches| { + matches + .into_record() + .unwrap() + .into_iter() + .map(|(key, value)| (key, value.coerce_into_string().unwrap())) + .collect::>() + }) + .collect::>>(); + + assert_eq!( + out, + vec![ + vec![ + ("href".to_string(), "https://example.org".to_string()), + ("target".to_string(), "_blank".to_string()) + ], + vec![ + ("href".to_string(), "https://example.com".to_string()), + ("target".to_string(), "_self".to_string()) + ] + ] + ) + } } diff --git a/crates/nu_plugin_query/src/query_webpage_info.rs b/crates/nu_plugin_query/src/query_webpage_info.rs new file mode 100644 index 0000000000..f38f7241f5 --- /dev/null +++ b/crates/nu_plugin_query/src/query_webpage_info.rs @@ -0,0 +1,470 @@ +use nu_plugin::{EngineInterface, EvaluatedCall, SimplePluginCommand}; +use nu_protocol::{Category, Example, LabeledError, Record, Signature, Span, Type, Value}; + +use crate::Query; + +pub struct QueryWebpageInfo; + +impl SimplePluginCommand for QueryWebpageInfo { + type Plugin = Query; + + fn name(&self) -> &str { + "query webpage-info" + } + + fn usage(&self) -> &str { + "uses the webpage crate to extract info from html: title, description, language, links, RSS feeds, Opengraph, Schema.org, and more" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .input_output_type(Type::String, Type::record()) + .category(Category::Network) + } + + fn examples(&self) -> Vec { + web_examples() + } + + fn run( + &self, + _plugin: &Query, + _engine: &EngineInterface, + _call: &EvaluatedCall, + input: &Value, + ) -> Result { + let span = input.span(); + match input { + Value::String { val, .. } => execute_webpage(val, span), + _ => Err(LabeledError::new("Requires text input") + .with_label("expected text from pipeline", span)), + } + } +} + +pub fn web_examples() -> Vec> { + vec![Example { + example: "http get https://phoronix.com | query webpage-info", + description: "extract detailed info from phoronix.com website", + result: None, + }] +} + +fn execute_webpage(html: &str, span: Span) -> Result { + let info = webpage::HTML::from_string(html.to_string(), None) + .map_err(|e| LabeledError::new(e.to_string()).with_label("error parsing html", span))?; + + let value = to_value(info, span).map_err(|e| { + LabeledError::new(e.to_string()).with_label("error convert Value::Record", span) + })?; + + Ok(value) +} + +// revive nu-serde sketch + +use serde::Serialize; + +/// Convert any serde:Serialize into a `nu_protocol::Value` +pub fn to_value(value: T, span: Span) -> Result +where + T: Serialize, +{ + value.serialize(&ValueSerializer { span }) +} + +struct ValueSerializer { + span: Span, +} + +struct MapSerializer<'a> { + record: Record, + serializer: &'a ValueSerializer, + current_key: Option, +} + +impl<'a> serde::Serializer for &'a ValueSerializer { + type Ok = Value; + type Error = Error; + + type SerializeSeq = SeqSerializer<'a>; + type SerializeTuple = SeqSerializer<'a>; + type SerializeTupleStruct = SeqSerializer<'a>; + type SerializeTupleVariant = SeqSerializer<'a>; + + type SerializeMap = MapSerializer<'a>; + type SerializeStruct = MapSerializer<'a>; + type SerializeStructVariant = MapSerializer<'a>; + + fn serialize_bool(self, v: bool) -> Result { + Ok(Value::bool(v, self.span)) + } + + fn serialize_i8(self, v: i8) -> Result { + Ok(Value::int(v.into(), self.span)) + } + + fn serialize_i16(self, v: i16) -> Result { + Ok(Value::int(v.into(), self.span)) + } + + fn serialize_i32(self, v: i32) -> Result { + Ok(Value::int(v.into(), self.span)) + } + + fn serialize_i64(self, v: i64) -> Result { + Ok(Value::int(v, self.span)) + } + + fn serialize_u8(self, v: u8) -> Result { + Ok(Value::int(v.into(), self.span)) + } + + fn serialize_u16(self, v: u16) -> Result { + Ok(Value::int(v.into(), self.span)) + } + + fn serialize_u32(self, v: u32) -> Result { + Ok(Value::int(v.into(), self.span)) + } + + fn serialize_u64(self, _v: u64) -> Result { + // TODO: how to represent a u64 value a Value? + Err(Error::new("the numbers are too big")) + // Ok(Value::int(v.into(), self.span)) + } + + fn serialize_f32(self, v: f32) -> Result { + Ok(Value::float(v.into(), self.span)) + } + + fn serialize_f64(self, v: f64) -> Result { + Ok(Value::float(v, self.span)) + } + + fn serialize_char(self, v: char) -> Result { + Ok(Value::string(v, self.span)) + } + + fn serialize_str(self, v: &str) -> Result { + Ok(Value::string(v, self.span)) + } + + fn serialize_bytes(self, v: &[u8]) -> Result { + Ok(Value::binary(v, self.span)) + } + + fn serialize_none(self) -> Result { + Ok(Value::nothing(self.span)) + } + + fn serialize_some(self, value: &T) -> Result + where + T: Serialize + ?Sized, + { + value.serialize(self) + } + + fn serialize_unit(self) -> Result { + // TODO: is this OK? + Ok(Value::nothing(self.span)) + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { + // TODO: is this OK? + Ok(Value::nothing(self.span)) + } + + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + ) -> Result { + // TODO: is this OK? + Ok(Value::nothing(self.span)) + } + + fn serialize_newtype_struct( + self, + _name: &'static str, + value: &T, + ) -> Result + where + T: Serialize + ?Sized, + { + value.serialize(self) + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + value: &T, + ) -> Result + where + T: Serialize + ?Sized, + { + value.serialize(self) + } + + fn serialize_seq(self, _len: Option) -> Result { + Ok(SeqSerializer::new(self)) + } + + fn serialize_tuple(self, _len: usize) -> Result { + Ok(SeqSerializer::new(self)) + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Ok(SeqSerializer::new(self)) + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Ok(SeqSerializer::new(self)) + } + + fn serialize_map(self, _len: Option) -> Result { + Ok(MapSerializer::new(self)) + } + + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Ok(MapSerializer::new(self)) + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Ok(MapSerializer::new(self)) + } +} + +pub struct Error { + message: String, +} + +impl Error { + pub fn new(msg: T) -> Self { + Error { + message: msg.to_string(), + } + } +} + +impl serde::ser::Error for Error { + fn custom(msg: T) -> Self { + Error::new(msg.to_string()) + } +} + +impl std::fmt::Debug for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message) + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message) + } +} + +impl std::error::Error for Error {} + +// +// maps +impl<'a> MapSerializer<'a> { + fn new(serializer: &'a ValueSerializer) -> Self { + Self { + record: Record::new(), + current_key: None, + serializer, + } + } +} + +impl<'a> serde::ser::SerializeStruct for MapSerializer<'a> { + type Ok = Value; + type Error = Error; + + fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<(), Self::Error> + where + T: Serialize + ?Sized, + { + self.record + .insert(key.to_owned(), value.serialize(self.serializer)?); + Ok(()) + } + + fn end(self) -> Result { + Ok(Value::record(self.record, self.serializer.span)) + } +} + +impl<'a> serde::ser::SerializeMap for MapSerializer<'a> { + type Ok = Value; + type Error = Error; + + fn serialize_key(&mut self, key: &T) -> Result<(), Self::Error> + where + T: Serialize + ?Sized, + { + let value = serde_json::to_value(key).map_err(Error::new)?; + let key = value + .as_str() + .ok_or(Error::new("key must be a string"))? + .to_string(); + self.current_key = Some(key); + Ok(()) + } + + fn serialize_value(&mut self, value: &T) -> Result<(), Self::Error> + where + T: Serialize + ?Sized, + { + let key = self.current_key.take().ok_or(Error::new("key expected"))?; + self.record.insert(key, value.serialize(self.serializer)?); + Ok(()) + } + + fn end(self) -> Result { + Ok(Value::record(self.record, self.serializer.span)) + } +} + +impl<'a> serde::ser::SerializeStructVariant for MapSerializer<'a> { + type Ok = Value; + type Error = Error; + + fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<(), Self::Error> + where + T: Serialize + ?Sized, + { + self.record + .insert(key.to_owned(), value.serialize(self.serializer)?); + Ok(()) + } + + fn end(self) -> Result { + Ok(Value::record(self.record, self.serializer.span)) + } +} + +// +// sequences +struct SeqSerializer<'a> { + seq: Vec, + serializer: &'a ValueSerializer, +} + +impl<'a> SeqSerializer<'a> { + fn new(serializer: &'a ValueSerializer) -> Self { + Self { + seq: Vec::new(), + serializer, + } + } +} + +impl<'a> serde::ser::SerializeSeq for SeqSerializer<'a> { + type Ok = Value; + type Error = Error; + + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> + where + T: Serialize + ?Sized, + { + self.seq.push(value.serialize(self.serializer)?); + Ok(()) + } + + fn end(self) -> Result { + Ok(Value::list(self.seq, self.serializer.span)) + } +} + +impl<'a> serde::ser::SerializeTuple for SeqSerializer<'a> { + type Ok = Value; + type Error = Error; + + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> + where + T: Serialize + ?Sized, + { + self.seq.push(value.serialize(self.serializer)?); + Ok(()) + } + + fn end(self) -> Result { + Ok(Value::list(self.seq, self.serializer.span)) + } +} + +impl<'a> serde::ser::SerializeTupleStruct for SeqSerializer<'a> { + type Ok = Value; + type Error = Error; + + fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> + where + T: Serialize + ?Sized, + { + self.seq.push(value.serialize(self.serializer)?); + Ok(()) + } + + fn end(self) -> Result { + Ok(Value::list(self.seq, self.serializer.span)) + } +} + +impl<'a> serde::ser::SerializeTupleVariant for SeqSerializer<'a> { + type Ok = Value; + type Error = Error; + + fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> + where + T: Serialize + ?Sized, + { + self.seq.push(value.serialize(self.serializer)?); + Ok(()) + } + + fn end(self) -> Result { + Ok(Value::list(self.seq, self.serializer.span)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const HTML: &str = r#" + My Title + "#; + + #[test] + fn test_basics() { + let info = execute_webpage(HTML, Span::test_data()).unwrap(); + let record = info.as_record().unwrap(); + assert_eq!(record.get("title").unwrap().as_str().unwrap(), "My Title"); + } +} diff --git a/crates/nu_plugin_stress_internals/Cargo.toml b/crates/nu_plugin_stress_internals/Cargo.toml index 7103d99cda..2a53eb6749 100644 --- a/crates/nu_plugin_stress_internals/Cargo.toml +++ b/crates/nu_plugin_stress_internals/Cargo.toml @@ -5,7 +5,7 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu_plugin_stre edition = "2021" license = "MIT" name = "nu_plugin_stress_internals" -version = "0.94.3" +version = "0.95.1" [[bin]] name = "nu_plugin_stress_internals" @@ -16,4 +16,4 @@ bench = false # assumptions about the serialized format serde = { workspace = true } serde_json = { workspace = true } -interprocess = { workspace = true } +interprocess = { workspace = true } \ No newline at end of file diff --git a/crates/nu_plugin_stress_internals/src/main.rs b/crates/nu_plugin_stress_internals/src/main.rs index bdbe5c8943..96023b44b7 100644 --- a/crates/nu_plugin_stress_internals/src/main.rs +++ b/crates/nu_plugin_stress_internals/src/main.rs @@ -136,7 +136,21 @@ fn handle_message( ) -> Result<(), Box> { if let Some(plugin_call) = message.get("Call") { let (id, plugin_call) = (&plugin_call[0], &plugin_call[1]); - if plugin_call.as_str() == Some("Signature") { + if plugin_call.as_str() == Some("Metadata") { + write( + output, + &json!({ + "CallResponse": [ + id, + { + "Metadata": { + "version": env!("CARGO_PKG_VERSION"), + } + } + ] + }), + ) + } else if plugin_call.as_str() == Some("Signature") { write( output, &json!({ diff --git a/crates/nuon/Cargo.toml b/crates/nuon/Cargo.toml index 31c0cdd363..bbb2dfe73c 100644 --- a/crates/nuon/Cargo.toml +++ b/crates/nuon/Cargo.toml @@ -5,16 +5,16 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nuon" edition = "2021" license = "MIT" name = "nuon" -version = "0.94.3" +version = "0.95.1" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -nu-parser = { path = "../nu-parser", version = "0.94.3" } -nu-protocol = { path = "../nu-protocol", version = "0.94.3" } -nu-engine = { path = "../nu-engine", version = "0.94.3" } +nu-parser = { path = "../nu-parser", version = "0.95.1" } +nu-protocol = { path = "../nu-protocol", version = "0.95.1" } +nu-engine = { path = "../nu-engine", version = "0.95.1" } once_cell = { workspace = true } fancy-regex = { workspace = true } [dev-dependencies] -chrono = { workspace = true } +chrono = { workspace = true } \ No newline at end of file diff --git a/crates/nuon/README.md b/crates/nuon/README.md index d11aa8104c..158a9d2e7d 100644 --- a/crates/nuon/README.md +++ b/crates/nuon/README.md @@ -4,7 +4,7 @@ The NUON format is a superset of JSON designed to fit the feel of Nushell. Some of its extra features are - trailing commas are allowed - commas are optional in lists -- quotes are not required around keys or any _bare_ string that do not contain spaces +- quotes are not required around keys or any _bare_ string that do not contain spaces or special characters - comments are allowed, though not preserved when using [`from_nuon`] ## Example diff --git a/crates/nuon/src/from.rs b/crates/nuon/src/from.rs index f9230d6140..2138e50ac5 100644 --- a/crates/nuon/src/from.rs +++ b/crates/nuon/src/from.rs @@ -323,6 +323,18 @@ fn convert_to_value( msg: "string interpolation not supported in nuon".into(), span: expr.span, }), + Expr::GlobInterpolation(..) => Err(ShellError::OutsideSpannedLabeledError { + src: original_text.to_string(), + error: "Error when loading".into(), + msg: "glob interpolation not supported in nuon".into(), + span: expr.span, + }), + Expr::Collect(..) => Err(ShellError::OutsideSpannedLabeledError { + src: original_text.to_string(), + error: "Error when loading".into(), + msg: "`$in` not supported in nuon".into(), + span: expr.span, + }), Expr::Subexpression(..) => Err(ShellError::OutsideSpannedLabeledError { src: original_text.to_string(), error: "Error when loading".into(), diff --git a/docker/Dockerfile b/docker/Dockerfile index 209b17d419..64c37f6230 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -24,7 +24,7 @@ RUN echo '/usr/bin/nu' >> /etc/shells \ && chmod +x /usr/bin/nu \ && chown -R nushell:nushell /home/nushell/.config/nushell \ && ls /usr/bin/nu_plugin* \ - | xargs -I{} su -c 'register {}' nushell \ + | xargs -I{} su -c 'plugin add {}' nushell \ && rm -rf /tmp/* USER nushell diff --git a/scripts/README.md b/scripts/README.md index 366ff76375..37388e74b5 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -23,5 +23,4 @@ | `coverage-local.sh` | x | x | x | | `install-all.ps1` | ? | x | ? | | `install-all.sh` | x | x | x | -| `register-plugins.nu` | x | x | x | | `uninstall-all.sh` | x | x | x | diff --git a/scripts/build.rs b/scripts/build.rs index beabba80dc..e366b4998c 100644 --- a/scripts/build.rs +++ b/scripts/build.rs @@ -14,5 +14,5 @@ fn main() { // Tango uses dynamic linking, to allow us to dynamically change between two bench suit at runtime. // This is currently not supported on non nightly rust, on windows. println!("cargo:rustc-link-arg-benches=-rdynamic"); - println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=scripts/build.rs"); } diff --git a/scripts/register-plugins.nu b/scripts/register-plugins.nu deleted file mode 100644 index 0447ffb0c1..0000000000 --- a/scripts/register-plugins.nu +++ /dev/null @@ -1,55 +0,0 @@ -use std log warning - -warning "./scripts/register-plugin.nu will be deprecated, please use the `toolkit plugin register` command instead" - -# are we on windows or not? -def windows? [] { - $nu.os-info.name == windows -} - -# filter out files that end in .d -def keep-plugin-executables [] { - if (windows?) { where name ends-with '.exe' } else { where name !~ '\.d' } -} - -# get list of all plugin files from their installed directory -let plugins = (ls ((which nu).path.0 | path dirname) | where name =~ nu_plugin | keep-plugin-executables) -for plugin in $plugins { - print -n $"registering ($plugin.name), " - nu -c $"register '($plugin.name)'" - print "success!" -} - -# print helpful message -print "\nplugins registered, please restart nushell" - -# Plugin Location -# https://github.com/nushell/nushell/tree/main/crates/nu_plugin_custom_values -# https://github.com/nushell/nushell/tree/main/crates/nu_plugin_example -# https://github.com/nushell/nushell/tree/main/crates/nu_plugin_gstat -# https://github.com/nushell/nushell/tree/main/crates/nu_plugin_inc -# https://github.com/nushell/nushell/tree/main/crates/nu_plugin_python -# https://github.com/nushell/nushell/tree/main/crates/nu_plugin_query -# https://github.com/fdncred/nu_plugin_from_parquet -# https://github.com/fdncred/nu_plugin_from_regex -# https://github.com/fdncred/nu_plugin_pnet -# https://github.com/JosephTLyons/nu_plugin_periodic_table -# https://github.com/Euphrasiologist/nu_plugin_bio -# https://github.com/realcundo/nu_plugin_dcm -# https://github.com/enerdgumen/nu_plugin_dotenv -# https://github.com/bluk/nu_plugin_from_bencode - -# Older plugins -# https://github.com/notryanb/nu_plugin_id3 -# https://github.com/notryanb/nu_plugin_weather -# https://github.com/tiffany352/nu-plugins/tree/main/from_nbt -# https://github.com/tiffany352/nu-plugins/tree/main/file_exists -# https://github.com/potan/nu_plugin_wifiscan -# https://github.com/autophagy/nu_plugin_from_dhall -# https://github.com/yanganto/nu_plugin_s3 -# https://github.com/lukasreuter/nu_plugin_unity -# https://github.com/filaretov/nu_plugin_path_temp -# https://github.com/cdecompilador/nu_plugin_bg -# https://github.com/aJuvan/nu_plugin_kubectl -# https://github.com/hedonihilist/nu_plugin_df - diff --git a/src/command.rs b/src/command.rs index 15da0e3e47..5ba8a60996 100644 --- a/src/command.rs +++ b/src/command.rs @@ -29,8 +29,10 @@ pub(crate) fn gather_commandline_args() -> (Vec, String, Vec) { } let flag_value = match arg.as_ref() { - "--commands" | "-c" | "--table-mode" | "-m" | "-e" | "--execute" | "--config" - | "--env-config" | "-I" | "ide-ast" => args.next().map(|a| escape_quote_string(&a)), + "--commands" | "-c" | "--table-mode" | "-m" | "--error-style" | "-e" | "--execute" + | "--config" | "--env-config" | "-I" | "ide-ast" => { + args.next().map(|a| escape_quote_string(&a)) + } #[cfg(feature = "plugin")] "--plugin-config" => args.next().map(|a| escape_quote_string(&a)), "--log-level" | "--log-target" | "--log-include" | "--log-exclude" | "--testbin" @@ -102,6 +104,8 @@ pub(crate) fn parse_commandline_args( let execute = call.get_flag_expr("execute"); let table_mode: Option = call.get_flag(engine_state, &mut stack, "table-mode")?; + let error_style: Option = + call.get_flag(engine_state, &mut stack, "error-style")?; let no_newline = call.get_named_arg("no-newline"); // ide flags @@ -245,6 +249,7 @@ pub(crate) fn parse_commandline_args( ide_check, ide_ast, table_mode, + error_style, no_newline, }); } @@ -278,6 +283,7 @@ pub(crate) struct NushellCliArgs { pub(crate) log_exclude: Option>>, pub(crate) execute: Option>, pub(crate) table_mode: Option, + pub(crate) error_style: Option, pub(crate) no_newline: Option>, pub(crate) include_path: Option>, pub(crate) lsp: bool, @@ -325,6 +331,12 @@ impl Command for Nu { "the table mode to use. rounded is default.", Some('m'), ) + .named( + "error-style", + SyntaxShape::String, + "the error style to use (fancy or plain). default: fancy", + None, + ) .switch("no-newline", "print the result for --commands(-c) without a newline", None) .switch( "no-config-file", diff --git a/src/config_files.rs b/src/config_files.rs index ec4511860f..30977a6d0e 100644 --- a/src/config_files.rs +++ b/src/config_files.rs @@ -9,8 +9,9 @@ use nu_protocol::{ }; use nu_utils::{get_default_config, get_default_env}; use std::{ + fs, fs::File, - io::Write, + io::{Result, Write}, panic::{catch_unwind, AssertUnwindSafe}, path::Path, sync::Arc, @@ -176,6 +177,46 @@ pub(crate) fn read_default_env_file(engine_state: &mut EngineState, stack: &mut } } +fn read_and_sort_directory(path: &Path) -> Result> { + let mut entries = Vec::new(); + + for entry in fs::read_dir(path)? { + let entry = entry?; + let file_name = entry.file_name(); + let file_name_str = file_name.into_string().unwrap_or_default(); + entries.push(file_name_str); + } + + entries.sort(); + + Ok(entries) +} + +pub(crate) fn read_vendor_autoload_files(engine_state: &mut EngineState, stack: &mut Stack) { + warn!( + "read_vendor_autoload_files() {}:{}:{}", + file!(), + line!(), + column!() + ); + + // read and source vendor_autoload_files file if exists + if let Some(autoload_dir) = nu_protocol::eval_const::get_vendor_autoload_dir(engine_state) { + warn!("read_vendor_autoload_files: {}", autoload_dir.display()); + + if autoload_dir.exists() { + let entries = read_and_sort_directory(&autoload_dir); + if let Ok(entries) = entries { + for entry in entries { + let path = autoload_dir.join(entry); + warn!("AutoLoading: {:?}", path); + eval_config_contents(path, engine_state, stack); + } + } + } + } +} + fn eval_default_config( engine_state: &mut EngineState, stack: &mut Stack, @@ -236,6 +277,8 @@ pub(crate) fn setup_config( if is_login_shell { read_loginshell_file(engine_state, stack); } + // read and auto load vendor autoload files + read_vendor_autoload_files(engine_state, stack); })); if result.is_err() { eprintln!( diff --git a/src/main.rs b/src/main.rs index f9fdff3ccf..f220731d1d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,14 +29,10 @@ use nu_protocol::{ Value, }; use nu_std::load_standard_library; -use nu_utils::utils::perf; +use nu_utils::perf; use run::{run_commands, run_file, run_repl}; use signals::ctrlc_protection; -use std::{ - path::PathBuf, - str::FromStr, - sync::{atomic::AtomicBool, Arc}, -}; +use std::{path::PathBuf, str::FromStr, sync::Arc}; fn get_engine_state() -> EngineState { let engine_state = nu_cmd_lang::create_default_context(); @@ -74,9 +70,8 @@ fn main() -> Result<()> { report_error_new(&engine_state, &err); } - let ctrlc = Arc::new(AtomicBool::new(false)); // TODO: make this conditional in the future - ctrlc_protection(&mut engine_state, &ctrlc); + ctrlc_protection(&mut engine_state); // Begin: Default NU_LIB_DIRS, NU_PLUGIN_DIRS // Set default NU_LIB_DIRS and NU_PLUGIN_DIRS here before the env.nu is processed. If @@ -104,7 +99,9 @@ fn main() -> Result<()> { default: nushell_config_path.display().to_string(), }, ); - } else if let Some(old_config) = nu_path::config_dir_old().map(|p| p.join("nushell")) { + } else if let Some(old_config) = + nu_path::get_canonicalized_path(dirs::config_dir()).map(|p| p.join("nushell")) + { let xdg_config_empty = nushell_config_path .read_dir() .map_or(true, |mut dir| dir.next().is_none()); @@ -125,13 +122,22 @@ fn main() -> Result<()> { } } + let default_nushell_completions_path = if let Some(mut path) = nu_path::data_dir() { + path.push("nushell"); + path.push("completions"); + path + } else { + std::path::PathBuf::new() + }; + let mut default_nu_lib_dirs_path = nushell_config_path.clone(); default_nu_lib_dirs_path.push("scripts"); engine_state.add_env_var( "NU_LIB_DIRS".to_string(), - Value::test_list(vec![Value::test_string( - default_nu_lib_dirs_path.to_string_lossy(), - )]), + Value::test_list(vec![ + Value::test_string(default_nu_lib_dirs_path.to_string_lossy()), + Value::test_string(default_nushell_completions_path.to_string_lossy()), + ]), ); let mut default_nu_plugin_dirs_path = nushell_config_path; @@ -208,14 +214,7 @@ fn main() -> Result<()> { logger(|builder| configure(&level, &target, filters, builder))?; // info!("start logging {}:{}:{}", file!(), line!(), column!()); - perf( - "start logging", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("start logging", start_time, use_color); } start_time = std::time::Instant::now(); @@ -234,27 +233,13 @@ fn main() -> Result<()> { "env-path", parsed_nu_cli_args.env_file.as_ref(), ); - perf( - "set_config_path", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("set_config_path", start_time, use_color); #[cfg(unix)] { start_time = std::time::Instant::now(); terminal::acquire(engine_state.is_interactive); - perf( - "acquire_terminal", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("acquire_terminal", start_time, use_color); } start_time = std::time::Instant::now(); @@ -268,26 +253,12 @@ fn main() -> Result<()> { engine_state.add_env_var("NU_LIB_DIRS".into(), Value::list(vals, span)); } - perf( - "NU_LIB_DIRS setup", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("NU_LIB_DIRS setup", start_time, use_color); start_time = std::time::Instant::now(); // First, set up env vars as strings only gather_parent_env_vars(&mut engine_state, &init_cwd); - perf( - "gather env vars", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("gather env vars", start_time, use_color); engine_state.add_env_var( "NU_VERSION".to_string(), @@ -348,14 +319,7 @@ fn main() -> Result<()> { } std::process::exit(0) } - perf( - "run test_bins", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("run test_bins", start_time, use_color); start_time = std::time::Instant::now(); let input = if let Some(redirect_stdin) = &parsed_nu_cli_args.redirect_stdin { @@ -365,31 +329,17 @@ fn main() -> Result<()> { trace!("not redirecting stdin"); PipelineData::empty() }; - perf( - "redirect stdin", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("redirect stdin", start_time, use_color); start_time = std::time::Instant::now(); // Set up the $nu constant before evaluating config files (need to have $nu available in them) engine_state.generate_nu_constant(); - perf( - "create_nu_constant", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("create_nu_constant", start_time, use_color); #[cfg(feature = "plugin")] if let Some(plugins) = &parsed_nu_cli_args.plugins { use nu_plugin_engine::{GetPlugin, PluginDeclaration}; - use nu_protocol::{engine::StateWorkingSet, ErrSpan, PluginIdentity}; + use nu_protocol::{engine::StateWorkingSet, ErrSpan, PluginIdentity, RegisteredPlugin}; // Load any plugins specified with --plugins start_time = std::time::Instant::now(); @@ -408,34 +358,26 @@ fn main() -> Result<()> { // Create the plugin and add it to the working set let plugin = nu_plugin_engine::add_plugin_to_working_set(&mut working_set, &identity)?; - // Spawn the plugin to get its signatures, and then add the commands to the working set - for signature in plugin.clone().get_plugin(None)?.get_signature()? { + // Spawn the plugin to get the metadata and signatures + let interface = plugin.clone().get_plugin(None)?; + + // Set its metadata + plugin.set_metadata(Some(interface.get_metadata()?)); + + // Add the commands from the signature to the working set + for signature in interface.get_signature()? { let decl = PluginDeclaration::new(plugin.clone(), signature); working_set.add_decl(Box::new(decl)); } } engine_state.merge_delta(working_set.render())?; - perf( - "load plugins specified in --plugins", - start_time, - file!(), - line!(), - column!(), - use_color, - ) + perf!("load plugins specified in --plugins", start_time, use_color) } start_time = std::time::Instant::now(); if parsed_nu_cli_args.lsp { - perf( - "lsp starting", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("lsp starting", start_time, use_color); if parsed_nu_cli_args.no_config_file.is_none() { let mut stack = nu_protocol::engine::Stack::new(); @@ -450,7 +392,7 @@ fn main() -> Result<()> { ); } - LanguageServer::initialize_stdio_connection()?.serve_requests(engine_state, ctrlc)? + LanguageServer::initialize_stdio_connection()?.serve_requests(engine_state)? } else if let Some(commands) = parsed_nu_cli_args.commands.clone() { run_commands( &mut engine_state, diff --git a/src/run.rs b/src/run.rs index 2996bc76f8..10a5043b25 100644 --- a/src/run.rs +++ b/src/run.rs @@ -7,12 +7,12 @@ use crate::{ use log::trace; #[cfg(feature = "plugin")] use nu_cli::read_plugin_file; -use nu_cli::{evaluate_commands, evaluate_file, evaluate_repl}; +use nu_cli::{evaluate_commands, evaluate_file, evaluate_repl, EvaluateCommandsOpts}; use nu_protocol::{ engine::{EngineState, Stack}, report_error_new, PipelineData, Spanned, }; -use nu_utils::utils::perf; +use nu_utils::perf; pub(crate) fn run_commands( engine_state: &mut EngineState, @@ -26,6 +26,10 @@ pub(crate) fn run_commands( let mut stack = Stack::new(); let start_time = std::time::Instant::now(); + if stack.has_env_var(engine_state, "NU_USE_IR") { + stack.use_ir = true; + } + // if the --no-config-file(-n) option is NOT passed, load the plugin file, // load the default env file or custom (depending on parsed_nu_cli_args.env_file), // and maybe a custom config file (depending on parsed_nu_cli_args.config_file) @@ -35,14 +39,7 @@ pub(crate) fn run_commands( #[cfg(feature = "plugin")] read_plugin_file(engine_state, parsed_nu_cli_args.plugin_file, NUSHELL_FOLDER); - perf( - "read plugins", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("read plugins", start_time, use_color); let start_time = std::time::Instant::now(); // If we have a env file parameter *OR* we have a login shell parameter, read the env file @@ -57,14 +54,7 @@ pub(crate) fn run_commands( config_files::read_default_env_file(engine_state, &mut stack) } - perf( - "read env.nu", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("read env.nu", start_time, use_color); let start_time = std::time::Instant::now(); // If we have a config file parameter *OR* we have a login shell parameter, read the config file @@ -77,14 +67,7 @@ pub(crate) fn run_commands( ); } - perf( - "read config.nu", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("read config.nu", start_time, use_color); // If we have a login shell parameter, read the login file let start_time = std::time::Instant::now(); @@ -92,14 +75,7 @@ pub(crate) fn run_commands( config_files::read_loginshell_file(engine_state, &mut stack); } - perf( - "read login.nu", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("read login.nu", start_time, use_color); } // Before running commands, set up the startup time @@ -114,20 +90,16 @@ pub(crate) fn run_commands( engine_state, &mut stack, input, - parsed_nu_cli_args.table_mode, - parsed_nu_cli_args.no_newline.is_some(), + EvaluateCommandsOpts { + table_mode: parsed_nu_cli_args.table_mode, + error_style: parsed_nu_cli_args.error_style, + no_newline: parsed_nu_cli_args.no_newline.is_some(), + }, ) { report_error_new(engine_state, &err); std::process::exit(1); } - perf( - "evaluate_commands", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("evaluate_commands", start_time, use_color); } pub(crate) fn run_file( @@ -141,6 +113,10 @@ pub(crate) fn run_file( trace!("run_file"); let mut stack = Stack::new(); + if stack.has_env_var(engine_state, "NU_USE_IR") { + stack.use_ir = true; + } + // if the --no-config-file(-n) option is NOT passed, load the plugin file, // load the default env file or custom (depending on parsed_nu_cli_args.env_file), // and maybe a custom config file (depending on parsed_nu_cli_args.config_file) @@ -150,14 +126,7 @@ pub(crate) fn run_file( let start_time = std::time::Instant::now(); #[cfg(feature = "plugin")] read_plugin_file(engine_state, parsed_nu_cli_args.plugin_file, NUSHELL_FOLDER); - perf( - "read plugins", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("read plugins", start_time, use_color); let start_time = std::time::Instant::now(); // only want to load config and env if relative argument is provided. @@ -171,14 +140,7 @@ pub(crate) fn run_file( } else { config_files::read_default_env_file(engine_state, &mut stack) } - perf( - "read env.nu", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("read env.nu", start_time, use_color); let start_time = std::time::Instant::now(); if parsed_nu_cli_args.config_file.is_some() { @@ -189,14 +151,7 @@ pub(crate) fn run_file( false, ); } - perf( - "read config.nu", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("read config.nu", start_time, use_color); } // Regenerate the $nu constant to contain the startup time and any other potential updates @@ -213,14 +168,7 @@ pub(crate) fn run_file( report_error_new(engine_state, &err); std::process::exit(1); } - perf( - "evaluate_file", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("evaluate_file", start_time, use_color); let start_time = std::time::Instant::now(); let last_exit_code = stack.get_env_var(&*engine_state, "LAST_EXIT_CODE"); @@ -232,14 +180,7 @@ pub(crate) fn run_file( } } } - perf( - "get exit code", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("get exit code", start_time, use_color); } pub(crate) fn run_repl( @@ -251,6 +192,10 @@ pub(crate) fn run_repl( let mut stack = Stack::new(); let start_time = std::time::Instant::now(); + if stack.has_env_var(engine_state, "NU_USE_IR") { + stack.use_ir = true; + } + if parsed_nu_cli_args.no_config_file.is_none() { setup_config( engine_state, @@ -265,14 +210,7 @@ pub(crate) fn run_repl( // Reload use_color from config in case it's different from the default value let use_color = engine_state.get_config().use_ansi_coloring; - perf( - "setup_config", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("setup_config", start_time, use_color); let start_time = std::time::Instant::now(); let ret_val = evaluate_repl( @@ -283,14 +221,7 @@ pub(crate) fn run_repl( parsed_nu_cli_args.no_std_lib, entire_start_time, ); - perf( - "evaluate_repl", - start_time, - file!(), - line!(), - column!(), - use_color, - ); + perf!("evaluate_repl", start_time, use_color); ret_val } diff --git a/src/signals.rs b/src/signals.rs index 9247ccb095..bb3539b95e 100644 --- a/src/signals.rs +++ b/src/signals.rs @@ -1,17 +1,14 @@ -use nu_protocol::engine::EngineState; +use nu_protocol::{engine::EngineState, Signals}; use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, }; -pub(crate) fn ctrlc_protection(engine_state: &mut EngineState, ctrlc: &Arc) { - let handler_ctrlc = ctrlc.clone(); - let engine_state_ctrlc = ctrlc.clone(); - +pub(crate) fn ctrlc_protection(engine_state: &mut EngineState) { + let interrupt = Arc::new(AtomicBool::new(false)); + engine_state.set_signals(Signals::new(interrupt.clone())); ctrlc::set_handler(move || { - handler_ctrlc.store(true, Ordering::SeqCst); + interrupt.store(true, Ordering::Relaxed); }) .expect("Error setting Ctrl-C handler"); - - engine_state.ctrlc = Some(engine_state_ctrlc); } diff --git a/tests/eval/mod.rs b/tests/eval/mod.rs index f3af92376a..e9afd401e9 100644 --- a/tests/eval/mod.rs +++ b/tests/eval/mod.rs @@ -1,7 +1,8 @@ -use nu_test_support::nu; +use nu_test_support::{nu, playground::Playground}; +use regex::Regex; #[test] -fn source_file_relative_to_file() { +fn record_with_redefined_key() { let actual = nu!("{x: 1, x: 2}"); assert!(actual.err.contains("redefined")); @@ -16,3 +17,455 @@ fn run_file_parse_error() { assert!(actual.err.contains("unknown type")); } + +enum ExpectedOut<'a> { + /// Equals a string exactly + Eq(&'a str), + /// Matches a regex + Matches(&'a str), + /// Produces an error (match regex) + Error(&'a str), + /// Drops a file that contains these contents + FileEq(&'a str, &'a str), +} +use self::ExpectedOut::*; + +fn test_eval(source: &str, expected_out: ExpectedOut) { + Playground::setup("test_eval_ast", |ast_dirs, _playground| { + Playground::setup("test_eval_ir", |ir_dirs, _playground| { + let actual_ast = nu!( + cwd: ast_dirs.test(), + use_ir: false, + source, + ); + let actual_ir = nu!( + cwd: ir_dirs.test(), + use_ir: true, + source, + ); + + match expected_out { + Eq(eq) => { + assert_eq!(actual_ast.out, eq); + assert_eq!(actual_ir.out, eq); + assert!(actual_ast.status.success()); + assert!(actual_ir.status.success()); + } + Matches(regex) => { + let compiled_regex = Regex::new(regex).expect("regex failed to compile"); + assert!( + compiled_regex.is_match(&actual_ast.out), + "AST eval out does not match: {}\n{}", + regex, + actual_ast.out + ); + assert!( + compiled_regex.is_match(&actual_ir.out), + "IR eval out does not match: {}\n{}", + regex, + actual_ir.out, + ); + assert!(actual_ast.status.success()); + assert!(actual_ir.status.success()); + } + Error(regex) => { + let compiled_regex = Regex::new(regex).expect("regex failed to compile"); + assert!( + compiled_regex.is_match(&actual_ast.err), + "AST eval err does not match: {}", + regex + ); + assert!( + compiled_regex.is_match(&actual_ir.err), + "IR eval err does not match: {}", + regex + ); + assert!(!actual_ast.status.success()); + assert!(!actual_ir.status.success()); + } + FileEq(path, contents) => { + let ast_contents = std::fs::read_to_string(ast_dirs.test().join(path)) + .expect("failed to read AST file"); + let ir_contents = std::fs::read_to_string(ir_dirs.test().join(path)) + .expect("failed to read IR file"); + assert_eq!(ast_contents.trim(), contents); + assert_eq!(ir_contents.trim(), contents); + assert!(actual_ast.status.success()); + assert!(actual_ir.status.success()); + } + } + assert_eq!(actual_ast.out, actual_ir.out); + }) + }); +} + +#[test] +fn literal_bool() { + test_eval("true", Eq("true")) +} + +#[test] +fn literal_int() { + test_eval("1", Eq("1")) +} + +#[test] +fn literal_float() { + test_eval("1.5", Eq("1.5")) +} + +#[test] +fn literal_filesize() { + test_eval("30MiB", Eq("30.0 MiB")) +} + +#[test] +fn literal_duration() { + test_eval("30ms", Eq("30ms")) +} + +#[test] +fn literal_binary() { + test_eval("0x[1f 2f f0]", Matches("Length.*1f.*2f.*f0")) +} + +#[test] +fn literal_closure() { + test_eval("{||}", Matches(" hello.txt", + FileEq("hello.txt", "hello"), + ) +} + +#[test] +fn let_variable() { + test_eval("let foo = 'test'; print $foo", Eq("test")) +} + +#[test] +fn let_variable_mutate_error() { + test_eval( + "let foo = 'test'; $foo = 'bar'; print $foo", + Error("immutable"), + ) +} + +#[test] +fn constant() { + test_eval("const foo = 1 + 2; print $foo", Eq("3")) +} + +#[test] +fn constant_assign_error() { + test_eval( + "const foo = 1 + 2; $foo = 4; print $foo", + Error("immutable"), + ) +} + +#[test] +fn mut_variable() { + test_eval("mut foo = 'test'; $foo = 'bar'; print $foo", Eq("bar")) +} + +#[test] +fn mut_variable_append_assign() { + test_eval( + "mut foo = 'test'; $foo ++= 'bar'; print $foo", + Eq("testbar"), + ) +} + +#[test] +fn bind_in_variable_to_input() { + test_eval("3 | (4 + $in)", Eq("7")) +} + +#[test] +fn if_true() { + test_eval("if true { 'foo' }", Eq("foo")) +} + +#[test] +fn if_false() { + test_eval("if false { 'foo' } | describe", Eq("nothing")) +} + +#[test] +fn if_else_true() { + test_eval("if 5 > 3 { 'foo' } else { 'bar' }", Eq("foo")) +} + +#[test] +fn if_else_false() { + test_eval("if 5 < 3 { 'foo' } else { 'bar' }", Eq("bar")) +} + +#[test] +fn match_empty_fallthrough() { + test_eval("match 42 { }; 'pass'", Eq("pass")) +} + +#[test] +fn match_value() { + test_eval("match 1 { 1 => 'pass', 2 => 'fail' }", Eq("pass")) +} + +#[test] +fn match_value_default() { + test_eval( + "match 3 { 1 => 'fail1', 2 => 'fail2', _ => 'pass' }", + Eq("pass"), + ) +} + +#[test] +fn match_value_fallthrough() { + test_eval("match 3 { 1 => 'fail1', 2 => 'fail2' }", Eq("")) +} + +#[test] +fn match_variable() { + test_eval( + "match 'pass' { $s => { print $s }, _ => { print 'fail' } }", + Eq("pass"), + ) +} + +#[test] +fn match_variable_in_list() { + test_eval("match [fail pass] { [$f, $p] => { print $p } }", Eq("pass")) +} + +#[test] +fn match_passthrough_input() { + test_eval( + "'yes' | match [pass fail] { [$p, ..] => (collect { |y| $y ++ $p }) }", + Eq("yespass"), + ) +} + +#[test] +fn while_mutate_var() { + test_eval("mut x = 2; while $x > 0 { print $x; $x -= 1 }", Eq("21")) +} + +#[test] +fn for_list() { + test_eval("for v in [1 2 3] { print ($v * 2) }", Eq(r"246")) +} + +#[test] +fn for_seq() { + test_eval("for v in (seq 1 4) { print ($v * 2) }", Eq("2468")) +} + +#[test] +fn early_return() { + test_eval("do { return 'foo'; 'bar' }", Eq("foo")) +} + +#[test] +fn early_return_from_if() { + test_eval("do { if true { return 'pass' }; 'fail' }", Eq("pass")) +} + +#[test] +fn early_return_from_loop() { + test_eval("do { loop { return 'pass' } }", Eq("pass")) +} + +#[test] +fn early_return_from_while() { + test_eval( + "do { let x = true; while $x { return 'pass' } }", + Eq("pass"), + ) +} + +#[test] +fn early_return_from_for() { + test_eval("do { for x in [pass fail] { return $x } }", Eq("pass")) +} + +#[test] +fn try_no_catch() { + test_eval("try { error make { msg: foo } }; 'pass'", Eq("pass")) +} + +#[test] +fn try_catch_no_var() { + test_eval( + "try { error make { msg: foo } } catch { 'pass' }", + Eq("pass"), + ) +} + +#[test] +fn try_catch_var() { + test_eval( + "try { error make { msg: foo } } catch { |err| $err.msg }", + Eq("foo"), + ) +} + +#[test] +fn try_catch_with_non_literal_closure_no_var() { + test_eval( + r#" + let error_handler = { || "pass" } + try { error make { msg: foobar } } catch $error_handler + "#, + Eq("pass"), + ) +} + +#[test] +fn try_catch_with_non_literal_closure() { + test_eval( + r#" + let error_handler = { |err| $err.msg } + try { error make { msg: foobar } } catch $error_handler + "#, + Eq("foobar"), + ) +} + +#[test] +fn row_condition() { + test_eval( + "[[a b]; [1 2] [3 4]] | where a < 3 | to nuon", + Eq("[[a, b]; [1, 2]]"), + ) +} + +#[test] +fn custom_command() { + test_eval( + r#" + def cmd [a: int, b: string = 'fail', ...c: string, --x: int] { $"($a)($b)($c)($x)" } + cmd 42 pass foo --x 30 + "#, + Eq("42pass[foo]30"), + ) +} diff --git a/tests/fixtures/partial_completions/partial_a/anotherfile b/tests/fixtures/dotnu_completions/dir_module/mod.nu similarity index 100% rename from tests/fixtures/partial_completions/partial_a/anotherfile rename to tests/fixtures/dotnu_completions/dir_module/mod.nu diff --git a/tests/fixtures/partial_completions/partial_a/have_ext.exe b/tests/fixtures/dotnu_completions/foo.nu similarity index 100% rename from tests/fixtures/partial_completions/partial_a/have_ext.exe rename to tests/fixtures/dotnu_completions/foo.nu diff --git a/tests/fixtures/partial_completions/partial_a/have_ext.txt b/tests/fixtures/dotnu_completions/lib-dir1/bar.nu similarity index 100% rename from tests/fixtures/partial_completions/partial_a/have_ext.txt rename to tests/fixtures/dotnu_completions/lib-dir1/bar.nu diff --git a/tests/fixtures/partial_completions/partial_a/hello b/tests/fixtures/dotnu_completions/lib-dir1/baz.nu similarity index 100% rename from tests/fixtures/partial_completions/partial_a/hello rename to tests/fixtures/dotnu_completions/lib-dir1/baz.nu diff --git a/tests/fixtures/partial_completions/partial_a/hola b/tests/fixtures/dotnu_completions/lib-dir1/xyzzy.nu similarity index 100% rename from tests/fixtures/partial_completions/partial_a/hola rename to tests/fixtures/dotnu_completions/lib-dir1/xyzzy.nu diff --git a/tests/fixtures/partial_completions/partial_b/hello_b b/tests/fixtures/dotnu_completions/lib-dir2/asdf.nu similarity index 100% rename from tests/fixtures/partial_completions/partial_b/hello_b rename to tests/fixtures/dotnu_completions/lib-dir2/asdf.nu diff --git a/tests/fixtures/partial_completions/partial_b/hi_b b/tests/fixtures/dotnu_completions/lib-dir2/bat.nu similarity index 100% rename from tests/fixtures/partial_completions/partial_b/hi_b rename to tests/fixtures/dotnu_completions/lib-dir2/bat.nu diff --git a/tests/fixtures/partial_completions/partial_c/hello_c b/tests/fixtures/dotnu_completions/lib-dir3/spam.nu similarity index 100% rename from tests/fixtures/partial_completions/partial_c/hello_c rename to tests/fixtures/dotnu_completions/lib-dir3/spam.nu diff --git a/tests/fixtures/partial_completions/partial-a/anotherfile b/tests/fixtures/partial_completions/partial-a/anotherfile new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/fixtures/partial_completions/partial-a/have_ext.exe b/tests/fixtures/partial_completions/partial-a/have_ext.exe new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/fixtures/partial_completions/partial-a/have_ext.txt b/tests/fixtures/partial_completions/partial-a/have_ext.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/fixtures/partial_completions/partial-a/hello b/tests/fixtures/partial_completions/partial-a/hello new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/fixtures/partial_completions/partial-a/hola b/tests/fixtures/partial_completions/partial-a/hola new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/fixtures/partial_completions/partial-b/hello_b b/tests/fixtures/partial_completions/partial-b/hello_b new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/fixtures/partial_completions/partial-b/hi_b b/tests/fixtures/partial_completions/partial-b/hi_b new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/fixtures/partial_completions/partial-c/hello_c b/tests/fixtures/partial_completions/partial-c/hello_c new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/fixtures/partial_completions/partial/hello.txt b/tests/fixtures/partial_completions/partial/hello.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/modules/mod.rs b/tests/modules/mod.rs index c556d6fa7a..9aa1282f21 100644 --- a/tests/modules/mod.rs +++ b/tests/modules/mod.rs @@ -1,4 +1,4 @@ -use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; +use nu_test_support::fs::Stub::{FileWithContent, FileWithContentToBeTrimmed}; use nu_test_support::playground::Playground; use nu_test_support::{nu, nu_repl_code}; use pretty_assertions::assert_eq; @@ -760,3 +760,162 @@ fn nested_list_export_works() { let actual = nu!(&inp.join("; ")); assert_eq!(actual.out, "bacon"); } + +#[test] +fn reload_submodules() { + Playground::setup("reload_submodule_changed_file", |dirs, sandbox| { + sandbox.with_files(&[ + FileWithContent("voice.nu", r#"export module animals.nu"#), + FileWithContent("animals.nu", "export def cat [] { 'meow'}"), + ]); + + let inp = [ + "use voice.nu", + r#""export def cat [] {'woem'}" | save -f animals.nu"#, + "use voice.nu", + "(voice animals cat) == 'woem'", + ]; + let actual = nu!(cwd: dirs.test(), nu_repl_code(&inp)); + assert_eq!(actual.out, "true"); + + // should also verify something unchanged if `use voice`. + let inp = [ + "use voice.nu", + r#""export def cat [] {'meow'}" | save -f animals.nu"#, + "use voice", + "(voice animals cat) == 'woem'", + ]; + let actual = nu!(cwd: dirs.test(), nu_repl_code(&inp)); + assert_eq!(actual.out, "true"); + + // should also works if we use members directly. + sandbox.with_files(&[ + FileWithContent("voice.nu", r#"export module animals.nu"#), + FileWithContent("animals.nu", "export def cat [] { 'meow'}"), + ]); + let inp = [ + "use voice.nu animals cat", + r#""export def cat [] {'woem'}" | save -f animals.nu"#, + "use voice.nu animals cat", + "(cat) == 'woem'", + ]; + let actual = nu!(cwd: dirs.test(), nu_repl_code(&inp)); + assert_eq!(actual.out, "true"); + }); +} + +#[test] +fn use_submodules() { + Playground::setup("use_submodules", |dirs, sandbox| { + sandbox.with_files(&[ + FileWithContent("voice.nu", r#"export use animals.nu"#), + FileWithContent("animals.nu", "export def cat [] { 'meow'}"), + ]); + + let inp = [ + "use voice.nu", + r#""export def cat [] {'woem'}" | save -f animals.nu"#, + "use voice.nu", + "(voice animals cat) == 'woem'", + ]; + let actual = nu!(cwd: dirs.test(), nu_repl_code(&inp)); + assert_eq!(actual.out, "true"); + + // should also verify something unchanged if `use voice`. + let inp = [ + "use voice.nu", + r#""export def cat [] {'meow'}" | save -f animals.nu"#, + "use voice", + "(voice animals cat) == 'woem'", + ]; + let actual = nu!(cwd: dirs.test(), nu_repl_code(&inp)); + assert_eq!(actual.out, "true"); + + // also verify something is changed when using members. + sandbox.with_files(&[ + FileWithContent("voice.nu", r#"export use animals.nu cat"#), + FileWithContent("animals.nu", "export def cat [] { 'meow'}"), + ]); + let inp = [ + "use voice.nu", + r#""export def cat [] {'woem'}" | save -f animals.nu"#, + "use voice.nu", + "(voice cat) == 'woem'", + ]; + let actual = nu!(cwd: dirs.test(), nu_repl_code(&inp)); + assert_eq!(actual.out, "true"); + + sandbox.with_files(&[ + FileWithContent("voice.nu", r#"export use animals.nu *"#), + FileWithContent("animals.nu", "export def cat [] { 'meow'}"), + ]); + let inp = [ + "use voice.nu", + r#""export def cat [] {'woem'}" | save -f animals.nu"#, + "use voice.nu", + "(voice cat) == 'woem'", + ]; + let actual = nu!(cwd: dirs.test(), nu_repl_code(&inp)); + assert_eq!(actual.out, "true"); + + sandbox.with_files(&[ + FileWithContent("voice.nu", r#"export use animals.nu [cat]"#), + FileWithContent("animals.nu", "export def cat [] { 'meow'}"), + ]); + let inp = [ + "use voice.nu", + r#""export def cat [] {'woem'}" | save -f animals.nu"#, + "use voice.nu", + "(voice cat) == 'woem'", + ]; + let actual = nu!(cwd: dirs.test(), nu_repl_code(&inp)); + assert_eq!(actual.out, "true"); + }); +} + +#[test] +fn use_nested_submodules() { + Playground::setup("use_submodules", |dirs, sandbox| { + sandbox.with_files(&[ + FileWithContent("voice.nu", r#"export use animals.nu"#), + FileWithContent("animals.nu", r#"export use nested_animals.nu"#), + FileWithContent("nested_animals.nu", "export def cat [] { 'meow'}"), + ]); + let inp = [ + "use voice.nu", + r#""export def cat [] {'woem'}" | save -f nested_animals.nu"#, + "use voice.nu", + "(voice animals nested_animals cat) == 'woem'", + ]; + let actual = nu!(cwd: dirs.test(), nu_repl_code(&inp)); + assert_eq!(actual.out, "true"); + + sandbox.with_files(&[ + FileWithContent("voice.nu", r#"export use animals.nu"#), + FileWithContent("animals.nu", r#"export use nested_animals.nu cat"#), + FileWithContent("nested_animals.nu", "export def cat [] { 'meow'}"), + ]); + let inp = [ + "use voice.nu", + r#""export def cat [] {'woem'}" | save -f nested_animals.nu"#, + "use voice.nu", + "(voice animals cat) == 'woem'", + ]; + let actual = nu!(cwd: dirs.test(), nu_repl_code(&inp)); + assert_eq!(actual.out, "true"); + + sandbox.with_files(&[ + FileWithContent("animals.nu", r#"export use nested_animals.nu cat"#), + FileWithContent("nested_animals.nu", "export def cat [] { 'meow' }"), + ]); + let inp = [ + "module voice { export module animals.nu }", + "use voice", + r#""export def cat [] {'woem'}" | save -f nested_animals.nu"#, + "use voice.nu", + "(voice animals cat) == 'woem'", + ]; + let actual = nu!(cwd: dirs.test(), nu_repl_code(&inp)); + assert_eq!(actual.out, "true"); + }) +} diff --git a/tests/plugin_persistence/mod.rs b/tests/plugin_persistence/mod.rs index d9925f6bbf..6729c657c7 100644 --- a/tests/plugin_persistence/mod.rs +++ b/tests/plugin_persistence/mod.rs @@ -16,6 +16,17 @@ fn plugin_list_shows_installed_plugins() { assert!(out.status.success()); } +#[test] +fn plugin_list_shows_installed_plugin_version() { + let out = nu_with_plugins!( + cwd: ".", + plugin: ("nu_plugin_inc"), + r#"(plugin list).version.0"# + ); + assert_eq!(env!("CARGO_PKG_VERSION"), out.out); + assert!(out.status.success()); +} + #[test] fn plugin_keeps_running_after_calling_it() { let out = nu_with_plugins!( diff --git a/tests/plugins/config.rs b/tests/plugins/config.rs index 44f2797ceb..b110d2a7fd 100644 --- a/tests/plugins/config.rs +++ b/tests/plugins/config.rs @@ -1,27 +1,5 @@ use nu_test_support::nu_with_plugins; -#[test] -fn closure() { - let actual = nu_with_plugins!( - cwd: "tests", - plugin: ("nu_plugin_example"), - r#" - $env.env_value = "value from env" - - $env.config = { - plugins: { - example: {|| - $env.env_value - } - } - } - example config - "# - ); - - assert!(actual.out.contains("value from env")); -} - #[test] fn none() { let actual = nu_with_plugins!( @@ -34,7 +12,7 @@ fn none() { } #[test] -fn record() { +fn some() { let actual = nu_with_plugins!( cwd: "tests", plugin: ("nu_plugin_example"), @@ -42,8 +20,11 @@ fn record() { $env.config = { plugins: { example: { - key1: "value" - key2: "other" + path: "some/path", + nested: { + bool: true, + string: "Hello Example!" + } } } } @@ -51,6 +32,6 @@ fn record() { "# ); - assert!(actual.out.contains("value")); - assert!(actual.out.contains("other")); + assert!(actual.out.contains("some/path")); + assert!(actual.out.contains("Hello Example!")); } diff --git a/tests/plugins/nu_plugin_nu_example.rs b/tests/plugins/nu_plugin_nu_example.rs index f178c64316..afeb487b60 100644 --- a/tests/plugins/nu_plugin_nu_example.rs +++ b/tests/plugins/nu_plugin_nu_example.rs @@ -4,7 +4,7 @@ use assert_cmd::Command; fn call() { // Add the `nu` binaries to the path env let path_env = std::env::join_paths( - std::iter::once(nu_test_support::fs::binaries()).chain( + std::iter::once(nu_test_support::fs::binaries().into()).chain( std::env::var_os(nu_test_support::NATIVE_PATH_ENV_VAR) .as_deref() .map(std::env::split_paths) diff --git a/tests/plugins/registry_file.rs b/tests/plugins/registry_file.rs index 3f7da6dd03..2cb1473b6f 100644 --- a/tests/plugins/registry_file.rs +++ b/tests/plugins/registry_file.rs @@ -18,6 +18,13 @@ fn example_plugin_path() -> PathBuf { .expect("nu_plugin_example not found") } +fn valid_plugin_item_data() -> PluginRegistryItemData { + PluginRegistryItemData::Valid { + metadata: Default::default(), + commands: vec![], + } +} + #[test] fn plugin_add_then_restart_nu() { let result = nu_with_plugins!( @@ -149,15 +156,15 @@ fn plugin_rm_then_restart_nu() { name: "example".into(), filename: example_plugin_path, shell: None, - data: PluginRegistryItemData::Valid { commands: vec![] }, + data: valid_plugin_item_data(), }); contents.upsert_plugin(PluginRegistryItem { name: "foo".into(), // this doesn't exist, but it should be ok - filename: dirs.test().join("nu_plugin_foo"), + filename: dirs.test().join("nu_plugin_foo").into(), shell: None, - data: PluginRegistryItemData::Valid { commands: vec![] }, + data: valid_plugin_item_data(), }); contents @@ -225,15 +232,15 @@ fn plugin_rm_from_custom_path() { name: "example".into(), filename: example_plugin_path, shell: None, - data: PluginRegistryItemData::Valid { commands: vec![] }, + data: valid_plugin_item_data(), }); contents.upsert_plugin(PluginRegistryItem { name: "foo".into(), // this doesn't exist, but it should be ok - filename: dirs.test().join("nu_plugin_foo"), + filename: dirs.test().join("nu_plugin_foo").into(), shell: None, - data: PluginRegistryItemData::Valid { commands: vec![] }, + data: valid_plugin_item_data(), }); contents @@ -273,15 +280,15 @@ fn plugin_rm_using_filename() { name: "example".into(), filename: example_plugin_path.clone(), shell: None, - data: PluginRegistryItemData::Valid { commands: vec![] }, + data: valid_plugin_item_data(), }); contents.upsert_plugin(PluginRegistryItem { name: "foo".into(), // this doesn't exist, but it should be ok - filename: dirs.test().join("nu_plugin_foo"), + filename: dirs.test().join("nu_plugin_foo").into(), shell: None, - data: PluginRegistryItemData::Valid { commands: vec![] }, + data: valid_plugin_item_data(), }); contents @@ -331,13 +338,13 @@ fn warning_on_invalid_plugin_item() { name: "example".into(), filename: example_plugin_path, shell: None, - data: PluginRegistryItemData::Valid { commands: vec![] }, + data: valid_plugin_item_data(), }); contents.upsert_plugin(PluginRegistryItem { name: "badtest".into(), // this doesn't exist, but it should be ok - filename: dirs.test().join("nu_plugin_badtest"), + filename: dirs.test().join("nu_plugin_badtest").into(), shell: None, data: PluginRegistryItemData::Invalid, }); diff --git a/tests/repl/test_config_path.rs b/tests/repl/test_config_path.rs index 534ac38a27..895b1bd8bf 100644 --- a/tests/repl/test_config_path.rs +++ b/tests/repl/test_config_path.rs @@ -235,7 +235,7 @@ fn test_xdg_config_empty() { playground.with_env("XDG_CONFIG_HOME", ""); let actual = run(playground, "$nu.default-config-dir"); - let expected = dirs_next::config_dir().unwrap().join("nushell"); + let expected = dirs::config_dir().unwrap().join("nushell"); assert_eq!( actual, adjust_canonicalization(expected.canonicalize().unwrap_or(expected)) @@ -250,7 +250,7 @@ fn test_xdg_config_bad() { playground.with_env("XDG_CONFIG_HOME", xdg_config_home); let actual = run(playground, "$nu.default-config-dir"); - let expected = dirs_next::config_dir().unwrap().join("nushell"); + let expected = dirs::config_dir().unwrap().join("nushell"); assert_eq!( actual, adjust_canonicalization(expected.canonicalize().unwrap_or(expected)) diff --git a/tests/repl/test_engine.rs b/tests/repl/test_engine.rs index e452295f42..1b74e741f7 100644 --- a/tests/repl/test_engine.rs +++ b/tests/repl/test_engine.rs @@ -52,6 +52,29 @@ fn in_and_if_else() -> TestResult { ) } +#[test] +fn in_with_closure() -> TestResult { + // Can use $in twice + run_test(r#"3 | do { let x = $in; let y = $in; $x + $y }"#, "6") +} + +#[test] +fn in_with_custom_command() -> TestResult { + // Can use $in twice + run_test( + r#"def foo [] { let x = $in; let y = $in; $x + $y }; 3 | foo"#, + "6", + ) +} + +#[test] +fn in_used_twice_and_also_in_pipeline() -> TestResult { + run_test( + r#"3 | do { let x = $in; let y = $in; $x + $y | $in * 4 }"#, + "24", + ) +} + #[test] fn help_works_with_missing_requirements() -> TestResult { run_test(r#"each --help | lines | length"#, "72") diff --git a/tests/repl/test_parser.rs b/tests/repl/test_parser.rs index 9d743b175d..a7a25e1c67 100644 --- a/tests/repl/test_parser.rs +++ b/tests/repl/test_parser.rs @@ -261,6 +261,22 @@ fn commands_have_usage() -> TestResult { ) } +#[test] +fn commands_from_crlf_source_have_short_usage() -> TestResult { + run_test_contains( + "# This is a test\r\n#\r\n# To see if I have cool usage\r\ndef foo [] {}\r\nscope commands | where name == foo | get usage.0", + "This is a test", + ) +} + +#[test] +fn commands_from_crlf_source_have_extra_usage() -> TestResult { + run_test_contains( + "# This is a test\r\n#\r\n# To see if I have cool usage\r\ndef foo [] {}\r\nscope commands | where name == foo | get extra_usage.0", + "To see if I have cool usage", + ) +} + #[test] fn equals_separates_long_flag() -> TestResult { run_test( @@ -556,39 +572,6 @@ fn unbalanced_parens2() -> TestResult { fail_test(r#"("("))"#, "unbalanced ( and )") } -#[test] -fn register_with_string_literal() -> TestResult { - fail_test(r#"register 'nu-plugin-math'"#, "File not found") -} - -#[test] -fn register_with_string_constant() -> TestResult { - let input = "\ -const file = 'nu-plugin-math' -register $file -"; - // should not fail with `not a constant` - fail_test(input, "File not found") -} - -#[test] -fn register_with_string_variable() -> TestResult { - let input = "\ -let file = 'nu-plugin-math' -register $file -"; - fail_test(input, "Value is not a parse-time constant") -} - -#[test] -fn register_with_non_string_constant() -> TestResult { - let input = "\ -const file = 6 -register $file -"; - fail_test(input, "expected string, found int") -} - #[test] fn plugin_use_with_string_literal() -> TestResult { fail_test( diff --git a/tests/repl/test_strings.rs b/tests/repl/test_strings.rs index 62960fed13..1c573f03ae 100644 --- a/tests/repl/test_strings.rs +++ b/tests/repl/test_strings.rs @@ -36,6 +36,12 @@ fn non_string_in_record() -> TestResult { ) } +#[test] +fn unbalance_string() -> TestResult { + fail_test(r#""aaaab"cc"#, "invalid characters")?; + fail_test(r#"'aaaab'cc"#, "invalid characters") +} + #[test] fn string_in_valuestream() -> TestResult { run_test( diff --git a/tests/repl/test_type_check.rs b/tests/repl/test_type_check.rs index 87216e6672..8a00f2f486 100644 --- a/tests/repl/test_type_check.rs +++ b/tests/repl/test_type_check.rs @@ -129,3 +129,16 @@ fn transpose_into_load_env() -> TestResult { "10", ) } + +#[test] +fn in_variable_expression_correct_output_type() -> TestResult { + run_test(r#"def foo []: nothing -> string { 'foo' | $"($in)" }"#, "") +} + +#[test] +fn in_variable_expression_wrong_output_type() -> TestResult { + fail_test( + r#"def foo []: nothing -> int { 'foo' | $"($in)" }"#, + "expected int", + ) +} diff --git a/tests/shell/environment/nu_env.rs b/tests/shell/environment/nu_env.rs index c19b724ceb..d249d90678 100644 --- a/tests/shell/environment/nu_env.rs +++ b/tests/shell/environment/nu_env.rs @@ -36,7 +36,6 @@ fn picks_up_env_keys_when_entering_trusted_directory() { }) } -#[cfg(feature = "which-support")] #[test] #[serial] fn picks_up_and_lets_go_env_keys_when_entering_trusted_directory_with_implied_cd() { @@ -433,7 +432,6 @@ fn given_a_hierarchy_of_trusted_directories_going_back_restores_overwritten_vari }) } -#[cfg(feature = "which-support")] #[test] #[serial] fn local_config_env_var_present_and_removed_correctly() { @@ -487,7 +485,6 @@ fn local_config_env_var_present_and_removed_correctly() { }); } -#[cfg(feature = "which-support")] #[test] #[serial] fn local_config_env_var_gets_overwritten() { @@ -553,7 +550,6 @@ fn local_config_env_var_gets_overwritten() { }); } -#[cfg(feature = "which-support")] #[test] #[serial] fn autoenv_test_entry_scripts() { @@ -601,7 +597,6 @@ fn autoenv_test_entry_scripts() { }); } -#[cfg(feature = "which-support")] #[test] #[serial] fn autoenv_test_exit_scripts() { diff --git a/tests/shell/mod.rs b/tests/shell/mod.rs index ba7465f5e7..62f79a59c1 100644 --- a/tests/shell/mod.rs +++ b/tests/shell/mod.rs @@ -3,9 +3,7 @@ use nu_test_support::playground::Playground; use nu_test_support::{nu, nu_repl_code, pipeline}; use pretty_assertions::assert_eq; -#[cfg(feature = "which-support")] mod environment; - mod pipeline; mod repl; diff --git a/tests/shell/pipeline/commands/external.rs b/tests/shell/pipeline/commands/external.rs index a73cbc878d..a6efe2b6c9 100644 --- a/tests/shell/pipeline/commands/external.rs +++ b/tests/shell/pipeline/commands/external.rs @@ -3,7 +3,6 @@ use nu_test_support::nu; use nu_test_support::playground::Playground; use pretty_assertions::assert_eq; -#[cfg(feature = "which-support")] #[test] fn shows_error_for_command_not_found() { let actual = nu!("ferris_is_not_here.exe"); @@ -11,7 +10,6 @@ fn shows_error_for_command_not_found() { assert!(!actual.err.is_empty()); } -#[cfg(feature = "which-support")] #[test] fn shows_error_for_command_not_found_in_pipeline() { let actual = nu!("ferris_is_not_here.exe | echo done"); @@ -20,7 +18,6 @@ fn shows_error_for_command_not_found_in_pipeline() { } #[ignore] // jt: we can't test this using the -c workaround currently -#[cfg(feature = "which-support")] #[test] fn automatically_change_directory() { use nu_test_support::playground::Playground; @@ -152,17 +149,26 @@ fn command_substitution_wont_output_extra_newline() { assert_eq!(actual.out, "bar"); } -#[test] -fn basic_err_pipe_works() { - let actual = - nu!(r#"with-env { FOO: "bar" } { nu --testbin echo_env_stderr FOO e>| str length }"#); +#[rstest::rstest] +#[case("err>|")] +#[case("e>|")] +fn basic_err_pipe_works(#[case] redirection: &str) { + let actual = nu!( + r#"with-env { FOO: "bar" } { nu --testbin echo_env_stderr FOO {redirection} str length }"# + .replace("{redirection}", redirection) + ); assert_eq!(actual.out, "3"); } -#[test] -fn basic_outerr_pipe_works() { +#[rstest::rstest] +#[case("out+err>|")] +#[case("err+out>|")] +#[case("o+e>|")] +#[case("e+o>|")] +fn basic_outerr_pipe_works(#[case] redirection: &str) { let actual = nu!( - r#"with-env { FOO: "bar" } { nu --testbin echo_env_mixed out-err FOO FOO o+e>| str length }"# + r#"with-env { FOO: "bar" } { nu --testbin echo_env_mixed out-err FOO FOO {redirection} str length }"# + .replace("{redirection}", redirection) ); assert_eq!(actual.out, "7"); } diff --git a/tests/shell/pipeline/commands/internal.rs b/tests/shell/pipeline/commands/internal.rs index 98f3fb88c9..59c9f31fce 100644 --- a/tests/shell/pipeline/commands/internal.rs +++ b/tests/shell/pipeline/commands/internal.rs @@ -550,7 +550,6 @@ fn dynamic_closure_rest_args() { assert_eq!(actual.out, "1, 2, 3"); } -#[cfg(feature = "which-support")] #[test] fn argument_subexpression_reports_errors() { let actual = nu!("echo (ferris_is_not_here.exe)"); @@ -1162,3 +1161,11 @@ fn command_not_found_error_shows_not_found_2() { && actual.err.contains("Did you mean `for`?") ); } + +#[test] +fn error_on_out_greater_pipe() { + let actual = nu!(r#""foo" o>| print"#); + assert!(actual + .err + .contains("Redirecting stdout to a pipe is the same as normal piping")) +} diff --git a/typos.toml b/typos.toml index 92fecd5cb6..971aef7c15 100644 --- a/typos.toml +++ b/typos.toml @@ -17,6 +17,7 @@ extend-ignore-re = [ "--find ba\\b", "0x\\[ba be\\]", "\\)BaR'", + "fo�.txt", ] [type.rust.extend-words]