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 cb94e5fe8b..25fcfde55e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ 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.9.0 @@ -66,7 +66,7 @@ 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.9.0 @@ -95,7 +95,7 @@ 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.9.0 @@ -146,7 +146,7 @@ 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.9.0 diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml index 4f6dccbcf9..31533cc3d8 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 @@ -112,7 +112,7 @@ jobs: runs-on: ${{matrix.os}} steps: - - uses: actions/checkout@v4.1.6 + - uses: actions/checkout@v4.1.7 with: ref: main fetch-depth: 0 @@ -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,7 +181,7 @@ jobs: - name: Waiting for Release run: sleep 1800 - - uses: actions/checkout@v4.1.6 + - uses: actions/checkout@v4.1.7 with: ref: main diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a71792e8c8..208acd42b5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,7 +62,7 @@ jobs: runs-on: ${{matrix.os}} steps: - - uses: actions/checkout@v4.1.6 + - uses: actions/checkout@v4.1.7 - name: Update Rust Toolchain Target run: | @@ -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 f81f5c89a5..95fc51b970 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.4 + uses: crate-ci/typos@v1.22.9 diff --git a/Cargo.lock b/Cargo.lock index b88af73c25..cd7fe99800 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -377,7 +377,7 @@ dependencies = [ "bitflags 2.5.0", "cexpr", "clang-sys", - "itertools 0.12.1", + "itertools 0.11.0", "lazy_static", "lazycell", "proc-macro2", @@ -1227,6 +1227,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" @@ -1687,9 +1693,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", @@ -1989,12 +1995,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" @@ -2026,10 +2026,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", @@ -2288,9 +2289,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", @@ -2762,7 +2763,7 @@ dependencies = [ [[package]] name = "nu" -version = "0.94.3" +version = "0.95.1" dependencies = [ "assert_cmd", "crossterm", @@ -2815,7 +2816,7 @@ dependencies = [ [[package]] name = "nu-cli" -version = "0.94.3" +version = "0.95.1" dependencies = [ "chrono", "crossterm", @@ -2850,7 +2851,7 @@ dependencies = [ [[package]] name = "nu-cmd-base" -version = "0.94.3" +version = "0.95.1" dependencies = [ "indexmap", "miette", @@ -2862,7 +2863,7 @@ dependencies = [ [[package]] name = "nu-cmd-extra" -version = "0.94.3" +version = "0.95.1" dependencies = [ "fancy-regex", "heck 0.5.0", @@ -2887,7 +2888,7 @@ dependencies = [ [[package]] name = "nu-cmd-lang" -version = "0.94.3" +version = "0.95.1" dependencies = [ "itertools 0.12.1", "nu-engine", @@ -2899,7 +2900,7 @@ dependencies = [ [[package]] name = "nu-cmd-plugin" -version = "0.94.3" +version = "0.95.1" dependencies = [ "itertools 0.12.1", "nu-engine", @@ -2910,7 +2911,7 @@ dependencies = [ [[package]] name = "nu-color-config" -version = "0.94.3" +version = "0.95.1" dependencies = [ "nu-ansi-term", "nu-engine", @@ -2922,7 +2923,7 @@ dependencies = [ [[package]] name = "nu-command" -version = "0.94.3" +version = "0.95.1" dependencies = [ "alphanumeric-sort", "base64 0.22.1", @@ -3031,7 +3032,7 @@ dependencies = [ [[package]] name = "nu-derive-value" -version = "0.94.3" +version = "0.95.1" dependencies = [ "convert_case", "proc-macro-error", @@ -3042,7 +3043,7 @@ dependencies = [ [[package]] name = "nu-engine" -version = "0.94.3" +version = "0.95.1" dependencies = [ "nu-glob", "nu-path", @@ -3052,7 +3053,7 @@ dependencies = [ [[package]] name = "nu-explore" -version = "0.94.3" +version = "0.95.1" dependencies = [ "ansi-str", "anyhow", @@ -3077,14 +3078,14 @@ 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 = [ "linked-hash-map", "num-traits", @@ -3094,7 +3095,7 @@ dependencies = [ [[package]] name = "nu-lsp" -version = "0.94.3" +version = "0.95.1" dependencies = [ "assert-json-diff", "crossbeam-channel", @@ -3115,7 +3116,7 @@ dependencies = [ [[package]] name = "nu-parser" -version = "0.94.3" +version = "0.95.1" dependencies = [ "bytesize", "chrono", @@ -3131,7 +3132,7 @@ dependencies = [ [[package]] name = "nu-path" -version = "0.94.3" +version = "0.95.1" dependencies = [ "dirs-next", "omnipath", @@ -3140,7 +3141,7 @@ dependencies = [ [[package]] name = "nu-plugin" -version = "0.94.3" +version = "0.95.1" dependencies = [ "log", "nix", @@ -3155,7 +3156,7 @@ dependencies = [ [[package]] name = "nu-plugin-core" -version = "0.94.3" +version = "0.95.1" dependencies = [ "interprocess", "log", @@ -3169,7 +3170,7 @@ dependencies = [ [[package]] name = "nu-plugin-engine" -version = "0.94.3" +version = "0.95.1" dependencies = [ "log", "nu-engine", @@ -3184,7 +3185,7 @@ dependencies = [ [[package]] name = "nu-plugin-protocol" -version = "0.94.3" +version = "0.95.1" dependencies = [ "bincode", "nu-protocol", @@ -3196,7 +3197,7 @@ dependencies = [ [[package]] name = "nu-plugin-test-support" -version = "0.94.3" +version = "0.95.1" dependencies = [ "nu-ansi-term", "nu-cmd-lang", @@ -3214,7 +3215,7 @@ dependencies = [ [[package]] name = "nu-pretty-hex" -version = "0.94.3" +version = "0.95.1" dependencies = [ "heapless", "nu-ansi-term", @@ -3223,7 +3224,7 @@ dependencies = [ [[package]] name = "nu-protocol" -version = "0.94.3" +version = "0.95.1" dependencies = [ "brotli", "byte-unit", @@ -3256,7 +3257,7 @@ dependencies = [ [[package]] name = "nu-std" -version = "0.94.3" +version = "0.95.1" dependencies = [ "log", "miette", @@ -3267,7 +3268,7 @@ dependencies = [ [[package]] name = "nu-system" -version = "0.94.3" +version = "0.95.1" dependencies = [ "chrono", "itertools 0.12.1", @@ -3285,7 +3286,7 @@ dependencies = [ [[package]] name = "nu-table" -version = "0.94.3" +version = "0.95.1" dependencies = [ "fancy-regex", "nu-ansi-term", @@ -3299,7 +3300,7 @@ dependencies = [ [[package]] name = "nu-term-grid" -version = "0.94.3" +version = "0.95.1" dependencies = [ "nu-utils", "unicode-width", @@ -3307,7 +3308,7 @@ dependencies = [ [[package]] name = "nu-test-support" -version = "0.94.3" +version = "0.95.1" dependencies = [ "nu-glob", "nu-path", @@ -3319,7 +3320,7 @@ dependencies = [ [[package]] name = "nu-utils" -version = "0.94.3" +version = "0.95.1" dependencies = [ "crossterm_winapi", "log", @@ -3345,7 +3346,7 @@ dependencies = [ [[package]] name = "nu_plugin_example" -version = "0.94.3" +version = "0.95.1" dependencies = [ "nu-cmd-lang", "nu-plugin", @@ -3355,7 +3356,7 @@ dependencies = [ [[package]] name = "nu_plugin_formats" -version = "0.94.3" +version = "0.95.1" dependencies = [ "eml-parser", "ical", @@ -3368,7 +3369,7 @@ dependencies = [ [[package]] name = "nu_plugin_gstat" -version = "0.94.3" +version = "0.95.1" dependencies = [ "git2", "nu-plugin", @@ -3377,7 +3378,7 @@ dependencies = [ [[package]] name = "nu_plugin_inc" -version = "0.94.3" +version = "0.95.1" dependencies = [ "nu-plugin", "nu-protocol", @@ -3386,7 +3387,7 @@ dependencies = [ [[package]] name = "nu_plugin_polars" -version = "0.94.3" +version = "0.95.1" dependencies = [ "chrono", "chrono-tz 0.9.0", @@ -3417,7 +3418,7 @@ dependencies = [ [[package]] name = "nu_plugin_query" -version = "0.94.3" +version = "0.95.1" dependencies = [ "gjson", "nu-plugin", @@ -3429,7 +3430,7 @@ dependencies = [ [[package]] name = "nu_plugin_stress_internals" -version = "0.94.3" +version = "0.95.1" dependencies = [ "interprocess", "serde", @@ -3555,7 +3556,7 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "nuon" -version = "0.94.3" +version = "0.95.1" dependencies = [ "chrono", "fancy-regex", @@ -4745,21 +4746,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", ] @@ -5434,9 +5435,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", @@ -6307,6 +6308,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" @@ -6486,9 +6497,9 @@ checksum = "425a23c7b7145bc7620c9c445817c37b1f78b6790aee9f208133f3c028975b60" [[package]] name = "uuid" -version = "1.8.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439" dependencies = [ "getrandom", "serde", diff --git a/Cargo.toml b/Cargo.toml index 18f45987e7..c88eee88ea 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 @@ -95,7 +95,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" @@ -172,7 +172,7 @@ uu_mv = "0.0.26" uu_whoami = "0.0.26" uu_uname = "0.0.26" uucore = "0.0.26" -uuid = "1.8.0" +uuid = "1.9.1" v_htmlescape = "0.15.0" wax = "0.6" which = "6.0.0" @@ -180,22 +180,22 @@ 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"] } @@ -225,9 +225,9 @@ 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 } tango-bench = "0.5" @@ -310,4 +310,4 @@ bench = false # Run individual benchmarks like `cargo bench -- ` e.g. `cargo bench -- parse` [[bench]] name = "benchmarks" -harness = false +harness = false \ No newline at end of file diff --git a/benches/benchmarks.rs b/benches/benchmarks.rs index 84552daef9..7a23715c8d 100644 --- a/benches/benchmarks.rs +++ b/benches/benchmarks.rs @@ -47,8 +47,7 @@ fn setup_stack_and_engine_from_command(command: &str) -> (Stack, EngineState) { &mut engine, &mut stack, PipelineData::empty(), - None, - false, + Default::default(), ) .unwrap(); @@ -90,8 +89,7 @@ fn bench_command( &mut engine, &mut stack, PipelineData::empty(), - None, - false, + Default::default(), ) .unwrap(), ); 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/src/config_files.rs b/crates/nu-cli/src/config_files.rs index ec7ad2f412..6cf4735c46 100644 --- a/crates/nu-cli/src/config_files.rs +++ b/crates/nu-cli/src/config_files.rs @@ -344,7 +344,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, + }, }); } diff --git a/crates/nu-cli/src/eval_cmds.rs b/crates/nu-cli/src/eval_cmds.rs index 8fa3bf30e5..13141f6174 100644 --- a/crates/nu-cli/src/eval_cmds.rs +++ b/crates/nu-cli/src/eval_cmds.rs @@ -8,15 +8,45 @@ 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)?; diff --git a/crates/nu-cli/src/lib.rs b/crates/nu-cli/src/lib.rs index c4342dc3a0..6f151adad1 100644 --- a/crates/nu-cli/src/lib.rs +++ b/crates/nu-cli/src/lib.rs @@ -17,7 +17,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/syntax_highlight.rs b/crates/nu-cli/src/syntax_highlight.rs index e296943af6..41ef168390 100644 --- a/crates/nu-cli/src/syntax_highlight.rs +++ b/crates/nu-cli/src/syntax_highlight.rs @@ -138,6 +138,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) @@ -452,15 +453,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/tests/completions/mod.rs b/crates/nu-cli/tests/completions/mod.rs index 107de98c80..35e9435b41 100644 --- a/crates/nu-cli/tests/completions/mod.rs +++ b/crates/nu-cli/tests/completions/mod.rs @@ -763,7 +763,7 @@ fn variables_completions() { // Test completions for $nu let suggestions = completer.complete("$nu.", 4); - assert_eq!(17, suggestions.len()); + assert_eq!(18, suggestions.len()); let expected: Vec = vec![ "cache-dir".into(), @@ -783,6 +783,7 @@ fn variables_completions() { "plugin-path".into(), "startup-time".into(), "temp-path".into(), + "vendor-autoload-dir".into(), ]; // Match results 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-extra/Cargo.toml b/crates/nu-cmd-extra/Cargo.toml index 038c9421e8..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 } @@ -33,6 +33,6 @@ v_htmlescape = { workspace = true } itertools = { 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" } \ No newline at end of file diff --git a/crates/nu-cmd-lang/Cargo.toml b/crates/nu-cmd-lang/Cargo.toml index dcd940ba58..16ac1b893a 100644 --- a/crates/nu-cmd-lang/Cargo.toml +++ b/crates/nu-cmd-lang/Cargo.toml @@ -6,26 +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 = [] 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/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/do_.rs b/crates/nu-cmd-lang/src/core_commands/do_.rs index 8ca3fbac56..4f0bd245d2 100644 --- a/crates/nu-cmd-lang/src/core_commands/do_.rs +++ b/crates/nu-cmd-lang/src/core_commands/do_.rs @@ -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/if_.rs b/crates/nu-cmd-lang/src/core_commands/if_.rs index a2071530ac..738d901759 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 } diff --git a/crates/nu-cmd-lang/src/core_commands/loop_.rs b/crates/nu-cmd-lang/src/core_commands/loop_.rs index 9b1e36a057..a9c642ca3c 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, diff --git a/crates/nu-cmd-lang/src/core_commands/match_.rs b/crates/nu-cmd-lang/src/core_commands/match_.rs index 41b5c24702..d28a59cbad 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, 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 1f16f97908..f99825b88d 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; @@ -31,6 +31,15 @@ impl Command for Try { .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/version.rs b/crates/nu-cmd-lang/src/core_commands/version.rs index 77283105a9..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( diff --git a/crates/nu-cmd-lang/src/core_commands/while_.rs b/crates/nu-cmd-lang/src/core_commands/while_.rs index bf9076aa0c..646b95c82e 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, 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/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-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/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-command/Cargo.toml b/crates/nu-command/Cargo.toml index f16d62d836..474742ae0a 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 } @@ -136,8 +136,8 @@ sqlite = ["rusqlite"] trash-support = ["trash"] [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 } mockito = { workspace = true, default-features = false } @@ -145,4 +145,4 @@ quickcheck = { workspace = true } quickcheck_macros = { workspace = true } rstest = { workspace = true, default-features = false } pretty_assertions = { workspace = true } -tempfile = { workspace = true } +tempfile = { workspace = true } \ No newline at end of file diff --git a/crates/nu-command/src/env/export_env.rs b/crates/nu-command/src/env/export_env.rs index 20605a9bb5..00f2c73ef4 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,15 @@ 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 run( &self, engine_state: &EngineState, diff --git a/crates/nu-command/src/env/source_env.rs b/crates/nu-command/src/env/source_env.rs index 71c1e6dc3f..0d8b118e8d 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, diff --git a/crates/nu-command/src/filesystem/ls.rs b/crates/nu-command/src/filesystem/ls.rs index a89cf04ed6..7eee57cfb6 100644 --- a/crates/nu-command/src/filesystem/ls.rs +++ b/crates/nu-command/src/filesystem/ls.rs @@ -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", diff --git a/crates/nu-command/src/filters/find.rs b/crates/nu-command/src/filters/find.rs index dfdef66969..af626c1e75 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", @@ -457,9 +457,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/where_.rs b/crates/nu-command/src/filters/where_.rs index 703a5c0130..3b0f9186ed 100644 --- a/crates/nu-command/src/filters/where_.rs +++ b/crates/nu-command/src/filters/where_.rs @@ -1,5 +1,6 @@ use nu_engine::{command_prelude::*, ClosureEval}; use nu_protocol::{engine::Closure, TryIntoValue}; +use nu_protocol::engine::{CommandType}; #[derive(Clone)] pub struct Where; @@ -19,6 +20,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![ diff --git a/crates/nu-command/src/formats/from/delimited.rs b/crates/nu-command/src/formats/from/delimited.rs index 853f3bd83e..8f84890645 100644 --- a/crates/nu-command/src/formats/from/delimited.rs +++ b/crates/nu-command/src/formats/from/delimited.rs @@ -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()) 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/generators/cal.rs b/crates/nu-command/src/generators/cal.rs index e1ecc771de..a257f3ab7c 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::{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,27 @@ pub fn cal( style_computer, )?; - Ok(Value::list(calendar_vec_deque.into_iter().collect(), tag).into_pipeline_data()) + let mut table_no_index = 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, 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 f97f7eb882..10fb459444 100644 --- a/crates/nu-command/src/generators/generate.rs +++ b/crates/nu-command/src/generators/generate.rs @@ -12,13 +12,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 +57,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, }, ] } diff --git a/crates/nu-command/src/strings/char_.rs b/crates/nu-command/src/strings/char_.rs index dd7b5cf39b..6834134b26 100644 --- a/crates/nu-command/src/strings/char_.rs +++ b/crates/nu-command/src/strings/char_.rs @@ -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(), diff --git a/crates/nu-command/src/system/run_external.rs b/crates/nu-command/src/system/run_external.rs index bc6152e7c8..f82c23a0e0 100644 --- a/crates/nu-command/src/system/run_external.rs +++ b/crates/nu-command/src/system/run_external.rs @@ -1,16 +1,15 @@ use nu_cmd_base::hook::eval_hook; use nu_engine::{command_prelude::*, env_to_strings, get_eval_expression}; +use nu_path::{dots::expand_ndots, expand_tilde}; use nu_protocol::{ - ast::{Expr, Expression}, - did_you_mean, - process::ChildProcess, - ByteStream, NuGlob, OutDest, + ast::Expression, did_you_mean, process::ChildProcess, ByteStream, NuGlob, OutDest, }; 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, @@ -33,8 +32,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 +54,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) else { + return Err(command_not_found(&name_str, call.head, engine_state, stack)); }; executable }; @@ -101,15 +99,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,76 +215,54 @@ 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> { +) -> Result>, ShellError> { let ctrlc = &engine_state.ctrlc; let cwd = engine_state.cwd(Some(stack))?; - let mut args: Vec> = vec![]; + 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)); + for arg in eval_argument(engine_state, stack, expr, spread)? { + match arg { + // Expand globs passed to run-external + Value::Glob { val, no_expand, .. } if !no_expand => args.extend( + expand_glob(&val, &cwd, expr.span, ctrlc)? + .into_iter() + .map(|s| s.into_spanned(expr.span)), + ), + other => { + args.push(OsString::from(coerce_into_string(other)?).into_spanned(expr.span)) } } - } else { - for arg in eval_argument(engine_state, stack, expr, spread)? { - args.push(arg.into_spanned(expr.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. +/// Custom `coerce_into_string()`, including globs, since those are often args to `run-external` +/// as well +fn coerce_into_string(val: Value) -> Result { + match val { + Value::Glob { val, .. } => Ok(val), + _ => val.coerce_into_string(), + } +} + +/// Evaluate an argument, returning more than one value if it was a list to be spread. 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()); - } - +) -> Result, ShellError> { let eval = get_eval_expression(engine_state); - match eval(engine_state, stack, &expr)? { + match eval(engine_state, stack, expr)? { Value::List { vals, .. } => { if spread { - vals.into_iter() - .map(|val| val.coerce_into_string()) - .collect() + Ok(vals) } else { Err(ShellError::CannotPassListToExternal { arg: String::from_utf8_lossy(engine_state.get_span_contents(expr.span)).into(), @@ -298,31 +274,12 @@ fn eval_argument( if spread { Err(ShellError::CannotSpreadAsList { span: expr.span }) } else { - Ok(vec![value.coerce_into_string()?]) + Ok(vec![value]) } } } } -/// 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 /// is not a valid glob, then this returns the original string as the expansion result. /// @@ -333,19 +290,21 @@ fn expand_glob( cwd: &Path, span: Span, interrupt: &Option>, -) -> Result, ShellError> { +) -> 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) { @@ -353,7 +312,7 @@ fn expand_glob( } 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 +351,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. /// @@ -577,7 +519,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 +535,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 +555,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,76 +571,39 @@ 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| { @@ -727,40 +633,14 @@ mod test { let actual = expand_glob("[*.txt", cwd, Span::unknown(), &None).unwrap(); let expected = &["[*.txt"]; assert_eq!(actual, expected); + + let actual = expand_glob("~/foo.txt", cwd, Span::unknown(), &None).unwrap(); + let home = dirs_next::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(); diff --git a/crates/nu-command/src/viewers/table.rs b/crates/nu-command/src/viewers/table.rs index 2fe9319821..075a4e31c4 100644 --- a/crates/nu-command/src/viewers/table.rs +++ b/crates/nu-command/src/viewers/table.rs @@ -170,7 +170,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 +187,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), + ]) }), ])), }, 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/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/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/run_external.rs b/crates/nu-command/tests/commands/run_external.rs index 14428edcbe..154c31b71c 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_next::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/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 index 45395b2ddb..50ca5d2407 100644 --- a/crates/nu-derive-value/Cargo.toml +++ b/crates/nu-derive-value/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" license = "MIT" name = "nu-derive-value" repository = "https://github.com/nushell/nushell/tree/main/crates/nu-derive-value" -version = "0.94.3" +version = "0.95.1" [lib] proc-macro = true @@ -18,4 +18,4 @@ proc-macro2 = { workspace = true } syn = { workspace = true } quote = { workspace = true } proc-macro-error = { workspace = true } -convert_case = { workspace = true } +convert_case = { workspace = true } \ No newline at end of file diff --git a/crates/nu-derive-value/src/from.rs b/crates/nu-derive-value/src/from.rs index 033026c149..783a22920e 100644 --- a/crates/nu-derive-value/src/from.rs +++ b/crates/nu-derive-value/src/from.rs @@ -3,6 +3,7 @@ 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}; @@ -116,15 +117,11 @@ fn derive_struct_from_value( /// src_span: span /// })?, /// )?, -/// favorite_toy: as nu_protocol::FromValue>::from_value( -/// record -/// .remove("favorite_toy") -/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn { -/// col_name: std::string::ToString::to_string("favorite_toy"), -/// 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(), /// }) /// } /// } @@ -480,20 +477,29 @@ fn parse_value_via_fields(fields: &Fields, self_ident: impl ToTokens) -> TokenSt match fields { Fields::Named(fields) => { let fields = fields.named.iter().map(|field| { - // TODO: handle missing fields for Options as None let ident = field.ident.as_ref().expect("named has idents"); let ident_s = ident.to_string(); let ty = &field.ty; - quote! { - #ident: <#ty as nu_protocol::FromValue>::from_value( - record + match type_is_option(ty) { + true => quote! { + #ident: 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 - })?, - )? + .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! { @@ -537,3 +543,25 @@ fn parse_value_via_fields(fields: &Fields, self_ident: impl ToTokens) -> TokenSt }, } } + +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-engine/Cargo.toml b/crates/nu-engine/Cargo.toml index fae2b3eff4..3e6c3f787b 100644 --- a/crates/nu-engine/Cargo.toml +++ b/crates/nu-engine/Cargo.toml @@ -5,16 +5,16 @@ 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" } [features] -plugin = [] +plugin = [] \ No newline at end of file diff --git a/crates/nu-engine/src/documentation.rs b/crates/nu-engine/src/documentation.rs index 9c383ee957..1a080b8556 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, TryIntoValue, Type, Value, + record, Category, Example, IntoPipelineData, PipelineData, Signature, Span, SpanId, Spanned, + SyntaxShape, Type, Value, IntoValue, TryIntoValue }; use std::{collections::HashMap, fmt::Write}; @@ -296,6 +296,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 +326,7 @@ fn get_documentation( .run( engine_state, stack, - &Call::new(Span::new(0, 0)), + &table_call, PipelineData::Value(result.clone(), None), ) .ok() 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/src/views/record/mod.rs b/crates/nu-explore/src/views/record/mod.rs index 9e67e08120..87b0952a68 100644 --- a/crates/nu-explore/src/views/record/mod.rs +++ b/crates/nu-explore/src/views/record/mod.rs @@ -131,7 +131,7 @@ impl RecordView { Orientation::Left => (column, row), }; - if row >= layer.count_rows() || column >= layer.count_columns() { + if row >= layer.record_values.len() || column >= layer.column_names.len() { // actually must never happen; unless cursor works incorrectly // if being sure about cursor it can be deleted; return Value::nothing(Span::unknown()); @@ -610,7 +610,7 @@ 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.count_rows(); + let count_rows = layer.record_values.len(); if count_rows > page_size { layer .cursor @@ -722,43 +722,66 @@ fn get_percentage(value: usize, max: usize) -> usize { } fn transpose_table(layer: &mut RecordLayer) { + if layer.was_transposed { + transpose_from(layer); + } else { + transpose_to(layer); + } + + layer.was_transposed = !layer.was_transposed; +} + +fn transpose_from(layer: &mut RecordLayer) { let count_rows = layer.record_values.len(); let count_columns = layer.column_names.len(); - if layer.was_transposed { - 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(); + if let Some(data) = &mut layer.record_text { + pop_first_column(data); + *data = _transpose_table(data, count_rows, count_columns - 1); + } - let data = _transpose_table(&layer.record_values, 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(); - layer.record_values = data; - layer.column_names = headers; + let data = _transpose_table(&layer.record_values, count_rows, count_columns - 1); - return; + 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(); - - layer.was_transposed = !layer.was_transposed; } -fn pop_first_column(values: &mut [Vec]) -> Vec { - let mut data = vec![Value::default(); values.len()]; +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); } @@ -766,12 +789,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); diff --git a/crates/nu-explore/src/views/record/table_widget.rs b/crates/nu-explore/src/views/record/table_widget.rs index 7149a7ce60..ae39d46858 100644 --- a/crates/nu-explore/src/views/record/table_widget.rs +++ b/crates/nu-explore/src/views/record/table_widget.rs @@ -88,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; @@ -130,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 @@ -162,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); @@ -200,6 +197,7 @@ impl<'a> TableWidget<'a> { } 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, head_iter); @@ -209,10 +207,10 @@ impl<'a> TableWidget<'a> { state.layout.push(&head, x, head_y, use_space, 1); } - let head_rows = column.iter().map(|(t, s)| (t, *s)); + 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, head_rows); + 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; @@ -353,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(); @@ -361,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(); @@ -555,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, @@ -668,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() 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..04c39bdac5 100644 --- a/crates/nu-json/Cargo.toml +++ b/crates/nu-json/Cargo.toml @@ -5,7 +5,7 @@ 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 @@ -23,5 +23,5 @@ serde = { workspace = true } serde_json = { workspace = true } [dev-dependencies] -# nu-path = { path="../nu-path", version = "0.94.3" } -# serde_json = "1.0" +# nu-path = { path="../nu-path", version = "0.95.1" } +# serde_json = "1.0" \ No newline at end of file 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-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/src/flatten.rs b/crates/nu-parser/src/flatten.rs index 92b424783a..88638b2475 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", @@ -277,7 +279,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 +288,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 +433,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/lib.rs b/crates/nu-parser/src/lib.rs index eeb6a590b8..89cba8d243 100644 --- a/crates/nu-parser/src/lib.rs +++ b/crates/nu-parser/src/lib.rs @@ -8,7 +8,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,8 +17,8 @@ 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, diff --git a/crates/nu-parser/src/parse_keywords.rs b/crates/nu-parser/src/parse_keywords.rs index 2849b4e39c..70d217c564 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,32 +42,44 @@ 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", @@ -1192,7 +1204,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 +1223,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 +1429,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 +1773,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 +1788,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 +1898,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 +1978,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 +2287,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 +2433,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 +2482,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 +2918,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 +2929,7 @@ pub fn parse_overlay_use(working_set: &mut StateWorkingSet, call: Box) -> }], Some(final_overlay_name.as_bytes()), call.head, + &mut vec![], ) } } else { @@ -3740,28 +3807,37 @@ pub fn parse_register(working_set: &mut StateWorkingSet, lite_command: &LiteComm ) })?; - let signatures = plugin + let metadata_and_signatures = plugin .clone() .get(get_envs) - .and_then(|p| p.get_signature()) + .and_then(|p| { + let meta = p.get_metadata()?; + let sigs = p.get_signature()?; + Ok((meta, sigs)) + }) .map_err(|err| { - log::warn!("Error getting signatures: {err:?}"); + log::warn!("Error getting metadata and signatures: {err:?}"); ParseError::LabeledError( - "Error getting signatures".into(), + "Error getting metadata and 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(), - )); + match metadata_and_signatures { + Ok((meta, sigs)) => { + // Set the metadata on the plugin + plugin.set_metadata(Some(meta.clone())); + // Add the loaded plugin to the delta + working_set.update_plugin_registry(PluginRegistryItem::new( + &identity, + meta, + sigs.clone(), + )); + Ok(sigs) + } + Err(err) => Err(err), } - - signatures }, |sig| sig.map(|sig| vec![sig]), )?; diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 149bce8958..f6bbd84f6c 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"); @@ -5219,7 +5377,7 @@ pub fn parse_builtin_commands( } 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)); @@ -6012,7 +6170,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)?; } diff --git a/crates/nu-parser/tests/test_parser.rs b/crates/nu-parser/tests/test_parser.rs index 2646b3cc90..0784fe69d4 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}, + ast::{Argument, Call, Expr, Expression, ExternalArgument, PathMember, Range}, engine::{Command, EngineState, Stack, StateWorkingSet}, - ParseError, PipelineData, ShellError, Signature, Span, SyntaxShape, + ParseError, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, }; use rstest::rstest; @@ -182,7 +182,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 +694,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 +757,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] @@ -1132,6 +1346,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(); diff --git a/crates/nu-path/Cargo.toml b/crates/nu-path/Cargo.toml index e3c84acdb3..dc2f870a9d 100644 --- a/crates/nu-path/Cargo.toml +++ b/crates/nu-path/Cargo.toml @@ -5,7 +5,7 @@ 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] @@ -18,4 +18,4 @@ dirs-next = { workspace = true } omnipath = { workspace = true } [target.'cfg(all(unix, not(target_os = "macos"), not(target_os = "android")))'.dependencies] -pwd = { workspace = true } +pwd = { workspace = true } \ No newline at end of file diff --git a/crates/nu-path/src/form.rs b/crates/nu-path/src/form.rs new file mode 100644 index 0000000000..4266905a20 --- /dev/null +++ b/crates/nu-path/src/form.rs @@ -0,0 +1,161 @@ +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. +/// +/// [`push`](crate::PathBuf::push) and [`join`](crate::Path::join) +/// operations only support [`MaybeRelative`] path forms as input. +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 [`MaybeRelative`] 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 non-canonical path is pushed onto it. +pub trait PathPush: PathSet {} +impl PathPush for Any {} +impl PathPush for Absolute {} diff --git a/crates/nu-path/src/lib.rs b/crates/nu-path/src/lib.rs index 13640acd2f..8553495439 100644 --- a/crates/nu-path/src/lib.rs +++ b/crates/nu-path/src/lib.rs @@ -2,12 +2,15 @@ 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::{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-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-engine/Cargo.toml b/crates/nu-plugin-engine/Cargo.toml index 95c0298c09..f86c1ae703 100644 --- a/crates/nu-plugin-engine/Cargo.toml +++ b/crates/nu-plugin-engine/Cargo.toml @@ -5,17 +5,17 @@ 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 } serde = { workspace = true } log = { workspace = true } @@ -31,4 +31,4 @@ local-socket = ["nu-plugin-core/local-socket"] windows = { workspace = true, features = [ # For setting process creation flags "Win32_System_Threading", -] } +] } \ No newline at end of file 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 90b908aadf..cf23b3e251 100644 --- a/crates/nu-plugin-engine/src/interface/mod.rs +++ b/crates/nu-plugin-engine/src/interface/mod.rs @@ -11,8 +11,8 @@ use nu_plugin_protocol::{ PluginOutput, ProtocolInfo, StreamId, StreamMessage, }; use nu_protocol::{ - ast::Operator, CustomValue, IntoSpanned, PipelineData, PluginSignature, ShellError, Span, - Spanned, TryIntoValue, Value, + ast::Operator, CustomValue, IntoSpanned, PipelineData, PluginMetadata, PluginSignature, + ShellError, Span, Spanned, Value, IntoValue, TryIntoValue }; use std::{ collections::{btree_map, BTreeMap}, @@ -716,6 +716,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()) @@ -913,6 +914,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 +1218,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) => { diff --git a/crates/nu-plugin-engine/src/interface/tests.rs b/crates/nu-plugin-engine/src/interface/tests.rs index 2c590ef814..9d1f2e70b1 100644 --- a/crates/nu-plugin-engine/src/interface/tests.rs +++ b/crates/nu-plugin-engine/src/interface/tests.rs @@ -17,8 +17,8 @@ use nu_plugin_protocol::{ use nu_protocol::{ ast::{Math, Operator}, engine::Closure, - ByteStreamType, CustomValue, IntoInterruptiblePipelineData, IntoSpanned, IntoValue, - PipelineData, PluginSignature, ShellError, Span, Spanned, TryIntoValue, Value, + ByteStreamType, CustomValue, IntoInterruptiblePipelineData, IntoSpanned, PipelineData, + PluginMetadata, PluginSignature, ShellError, Span, Spanned, Value, IntoValue, TryIntoValue }; use serde::{Deserialize, Serialize}; use std::{ @@ -1019,6 +1019,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(); 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/lib.rs b/crates/nu-plugin-protocol/src/lib.rs index db19ee02f6..2f582c3009 100644 --- a/crates/nu-plugin-protocol/src/lib.rs +++ b/crates/nu-plugin-protocol/src/lib.rs @@ -23,7 +23,7 @@ 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 serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -119,6 +119,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 +133,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 +145,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 +312,7 @@ pub enum StreamMessage { #[derive(Serialize, Deserialize, Debug, Clone)] pub enum PluginCallResponse { Error(LabeledError), + Metadata(PluginMetadata), Signature(Vec), Ordering(Option), PipelineData(D), @@ -323,6 +327,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)?), 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..a53b353981 100644 --- a/crates/nu-plugin-test-support/src/lib.rs +++ b/crates/nu-plugin-test-support/src/lib.rs @@ -66,6 +66,10 @@ //! } //! //! impl Plugin for LowercasePlugin { +//! fn version(&self) -> String { +//! env!("CARGO_PKG_VERSION").into() +//! } +//! //! fn commands(&self) -> Vec>> { //! vec![Box::new(Lowercase)] //! } 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 acdbbf108c..7136eba16b 100644 --- a/crates/nu-plugin-test-support/tests/custom_value/mod.rs +++ b/crates/nu-plugin-test-support/tests/custom_value/mod.rs @@ -54,6 +54,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 a36ed63aed..3b1bd98417 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 4c270baaa5..94a52c7dc9 100644 --- a/crates/nu-plugin-test-support/tests/lowercase/mod.rs +++ b/crates/nu-plugin-test-support/tests/lowercase/mod.rs @@ -59,6 +59,10 @@ impl PluginCommand for Lowercase { } impl Plugin for LowercasePlugin { + fn version(&self) -> String { + "0.0.0".into() + } + fn commands(&self) -> Vec>> { vec![Box::new(Lowercase)] } diff --git a/crates/nu-plugin/Cargo.toml b/crates/nu-plugin/Cargo.toml index 68b3a7cadd..a3b4455d15 100644 --- a/crates/nu-plugin/Cargo.toml +++ b/crates/nu-plugin/Cargo.toml @@ -5,16 +5,16 @@ 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 } log = { workspace = true } thiserror = "1.0" @@ -29,4 +29,4 @@ local-socket = ["nu-plugin-core/local-socket"] [target.'cfg(target_family = "unix")'.dependencies] # For setting the process group ID (EnterForeground / LeaveForeground) -nix = { workspace = true, default-features = false, features = ["process"] } +nix = { workspace = true, default-features = false, features = ["process"] } \ No newline at end of file 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 f185b56aa8..4ef968ef2f 100644 --- a/crates/nu-plugin/src/plugin/command.rs +++ b/crates/nu-plugin/src/plugin/command.rs @@ -60,6 +60,9 @@ use crate::{EngineInterface, EvaluatedCall, Plugin}; /// } /// /// # 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)] /// # } diff --git a/crates/nu-plugin/src/plugin/interface/mod.rs b/crates/nu-plugin/src/plugin/interface/mod.rs index 1c024bbd9c..0bed351841 100644 --- a/crates/nu-plugin/src/plugin/interface/mod.rs +++ b/crates/nu-plugin/src/plugin/interface/mod.rs @@ -11,8 +11,8 @@ use nu_plugin_protocol::{ ProtocolInfo, }; use nu_protocol::{ - engine::Closure, Config, LabeledError, PipelineData, PluginSignature, ShellError, Span, - Spanned, TryIntoValue, Value, + engine::Closure, Config, LabeledError, PipelineData, PluginMetadata, PluginSignature, + ShellError, Span, Spanned, Value, IntoValue, TryIntoValue }; use std::{ collections::{btree_map, BTreeMap, HashMap}, @@ -29,6 +29,9 @@ use std::{ #[derive(Debug)] #[doc(hidden)] pub enum ReceivedPluginCall { + Metadata { + engine: EngineInterface, + }, Signature { engine: EngineInterface, }, @@ -280,8 +283,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 }) } @@ -416,6 +422,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()`. diff --git a/crates/nu-plugin/src/plugin/interface/tests.rs b/crates/nu-plugin/src/plugin/interface/tests.rs index 875225abc2..c3f46cdd99 100644 --- a/crates/nu-plugin/src/plugin/interface/tests.rs +++ b/crates/nu-plugin/src/plugin/interface/tests.rs @@ -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(); diff --git a/crates/nu-plugin/src/plugin/mod.rs b/crates/nu-plugin/src/plugin/mod.rs index 85283aadd0..997381f97b 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; @@ -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 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 849a968eb8..ee0f5a8221 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,10 +13,10 @@ 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-derive-value = { path = "../nu-derive-value", 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" ] } @@ -47,11 +47,11 @@ 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 } os_pipe = { workspace = true } [package.metadata.docs.rs] -all-features = true +all-features = true \ No newline at end of file diff --git a/crates/nu-protocol/src/ast/expr.rs b/crates/nu-protocol/src/ast/expr.rs index 13d9e42985..0e561e5c8f 100644 --- a/crates/nu-protocol/src/ast/expr.rs +++ b/crates/nu-protocol/src/ast/expr.rs @@ -32,8 +32,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 +46,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, } @@ -84,6 +89,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`. diff --git a/crates/nu-protocol/src/ast/expression.rs b/crates/nu-protocol/src/ast/expression.rs index 08ecc59aaf..040e140cab 100644 --- a/crates/nu-protocol/src/ast/expression.rs +++ b/crates/nu-protocol/src/ast/expression.rs @@ -232,7 +232,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; @@ -441,7 +441,7 @@ 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) } diff --git a/crates/nu-protocol/src/debugger/profiler.rs b/crates/nu-protocol/src/debugger/profiler.rs index 0fa2d4f3aa..c3066c0a14 100644 --- a/crates/nu-protocol/src/debugger/profiler.rs +++ b/crates/nu-protocol/src/debugger/profiler.rs @@ -258,6 +258,7 @@ 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::Subexpression(_) => "subexpression".to_string(), Expr::Table(_) => "table".to_string(), Expr::UnaryNot(_) => "unary not".to_string(), 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/eval_base.rs b/crates/nu-protocol/src/eval_base.rs index 2317149460..7e1e2b0a7c 100644 --- a/crates/nu-protocol/src/eval_base.rs +++ b/crates/nu-protocol/src/eval_base.rs @@ -290,6 +290,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 diff --git a/crates/nu-protocol/src/eval_const.rs b/crates/nu-protocol/src/eval_const.rs index 875ce1ad07..0afc695c6a 100644 --- a/crates/nu-protocol/src/eval_const.rs +++ b/crates/nu-protocol/src/eval_const.rs @@ -182,6 +182,26 @@ pub(crate) fn create_nu_constant(engine_state: &EngineState, span: Span) -> Valu }, ); + // 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) @@ -236,6 +256,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, diff --git a/crates/nu-protocol/src/lib.rs b/crates/nu-protocol/src/lib.rs index 78e7d40680..9c176953d5 100644 --- a/crates/nu-protocol/src/lib.rs +++ b/crates/nu-protocol/src/lib.rs @@ -11,6 +11,7 @@ mod example; mod id; mod lev_distance; mod module; +pub mod parser_path; mod pipeline; #[cfg(feature = "plugin")] mod plugin; 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/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..abd75b4dc6 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); 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/value/test_derive.rs b/crates/nu-protocol/src/value/test_derive.rs index 1865a05418..56bccabd2a 100644 --- a/crates/nu-protocol/src/value/test_derive.rs +++ b/crates/nu-protocol/src/value/test_derive.rs @@ -171,6 +171,62 @@ fn named_fields_struct_incorrect_type() { 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 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-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/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-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-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-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/src/macros.rs b/crates/nu-test-support/src/macros.rs index c18dd5ac81..e83b4354da 100644 --- a/crates/nu-test-support/src/macros.rs +++ b/crates/nu-test-support/src/macros.rs @@ -245,6 +245,7 @@ use tempfile::tempdir; pub struct NuOpts { pub cwd: Option, pub locale: Option, + pub envs: Option>, pub collapse_output: Option, } @@ -278,11 +279,18 @@ 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()) @@ -369,6 +377,8 @@ where .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") 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/src/sample_config/default_config.nu b/crates/nu-utils/src/sample_config/default_config.nu index 34418feb16..5856e385c4 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 @@ -48,6 +48,7 @@ let dark_theme = { shape_float: purple_bold # shapes are used to change the cli syntax highlighting 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 @@ -887,4 +888,4 @@ $env.config = { event: { edit: selectall } } ] -} +} \ No newline at end of file diff --git a/crates/nu-utils/src/sample_config/default_env.nu b/crates/nu-utils/src/sample_config/default_env.nu index effe76c98a..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 }) { @@ -98,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_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/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/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/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/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/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/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..09906732b7 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,9 @@ 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" } # Potential dependencies for extras chrono = { workspace = true, features = ["std", "unstable-locales"], default-features = false } @@ -36,7 +36,7 @@ polars-ops = { version = "0.40"} polars-plan = { version = "0.40", features = ["regex"]} polars-utils = { version = "0.40"} typetag = "0.2" -uuid = { version = "1.7", features = ["v4", "serde"] } +uuid = { version = "1.9", features = ["v4", "serde"] } [dependencies.polars] features = [ @@ -73,9 +73,9 @@ optional = false version = "0.40" [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" } -tempfile.workspace = true +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 \ No newline at end of file diff --git a/crates/nu_plugin_polars/src/cache/mod.rs b/crates/nu_plugin_polars/src/cache/mod.rs index 8862f5bb51..08a7a9d7f7 100644 --- a/crates/nu_plugin_polars/src/cache/mod.rs +++ b/crates/nu_plugin_polars/src/cache/mod.rs @@ -13,7 +13,7 @@ use nu_plugin::{EngineInterface, PluginCommand}; use nu_protocol::{LabeledError, ShellError, Span}; use uuid::Uuid; -use crate::{plugin_debug, values::PolarsPluginObject, PolarsPlugin}; +use crate::{plugin_debug, values::PolarsPluginObject, EngineWrapper, PolarsPlugin}; #[derive(Debug, Clone)] pub struct CacheValue { @@ -47,7 +47,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 +60,23 @@ 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:?}"); + plugin_debug!( + engine, + "PolarsPlugin: removing {key} from cache: {removed:?}" + ); removed } else { - plugin_debug!("PolarsPlugin: decrementing reference count for {key}"); + plugin_debug!( + engine, + "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)?; - } - _ => (), - }; + plugin_debug!(engine, "PolarsPlugin: Cache is empty enabling GC"); + engine.set_gc_disabled(false).map_err(LabeledError::from)?; drop(lock); Ok(removed) } @@ -84,23 +85,21 @@ 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:?}"); + plugin_debug!( + engine, + "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)?; - } - _ => (), - }; + plugin_debug!(engine, "PolarsPlugin: Cache has values disabling GC"); + engine.set_gc_disabled(true).map_err(LabeledError::from)?; let cache_value = CacheValue { uuid, value, @@ -154,7 +153,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, diff --git a/crates/nu_plugin_polars/src/cache/rm.rs b/crates/nu_plugin_polars/src/cache/rm.rs index fb78e91c9c..ab024148a7 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/open.rs b/crates/nu_plugin_polars/src/dataframe/eager/open.rs index 13a65074f0..f7ee467877 100644 --- a/crates/nu_plugin_polars/src/dataframe/eager/open.rs +++ b/crates/nu_plugin_polars/src/dataframe/eager/open.rs @@ -1,5 +1,6 @@ use crate::{ dataframe::values::NuSchema, + perf, values::{CustomValueSupport, NuLazyFrame}, PolarsPlugin, }; @@ -19,15 +20,20 @@ use std::{ sync::Arc, }; -use polars::prelude::{ - CsvEncoding, IpcReader, JsonFormat, JsonReader, LazyCsvReader, LazyFileListReader, LazyFrame, - ParquetReader, ScanArgsIpc, ScanArgsParquet, SerReader, +use polars::{ + lazy::frame::LazyJsonLineReader, + prelude::{ + CsvEncoding, IpcReader, JsonFormat, JsonReader, LazyCsvReader, LazyFileListReader, + LazyFrame, ParquetReader, ScanArgsIpc, ScanArgsParquet, SerReader, + }, }; use polars_io::{ avro::AvroReader, csv::read::CsvReadOptions, prelude::ParallelStrategy, HiveOptions, }; +const DEFAULT_INFER_SCHEMA: usize = 100; + #[derive(Clone)] pub struct OpenDataFrame; @@ -370,41 +376,82 @@ fn from_jsonl( file_path: &Path, file_span: Span, ) -> Result { - let infer_schema: Option = call.get_flag("infer-schema")?; + let infer_schema: usize = call + .get_flag("infer-schema")? + .unwrap_or(DEFAULT_INFER_SCHEMA); 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( + engine, + "Lazy json lines dataframe open", + start_time, + file!(), + line!(), + column!(), + ); + + 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( + engine, + "Eager json lines dataframe open", + start_time, + file!(), + line!(), + column!(), + ); + + df.cache_and_to_value(plugin, engine, call.head) + } } fn from_csv( @@ -416,7 +463,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 +505,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,11 +524,21 @@ fn from_csv( })? .into(); + perf( + engine, + "Lazy CSV dataframe open", + start_time, + file!(), + line!(), + column!(), + ); + 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)) @@ -511,6 +568,16 @@ fn from_csv( help: None, inner: vec![], })?; + + perf( + engine, + "Eager CSV dataframe open", + start_time, + file!(), + line!(), + column!(), + ); + let df = NuDataFrame::new(false, df); df.cache_and_to_value(plugin, engine, call.head) } diff --git a/crates/nu_plugin_polars/src/lib.rs b/crates/nu_plugin_polars/src/lib.rs index 3baca54ad9..9fc9a7ab2d 100644 --- a/crates/nu_plugin_polars/src/lib.rs +++ b/crates/nu_plugin_polars/src/lib.rs @@ -8,25 +8,89 @@ 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, }; +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() + .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> { + EngineInterface::set_gc_disabled(self, disabled) + } +} + #[macro_export] macro_rules! plugin_debug { - ($($arg:tt)*) => {{ - if std::env::var("POLARS_PLUGIN_DEBUG") - .ok() - .filter(|x| x == "1" || x == "true") + ($env_var_provider:tt, $($arg:tt)*) => {{ + if $env_var_provider.get_env_var("POLARS_PLUGIN_DEBUG") + .filter(|s| s == "1" || s == "true") .is_some() { eprintln!($($arg)*); } }}; } +pub fn perf( + env: impl EngineWrapper, + msg: &str, + dur: std::time::Instant, + file: &str, + line: u32, + column: u32, +) { + if env + .get_env_var("POLARS_PLUGIN_PERF") + .filter(|s| s == "1" || s == "true") + .is_some() + { + if env.use_color() { + eprintln!( + "perf: {}:{}:{} \x1b[32m{}\x1b[0m took \x1b[33m{:?}\x1b[0m", + file, + line, + column, + msg, + dur.elapsed(), + ); + } else { + eprintln!( + "perf: {}:{}:{} {} took {:?}", + file, + line, + column, + msg, + dur.elapsed(), + ); + } + } +} + #[derive(Default)] pub struct PolarsPlugin { pub(crate) cache: Cache, @@ -35,6 +99,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()); @@ -52,7 +120,7 @@ impl Plugin for PolarsPlugin { ) -> Result<(), LabeledError> { 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 +261,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 +296,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_python/nu_plugin_python_example.py b/crates/nu_plugin_python/nu_plugin_python_example.py index 3db89a0afc..34e54fea71 100755 --- a/crates/nu_plugin_python/nu_plugin_python_example.py +++ b/crates/nu_plugin_python/nu_plugin_python_example.py @@ -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"]) @@ -251,4 +258,4 @@ if __name__ == "__main__": if len(sys.argv) == 2 and sys.argv[1] == "--stdio": plugin() else: - print("Run me from inside nushell!") + print("Run me from inside nushell!") \ No newline at end of file diff --git a/crates/nu_plugin_query/Cargo.toml b/crates/nu_plugin_query/Cargo.toml index 20a2a2729d..4a54e9bf60 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,10 @@ 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" +sxd-xpath = "0.4" \ No newline at end of file diff --git a/crates/nu_plugin_query/src/query.rs b/crates/nu_plugin_query/src/query.rs index 1e143068c4..c22339ab4a 100644 --- a/crates/nu_plugin_query/src/query.rs +++ b/crates/nu_plugin_query/src/query.rs @@ -16,6 +16,10 @@ impl Query { } impl Plugin for Query { + fn version(&self) -> String { + env!("CARGO_PKG_VERSION").into() + } + fn commands(&self) -> Vec>> { vec![ Box::new(QueryCommand), 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/src/from.rs b/crates/nuon/src/from.rs index f9230d6140..2cfeaebc61 100644 --- a/crates/nuon/src/from.rs +++ b/crates/nuon/src/from.rs @@ -323,6 +323,12 @@ 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::Subexpression(..) => Err(ShellError::OutsideSpannedLabeledError { src: original_text.to_string(), error: "Error when loading".into(), 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 41d5534bb0..c74fd7641d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -400,7 +400,7 @@ fn main() -> Result<()> { #[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(); @@ -419,8 +419,14 @@ 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)); } diff --git a/src/run.rs b/src/run.rs index 2996bc76f8..db779b0058 100644 --- a/src/run.rs +++ b/src/run.rs @@ -7,7 +7,7 @@ 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, @@ -114,8 +114,11 @@ 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); 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/registry_file.rs b/tests/plugins/registry_file.rs index 3f7da6dd03..c0f0fa0724 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,7 +156,7 @@ 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 { @@ -157,7 +164,7 @@ fn plugin_rm_then_restart_nu() { // this doesn't exist, but it should be ok filename: dirs.test().join("nu_plugin_foo"), shell: None, - data: PluginRegistryItemData::Valid { commands: vec![] }, + data: valid_plugin_item_data(), }); contents @@ -225,7 +232,7 @@ 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 { @@ -233,7 +240,7 @@ fn plugin_rm_from_custom_path() { // this doesn't exist, but it should be ok filename: dirs.test().join("nu_plugin_foo"), shell: None, - data: PluginRegistryItemData::Valid { commands: vec![] }, + data: valid_plugin_item_data(), }); contents @@ -273,7 +280,7 @@ 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 { @@ -281,7 +288,7 @@ fn plugin_rm_using_filename() { // this doesn't exist, but it should be ok filename: dirs.test().join("nu_plugin_foo"), shell: None, - data: PluginRegistryItemData::Valid { commands: vec![] }, + data: valid_plugin_item_data(), }); contents @@ -331,7 +338,7 @@ 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 { diff --git a/tests/repl/test_parser.rs b/tests/repl/test_parser.rs index 9d743b175d..0efb1d087a 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( 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]