Merge branch 'main' into use-into-value-trait

This commit is contained in:
Tim 'Piepmatz' Hesse 2024-06-27 22:15:21 +02:00
commit 037768205c
155 changed files with 6124 additions and 1147 deletions

View File

@ -19,7 +19,7 @@ jobs:
# Prevent sudden announcement of a new advisory from failing ci: # Prevent sudden announcement of a new advisory from failing ci:
continue-on-error: true continue-on-error: true
steps: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.7
- uses: rustsec/audit-check@v1.4.1 - uses: rustsec/audit-check@v1.4.1
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -33,7 +33,7 @@ jobs:
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.7
- name: Setup Rust toolchain and cache - name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
@ -66,7 +66,7 @@ jobs:
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.7
- name: Setup Rust toolchain and cache - name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
@ -95,7 +95,7 @@ jobs:
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.7
- name: Setup Rust toolchain and cache - name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
@ -146,7 +146,7 @@ jobs:
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.7
- name: Setup Rust toolchain and cache - name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 uses: actions-rust-lang/setup-rust-toolchain@v1.9.0

View File

@ -27,7 +27,7 @@ jobs:
# if: github.repository == 'nushell/nightly' # if: github.repository == 'nushell/nightly'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.7
if: github.repository == 'nushell/nightly' if: github.repository == 'nushell/nightly'
with: with:
ref: main ref: main
@ -112,7 +112,7 @@ jobs:
runs-on: ${{matrix.os}} runs-on: ${{matrix.os}}
steps: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.7
with: with:
ref: main ref: main
fetch-depth: 0 fetch-depth: 0
@ -161,7 +161,7 @@ jobs:
# REF: https://github.com/marketplace/actions/gh-release # REF: https://github.com/marketplace/actions/gh-release
# Create a release only in nushell/nightly repo # Create a release only in nushell/nightly repo
- name: Publish Archive - 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') }} if: ${{ startsWith(github.repository, 'nushell/nightly') }}
with: with:
prerelease: true prerelease: true
@ -181,7 +181,7 @@ jobs:
- name: Waiting for Release - name: Waiting for Release
run: sleep 1800 run: sleep 1800
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.7
with: with:
ref: main ref: main

View File

@ -62,7 +62,7 @@ jobs:
runs-on: ${{matrix.os}} runs-on: ${{matrix.os}}
steps: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v4.1.7
- name: Update Rust Toolchain Target - name: Update Rust Toolchain Target
run: | run: |
@ -91,7 +91,7 @@ jobs:
# REF: https://github.com/marketplace/actions/gh-release # REF: https://github.com/marketplace/actions/gh-release
- name: Publish Archive - 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/') }} if: ${{ startsWith(github.ref, 'refs/tags/') }}
with: with:
draft: true draft: true

View File

@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout Actions Repository - name: Checkout Actions Repository
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.7
- name: Check spelling - name: Check spelling
uses: crate-ci/typos@v1.22.4 uses: crate-ci/typos@v1.22.9

125
Cargo.lock generated
View File

@ -377,7 +377,7 @@ dependencies = [
"bitflags 2.5.0", "bitflags 2.5.0",
"cexpr", "cexpr",
"clang-sys", "clang-sys",
"itertools 0.12.1", "itertools 0.11.0",
"lazy_static", "lazy_static",
"lazycell", "lazycell",
"proc-macro2", "proc-macro2",
@ -1227,6 +1227,12 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "doctest-file"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562"
[[package]] [[package]]
name = "downcast-rs" name = "downcast-rs"
version = "1.2.1" version = "1.2.1"
@ -1687,9 +1693,9 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]] [[package]]
name = "git2" name = "git2"
version = "0.18.3" version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70" checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724"
dependencies = [ dependencies = [
"bitflags 2.5.0", "bitflags 2.5.0",
"libc", "libc",
@ -1989,12 +1995,6 @@ dependencies = [
"unicode-width", "unicode-width",
] ]
[[package]]
name = "indoc"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
[[package]] [[package]]
name = "inotify" name = "inotify"
version = "0.9.6" version = "0.9.6"
@ -2026,10 +2026,11 @@ dependencies = [
[[package]] [[package]]
name = "interprocess" name = "interprocess"
version = "2.1.0" version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4d0250d41da118226e55b3d50ca3f0d9e0a0f6829b92f543ac0054aeea1572" checksum = "67bafc2f5dbdad79a6d925649758d5472647b416028099f0b829d1b67fdd47d3"
dependencies = [ dependencies = [
"doctest-file",
"libc", "libc",
"recvmsg", "recvmsg",
"widestring", "widestring",
@ -2288,9 +2289,9 @@ dependencies = [
[[package]] [[package]]
name = "libgit2-sys" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8" checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
@ -2762,7 +2763,7 @@ dependencies = [
[[package]] [[package]]
name = "nu" name = "nu"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"assert_cmd", "assert_cmd",
"crossterm", "crossterm",
@ -2815,7 +2816,7 @@ dependencies = [
[[package]] [[package]]
name = "nu-cli" name = "nu-cli"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"chrono", "chrono",
"crossterm", "crossterm",
@ -2850,7 +2851,7 @@ dependencies = [
[[package]] [[package]]
name = "nu-cmd-base" name = "nu-cmd-base"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"miette", "miette",
@ -2862,7 +2863,7 @@ dependencies = [
[[package]] [[package]]
name = "nu-cmd-extra" name = "nu-cmd-extra"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"fancy-regex", "fancy-regex",
"heck 0.5.0", "heck 0.5.0",
@ -2887,7 +2888,7 @@ dependencies = [
[[package]] [[package]]
name = "nu-cmd-lang" name = "nu-cmd-lang"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"itertools 0.12.1", "itertools 0.12.1",
"nu-engine", "nu-engine",
@ -2899,7 +2900,7 @@ dependencies = [
[[package]] [[package]]
name = "nu-cmd-plugin" name = "nu-cmd-plugin"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"itertools 0.12.1", "itertools 0.12.1",
"nu-engine", "nu-engine",
@ -2910,7 +2911,7 @@ dependencies = [
[[package]] [[package]]
name = "nu-color-config" name = "nu-color-config"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"nu-ansi-term", "nu-ansi-term",
"nu-engine", "nu-engine",
@ -2922,7 +2923,7 @@ dependencies = [
[[package]] [[package]]
name = "nu-command" name = "nu-command"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"alphanumeric-sort", "alphanumeric-sort",
"base64 0.22.1", "base64 0.22.1",
@ -3031,7 +3032,7 @@ dependencies = [
[[package]] [[package]]
name = "nu-derive-value" name = "nu-derive-value"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"convert_case", "convert_case",
"proc-macro-error", "proc-macro-error",
@ -3042,7 +3043,7 @@ dependencies = [
[[package]] [[package]]
name = "nu-engine" name = "nu-engine"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"nu-glob", "nu-glob",
"nu-path", "nu-path",
@ -3052,7 +3053,7 @@ dependencies = [
[[package]] [[package]]
name = "nu-explore" name = "nu-explore"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"ansi-str", "ansi-str",
"anyhow", "anyhow",
@ -3077,14 +3078,14 @@ dependencies = [
[[package]] [[package]]
name = "nu-glob" name = "nu-glob"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"doc-comment", "doc-comment",
] ]
[[package]] [[package]]
name = "nu-json" name = "nu-json"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"linked-hash-map", "linked-hash-map",
"num-traits", "num-traits",
@ -3094,7 +3095,7 @@ dependencies = [
[[package]] [[package]]
name = "nu-lsp" name = "nu-lsp"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"assert-json-diff", "assert-json-diff",
"crossbeam-channel", "crossbeam-channel",
@ -3115,7 +3116,7 @@ dependencies = [
[[package]] [[package]]
name = "nu-parser" name = "nu-parser"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"bytesize", "bytesize",
"chrono", "chrono",
@ -3131,7 +3132,7 @@ dependencies = [
[[package]] [[package]]
name = "nu-path" name = "nu-path"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"dirs-next", "dirs-next",
"omnipath", "omnipath",
@ -3140,7 +3141,7 @@ dependencies = [
[[package]] [[package]]
name = "nu-plugin" name = "nu-plugin"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"log", "log",
"nix", "nix",
@ -3155,7 +3156,7 @@ dependencies = [
[[package]] [[package]]
name = "nu-plugin-core" name = "nu-plugin-core"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"interprocess", "interprocess",
"log", "log",
@ -3169,7 +3170,7 @@ dependencies = [
[[package]] [[package]]
name = "nu-plugin-engine" name = "nu-plugin-engine"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"log", "log",
"nu-engine", "nu-engine",
@ -3184,7 +3185,7 @@ dependencies = [
[[package]] [[package]]
name = "nu-plugin-protocol" name = "nu-plugin-protocol"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"bincode", "bincode",
"nu-protocol", "nu-protocol",
@ -3196,7 +3197,7 @@ dependencies = [
[[package]] [[package]]
name = "nu-plugin-test-support" name = "nu-plugin-test-support"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"nu-ansi-term", "nu-ansi-term",
"nu-cmd-lang", "nu-cmd-lang",
@ -3214,7 +3215,7 @@ dependencies = [
[[package]] [[package]]
name = "nu-pretty-hex" name = "nu-pretty-hex"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"heapless", "heapless",
"nu-ansi-term", "nu-ansi-term",
@ -3223,7 +3224,7 @@ dependencies = [
[[package]] [[package]]
name = "nu-protocol" name = "nu-protocol"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"brotli", "brotli",
"byte-unit", "byte-unit",
@ -3256,7 +3257,7 @@ dependencies = [
[[package]] [[package]]
name = "nu-std" name = "nu-std"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"log", "log",
"miette", "miette",
@ -3267,7 +3268,7 @@ dependencies = [
[[package]] [[package]]
name = "nu-system" name = "nu-system"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"chrono", "chrono",
"itertools 0.12.1", "itertools 0.12.1",
@ -3285,7 +3286,7 @@ dependencies = [
[[package]] [[package]]
name = "nu-table" name = "nu-table"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"fancy-regex", "fancy-regex",
"nu-ansi-term", "nu-ansi-term",
@ -3299,7 +3300,7 @@ dependencies = [
[[package]] [[package]]
name = "nu-term-grid" name = "nu-term-grid"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"nu-utils", "nu-utils",
"unicode-width", "unicode-width",
@ -3307,7 +3308,7 @@ dependencies = [
[[package]] [[package]]
name = "nu-test-support" name = "nu-test-support"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"nu-glob", "nu-glob",
"nu-path", "nu-path",
@ -3319,7 +3320,7 @@ dependencies = [
[[package]] [[package]]
name = "nu-utils" name = "nu-utils"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"crossterm_winapi", "crossterm_winapi",
"log", "log",
@ -3345,7 +3346,7 @@ dependencies = [
[[package]] [[package]]
name = "nu_plugin_example" name = "nu_plugin_example"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"nu-cmd-lang", "nu-cmd-lang",
"nu-plugin", "nu-plugin",
@ -3355,7 +3356,7 @@ dependencies = [
[[package]] [[package]]
name = "nu_plugin_formats" name = "nu_plugin_formats"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"eml-parser", "eml-parser",
"ical", "ical",
@ -3368,7 +3369,7 @@ dependencies = [
[[package]] [[package]]
name = "nu_plugin_gstat" name = "nu_plugin_gstat"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"git2", "git2",
"nu-plugin", "nu-plugin",
@ -3377,7 +3378,7 @@ dependencies = [
[[package]] [[package]]
name = "nu_plugin_inc" name = "nu_plugin_inc"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"nu-plugin", "nu-plugin",
"nu-protocol", "nu-protocol",
@ -3386,7 +3387,7 @@ dependencies = [
[[package]] [[package]]
name = "nu_plugin_polars" name = "nu_plugin_polars"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"chrono", "chrono",
"chrono-tz 0.9.0", "chrono-tz 0.9.0",
@ -3417,7 +3418,7 @@ dependencies = [
[[package]] [[package]]
name = "nu_plugin_query" name = "nu_plugin_query"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"gjson", "gjson",
"nu-plugin", "nu-plugin",
@ -3429,7 +3430,7 @@ dependencies = [
[[package]] [[package]]
name = "nu_plugin_stress_internals" name = "nu_plugin_stress_internals"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"interprocess", "interprocess",
"serde", "serde",
@ -3555,7 +3556,7 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]] [[package]]
name = "nuon" name = "nuon"
version = "0.94.3" version = "0.95.1"
dependencies = [ dependencies = [
"chrono", "chrono",
"fancy-regex", "fancy-regex",
@ -4745,21 +4746,21 @@ dependencies = [
[[package]] [[package]]
name = "ratatui" name = "ratatui"
version = "0.26.2" version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a564a852040e82671dc50a37d88f3aa83bbc690dfc6844cfe7a2591620206a80" checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef"
dependencies = [ dependencies = [
"bitflags 2.5.0", "bitflags 2.5.0",
"cassowary", "cassowary",
"compact_str", "compact_str",
"crossterm", "crossterm",
"indoc",
"itertools 0.12.1", "itertools 0.12.1",
"lru", "lru",
"paste", "paste",
"stability", "stability",
"strum", "strum",
"unicode-segmentation", "unicode-segmentation",
"unicode-truncate",
"unicode-width", "unicode-width",
] ]
@ -5434,9 +5435,9 @@ dependencies = [
[[package]] [[package]]
name = "shadow-rs" name = "shadow-rs"
version = "0.28.0" version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d75516bdaee8f640543ad1f6e292448c23ce57143f812c3736ab4b0874383df" checksum = "0a600f795d0894cda22235b44eea4b85c2a35b405f65523645ac8e35b306817a"
dependencies = [ dependencies = [
"const_format", "const_format",
"is_debug", "is_debug",
@ -6307,6 +6308,16 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" 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]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.1.12" version = "0.1.12"
@ -6486,9 +6497,9 @@ checksum = "425a23c7b7145bc7620c9c445817c37b1f78b6790aee9f208133f3c028975b60"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.8.0" version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439"
dependencies = [ dependencies = [
"getrandom", "getrandom",
"serde", "serde",

View File

@ -11,7 +11,7 @@ license = "MIT"
name = "nu" name = "nu"
repository = "https://github.com/nushell/nushell" repository = "https://github.com/nushell/nushell"
rust-version = "1.77.2" 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 # 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" human-date-parser = "0.1.1"
indexmap = "2.2" indexmap = "2.2"
indicatif = "0.17" indicatif = "0.17"
interprocess = "2.1.0" interprocess = "2.2.0"
is_executable = "1.0" is_executable = "1.0"
itertools = "0.12" itertools = "0.12"
libc = "0.2" libc = "0.2"
@ -172,7 +172,7 @@ uu_mv = "0.0.26"
uu_whoami = "0.0.26" uu_whoami = "0.0.26"
uu_uname = "0.0.26" uu_uname = "0.0.26"
uucore = "0.0.26" uucore = "0.0.26"
uuid = "1.8.0" uuid = "1.9.1"
v_htmlescape = "0.15.0" v_htmlescape = "0.15.0"
wax = "0.6" wax = "0.6"
which = "6.0.0" which = "6.0.0"
@ -180,22 +180,22 @@ windows = "0.54"
winreg = "0.52" winreg = "0.52"
[dependencies] [dependencies]
nu-cli = { path = "./crates/nu-cli", version = "0.94.3" } nu-cli = { path = "./crates/nu-cli", version = "0.95.1" }
nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.94.3" } nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.95.1" }
nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.94.3" } nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.95.1" }
nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.94.3", optional = true } nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.95.1", optional = true }
nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.94.3" } nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.95.1" }
nu-command = { path = "./crates/nu-command", version = "0.94.3" } nu-command = { path = "./crates/nu-command", version = "0.95.1" }
nu-engine = { path = "./crates/nu-engine", version = "0.94.3" } nu-engine = { path = "./crates/nu-engine", version = "0.95.1" }
nu-explore = { path = "./crates/nu-explore", version = "0.94.3" } nu-explore = { path = "./crates/nu-explore", version = "0.95.1" }
nu-lsp = { path = "./crates/nu-lsp/", version = "0.94.3" } nu-lsp = { path = "./crates/nu-lsp/", version = "0.95.1" }
nu-parser = { path = "./crates/nu-parser", version = "0.94.3" } nu-parser = { path = "./crates/nu-parser", version = "0.95.1" }
nu-path = { path = "./crates/nu-path", version = "0.94.3" } nu-path = { path = "./crates/nu-path", version = "0.95.1" }
nu-plugin-engine = { path = "./crates/nu-plugin-engine", optional = true, version = "0.94.3" } nu-plugin-engine = { path = "./crates/nu-plugin-engine", optional = true, version = "0.95.1" }
nu-protocol = { path = "./crates/nu-protocol", version = "0.94.3" } nu-protocol = { path = "./crates/nu-protocol", version = "0.95.1" }
nu-std = { path = "./crates/nu-std", version = "0.94.3" } nu-std = { path = "./crates/nu-std", version = "0.95.1" }
nu-system = { path = "./crates/nu-system", version = "0.94.3" } nu-system = { path = "./crates/nu-system", version = "0.95.1" }
nu-utils = { path = "./crates/nu-utils", version = "0.94.3" } nu-utils = { path = "./crates/nu-utils", version = "0.95.1" }
reedline = { workspace = true, features = ["bashisms", "sqlite"] } reedline = { workspace = true, features = ["bashisms", "sqlite"] }
@ -225,9 +225,9 @@ nix = { workspace = true, default-features = false, features = [
] } ] }
[dev-dependencies] [dev-dependencies]
nu-test-support = { path = "./crates/nu-test-support", 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.94.3" } nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.95.1" }
nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.94.3" } nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.95.1" }
assert_cmd = "2.0" assert_cmd = "2.0"
dirs-next = { workspace = true } dirs-next = { workspace = true }
tango-bench = "0.5" tango-bench = "0.5"

View File

@ -47,8 +47,7 @@ fn setup_stack_and_engine_from_command(command: &str) -> (Stack, EngineState) {
&mut engine, &mut engine,
&mut stack, &mut stack,
PipelineData::empty(), PipelineData::empty(),
None, Default::default(),
false,
) )
.unwrap(); .unwrap();
@ -90,8 +89,7 @@ fn bench_command(
&mut engine, &mut engine,
&mut stack, &mut stack,
PipelineData::empty(), PipelineData::empty(),
None, Default::default(),
false,
) )
.unwrap(), .unwrap(),
); );

View File

@ -5,27 +5,27 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cli"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
name = "nu-cli" name = "nu-cli"
version = "0.94.3" version = "0.95.1"
[lib] [lib]
bench = false bench = false
[dev-dependencies] [dev-dependencies]
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.94.3" } nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.95.1" }
nu-command = { path = "../nu-command", version = "0.94.3" } nu-command = { path = "../nu-command", version = "0.95.1" }
nu-test-support = { path = "../nu-test-support", version = "0.94.3" } nu-test-support = { path = "../nu-test-support", version = "0.95.1" }
rstest = { workspace = true, default-features = false } rstest = { workspace = true, default-features = false }
tempfile = { workspace = true } tempfile = { workspace = true }
[dependencies] [dependencies]
nu-cmd-base = { path = "../nu-cmd-base", version = "0.94.3" } nu-cmd-base = { path = "../nu-cmd-base", version = "0.95.1" }
nu-engine = { path = "../nu-engine", version = "0.94.3" } nu-engine = { path = "../nu-engine", version = "0.95.1" }
nu-path = { path = "../nu-path", version = "0.94.3" } nu-path = { path = "../nu-path", version = "0.95.1" }
nu-parser = { path = "../nu-parser", version = "0.94.3" } nu-parser = { path = "../nu-parser", version = "0.95.1" }
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.94.3", optional = true } nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.95.1", optional = true }
nu-protocol = { path = "../nu-protocol", version = "0.94.3" } nu-protocol = { path = "../nu-protocol", version = "0.95.1" }
nu-utils = { path = "../nu-utils", version = "0.94.3" } nu-utils = { path = "../nu-utils", version = "0.95.1" }
nu-color-config = { path = "../nu-color-config", version = "0.94.3" } nu-color-config = { path = "../nu-color-config", version = "0.95.1" }
nu-ansi-term = { workspace = true } nu-ansi-term = { workspace = true }
reedline = { workspace = true, features = ["bashisms", "sqlite"] } reedline = { workspace = true, features = ["bashisms", "sqlite"] }

View File

@ -344,7 +344,10 @@ pub fn migrate_old_plugin_file(engine_state: &EngineState, storage_path: &str) -
name: identity.name().to_owned(), name: identity.name().to_owned(),
filename: identity.filename().to_owned(), filename: identity.filename().to_owned(),
shell: identity.shell().map(|p| p.to_owned()), shell: identity.shell().map(|p| p.to_owned()),
data: PluginRegistryItemData::Valid { commands }, data: PluginRegistryItemData::Valid {
metadata: Default::default(),
commands,
},
}); });
} }

View File

@ -8,15 +8,45 @@ use nu_protocol::{
}; };
use std::sync::Arc; use std::sync::Arc;
#[derive(Default)]
pub struct EvaluateCommandsOpts {
pub table_mode: Option<Value>,
pub error_style: Option<Value>,
pub no_newline: bool,
}
/// Run a command (or commands) given to us by the user /// Run a command (or commands) given to us by the user
pub fn evaluate_commands( pub fn evaluate_commands(
commands: &Spanned<String>, commands: &Spanned<String>,
engine_state: &mut EngineState, engine_state: &mut EngineState,
stack: &mut Stack, stack: &mut Stack,
input: PipelineData, input: PipelineData,
table_mode: Option<Value>, opts: EvaluateCommandsOpts,
no_newline: bool,
) -> Result<(), ShellError> { ) -> 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 // Translate environment variables from Strings to Values
convert_env_values(engine_state, stack)?; convert_env_values(engine_state, stack)?;

View File

@ -17,7 +17,7 @@ mod validation;
pub use commands::add_cli_context; pub use commands::add_cli_context;
pub use completions::{FileCompletion, NuCompleter, SemanticSuggestion, SuggestionKind}; pub use completions::{FileCompletion, NuCompleter, SemanticSuggestion, SuggestionKind};
pub use config_files::eval_config_contents; 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 eval_file::evaluate_file;
pub use menus::NuHelpCompleter; pub use menus::NuHelpCompleter;
pub use nu_cmd_base::util::get_init_cwd; pub use nu_cmd_base::util::get_init_cwd;

View File

@ -138,6 +138,7 @@ impl Highlighter for NuHighlighter {
FlatShape::Filepath => add_colored_token(&shape.1, next_token), FlatShape::Filepath => add_colored_token(&shape.1, next_token),
FlatShape::Directory => 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::GlobPattern => add_colored_token(&shape.1, next_token),
FlatShape::Variable(_) | FlatShape::VarDecl(_) => { FlatShape::Variable(_) | FlatShape::VarDecl(_) => {
add_colored_token(&shape.1, next_token) add_colored_token(&shape.1, next_token)
@ -452,7 +453,8 @@ fn find_matching_block_end_in_expr(
} }
} }
Expr::StringInterpolation(exprs) => exprs.iter().find_map(|expr| { Expr::StringInterpolation(exprs) | Expr::GlobInterpolation(exprs, _) => {
exprs.iter().find_map(|expr| {
find_matching_block_end_in_expr( find_matching_block_end_in_expr(
line, line,
working_set, working_set,
@ -460,7 +462,8 @@ fn find_matching_block_end_in_expr(
global_span_offset, global_span_offset,
global_cursor_offset, global_cursor_offset,
) )
}), })
}
Expr::List(list) => { Expr::List(list) => {
if expr_last == global_cursor_offset { if expr_last == global_cursor_offset {

View File

@ -763,7 +763,7 @@ fn variables_completions() {
// Test completions for $nu // Test completions for $nu
let suggestions = completer.complete("$nu.", 4); let suggestions = completer.complete("$nu.", 4);
assert_eq!(17, suggestions.len()); assert_eq!(18, suggestions.len());
let expected: Vec<String> = vec![ let expected: Vec<String> = vec![
"cache-dir".into(), "cache-dir".into(),
@ -783,6 +783,7 @@ fn variables_completions() {
"plugin-path".into(), "plugin-path".into(),
"startup-time".into(), "startup-time".into(),
"temp-path".into(), "temp-path".into(),
"vendor-autoload-dir".into(),
]; ];
// Match results // Match results

View File

@ -5,15 +5,15 @@ edition = "2021"
license = "MIT" license = "MIT"
name = "nu-cmd-base" name = "nu-cmd-base"
repository = "https://github.com/nushell/nushell/tree/main/crates/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 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
nu-engine = { path = "../nu-engine", version = "0.94.3" } nu-engine = { path = "../nu-engine", version = "0.95.1" }
nu-parser = { path = "../nu-parser", version = "0.94.3" } nu-parser = { path = "../nu-parser", version = "0.95.1" }
nu-path = { path = "../nu-path", version = "0.94.3" } nu-path = { path = "../nu-path", version = "0.95.1" }
nu-protocol = { path = "../nu-protocol", version = "0.94.3" } nu-protocol = { path = "../nu-protocol", version = "0.95.1" }
indexmap = { workspace = true } indexmap = { workspace = true }
miette = { workspace = true } miette = { workspace = true }

View File

@ -5,7 +5,7 @@ edition = "2021"
license = "MIT" license = "MIT"
name = "nu-cmd-extra" name = "nu-cmd-extra"
repository = "https://github.com/nushell/nushell/tree/main/crates/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 # 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 bench = false
[dependencies] [dependencies]
nu-cmd-base = { path = "../nu-cmd-base", version = "0.94.3" } nu-cmd-base = { path = "../nu-cmd-base", version = "0.95.1" }
nu-engine = { path = "../nu-engine", version = "0.94.3" } nu-engine = { path = "../nu-engine", version = "0.95.1" }
nu-json = { version = "0.94.3", path = "../nu-json" } nu-json = { version = "0.95.1", path = "../nu-json" }
nu-parser = { path = "../nu-parser", version = "0.94.3" } nu-parser = { path = "../nu-parser", version = "0.95.1" }
nu-pretty-hex = { version = "0.94.3", path = "../nu-pretty-hex" } nu-pretty-hex = { version = "0.95.1", path = "../nu-pretty-hex" }
nu-protocol = { path = "../nu-protocol", version = "0.94.3" } nu-protocol = { path = "../nu-protocol", version = "0.95.1" }
nu-utils = { path = "../nu-utils", version = "0.94.3" } nu-utils = { path = "../nu-utils", version = "0.95.1" }
# Potential dependencies for extras # Potential dependencies for extras
heck = { workspace = true } heck = { workspace = true }
@ -33,6 +33,6 @@ v_htmlescape = { workspace = true }
itertools = { workspace = true } itertools = { workspace = true }
[dev-dependencies] [dev-dependencies]
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.94.3" } nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.95.1" }
nu-command = { path = "../nu-command", version = "0.94.3" } nu-command = { path = "../nu-command", version = "0.95.1" }
nu-test-support = { path = "../nu-test-support", version = "0.94.3" } nu-test-support = { path = "../nu-test-support", version = "0.95.1" }

View File

@ -6,22 +6,22 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-lang"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
name = "nu-cmd-lang" name = "nu-cmd-lang"
version = "0.94.3" version = "0.95.1"
[lib] [lib]
bench = false bench = false
[dependencies] [dependencies]
nu-engine = { path = "../nu-engine", version = "0.94.3" } nu-engine = { path = "../nu-engine", version = "0.95.1" }
nu-parser = { path = "../nu-parser", version = "0.94.3" } nu-parser = { path = "../nu-parser", version = "0.95.1" }
nu-protocol = { path = "../nu-protocol", version = "0.94.3" } nu-protocol = { path = "../nu-protocol", version = "0.95.1" }
nu-utils = { path = "../nu-utils", version = "0.94.3" } nu-utils = { path = "../nu-utils", version = "0.95.1" }
itertools = { workspace = true } itertools = { workspace = true }
shadow-rs = { version = "0.28", default-features = false } shadow-rs = { version = "0.29", default-features = false }
[build-dependencies] [build-dependencies]
shadow-rs = { version = "0.28", default-features = false } shadow-rs = { version = "0.29", default-features = false }
[features] [features]
mimalloc = [] mimalloc = []

View File

@ -1,4 +1,5 @@
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
use nu_protocol::engine::CommandType;
#[derive(Clone)] #[derive(Clone)]
pub struct Break; pub struct Break;
@ -18,6 +19,15 @@ impl Command for Break {
.category(Category::Core) .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( fn run(
&self, &self,
_engine_state: &EngineState, _engine_state: &EngineState,

View File

@ -1,4 +1,5 @@
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
use nu_protocol::engine::CommandType;
#[derive(Clone)] #[derive(Clone)]
pub struct Continue; pub struct Continue;
@ -18,6 +19,14 @@ impl Command for Continue {
.category(Category::Core) .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( fn run(
&self, &self,
_engine_state: &EngineState, _engine_state: &EngineState,

View File

@ -229,14 +229,24 @@ impl Command for Do {
result: None, result: None,
}, },
Example { Example {
description: "Run the closure, with a positional parameter", description: "Run the closure with a positional, type-checked parameter",
example: r#"do {|x| 100 + $x } 77"#, example: r#"do {|x:int| 100 + $x } 77"#,
result: Some(Value::test_int(177)), result: Some(Value::test_int(177)),
}, },
Example { Example {
description: "Run the closure, with input", description: "Run the closure with pipeline input",
example: r#"77 | do {|x| 100 + $in }"#, example: r#"77 | do { 100 + $in }"#,
result: None, // TODO: returns 177 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 { Example {
description: "Run the closure and keep changes to the environment", description: "Run the closure and keep changes to the environment",

View File

@ -2,7 +2,7 @@ use nu_engine::{
command_prelude::*, get_eval_block, get_eval_expression, get_eval_expression_with_input, command_prelude::*, get_eval_block, get_eval_expression, get_eval_expression_with_input,
}; };
use nu_protocol::{ use nu_protocol::{
engine::StateWorkingSet, engine::{CommandType, StateWorkingSet},
eval_const::{eval_const_subexpression, eval_constant, eval_constant_with_input}, eval_const::{eval_const_subexpression, eval_constant, eval_constant_with_input},
}; };
@ -41,6 +41,15 @@ impl Command for If {
.category(Category::Core) .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 { fn is_const(&self) -> bool {
true true
} }

View File

@ -1,4 +1,5 @@
use nu_engine::{command_prelude::*, get_eval_block}; use nu_engine::{command_prelude::*, get_eval_block};
use nu_protocol::engine::CommandType;
#[derive(Clone)] #[derive(Clone)]
pub struct Loop; pub struct Loop;
@ -20,6 +21,15 @@ impl Command for Loop {
.category(Category::Core) .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( fn run(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,

View File

@ -1,7 +1,7 @@
use nu_engine::{ use nu_engine::{
command_prelude::*, get_eval_block, get_eval_expression, get_eval_expression_with_input, 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)] #[derive(Clone)]
pub struct Match; pub struct Match;
@ -27,6 +27,15 @@ impl Command for Match {
.category(Category::Core) .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( fn run(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,

View File

@ -1,5 +1,4 @@
use nu_engine::{command_prelude::*, get_full_help}; use nu_engine::{command_prelude::*, get_full_help};
use nu_protocol::engine::CommandType;
#[derive(Clone)] #[derive(Clone)]
pub struct Scope; pub struct Scope;
@ -20,10 +19,6 @@ impl Command for Scope {
"Commands for getting info about what is in scope." "Commands for getting info about what is in scope."
} }
fn command_type(&self) -> CommandType {
CommandType::Keyword
}
fn run( fn run(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,

View File

@ -1,5 +1,5 @@
use nu_engine::{command_prelude::*, get_eval_block, EvalBlockFn}; use nu_engine::{command_prelude::*, get_eval_block, EvalBlockFn};
use nu_protocol::engine::Closure; use nu_protocol::engine::{Closure, CommandType};
#[derive(Clone)] #[derive(Clone)]
pub struct Try; pub struct Try;
@ -31,6 +31,15 @@ impl Command for Try {
.category(Category::Core) .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( fn run(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,

View File

@ -116,11 +116,18 @@ pub fn version(engine_state: &EngineState, span: Span) -> Result<PipelineData, S
Value::string(features_enabled().join(", "), span), Value::string(features_enabled().join(", "), span),
); );
// Get a list of plugin names // Get a list of plugin names and versions if present
let installed_plugins = engine_state let installed_plugins = engine_state
.plugins() .plugins()
.iter() .iter()
.map(|x| x.identity().name()) .map(|x| {
let name = x.identity().name();
if let Some(version) = x.metadata().and_then(|m| m.version) {
format!("{name} {version}")
} else {
name.into()
}
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
record.push( record.push(

View File

@ -1,4 +1,5 @@
use nu_engine::{command_prelude::*, get_eval_block, get_eval_expression}; use nu_engine::{command_prelude::*, get_eval_block, get_eval_expression};
use nu_protocol::engine::CommandType;
#[derive(Clone)] #[derive(Clone)]
pub struct While; pub struct While;
@ -29,6 +30,15 @@ impl Command for While {
vec!["loop"] 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( fn run(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,

View File

@ -5,15 +5,15 @@ edition = "2021"
license = "MIT" license = "MIT"
name = "nu-cmd-plugin" name = "nu-cmd-plugin"
repository = "https://github.com/nushell/nushell/tree/main/crates/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 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
nu-engine = { path = "../nu-engine", version = "0.94.3" } nu-engine = { path = "../nu-engine", version = "0.95.1" }
nu-path = { path = "../nu-path", version = "0.94.3" } nu-path = { path = "../nu-path", version = "0.95.1" }
nu-protocol = { path = "../nu-protocol", version = "0.94.3", features = ["plugin"] } nu-protocol = { path = "../nu-protocol", version = "0.95.1", features = ["plugin"] }
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.94.3" } nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.95.1" }
itertools = { workspace = true } itertools = { workspace = true }

View File

@ -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 interface = plugin.clone().get_plugin(Some((engine_state, stack)))?;
let metadata = interface.get_metadata()?;
let commands = interface.get_signature()?; let commands = interface.get_signature()?;
modify_plugin_file(engine_state, stack, call.head, custom_path, |contents| { modify_plugin_file(engine_state, stack, call.head, custom_path, |contents| {
// Update the file with the received signatures // Update the file with the received metadata and signatures
let item = PluginRegistryItem::new(plugin.identity(), commands); let item = PluginRegistryItem::new(plugin.identity(), metadata, commands);
contents.upsert_plugin(item); contents.upsert_plugin(item);
Ok(()) Ok(())
})?; })?;

View File

@ -16,6 +16,7 @@ impl Command for PluginList {
Type::Table( Type::Table(
[ [
("name".into(), Type::String), ("name".into(), Type::String),
("version".into(), Type::String),
("is_running".into(), Type::Bool), ("is_running".into(), Type::Bool),
("pid".into(), Type::Int), ("pid".into(), Type::Int),
("filename".into(), Type::String), ("filename".into(), Type::String),
@ -43,6 +44,7 @@ impl Command for PluginList {
description: "List installed plugins.", description: "List installed plugins.",
result: Some(Value::test_list(vec![Value::test_record(record! { result: Some(Value::test_list(vec![Value::test_record(record! {
"name" => Value::test_string("inc"), "name" => Value::test_string("inc"),
"version" => Value::test_string(env!("CARGO_PKG_VERSION")),
"is_running" => Value::test_bool(true), "is_running" => Value::test_bool(true),
"pid" => Value::test_int(106480), "pid" => Value::test_int(106480),
"filename" => if cfg!(windows) { "filename" => if cfg!(windows) {
@ -98,8 +100,15 @@ impl Command for PluginList {
.map(|s| Value::string(s.to_string_lossy(), head)) .map(|s| Value::string(s.to_string_lossy(), head))
.unwrap_or(Value::nothing(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! { let record = record! {
"name" => Value::string(plugin.identity().name(), head), "name" => Value::string(plugin.identity().name(), head),
"version" => version,
"is_running" => Value::bool(plugin.is_running(), head), "is_running" => Value::bool(plugin.is_running(), head),
"pid" => pid, "pid" => pid,
"filename" => Value::string(plugin.identity().filename().to_string_lossy(), head), "filename" => Value::string(plugin.identity().filename().to_string_lossy(), head),

View File

@ -5,18 +5,18 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-color-confi
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
name = "nu-color-config" name = "nu-color-config"
version = "0.94.3" version = "0.95.1"
[lib] [lib]
bench = false bench = false
[dependencies] [dependencies]
nu-protocol = { path = "../nu-protocol", version = "0.94.3" } nu-protocol = { path = "../nu-protocol", version = "0.95.1" }
nu-engine = { path = "../nu-engine", version = "0.94.3" } nu-engine = { path = "../nu-engine", version = "0.95.1" }
nu-json = { path = "../nu-json", version = "0.94.3" } nu-json = { path = "../nu-json", version = "0.95.1" }
nu-ansi-term = { workspace = true } nu-ansi-term = { workspace = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
[dev-dependencies] [dev-dependencies]
nu-test-support = { path = "../nu-test-support", version = "0.94.3" } nu-test-support = { path = "../nu-test-support", version = "0.95.1" }

View File

@ -20,6 +20,7 @@ pub fn default_shape_color(shape: &str) -> Style {
"shape_flag" => Style::new().fg(Color::Blue).bold(), "shape_flag" => Style::new().fg(Color::Blue).bold(),
"shape_float" => Style::new().fg(Color::Purple).bold(), "shape_float" => Style::new().fg(Color::Purple).bold(),
"shape_garbage" => Style::new().fg(Color::White).on(Color::Red).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_globpattern" => Style::new().fg(Color::Cyan).bold(),
"shape_int" => Style::new().fg(Color::Purple).bold(), "shape_int" => Style::new().fg(Color::Purple).bold(),
"shape_internalcall" => Style::new().fg(Color::Cyan).bold(), "shape_internalcall" => Style::new().fg(Color::Cyan).bold(),

View File

@ -5,7 +5,7 @@ edition = "2021"
license = "MIT" license = "MIT"
name = "nu-command" name = "nu-command"
repository = "https://github.com/nushell/nushell/tree/main/crates/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 # 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 bench = false
[dependencies] [dependencies]
nu-cmd-base = { path = "../nu-cmd-base", version = "0.94.3" } nu-cmd-base = { path = "../nu-cmd-base", version = "0.95.1" }
nu-color-config = { path = "../nu-color-config", version = "0.94.3" } nu-color-config = { path = "../nu-color-config", version = "0.95.1" }
nu-engine = { path = "../nu-engine", version = "0.94.3" } nu-engine = { path = "../nu-engine", version = "0.95.1" }
nu-glob = { path = "../nu-glob", version = "0.94.3" } nu-glob = { path = "../nu-glob", version = "0.95.1" }
nu-json = { path = "../nu-json", version = "0.94.3" } nu-json = { path = "../nu-json", version = "0.95.1" }
nu-parser = { path = "../nu-parser", version = "0.94.3" } nu-parser = { path = "../nu-parser", version = "0.95.1" }
nu-path = { path = "../nu-path", version = "0.94.3" } nu-path = { path = "../nu-path", version = "0.95.1" }
nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.94.3" } nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.95.1" }
nu-protocol = { path = "../nu-protocol", version = "0.94.3" } nu-protocol = { path = "../nu-protocol", version = "0.95.1" }
nu-system = { path = "../nu-system", version = "0.94.3" } nu-system = { path = "../nu-system", version = "0.95.1" }
nu-table = { path = "../nu-table", version = "0.94.3" } nu-table = { path = "../nu-table", version = "0.95.1" }
nu-term-grid = { path = "../nu-term-grid", version = "0.94.3" } nu-term-grid = { path = "../nu-term-grid", version = "0.95.1" }
nu-utils = { path = "../nu-utils", version = "0.94.3" } nu-utils = { path = "../nu-utils", version = "0.95.1" }
nu-ansi-term = { workspace = true } nu-ansi-term = { workspace = true }
nuon = { path = "../nuon", version = "0.94.3" } nuon = { path = "../nuon", version = "0.95.1" }
alphanumeric-sort = { workspace = true } alphanumeric-sort = { workspace = true }
base64 = { workspace = true } base64 = { workspace = true }
@ -136,8 +136,8 @@ sqlite = ["rusqlite"]
trash-support = ["trash"] trash-support = ["trash"]
[dev-dependencies] [dev-dependencies]
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.94.3" } nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.95.1" }
nu-test-support = { path = "../nu-test-support", version = "0.94.3" } nu-test-support = { path = "../nu-test-support", version = "0.95.1" }
dirs-next = { workspace = true } dirs-next = { workspace = true }
mockito = { workspace = true, default-features = false } mockito = { workspace = true, default-features = false }

View File

@ -1,4 +1,5 @@
use nu_engine::{command_prelude::*, get_eval_block, redirect_env}; use nu_engine::{command_prelude::*, get_eval_block, redirect_env};
use nu_protocol::engine::CommandType;
#[derive(Clone)] #[derive(Clone)]
pub struct ExportEnv; pub struct ExportEnv;
@ -23,6 +24,15 @@ impl Command for ExportEnv {
"Run a block and preserve its environment in a current scope." "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( fn run(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,

View File

@ -2,6 +2,7 @@ use nu_engine::{
command_prelude::*, find_in_dirs_env, get_dirs_var_from_call, get_eval_block_with_early_return, command_prelude::*, find_in_dirs_env, get_dirs_var_from_call, get_eval_block_with_early_return,
redirect_env, redirect_env,
}; };
use nu_protocol::engine::CommandType;
use std::path::PathBuf; use std::path::PathBuf;
/// Source a file for environment variables. /// 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." "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( fn run(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,

View File

@ -175,20 +175,32 @@ impl Command for Ls {
}, },
Example { Example {
description: "List files and directories whose name do not contain 'bar'", description: "List files and directories whose name do not contain 'bar'",
example: "ls -s | where name !~ bar", example: "ls | where name !~ bar",
result: None, result: None,
}, },
Example { 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", example: "ls -a ~ | where type == dir",
result: None, result: None,
}, },
Example { Example {
description: 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)", example: "ls -as ~ | where type == dir and modified < ((date now) - 7day)",
result: None, 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 { Example {
description: "List given paths and show directories themselves", description: "List given paths and show directories themselves",
example: "['/path/to/directory' '/path/to/file'] | each {|| ls -D $in } | flatten", example: "['/path/to/directory' '/path/to/file'] | each {|| ls -D $in } | flatten",

View File

@ -69,9 +69,9 @@ impl Command for Find {
result: None, result: None,
}, },
Example { Example {
description: "Search and highlight text for a term in a string", description: "Search and highlight text for a term in a string. Note that regular search is case insensitive",
example: r#"'Cargo.toml' | find toml"#, example: r#"'Cargo.toml' | find cargo"#,
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())), 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 { Example {
description: "Search a number or a file size in a list of numbers", 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<Value> = vec![]; let mut output: Vec<Value> = vec![];
for line in lines { for line in lines {
let line = line?.to_lowercase(); let line = line?;
let lower_val = line.to_lowercase();
for term in &terms { for term in &terms {
if line.contains(term) { if lower_val.contains(term) {
output.push(Value::string( output.push(Value::string(
highlight_search_string( highlight_search_string(
&line, &line,

View File

@ -1,5 +1,6 @@
use nu_engine::{command_prelude::*, ClosureEval}; use nu_engine::{command_prelude::*, ClosureEval};
use nu_protocol::{engine::Closure, TryIntoValue}; use nu_protocol::{engine::Closure, TryIntoValue};
use nu_protocol::engine::{CommandType};
#[derive(Clone)] #[derive(Clone)]
pub struct Where; pub struct Where;
@ -19,6 +20,10 @@ tables, known as "row conditions". On the other hand, reading the condition from
not supported."# not supported."#
} }
fn command_type(&self) -> CommandType {
CommandType::Keyword
}
fn signature(&self) -> nu_protocol::Signature { fn signature(&self) -> nu_protocol::Signature {
Signature::build("where") Signature::build("where")
.input_output_types(vec![ .input_output_types(vec![

View File

@ -39,7 +39,7 @@ fn from_delimited_stream(
.from_reader(input_reader); .from_reader(input_reader);
let headers = if noheaders { let headers = if noheaders {
(1..=reader (0..reader
.headers() .headers()
.map_err(|err| from_csv_error(err, span))? .map_err(|err| from_csv_error(err, span))?
.len()) .len())

View File

@ -52,12 +52,12 @@ impl Command for FromSsv {
Value::test_list( Value::test_list(
vec![ vec![
Value::test_record(record! { Value::test_record(record! {
"column1" => Value::test_string("FOO"), "column0" => Value::test_string("FOO"),
"column2" => Value::test_string("BAR"), "column1" => Value::test_string("BAR"),
}), }),
Value::test_record(record! { Value::test_record(record! {
"column1" => Value::test_string("1"), "column0" => Value::test_string("1"),
"column2" => Value::test_string("2"), "column1" => Value::test_string("2"),
}), }),
], ],
) )
@ -170,7 +170,7 @@ fn parse_aligned_columns<'a>(
let headers: Vec<(String, usize)> = indices let headers: Vec<(String, usize)> = indices
.iter() .iter()
.enumerate() .enumerate()
.map(|(i, position)| (format!("column{}", i + 1), *position)) .map(|(i, position)| (format!("column{}", i), *position))
.collect(); .collect();
construct(ls.iter().map(|s| s.to_owned()), headers) 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 parse_without_headers = |ls: Vec<&str>| {
let num_columns = ls.iter().map(|r| r.len()).max().unwrap_or(0); 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}")) .map(|i| format!("column{i}"))
.collect::<Vec<String>>(); .collect::<Vec<String>>();
collect(headers, ls.into_iter(), separator) collect(headers, ls.into_iter(), separator)
@ -370,9 +370,9 @@ mod tests {
assert_eq!( assert_eq!(
result, result,
vec![ vec![
vec![owned("column1", "a"), owned("column2", "b")], vec![owned("column0", "a"), owned("column1", "b")],
vec![owned("column1", "1"), owned("column2", "2")], vec![owned("column0", "1"), owned("column1", "2")],
vec![owned("column1", "3"), owned("column2", "4")] vec![owned("column0", "3"), owned("column1", "4")]
] ]
); );
} }
@ -484,25 +484,25 @@ mod tests {
result, result,
vec![ vec![
vec![ vec![
owned("column1", "a multi-word value"), owned("column0", "a multi-word value"),
owned("column2", "b"), owned("column1", "b"),
owned("column3", ""),
owned("column4", "d"),
owned("column5", "")
],
vec![
owned("column1", "1"),
owned("column2", ""), owned("column2", ""),
owned("column3", "3-3"), owned("column3", "d"),
owned("column4", "4"), owned("column4", "")
owned("column5", "")
], ],
vec![ vec![
owned("column0", "1"),
owned("column1", ""),
owned("column2", "3-3"),
owned("column3", "4"),
owned("column4", "")
],
vec![
owned("column0", ""),
owned("column1", ""), owned("column1", ""),
owned("column2", ""), owned("column2", ""),
owned("column3", ""), owned("column3", ""),
owned("column4", ""), owned("column4", "last")
owned("column5", "last")
], ],
] ]
); );

View File

@ -1,6 +1,7 @@
use chrono::{Datelike, Local, NaiveDate}; use chrono::{Datelike, Local, NaiveDate};
use nu_color_config::StyleComputer; use nu_color_config::StyleComputer;
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
use nu_protocol::ast::{Expr, Expression};
use std::collections::VecDeque; use std::collections::VecDeque;
@ -14,6 +15,7 @@ struct Arguments {
month_names: bool, month_names: bool,
full_year: Option<Spanned<i64>>, full_year: Option<Spanned<i64>>,
week_start: Option<Spanned<String>>, week_start: Option<Spanned<String>>,
as_table: bool,
} }
impl Command for Cal { impl Command for Cal {
@ -26,6 +28,7 @@ impl Command for Cal {
.switch("year", "Display the year column", Some('y')) .switch("year", "Display the year column", Some('y'))
.switch("quarter", "Display the quarter column", Some('q')) .switch("quarter", "Display the quarter column", Some('q'))
.switch("month", "Display the month column", Some('m')) .switch("month", "Display the month column", Some('m'))
.switch("as-table", "output as a table", Some('t'))
.named( .named(
"full-year", "full-year",
SyntaxShape::Int, SyntaxShape::Int,
@ -43,7 +46,10 @@ impl Command for Cal {
"Display the month names instead of integers", "Display the month names instead of integers",
None, 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 .allow_variants_without_examples(true) // TODO: supply exhaustive examples
.category(Category::Generators) .category(Category::Generators)
} }
@ -75,10 +81,15 @@ impl Command for Cal {
result: None, result: None,
}, },
Example { 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", example: "cal --week-start mo",
result: None, 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")?, quarter: call.has_flag(engine_state, stack, "quarter")?,
full_year: call.get_flag(engine_state, stack, "full-year")?, full_year: call.get_flag(engine_state, stack, "full-year")?,
week_start: call.get_flag(engine_state, stack, "week-start")?, 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); let style_computer = &StyleComputer::from_config(engine_state, stack);
@ -131,7 +143,27 @@ pub fn cal(
style_computer, 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 { fn get_invalid_year_shell_error(head: Span) -> ShellError {

View File

@ -12,13 +12,7 @@ impl Command for Generate {
fn signature(&self) -> Signature { fn signature(&self) -> Signature {
Signature::build("generate") Signature::build("generate")
.input_output_types(vec![ .input_output_types(vec![(Type::Nothing, Type::List(Box::new(Type::Any)))])
(Type::Nothing, Type::List(Box::new(Type::Any))),
(
Type::List(Box::new(Type::Any)),
Type::List(Box::new(Type::Any)),
),
])
.required("initial", SyntaxShape::Any, "Initial value.") .required("initial", SyntaxShape::Any, "Initial value.")
.required( .required(
"closure", "closure",
@ -63,23 +57,10 @@ used as the next argument to the closure, otherwise generation stops.
)), )),
}, },
Example { Example {
example: "generate [0, 1] {|fib| {out: $fib.0, next: [$fib.1, ($fib.0 + $fib.1)]} } | first 10", example:
description: "Generate a stream of fibonacci numbers", "generate [0, 1] {|fib| {out: $fib.0, next: [$fib.1, ($fib.0 + $fib.1)]} }",
result: Some(Value::list( description: "Generate a continuous stream of Fibonacci numbers",
vec![ result: None,
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(),
)),
}, },
] ]
} }

View File

@ -19,6 +19,9 @@ static CHAR_MAP: Lazy<IndexMap<&'static str, String>> = Lazy::new(|| {
// These are some regular characters that either can't be used or // These are some regular characters that either can't be used or
// it's just easier to use them like this. // 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 // This are the "normal" characters section
"newline" => '\n'.to_string(), "newline" => '\n'.to_string(),
"enter" => '\n'.to_string(), "enter" => '\n'.to_string(),

View File

@ -1,16 +1,15 @@
use nu_cmd_base::hook::eval_hook; use nu_cmd_base::hook::eval_hook;
use nu_engine::{command_prelude::*, env_to_strings, get_eval_expression}; use nu_engine::{command_prelude::*, env_to_strings, get_eval_expression};
use nu_path::{dots::expand_ndots, expand_tilde};
use nu_protocol::{ use nu_protocol::{
ast::{Expr, Expression}, ast::Expression, did_you_mean, process::ChildProcess, ByteStream, NuGlob, OutDest,
did_you_mean,
process::ChildProcess,
ByteStream, NuGlob, OutDest,
}; };
use nu_system::ForegroundChild; use nu_system::ForegroundChild;
use nu_utils::IgnoreCaseExt; use nu_utils::IgnoreCaseExt;
use pathdiff::diff_paths; use pathdiff::diff_paths;
use std::{ use std::{
borrow::Cow, borrow::Cow,
ffi::{OsStr, OsString},
io::Write, io::Write,
path::{Path, PathBuf}, path::{Path, PathBuf},
process::Stdio, process::Stdio,
@ -33,8 +32,16 @@ impl Command for External {
fn signature(&self) -> nu_protocol::Signature { fn signature(&self) -> nu_protocol::Signature {
Signature::build(self.name()) Signature::build(self.name())
.input_output_types(vec![(Type::Any, Type::Any)]) .input_output_types(vec![(Type::Any, Type::Any)])
.required("command", SyntaxShape::String, "External command to run.") .required(
.rest("args", SyntaxShape::Any, "Arguments for external command.") "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) .category(Category::System)
} }
@ -47,42 +54,33 @@ impl Command for External {
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let cwd = engine_state.cwd(Some(stack))?; let cwd = engine_state.cwd(Some(stack))?;
// Evaluate the command name in the same way the arguments are evaluated. Since this isn't let name: Value = call.req(engine_state, stack, 0)?;
// a spread, it should return a one-element vec.
let name_expr = call let name_str: Cow<str> = match &name {
.positional_nth(0) Value::Glob { val, .. } => Cow::Borrowed(val),
.ok_or_else(|| ShellError::MissingParameter { Value::String { val, .. } => Cow::Borrowed(val),
param_name: "command".into(), _ => Cow::Owned(name.clone().coerce_into_string()?),
span: call.head, };
})?;
let name = eval_argument(engine_state, stack, name_expr, false)? let expanded_name = match &name {
.pop() // Expand tilde and ndots on the name if it's a bare string / glob (#13000)
.expect("eval_argument returned zero-element vec") Value::Glob { no_expand, .. } if !*no_expand => {
.into_spanned(name_expr.span); expand_ndots_safe(expand_tilde(&*name_str))
}
_ => Path::new(&*name_str).to_owned(),
};
// Find the absolute path to the executable. On Windows, set the // 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 // executable to "cmd.exe" if it's is a CMD internal command. If the
// command is not found, display a helpful error message. // 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") PathBuf::from("cmd.exe")
} else { } 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 // 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 // effect if it's an absolute path already
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?; let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
let Some(executable) = which(&expanded_name, &paths, &cwd) else { let Some(executable) = which(expanded_name, &paths, &cwd) else {
return Err(command_not_found( return Err(command_not_found(&name_str, call.head, engine_state, stack));
&name.item,
call.head,
engine_state,
stack,
));
}; };
executable executable
}; };
@ -101,15 +99,15 @@ impl Command for External {
// Configure args. // Configure args.
let args = eval_arguments_from_call(engine_state, stack, call)?; let args = eval_arguments_from_call(engine_state, stack, call)?;
#[cfg(windows)] #[cfg(windows)]
if is_cmd_internal_command(&name.item) { if is_cmd_internal_command(&name_str) {
use std::os::windows::process::CommandExt; use std::os::windows::process::CommandExt;
// The /D flag disables execution of AutoRun commands from registry. // The /D flag disables execution of AutoRun commands from registry.
// The /C flag followed by a command name instructs CMD to execute // The /C flag followed by a command name instructs CMD to execute
// that command and quit. // that command and quit.
command.args(["/D", "/C", &name.item]); command.args(["/D", "/C", &name_str]);
for arg in &args { for arg in &args {
command.raw_arg(escape_cmd_argument(arg)?.as_ref()); command.raw_arg(escape_cmd_argument(arg)?);
} }
} else { } else {
command.args(args.into_iter().map(|s| s.item)); 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. /// Evaluate all arguments from a call, performing expansions when necessary.
pub fn eval_arguments_from_call( pub fn eval_arguments_from_call(
engine_state: &EngineState, engine_state: &EngineState,
stack: &mut Stack, stack: &mut Stack,
call: &Call, call: &Call,
) -> Result<Vec<Spanned<String>>, ShellError> { ) -> Result<Vec<Spanned<OsString>>, ShellError> {
let ctrlc = &engine_state.ctrlc; let ctrlc = &engine_state.ctrlc;
let cwd = engine_state.cwd(Some(stack))?; let cwd = engine_state.cwd(Some(stack))?;
let mut args: Vec<Spanned<String>> = vec![]; let mut args: Vec<Spanned<OsString>> = vec![];
for (expr, spread) in call.rest_iter(1) { 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)? { for arg in eval_argument(engine_state, stack, expr, spread)? {
let tilde_expanded = expand_tilde(&arg); match arg {
for glob_expanded in expand_glob(&tilde_expanded, &cwd, expr.span, ctrlc)? { // Expand globs passed to run-external
let inner_quotes_removed = remove_inner_quotes(&glob_expanded); Value::Glob { val, no_expand, .. } if !no_expand => args.extend(
args.push(inner_quotes_removed.into_owned().into_spanned(expr.span)); 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) Ok(args)
} }
/// Evaluates an expression, coercing the values to strings. /// Custom `coerce_into_string()`, including globs, since those are often args to `run-external`
/// /// as well
/// Note: The parser currently has a special hack that retains surrounding fn coerce_into_string(val: Value) -> Result<String, ShellError> {
/// quotes for string literals in `Expression`, so that we can decide whether match val {
/// the expression is considered a bare string. The hack doesn't affect string Value::Glob { val, .. } => Ok(val),
/// literals within lists or records. This function will remove the quotes _ => val.coerce_into_string(),
/// before evaluating the expression. }
}
/// Evaluate an argument, returning more than one value if it was a list to be spread.
fn eval_argument( fn eval_argument(
engine_state: &EngineState, engine_state: &EngineState,
stack: &mut Stack, stack: &mut Stack,
expr: &Expression, expr: &Expression,
spread: bool, spread: bool,
) -> Result<Vec<String>, ShellError> { ) -> Result<Vec<Value>, 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());
}
let eval = get_eval_expression(engine_state); let eval = get_eval_expression(engine_state);
match eval(engine_state, stack, &expr)? { match eval(engine_state, stack, expr)? {
Value::List { vals, .. } => { Value::List { vals, .. } => {
if spread { if spread {
vals.into_iter() Ok(vals)
.map(|val| val.coerce_into_string())
.collect()
} else { } else {
Err(ShellError::CannotPassListToExternal { Err(ShellError::CannotPassListToExternal {
arg: String::from_utf8_lossy(engine_state.get_span_contents(expr.span)).into(), arg: String::from_utf8_lossy(engine_state.get_span_contents(expr.span)).into(),
@ -298,31 +274,12 @@ fn eval_argument(
if spread { if spread {
Err(ShellError::CannotSpreadAsList { span: expr.span }) Err(ShellError::CannotSpreadAsList { span: expr.span })
} else { } 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 /// 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. /// is not a valid glob, then this returns the original string as the expansion result.
/// ///
@ -333,19 +290,21 @@ fn expand_glob(
cwd: &Path, cwd: &Path,
span: Span, span: Span,
interrupt: &Option<Arc<AtomicBool>>, interrupt: &Option<Arc<AtomicBool>>,
) -> Result<Vec<String>, ShellError> { ) -> Result<Vec<OsString>, ShellError> {
const GLOB_CHARS: &[char] = &['*', '?', '[']; 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) { 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 // We must use `nu_engine::glob_from` here, in order to ensure we get paths from the correct
// dir // dir
let glob = NuGlob::Expand(arg.to_owned()).into_spanned(span); let glob = NuGlob::Expand(arg.to_owned()).into_spanned(span);
if let Ok((prefix, matches)) = nu_engine::glob_from(&glob, cwd, span, None) { if let Ok((prefix, matches)) = nu_engine::glob_from(&glob, cwd, span, None) {
let mut result = vec![]; let mut result: Vec<OsString> = vec![];
for m in matches { for m in matches {
if nu_utils::ctrl_c::was_pressed(interrupt) { if nu_utils::ctrl_c::was_pressed(interrupt) {
@ -353,7 +312,7 @@ fn expand_glob(
} }
if let Ok(arg) = m { if let Ok(arg) = m {
let arg = resolve_globbed_path_to_cwd_relative(arg, prefix.as_ref(), cwd); 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 { } else {
result.push(arg.into()); 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 /// Write `PipelineData` into `writer`. If `PipelineData` is not binary, it is
/// first rendered using the `table` command. /// 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 /// Note: the `which.rs` crate always uses PATHEXT from the environment. As
/// such, changing PATHEXT within Nushell doesn't work without updating the /// such, changing PATHEXT within Nushell doesn't work without updating the
/// actual environment of the Nushell process. /// actual environment of the Nushell process.
pub fn which(name: &str, paths: &str, cwd: &Path) -> Option<PathBuf> { pub fn which(name: impl AsRef<OsStr>, paths: &str, cwd: &Path) -> Option<PathBuf> {
#[cfg(windows)] #[cfg(windows)]
let paths = format!("{};{}", cwd.display(), paths); let paths = format!("{};{}", cwd.display(), paths);
which::which_in(name, Some(paths), cwd).ok() 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. /// Returns true if a string contains CMD special characters.
#[cfg(windows)] fn has_cmd_special_character(s: impl AsRef<[u8]>) -> bool {
fn has_cmd_special_character(s: &str) -> bool { s.as_ref()
const SPECIAL_CHARS: &[char] = &['<', '>', '&', '|', '^']; .iter()
SPECIAL_CHARS.iter().any(|c| s.contains(*c)) .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()`. /// Escape an argument for CMD internal commands. The result can be safely passed to `raw_arg()`.
#[cfg(windows)] #[cfg_attr(not(windows), allow(dead_code))]
fn escape_cmd_argument(arg: &Spanned<String>) -> Result<Cow<'_, str>, ShellError> { fn escape_cmd_argument(arg: &Spanned<OsString>) -> Result<Cow<'_, OsStr>, ShellError> {
let Spanned { item: arg, span } = arg; 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 // \r and \n trunacte the rest of the arguments and % can expand environment variables
Err(ShellError::ExternalCommand { Err(ShellError::ExternalCommand {
label: label:
@ -612,12 +555,12 @@ fn escape_cmd_argument(arg: &Spanned<String>) -> Result<Cow<'_, str>, ShellError
help: "some characters currently cannot be securely escaped".into(), help: "some characters currently cannot be securely escaped".into(),
span: *span, span: *span,
}) })
} else if arg.contains('"') { } else if bytes.contains(&b'"') {
// If `arg` is already quoted by double quotes, confirm there's no // If `arg` is already quoted by double quotes, confirm there's no
// embedded double quotes, then leave it as is. // embedded double quotes, then leave it as is.
if arg.chars().filter(|c| *c == '"').count() == 2 if bytes.iter().filter(|b| **b == b'"').count() == 2
&& arg.starts_with('"') && bytes.starts_with(b"\"")
&& arg.ends_with('"') && bytes.ends_with(b"\"")
{ {
Ok(Cow::Borrowed(arg)) Ok(Cow::Borrowed(arg))
} else { } else {
@ -628,76 +571,39 @@ fn escape_cmd_argument(arg: &Spanned<String>) -> Result<Cow<'_, str>, ShellError
span: *span, 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. // 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 { } else {
// FIXME?: what if `arg.is_empty()`? // FIXME?: what if `arg.is_empty()`?
Ok(Cow::Borrowed(arg)) 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<Path>) -> 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)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use nu_protocol::ast::ListItem;
use nu_test_support::{fs::Stub, playground::Playground}; 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<Vec<String>, 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] #[test]
fn test_expand_glob() { fn test_expand_glob() {
Playground::setup("test_expand_glob", |dirs, play| { Playground::setup("test_expand_glob", |dirs, play| {
@ -727,40 +633,14 @@ mod test {
let actual = expand_glob("[*.txt", cwd, Span::unknown(), &None).unwrap(); let actual = expand_glob("[*.txt", cwd, Span::unknown(), &None).unwrap();
let expected = &["[*.txt"]; let expected = &["[*.txt"];
assert_eq!(actual, expected); 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<OsString> = 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] #[test]
fn test_write_pipeline_data() { fn test_write_pipeline_data() {
let engine_state = EngineState::new(); let engine_state = EngineState::new();

View File

@ -170,7 +170,10 @@ impl Command for Table {
}), }),
Value::test_record(record! { Value::test_record(record! {
"a" => Value::test_int(3), "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! { Value::test_record(record! {
"a" => Value::test_int(3), "a" => Value::test_int(3),
"b" => Value::test_int(4), "b" => Value::test_list(vec![
Value::test_int(4),
Value::test_int(4),
])
}), }),
])), ])),
}, },

View File

@ -1,8 +1,9 @@
use nu_test_support::{nu, pipeline}; use nu_test_support::{nu, pipeline};
// Tests against table/structured data
#[test] #[test]
fn cal_full_year() { 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 = let first_week_2010_json =
r#"{"year":2010,"su":null,"mo":null,"tu":null,"we":null,"th":null,"fr":1,"sa":2}"#; 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() { fn cal_february_2020_leap_year() {
let actual = nu!(pipeline( let actual = nu!(pipeline(
r#" 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() { fn cal_fr_the_thirteenths_in_2015() {
let actual = nu!(pipeline( let actual = nu!(pipeline(
r#" 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() { fn cal_rows_in_2020() {
let actual = nu!(pipeline( let actual = nu!(pipeline(
r#" 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() { fn cal_week_day_start_mo() {
let actual = nu!(pipeline( let actual = nu!(pipeline(
r#" 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() { fn cal_sees_pipeline_year() {
let actual = nu!(pipeline( let actual = nu!(pipeline(
r#" 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]"); 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");
}

View File

@ -4,8 +4,9 @@ use nu_test_support::nu;
fn error_label_works() { fn error_label_works() {
let actual = nu!("error make {msg:foo label:{text:unseen}}"); let actual = nu!("error make {msg:foo label:{text:unseen}}");
assert!(actual.err.contains("unseen")); assert!(actual
assert!(actual.err.contains("╰──")); .err
.contains("label at line 1, columns 1 to 10: unseen"));
} }
#[test] #[test]

View File

@ -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\"]"); 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] #[test]
fn find_with_list_search_with_number() { fn find_with_list_search_with_number() {
let actual = nu!("[1 2 3 4 5] | find 3 | get 0"); let actual = nu!("[1 2 3 4 5] | find 3 | get 0");

View File

@ -2,7 +2,7 @@ use nu_test_support::nu;
#[test] #[test]
fn length_columns_in_cal_table() { 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"); assert_eq!(actual.out, "7");
} }

View File

@ -1,4 +1,3 @@
#[cfg(not(windows))]
use nu_test_support::fs::Stub::EmptyFile; use nu_test_support::fs::Stub::EmptyFile;
use nu_test_support::playground::Playground; use nu_test_support::playground::Playground;
use nu_test_support::{nu, pipeline}; use nu_test_support::{nu, pipeline};
@ -17,7 +16,6 @@ fn better_empty_redirection() {
assert!(!actual.out.contains('2')); assert!(!actual.out.contains('2'));
} }
#[cfg(not(windows))]
#[test] #[test]
fn explicit_glob() { fn explicit_glob() {
Playground::setup("external with explicit glob", |dirs, sandbox| { Playground::setup("external with explicit glob", |dirs, sandbox| {
@ -30,15 +28,15 @@ fn explicit_glob() {
let actual = nu!( let actual = nu!(
cwd: dirs.test(), pipeline( cwd: dirs.test(), pipeline(
r#" 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] #[test]
fn bare_word_expand_path_glob() { fn bare_word_expand_path_glob() {
Playground::setup("bare word should do the expansion", |dirs, sandbox| { Playground::setup("bare word should do the expansion", |dirs, sandbox| {
@ -51,7 +49,7 @@ fn bare_word_expand_path_glob() {
let actual = nu!( let actual = nu!(
cwd: dirs.test(), pipeline( cwd: dirs.test(), pipeline(
" "
^ls *.txt ^nu --testbin cococo *.txt
" "
)); ));
@ -60,7 +58,6 @@ fn bare_word_expand_path_glob() {
}) })
} }
#[cfg(not(windows))]
#[test] #[test]
fn backtick_expand_path_glob() { fn backtick_expand_path_glob() {
Playground::setup("backtick should do the expansion", |dirs, sandbox| { Playground::setup("backtick should do the expansion", |dirs, sandbox| {
@ -73,7 +70,7 @@ fn backtick_expand_path_glob() {
let actual = nu!( let actual = nu!(
cwd: dirs.test(), pipeline( cwd: dirs.test(), pipeline(
r#" r#"
^ls `*.txt` ^nu --testbin cococo `*.txt`
"# "#
)); ));
@ -82,7 +79,6 @@ fn backtick_expand_path_glob() {
}) })
} }
#[cfg(not(windows))]
#[test] #[test]
fn single_quote_does_not_expand_path_glob() { fn single_quote_does_not_expand_path_glob() {
Playground::setup("single quote do not run the expansion", |dirs, sandbox| { 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!( let actual = nu!(
cwd: dirs.test(), pipeline( cwd: dirs.test(), pipeline(
r#" 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] #[test]
fn double_quote_does_not_expand_path_glob() { fn double_quote_does_not_expand_path_glob() {
Playground::setup("double quote do not run the expansion", |dirs, sandbox| { 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!( let actual = nu!(
cwd: dirs.test(), pipeline( cwd: dirs.test(), pipeline(
r#" 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] #[test]
fn failed_command_with_semicolon_will_not_execute_following_cmds() { fn failed_command_with_semicolon_will_not_execute_following_cmds() {
Playground::setup("external failed command with semicolon", |dirs, _| { Playground::setup("external failed command with semicolon", |dirs, _| {
let actual = nu!( let actual = nu!(
cwd: dirs.test(), pipeline( 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))] #[cfg(not(windows))]
#[test] #[test]
fn external_arg_with_long_flag_value_quoted() { fn external_arg_with_option_like_embedded_quotes() {
Playground::setup("external failed command with semicolon", |dirs, _| { // 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!( let actual = nu!(
cwd: dirs.test(), pipeline( cwd: dirs.test(), pipeline(
r#" r#"
^echo --foo='bar' ^echo --foo='bar' -foo='bar'
"# "#
)); ));
assert_eq!(actual.out, "--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#"
^nu --testbin cococo foo=(2 + 2) $"foo=(2 + 2)" foo=$"(2 + 2)"
"#
));
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] #[test]
fn external_command_not_expand_tilde_with_quotes() { fn external_command_not_expand_tilde_with_quotes() {
Playground::setup( 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)] #[cfg(windows)]
#[test] #[test]
fn can_run_batch_files() { fn can_run_batch_files() {

View File

@ -284,7 +284,7 @@ fn from_csv_text_skipping_headers_to_table() {
r#" r#"
open los_tres_amigos.txt open los_tres_amigos.txt
| from csv --noheaders | from csv --noheaders
| get column3 | get column2
| length | length
"# "#
)); ));

View File

@ -74,7 +74,7 @@ fn from_ssv_text_treating_first_line_as_data_with_flag() {
open oc_get_svc.txt open oc_get_svc.txt
| from ssv --noheaders -a | from ssv --noheaders -a
| first | 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 open oc_get_svc.txt
| from ssv --noheaders | from ssv --noheaders
| first | first
| get column1 | get column0
"# "#
)); ));

View File

@ -207,7 +207,7 @@ fn from_tsv_text_skipping_headers_to_table() {
r#" r#"
open los_tres_amigos.txt open los_tres_amigos.txt
| from tsv --noheaders | from tsv --noheaders
| get column3 | get column2
| length | length
"# "#
)); ));

View File

@ -5,7 +5,7 @@ edition = "2021"
license = "MIT" license = "MIT"
name = "nu-derive-value" name = "nu-derive-value"
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-derive-value" repository = "https://github.com/nushell/nushell/tree/main/crates/nu-derive-value"
version = "0.94.3" version = "0.95.1"
[lib] [lib]
proc-macro = true proc-macro = true

View File

@ -3,6 +3,7 @@ use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens}; use quote::{quote, ToTokens};
use syn::{ use syn::{
spanned::Spanned, Attribute, Data, DataEnum, DataStruct, DeriveInput, Fields, Generics, Ident, spanned::Spanned, Attribute, Data, DataEnum, DataStruct, DeriveInput, Fields, Generics, Ident,
Type,
}; };
use crate::attributes::{self, ContainerAttributes}; use crate::attributes::{self, ContainerAttributes};
@ -116,15 +117,11 @@ fn derive_struct_from_value(
/// src_span: span /// src_span: span
/// })?, /// })?,
/// )?, /// )?,
/// favorite_toy: <Option<String> as nu_protocol::FromValue>::from_value( /// favorite_toy: record
/// record
/// .remove("favorite_toy") /// .remove("favorite_toy")
/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn { /// .map(|v| <#ty as nu_protocol::FromValue>::from_value(v))
/// col_name: std::string::ToString::to_string("favorite_toy"), /// .transpose()?
/// span: std::option::Option::None, /// .flatten(),
/// src_span: span
/// })?,
/// )?,
/// }) /// })
/// } /// }
/// } /// }
@ -480,11 +477,19 @@ fn parse_value_via_fields(fields: &Fields, self_ident: impl ToTokens) -> TokenSt
match fields { match fields {
Fields::Named(fields) => { Fields::Named(fields) => {
let fields = fields.named.iter().map(|field| { 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 = field.ident.as_ref().expect("named has idents");
let ident_s = ident.to_string(); let ident_s = ident.to_string();
let ty = &field.ty; let ty = &field.ty;
quote! { match type_is_option(ty) {
true => quote! {
#ident: record
.remove(#ident_s)
.map(|v| <#ty as nu_protocol::FromValue>::from_value(v))
.transpose()?
.flatten()
},
false => quote! {
#ident: <#ty as nu_protocol::FromValue>::from_value( #ident: <#ty as nu_protocol::FromValue>::from_value(
record record
.remove(#ident_s) .remove(#ident_s)
@ -494,6 +499,7 @@ fn parse_value_via_fields(fields: &Fields, self_ident: impl ToTokens) -> TokenSt
src_span: span src_span: span
})?, })?,
)? )?
},
} }
}); });
quote! { 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)
}

View File

@ -5,16 +5,16 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-engine"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
name = "nu-engine" name = "nu-engine"
version = "0.94.3" version = "0.95.1"
[lib] [lib]
bench = false bench = false
[dependencies] [dependencies]
nu-protocol = { path = "../nu-protocol", features = ["plugin"], version = "0.94.3" } nu-protocol = { path = "../nu-protocol", features = ["plugin"], version = "0.95.1" }
nu-path = { path = "../nu-path", version = "0.94.3" } nu-path = { path = "../nu-path", version = "0.95.1" }
nu-glob = { path = "../nu-glob", version = "0.94.3" } nu-glob = { path = "../nu-glob", version = "0.95.1" }
nu-utils = { path = "../nu-utils", version = "0.94.3" } nu-utils = { path = "../nu-utils", version = "0.95.1" }
[features] [features]
plugin = [] plugin = []

View File

@ -3,8 +3,8 @@ use nu_protocol::{
ast::{Argument, Call, Expr, Expression, RecordItem}, ast::{Argument, Call, Expr, Expression, RecordItem},
debugger::WithoutDebug, debugger::WithoutDebug,
engine::{Command, EngineState, Stack, UNKNOWN_SPAN_ID}, engine::{Command, EngineState, Stack, UNKNOWN_SPAN_ID},
record, Category, Example, IntoPipelineData, PipelineData, Signature, Span, SpanId, record, Category, Example, IntoPipelineData, PipelineData, Signature, Span, SpanId, Spanned,
SyntaxShape, TryIntoValue, Type, Value, SyntaxShape, Type, Value, IntoValue, TryIntoValue
}; };
use std::{collections::HashMap, fmt::Write}; use std::{collections::HashMap, fmt::Write};
@ -296,6 +296,28 @@ fn get_documentation(
} }
if let Some(result) = &example.result { 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 let table = engine_state
.find_decl("table".as_bytes(), &[]) .find_decl("table".as_bytes(), &[])
.and_then(|decl_id| { .and_then(|decl_id| {
@ -304,7 +326,7 @@ fn get_documentation(
.run( .run(
engine_state, engine_state,
stack, stack,
&Call::new(Span::new(0, 0)), &table_call,
PipelineData::Value(result.clone(), None), PipelineData::Value(result.clone(), None),
) )
.ok() .ok()

View File

@ -5,21 +5,21 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-explore"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
name = "nu-explore" name = "nu-explore"
version = "0.94.3" version = "0.95.1"
[lib] [lib]
bench = false bench = false
[dependencies] [dependencies]
nu-protocol = { path = "../nu-protocol", version = "0.94.3" } nu-protocol = { path = "../nu-protocol", version = "0.95.1" }
nu-parser = { path = "../nu-parser", version = "0.94.3" } nu-parser = { path = "../nu-parser", version = "0.95.1" }
nu-color-config = { path = "../nu-color-config", version = "0.94.3" } nu-color-config = { path = "../nu-color-config", version = "0.95.1" }
nu-engine = { path = "../nu-engine", version = "0.94.3" } nu-engine = { path = "../nu-engine", version = "0.95.1" }
nu-table = { path = "../nu-table", version = "0.94.3" } nu-table = { path = "../nu-table", version = "0.95.1" }
nu-json = { path = "../nu-json", version = "0.94.3" } nu-json = { path = "../nu-json", version = "0.95.1" }
nu-utils = { path = "../nu-utils", version = "0.94.3" } nu-utils = { path = "../nu-utils", version = "0.95.1" }
nu-ansi-term = { workspace = true } 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 } anyhow = { workspace = true }
log = { workspace = true } log = { workspace = true }

View File

@ -131,7 +131,7 @@ impl RecordView {
Orientation::Left => (column, row), 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 // actually must never happen; unless cursor works incorrectly
// if being sure about cursor it can be deleted; // if being sure about cursor it can be deleted;
return Value::nothing(Span::unknown()); 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 /// scroll to the end of the data
fn tail_data(state: &mut RecordView, page_size: usize) { fn tail_data(state: &mut RecordView, page_size: usize) {
let layer = state.get_layer_last_mut(); 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 { if count_rows > page_size {
layer layer
.cursor .cursor
@ -722,10 +722,24 @@ fn get_percentage(value: usize, max: usize) -> usize {
} }
fn transpose_table(layer: &mut RecordLayer) { 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_rows = layer.record_values.len();
let count_columns = layer.column_names.len(); let count_columns = layer.column_names.len();
if layer.was_transposed { if let Some(data) = &mut layer.record_text {
pop_first_column(data);
*data = _transpose_table(data, count_rows, count_columns - 1);
}
let headers = pop_first_column(&mut layer.record_values); let headers = pop_first_column(&mut layer.record_values);
let headers = headers let headers = headers
.into_iter() .into_iter()
@ -739,26 +753,35 @@ fn transpose_table(layer: &mut RecordLayer) {
layer.record_values = data; layer.record_values = data;
layer.column_names = headers; layer.column_names = headers;
}
return; 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); let mut data = _transpose_table(&layer.record_values, count_rows, count_columns);
for (column, column_name) in layer.column_names.iter().enumerate() { for (column, column_name) in layer.column_names.iter().enumerate() {
let value = Value::string(column_name, NuSpan::unknown()); let value = Value::string(column_name, NuSpan::unknown());
data[column].insert(0, value); data[column].insert(0, value);
} }
layer.record_values = data; layer.record_values = data;
layer.column_names = (1..count_rows + 1 + 1).map(|i| i.to_string()).collect(); 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<Value>]) -> Vec<Value> { fn pop_first_column<T>(values: &mut [Vec<T>]) -> Vec<T>
let mut data = vec![Value::default(); values.len()]; where
T: Default + Clone,
{
let mut data = vec![T::default(); values.len()];
for (row, values) in values.iter_mut().enumerate() { for (row, values) in values.iter_mut().enumerate() {
data[row] = values.remove(0); data[row] = values.remove(0);
} }
@ -766,12 +789,11 @@ fn pop_first_column(values: &mut [Vec<Value>]) -> Vec<Value> {
data data
} }
fn _transpose_table( fn _transpose_table<T>(values: &[Vec<T>], count_rows: usize, count_columns: usize) -> Vec<Vec<T>>
values: &[Vec<Value>], where
count_rows: usize, T: Clone + Default,
count_columns: usize, {
) -> Vec<Vec<Value>> { let mut data = vec![vec![T::default(); count_rows]; count_columns];
let mut data = vec![vec![Value::default(); count_rows]; count_columns];
for (row, values) in values.iter().enumerate() { for (row, values) in values.iter().enumerate() {
for (column, value) in values.iter().enumerate() { for (column, value) in values.iter().enumerate() {
data[column][row].clone_from(value); data[column][row].clone_from(value);

View File

@ -88,6 +88,7 @@ impl StatefulWidget for TableWidget<'_> {
// todo: refactoring these to methods as they have quite a bit in common. // todo: refactoring these to methods as they have quite a bit in common.
impl<'a> TableWidget<'a> { 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) { 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_l = self.config.column_padding_left as u16;
let padding_r = self.config.column_padding_right as u16; let padding_r = self.config.column_padding_right as u16;
@ -130,25 +131,16 @@ impl<'a> TableWidget<'a> {
} }
if show_index { if show_index {
let area = Rect::new(width, data_y, area.width, data_height);
width += render_index( width += render_index(
buf, buf,
area, Rect::new(width, data_y, area.width, data_height),
self.style_computer, self.style_computer,
self.index_row, self.index_row,
padding_l, padding_l,
padding_r, padding_r,
); );
width += render_vertical_line_with_split( width += render_split_line(buf, width, area.y, area.height, show_head, separator_s);
buf,
width,
data_y,
data_height,
show_head,
false,
separator_s,
);
} }
// if there is more data than we can show, add an ellipsis to the column headers to hint at that // 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() { 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 mut column = create_column(data, col);
let column_width = calculate_column_width(&column); let column_width = calculate_column_width(&column);
@ -200,6 +197,7 @@ impl<'a> TableWidget<'a> {
} }
let head_iter = [(&head, head_style)].into_iter(); 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; let mut w = width;
w += render_space(buf, w, head_y, 1, padding_l); w += render_space(buf, w, head_y, 1, padding_l);
w += render_column(buf, w, head_y, use_space, head_iter); 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); 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_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); width += render_space(buf, width, data_y, data_height, padding_r);
for (row, (text, _)) in column.iter().enumerate() { for (row, (text, _)) in column.iter().enumerate() {
@ -235,15 +233,7 @@ impl<'a> TableWidget<'a> {
} }
if width < area.width { if width < area.width {
width += render_vertical_line_with_split( width += render_split_line(buf, width, area.y, area.height, show_head, separator_s);
buf,
width,
data_y,
data_height,
show_head,
false,
separator_s,
);
} }
let rest = area.width.saturating_sub(width); 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) { fn render_table_vertical(self, area: Rect, buf: &mut Buffer, state: &mut TableWidgetState) {
if area.width == 0 || area.height == 0 { if area.width == 0 || area.height == 0 {
return; return;
@ -353,6 +344,9 @@ impl<'a> TableWidget<'a> {
state.count_rows = columns.len(); state.count_rows = columns.len();
state.count_columns = 0; 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() { for col in self.index_column..self.data.len() {
let mut column = let mut column =
self.data[col][self.index_row..self.index_row + columns.len()].to_vec(); self.data[col][self.index_row..self.index_row + columns.len()].to_vec();
@ -361,6 +355,13 @@ impl<'a> TableWidget<'a> {
break; 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 column_width = column_width as u16;
let available = area.width - left_w; let available = area.width - left_w;
let is_last = col + 1 == self.data.len(); let is_last = col + 1 == self.data.len();
@ -555,6 +556,51 @@ fn render_index(
width 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( fn render_vertical_line_with_split(
buf: &mut Buffer, buf: &mut Buffer,
x: u16, 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); 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 { fn calculate_column_width(column: &[NuText]) -> usize {
column column
.iter() .iter()

View File

@ -1,6 +1,6 @@
[package] [package]
name = "nu-glob" name = "nu-glob"
version = "0.94.3" version = "0.95.1"
authors = ["The Nushell Project Developers", "The Rust Project Developers"] authors = ["The Nushell Project Developers", "The Rust Project Developers"]
license = "MIT/Apache-2.0" license = "MIT/Apache-2.0"
description = """ description = """

View File

@ -5,7 +5,7 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-json"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
name = "nu-json" 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 # 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 } serde_json = { workspace = true }
[dev-dependencies] [dev-dependencies]
# nu-path = { path="../nu-path", version = "0.94.3" } # nu-path = { path="../nu-path", version = "0.95.1" }
# serde_json = "1.0" # serde_json = "1.0"

View File

@ -3,14 +3,14 @@ authors = ["The Nushell Project Developers"]
description = "Nushell's integrated LSP server" description = "Nushell's integrated LSP server"
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-lsp" repository = "https://github.com/nushell/nushell/tree/main/crates/nu-lsp"
name = "nu-lsp" name = "nu-lsp"
version = "0.94.3" version = "0.95.1"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
[dependencies] [dependencies]
nu-cli = { path = "../nu-cli", version = "0.94.3" } nu-cli = { path = "../nu-cli", version = "0.95.1" }
nu-parser = { path = "../nu-parser", version = "0.94.3" } nu-parser = { path = "../nu-parser", version = "0.95.1" }
nu-protocol = { path = "../nu-protocol", version = "0.94.3" } nu-protocol = { path = "../nu-protocol", version = "0.95.1" }
reedline = { workspace = true } reedline = { workspace = true }
@ -23,8 +23,8 @@ serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
[dev-dependencies] [dev-dependencies]
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.94.3" } nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.95.1" }
nu-command = { path = "../nu-command", version = "0.94.3" } nu-command = { path = "../nu-command", version = "0.95.1" }
nu-test-support = { path = "../nu-test-support", version = "0.94.3" } nu-test-support = { path = "../nu-test-support", version = "0.95.1" }
assert-json-diff = "2.0" assert-json-diff = "2.0"

View File

@ -5,17 +5,17 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-parser"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
name = "nu-parser" name = "nu-parser"
version = "0.94.3" version = "0.95.1"
exclude = ["/fuzz"] exclude = ["/fuzz"]
[lib] [lib]
bench = false bench = false
[dependencies] [dependencies]
nu-engine = { path = "../nu-engine", version = "0.94.3" } nu-engine = { path = "../nu-engine", version = "0.95.1" }
nu-path = { path = "../nu-path", version = "0.94.3" } nu-path = { path = "../nu-path", version = "0.95.1" }
nu-plugin-engine = { path = "../nu-plugin-engine", optional = true, version = "0.94.3" } nu-plugin-engine = { path = "../nu-plugin-engine", optional = true, version = "0.95.1" }
nu-protocol = { path = "../nu-protocol", version = "0.94.3" } nu-protocol = { path = "../nu-protocol", version = "0.95.1" }
bytesize = { workspace = true } bytesize = { workspace = true }
chrono = { default-features = false, features = ['std'], workspace = true } chrono = { default-features = false, features = ['std'], workspace = true }

View File

@ -26,6 +26,7 @@ pub enum FlatShape {
Flag, Flag,
Float, Float,
Garbage, Garbage,
GlobInterpolation,
GlobPattern, GlobPattern,
Int, Int,
InternalCall(DeclId), InternalCall(DeclId),
@ -67,6 +68,7 @@ impl FlatShape {
FlatShape::Flag => "shape_flag", FlatShape::Flag => "shape_flag",
FlatShape::Float => "shape_float", FlatShape::Float => "shape_float",
FlatShape::Garbage => "shape_garbage", FlatShape::Garbage => "shape_garbage",
FlatShape::GlobInterpolation => "shape_glob_interpolation",
FlatShape::GlobPattern => "shape_globpattern", FlatShape::GlobPattern => "shape_globpattern",
FlatShape::Int => "shape_int", FlatShape::Int => "shape_int",
FlatShape::InternalCall(_) => "shape_internalcall", FlatShape::InternalCall(_) => "shape_internalcall",
@ -277,7 +279,7 @@ fn flatten_expression_into(
output[arg_start..].sort(); output[arg_start..].sort();
} }
Expr::ExternalCall(head, args) => { Expr::ExternalCall(head, args) => {
if let Expr::String(..) = &head.expr { if let Expr::String(..) | Expr::GlobPattern(..) = &head.expr {
output.push((head.span, FlatShape::External)); output.push((head.span, FlatShape::External));
} else { } else {
flatten_expression_into(working_set, head, output); flatten_expression_into(working_set, head, output);
@ -286,7 +288,7 @@ fn flatten_expression_into(
for arg in args.as_ref() { for arg in args.as_ref() {
match arg { match arg {
ExternalArgument::Regular(expr) => { ExternalArgument::Regular(expr) => {
if let Expr::String(..) = &expr.expr { if let Expr::String(..) | Expr::GlobPattern(..) = &expr.expr {
output.push((expr.span, FlatShape::ExternalArg)); output.push((expr.span, FlatShape::ExternalArg));
} else { } else {
flatten_expression_into(working_set, expr, output); flatten_expression_into(working_set, expr, output);
@ -431,6 +433,25 @@ fn flatten_expression_into(
} }
output.extend(flattened); 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) => { Expr::Record(list) => {
let outer_span = expr.span; let outer_span = expr.span;
let mut last_end = outer_span.start; let mut last_end = outer_span.start;

View File

@ -8,7 +8,6 @@ mod parse_keywords;
mod parse_patterns; mod parse_patterns;
mod parse_shape_specs; mod parse_shape_specs;
mod parser; mod parser;
mod parser_path;
mod type_check; mod type_check;
pub use deparse::{escape_for_script_arg, escape_quote_string}; pub use deparse::{escape_for_script_arg, escape_quote_string};
@ -18,8 +17,8 @@ pub use flatten::{
pub use known_external::KnownExternal; pub use known_external::KnownExternal;
pub use lex::{lex, lex_signature, Token, TokenContents}; pub use lex::{lex, lex_signature, Token, TokenContents};
pub use lite_parser::{lite_parse, LiteBlock, LiteCommand}; pub use lite_parser::{lite_parse, LiteBlock, LiteCommand};
pub use nu_protocol::parser_path::*;
pub use parse_keywords::*; pub use parse_keywords::*;
pub use parser_path::*;
pub use parser::{ pub use parser::{
is_math_expression_like, parse, parse_block, parse_expression, parse_external_call, is_math_expression_like, parse, parse_block, parse_expression, parse_external_call,

View File

@ -2,7 +2,6 @@ use crate::{
exportable::Exportable, exportable::Exportable,
parse_block, parse_block,
parser::{parse_redirection, redirecting_builtin_error}, parser::{parse_redirection, redirecting_builtin_error},
parser_path::ParserPath,
type_check::{check_block_input_output, type_compatible}, type_check::{check_block_input_output, type_compatible},
}; };
use itertools::Itertools; use itertools::Itertools;
@ -15,6 +14,7 @@ use nu_protocol::{
}, },
engine::{StateWorkingSet, DEFAULT_OVERLAY_NAME}, engine::{StateWorkingSet, DEFAULT_OVERLAY_NAME},
eval_const::eval_constant, eval_const::eval_constant,
parser_path::ParserPath,
Alias, BlockId, DeclId, Module, ModuleId, ParseError, PositionalArg, ResolvedImportPattern, Alias, BlockId, DeclId, Module, ModuleId, ParseError, PositionalArg, ResolvedImportPattern,
Span, Spanned, SyntaxShape, Type, Value, VarId, Span, Spanned, SyntaxShape, Type, Value, VarId,
}; };
@ -42,32 +42,44 @@ use crate::{
}; };
/// These parser keywords can be aliased /// 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"]; pub const RESERVED_VARIABLE_NAMES: [&str; 3] = ["in", "nu", "env"];
/// These parser keywords cannot be aliased (either not possible, or support not yet added) /// These parser keywords cannot be aliased (either not possible, or support not yet added)
pub const UNALIASABLE_PARSER_KEYWORDS: &[&[u8]] = &[ pub const UNALIASABLE_PARSER_KEYWORDS: &[&[u8]] = &[
b"export",
b"def",
b"export def",
b"for",
b"extern",
b"export extern",
b"alias", b"alias",
b"export alias", b"const",
b"export-env", b"def",
b"extern",
b"module", b"module",
b"use", b"use",
b"export",
b"export alias",
b"export const",
b"export def",
b"export extern",
b"export module",
b"export use", b"export use",
b"hide", b"for",
// b"overlay", b"loop",
// b"overlay hide", b"while",
// b"overlay new", b"return",
// b"overlay use", b"break",
b"continue",
b"let", b"let",
b"const",
b"mut", b"mut",
b"hide",
b"export-env",
b"source-env",
b"source", b"source",
b"where", b"where",
b"register", b"register",
@ -1192,7 +1204,7 @@ pub fn parse_export_in_block(
"export alias" => parse_alias(working_set, lite_command, None), "export alias" => parse_alias(working_set, lite_command, None),
"export def" => parse_def(working_set, lite_command, None).0, "export def" => parse_def(working_set, lite_command, None).0,
"export const" => parse_const(working_set, &lite_command.parts[1..]), "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 module" => parse_module(working_set, lite_command, None).0,
"export extern" => parse_extern(working_set, lite_command, None), "export extern" => parse_extern(working_set, lite_command, None),
_ => { _ => {
@ -1211,6 +1223,7 @@ pub fn parse_export_in_module(
working_set: &mut StateWorkingSet, working_set: &mut StateWorkingSet,
lite_command: &LiteCommand, lite_command: &LiteCommand,
module_name: &[u8], module_name: &[u8],
parent_module: &mut Module,
) -> (Pipeline, Vec<Exportable>) { ) -> (Pipeline, Vec<Exportable>) {
let spans = &lite_command.parts[..]; let spans = &lite_command.parts[..];
@ -1416,7 +1429,8 @@ pub fn parse_export_in_module(
pipe: lite_command.pipe, pipe: lite_command.pipe,
redirection: lite_command.redirection.clone(), 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") { let export_use_decl_id = if let Some(id) = working_set.find_decl(b"export use") {
id id
@ -1759,7 +1773,7 @@ pub fn parse_module_block(
)) ))
} }
b"use" => { b"use" => {
let (pipeline, _) = parse_use(working_set, command); let (pipeline, _) = parse_use(working_set, command, Some(&mut module));
block.pipelines.push(pipeline) block.pipelines.push(pipeline)
} }
@ -1774,7 +1788,7 @@ pub fn parse_module_block(
} }
b"export" => { b"export" => {
let (pipe, exportables) = 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 { for exportable in exportables {
match exportable { match exportable {
@ -1884,6 +1898,48 @@ pub fn parse_module_block(
(block, module, module_comments) (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. /// Parse a module from a file.
/// ///
/// The module name is inferred from the stem of the file, unless specified in `name_override`. /// 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. // Check if we've parsed the module before.
if let Some(module_id) = working_set.find_module_by_span(new_span) { if let Some(module_id) = working_set.find_module_by_span(new_span) {
if !module_needs_reloading(working_set, module_id) {
return Some(module_id); return Some(module_id);
} }
}
// Add the file to the stack of files being processed. // 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); working_set.error(e);
return None; return None;
} }
// Parse the module // 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()); parse_module_block(working_set, new_span, module_name.as_bytes());
// Remove the file from the stack of files being processed. // Remove the file from the stack of files being processed.
working_set.files.pop(); working_set.files.pop();
let _ = working_set.add_block(Arc::new(block)); 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); let module_id = working_set.add_module(&module_name, module, module_comments);
Some(module_id) Some(module_id)
@ -2228,6 +2287,7 @@ pub fn parse_module(
pub fn parse_use( pub fn parse_use(
working_set: &mut StateWorkingSet, working_set: &mut StateWorkingSet,
lite_command: &LiteCommand, lite_command: &LiteCommand,
parent_module: Option<&mut Module>,
) -> (Pipeline, Vec<Exportable>) { ) -> (Pipeline, Vec<Exportable>) {
let spans = &lite_command.parts; 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( let (definitions, errors) = module.resolve_import_pattern(
working_set, working_set,
module_id, module_id,
&import_pattern.members, &import_pattern.members,
None, None,
name_span, name_span,
&mut imported_modules,
); );
working_set.parse_errors.extend(errors); working_set.parse_errors.extend(errors);
@ -2420,6 +2482,9 @@ pub fn parse_use(
import_pattern.constants = constants.iter().map(|(_, id)| *id).collect(); 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 // Extend the current scope with the module's exportables
working_set.use_decls(definitions.decls); working_set.use_decls(definitions.decls);
working_set.use_modules(definitions.modules); working_set.use_modules(definitions.modules);
@ -2853,6 +2918,7 @@ pub fn parse_overlay_use(working_set: &mut StateWorkingSet, call: Box<Call>) ->
&[], &[],
Some(final_overlay_name.as_bytes()), Some(final_overlay_name.as_bytes()),
call.head, call.head,
&mut vec![],
) )
} else { } else {
origin_module.resolve_import_pattern( origin_module.resolve_import_pattern(
@ -2863,6 +2929,7 @@ pub fn parse_overlay_use(working_set: &mut StateWorkingSet, call: Box<Call>) ->
}], }],
Some(final_overlay_name.as_bytes()), Some(final_overlay_name.as_bytes()),
call.head, call.head,
&mut vec![],
) )
} }
} else { } 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() .clone()
.get(get_envs) .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| { .map_err(|err| {
log::warn!("Error getting signatures: {err:?}"); log::warn!("Error getting metadata and signatures: {err:?}");
ParseError::LabeledError( ParseError::LabeledError(
"Error getting signatures".into(), "Error getting metadata and signatures".into(),
err.to_string(), err.to_string(),
spans[0], spans[0],
) )
}); });
if let Ok(ref signatures) = signatures { 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 // Add the loaded plugin to the delta
working_set.update_plugin_registry(PluginRegistryItem::new( working_set.update_plugin_registry(PluginRegistryItem::new(
&identity, &identity,
signatures.clone(), meta,
sigs.clone(),
)); ));
Ok(sigs)
}
Err(err) => Err(err),
} }
signatures
}, },
|sig| sig.map(|sig| vec![sig]), |sig| sig.map(|sig| vec![sig]),
)?; )?;

View File

@ -16,7 +16,6 @@ use nu_protocol::{
IN_VARIABLE_ID, IN_VARIABLE_ID,
}; };
use std::{ use std::{
borrow::Cow,
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
num::ParseIntError, num::ParseIntError,
str, 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(&quote_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::<Vec<_>>();
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<Expression> = 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::<String>();
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 { fn parse_external_arg(working_set: &mut StateWorkingSet, span: Span) -> ExternalArgument {
let contents = working_set.get_span_contents(span); 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)) ExternalArgument::Regular(parse_dollar_expr(working_set, span))
} else if contents.starts_with(b"[") { } else if contents.starts_with(b"[") {
ExternalArgument::Regular(parse_list_expression(working_set, span, &SyntaxShape::Any)) 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 } else if contents.len() > 3
&& contents.starts_with(b"...") && contents.starts_with(b"...")
&& (contents[3] == b'$' || contents[3] == b'[' || contents[3] == 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)), &SyntaxShape::List(Box::new(SyntaxShape::Any)),
)) ))
} else { } else {
// Eval stage trims the quotes, so we don't have to do the same thing when parsing. ExternalArgument::Regular(parse_external_string(working_set, span))
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,
))
} }
} }
@ -274,18 +463,7 @@ pub fn parse_external_call(working_set: &mut StateWorkingSet, spans: &[Span]) ->
let arg = parse_expression(working_set, &[head_span]); let arg = parse_expression(working_set, &[head_span]);
Box::new(arg) Box::new(arg)
} else { } else {
// Eval stage will unquote the string, so we don't bother with that here Box::new(parse_external_string(working_set, head_span))
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,
))
}; };
let args = spans[1..] let args = spans[1..]
@ -756,15 +934,12 @@ pub fn parse_internal_call(
let output = signature.get_output_type(); let output = signature.get_output_type();
// storing the var ID for later due to borrowing issues // storing the var ID for later due to borrowing issues
let lib_dirs_var_id = if decl.is_builtin() { let lib_dirs_var_id = match decl.name() {
match decl.name() { "use" | "overlay use" | "source-env" if decl.is_keyword() => {
"use" | "overlay use" | "source-env" | "nu-check" => {
find_dirs_var(working_set, LIB_DIRS_VAR) find_dirs_var(working_set, LIB_DIRS_VAR)
} }
"nu-check" if decl.is_builtin() => find_dirs_var(working_set, LIB_DIRS_VAR),
_ => None, _ => None,
}
} else {
None
}; };
// The index into the positional parameter in the definition // The index into the positional parameter in the definition
@ -2639,23 +2814,6 @@ pub fn unescape_unquote_string(bytes: &[u8], span: Span) -> (String, Option<Pars
} }
} }
/// XXX: This is here temporarily as a patch, but we should replace this with properly representing
/// the quoted state of a string in the AST
fn unescape_string_preserving_quotes(bytes: &[u8], span: Span) -> (String, Option<ParseError>) {
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 { pub fn parse_string(working_set: &mut StateWorkingSet, span: Span) -> Expression {
trace!("parsing: string"); trace!("parsing: string");
@ -5219,7 +5377,7 @@ pub fn parse_builtin_commands(
} }
b"alias" => parse_alias(working_set, lite_command, None), b"alias" => parse_alias(working_set, lite_command, None),
b"module" => parse_module(working_set, lite_command, None).0, 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" => { b"overlay" => {
if let Some(redirection) = lite_command.redirection.as_ref() { if let Some(redirection) = lite_command.redirection.as_ref() {
working_set.error(redirecting_builtin_error("overlay", redirection)); working_set.error(redirecting_builtin_error("overlay", redirection));
@ -6012,7 +6170,7 @@ pub fn discover_captures_in_expr(
} }
Expr::String(_) => {} Expr::String(_) => {}
Expr::RawString(_) => {} Expr::RawString(_) => {}
Expr::StringInterpolation(exprs) => { Expr::StringInterpolation(exprs) | Expr::GlobInterpolation(exprs, _) => {
for expr in exprs { for expr in exprs {
discover_captures_in_expr(working_set, expr, seen, seen_blocks, output)?; discover_captures_in_expr(working_set, expr, seen, seen_blocks, output)?;
} }

View File

@ -1,8 +1,8 @@
use nu_parser::*; use nu_parser::*;
use nu_protocol::{ use nu_protocol::{
ast::{Argument, Call, Expr, ExternalArgument, PathMember, Range}, ast::{Argument, Call, Expr, Expression, ExternalArgument, PathMember, Range},
engine::{Command, EngineState, Stack, StateWorkingSet}, engine::{Command, EngineState, Stack, StateWorkingSet},
ParseError, PipelineData, ShellError, Signature, Span, SyntaxShape, ParseError, PipelineData, ShellError, Signature, Span, SyntaxShape, Type,
}; };
use rstest::rstest; use rstest::rstest;
@ -182,7 +182,7 @@ pub fn multi_test_parse_int() {
Test( Test(
"ranges or relative paths not confused for int", "ranges or relative paths not confused for int",
b"./a/b", b"./a/b",
Expr::String("./a/b".into()), Expr::GlobPattern("./a/b".into(), false),
None, None,
), ),
Test( 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] #[rstest]
#[case("foo-external-call", "foo-external-call", "bare word")] #[case("foo-external-call", "foo-external-call", "bare word")]
#[case("^foo-external-call", "foo-external-call", "bare word with caret")] #[case("^foo-external-call", "foo-external-call", "bare word with caret")]
@ -713,77 +757,118 @@ pub fn parse_call_missing_req_flag() {
r"foo\external-call", r"foo\external-call",
"bare word with backslash and caret" "bare word with backslash and caret"
)] )]
#[case( #[case("`foo external call`", "foo external call", "backtick quote")]
"^'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( #[case(
"^`foo external call`", "^`foo external call`",
"`foo external call`", "foo external call",
"backtick quote with caret" "backtick quote with caret"
)] )]
#[case( #[case(
"`foo/external call`", "`foo/external call`",
"`foo/external call`", "foo/external call",
"backtick quote with forward slash" "backtick quote with forward slash"
)] )]
#[case( #[case(
"^`foo/external call`", "^`foo/external call`",
"`foo/external call`", "foo/external call",
"backtick quote with forward slash and caret" "backtick quote with forward slash and caret"
)] )]
#[case( #[case(
r"^`foo\external call`",
r"`foo\external call`", r"`foo\external call`",
r"foo\external call",
"backtick quote with backslash" "backtick quote with backslash"
)] )]
#[case( #[case(
r"^`foo\external call`", r"^`foo\external call`",
r"`foo\external call`", r"foo\external call",
"backtick quote with backslash and caret" "backtick quote with backslash and caret"
)] )]
fn test_external_call_name(#[case] input: &str, #[case] expected: &str, #[case] tag: &str) { pub fn test_external_call_head_glob(
let engine_state = EngineState::new(); #[case] input: &str,
let mut working_set = StateWorkingSet::new(&engine_state); #[case] expected: &str,
let block = parse(&mut working_set, None, input.as_bytes(), true); #[case] tag: &str,
assert!( ) {
working_set.parse_errors.is_empty(), test_external_call(input, tag, |name, args| {
"{tag}: errors: {:?}", match &name.expr {
working_set.parse_errors 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]; #[rstest]
assert_eq!(1, pipeline.len()); #[case(
let element = &pipeline.elements[0]; r##"^r#'foo-external-call'#"##,
match &element.expr.expr { "foo-external-call",
Expr::ExternalCall(name, args) => { "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 { match &name.expr {
Expr::String(string) => { Expr::String(string) => {
assert_eq!(expected, string); assert_eq!(expected, string);
@ -793,47 +878,162 @@ fn test_external_call_name(#[case] input: &str, #[case] expected: &str, #[case]
} }
} }
assert_eq!(0, args.len()); assert_eq!(0, args.len());
} })
other => {
panic!("{tag}: Unexpected expression in pipeline: {other:?}");
}
}
} }
#[rstest] #[rstest]
#[case("^foo bar-baz", "bar-baz", "bare word")] #[case(r"~/.foo/(1)", 2, false, "unquoted interpolated string")]
#[case("^foo bar/baz", "bar/baz", "bare word with forward slash")] #[case(
#[case(r"^foo bar\baz", r"bar\baz", "bare word with backslash")] r"~\.foo(2)\(1)",
#[case("^foo 'bar baz'", "'bar baz'", "single quote")] 4,
#[case("foo 'bar/baz'", "'bar/baz'", "single quote with forward slash")] false,
#[case(r"foo 'bar\baz'", r"'bar\baz'", "single quote with backslash")] "unquoted interpolated string 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/(1)", 2, false, "unquoted interpolated string with caret")]
#[case(r#"^foo "bar\\baz""#, r#""bar\baz""#, "double quote with backslash")] #[case(r#"^$"~/.foo/(1)""#, 2, true, "quoted interpolated string with caret")]
#[case("^foo `bar baz`", "`bar baz`", "backtick quote")] pub fn test_external_call_head_interpolated_string(
#[case("^foo `bar/baz`", "`bar/baz`", "backtick quote with forward slash")] #[case] input: &str,
#[case(r"^foo `bar\baz`", r"`bar\baz`", "backtick quote with backslash")] #[case] subexpr_count: usize,
fn test_external_call_argument_regular( #[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!("Unexpected expression in command arg position: {other:?}")
}
},
other @ ExternalArgument::Spread(..) => {
panic!("Unexpected external spread argument in command arg position: {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] input: &str,
#[case] expected: &str, #[case] expected: &str,
#[case] tag: &str, #[case] tag: &str,
) { ) {
let engine_state = EngineState::new(); test_external_call(input, tag, |name, args| {
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 { match &name.expr {
Expr::String(string) => { 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"); assert_eq!("foo", string, "{tag}: incorrect name");
} }
other => { other => {
@ -847,38 +1047,58 @@ fn test_external_call_argument_regular(
assert_eq!(expected, string, "{tag}: incorrect arg"); assert_eq!(expected, string, "{tag}: incorrect arg");
} }
other => { other => {
panic!("Unexpected expression in command arg position: {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(..) => { other @ ExternalArgument::Spread(..) => {
panic!("Unexpected external spread argument in command arg position: {other:?}") panic!("Unexpected external spread argument in command arg position: {other:?}")
} }
} }
} })
other => {
panic!("{tag}: Unexpected expression in pipeline: {other:?}");
}
}
} }
#[test] #[test]
fn test_external_call_argument_spread() { fn test_external_call_argument_spread() {
let engine_state = EngineState::new(); let input = r"^foo ...[a b c]";
let mut working_set = StateWorkingSet::new(&engine_state); let tag = "spread";
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 pipeline = &block.pipelines[0]; test_external_call(input, tag, |name, args| {
assert_eq!(1, pipeline.len());
let element = &pipeline.elements[0];
match &element.expr.expr {
Expr::ExternalCall(name, args) => {
match &name.expr { match &name.expr {
Expr::String(string) => { Expr::GlobPattern(string, _) => {
assert_eq!("foo", string, "incorrect name"); assert_eq!("foo", string, "incorrect name");
} }
other => { other => {
@ -897,16 +1117,10 @@ fn test_external_call_argument_spread() {
} }
}, },
other @ ExternalArgument::Regular(..) => { other @ ExternalArgument::Regular(..) => {
panic!( panic!("Unexpected external regular argument in command arg position: {other:?}")
"Unexpected external regular argument in command arg position: {other:?}"
)
}
}
}
other => {
panic!("Unexpected expression in pipeline: {other:?}");
} }
} }
})
} }
#[test] #[test]
@ -1132,6 +1346,44 @@ mod string {
assert_eq!(subexprs[0], &Expr::String("(1 + 3)(7 - 5)".to_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] #[test]
pub fn parse_nested_expressions() { pub fn parse_nested_expressions() {
let engine_state = EngineState::new(); let engine_state = EngineState::new();

View File

@ -5,7 +5,7 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-path"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
name = "nu-path" name = "nu-path"
version = "0.94.3" version = "0.95.1"
exclude = ["/fuzz"] exclude = ["/fuzz"]
[lib] [lib]

161
crates/nu-path/src/form.rs Normal file
View File

@ -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<P: AsRef<OsStr> + ?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<P: AsRef<OsStr> + ?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<P: AsRef<OsStr> + ?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<P: AsRef<OsStr> + ?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<P: AsRef<OsStr> + ?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<Form: PathForm>: PathForm {}
impl<Form: PathForm> PathCast<Form> for Form {}
impl PathCast<Any> for Relative {}
impl PathCast<Any> for Absolute {}
impl PathCast<Any> for Canonical {}
impl PathCast<Absolute> 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 {}

View File

@ -2,12 +2,15 @@ mod assert_path_eq;
mod components; mod components;
pub mod dots; pub mod dots;
pub mod expansions; pub mod expansions;
pub mod form;
mod helpers; mod helpers;
mod path;
mod tilde; mod tilde;
mod trailing_slash; mod trailing_slash;
pub use components::components; pub use components::components;
pub use expansions::{canonicalize_with, expand_path_with, expand_to_real_path, locate_in_dirs}; 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 helpers::{cache_dir, config_dir, data_dir, get_canonicalized_path, home_dir};
pub use path::*;
pub use tilde::expand_tilde; pub use tilde::expand_tilde;
pub use trailing_slash::{has_trailing_slash, strip_trailing_slash}; pub use trailing_slash::{has_trailing_slash, strip_trailing_slash};

3095
crates/nu-path/src/path.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -5,14 +5,14 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-plugin-core
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
name = "nu-plugin-core" name = "nu-plugin-core"
version = "0.94.3" version = "0.95.1"
[lib] [lib]
bench = false bench = false
[dependencies] [dependencies]
nu-protocol = { path = "../nu-protocol", version = "0.94.3" } nu-protocol = { path = "../nu-protocol", version = "0.95.1" }
nu-plugin-protocol = { path = "../nu-plugin-protocol", version = "0.94.3", default-features = false } nu-plugin-protocol = { path = "../nu-plugin-protocol", version = "0.95.1", default-features = false }
rmp-serde = { workspace = true } rmp-serde = { workspace = true }
serde = { workspace = true } serde = { workspace = true }

View File

@ -5,17 +5,17 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-plugin-engi
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
name = "nu-plugin-engine" name = "nu-plugin-engine"
version = "0.94.3" version = "0.95.1"
[lib] [lib]
bench = false bench = false
[dependencies] [dependencies]
nu-engine = { path = "../nu-engine", version = "0.94.3" } nu-engine = { path = "../nu-engine", version = "0.95.1" }
nu-protocol = { path = "../nu-protocol", version = "0.94.3" } nu-protocol = { path = "../nu-protocol", version = "0.95.1" }
nu-system = { path = "../nu-system", version = "0.94.3" } nu-system = { path = "../nu-system", version = "0.95.1" }
nu-plugin-protocol = { path = "../nu-plugin-protocol", version = "0.94.3" } nu-plugin-protocol = { path = "../nu-plugin-protocol", version = "0.95.1" }
nu-plugin-core = { path = "../nu-plugin-core", version = "0.94.3", default-features = false } nu-plugin-core = { path = "../nu-plugin-core", version = "0.95.1", default-features = false }
serde = { workspace = true } serde = { workspace = true }
log = { workspace = true } log = { workspace = true }

View File

@ -252,7 +252,7 @@ pub fn load_plugin_registry_item(
})?; })?;
match &plugin.data { match &plugin.data {
PluginRegistryItemData::Valid { commands } => { PluginRegistryItemData::Valid { metadata, commands } => {
let plugin = add_plugin_to_working_set(working_set, &identity)?; 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 // 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. // doesn't.
plugin.reset()?; plugin.reset()?;
// Set the plugin metadata from the file
plugin.set_metadata(Some(metadata.clone()));
// Create the declarations from the commands // Create the declarations from the commands
for signature in commands { for signature in commands {
let decl = PluginDeclaration::new(plugin.clone(), signature.clone()); let decl = PluginDeclaration::new(plugin.clone(), signature.clone());

View File

@ -11,8 +11,8 @@ use nu_plugin_protocol::{
PluginOutput, ProtocolInfo, StreamId, StreamMessage, PluginOutput, ProtocolInfo, StreamId, StreamMessage,
}; };
use nu_protocol::{ use nu_protocol::{
ast::Operator, CustomValue, IntoSpanned, PipelineData, PluginSignature, ShellError, Span, ast::Operator, CustomValue, IntoSpanned, PipelineData, PluginMetadata, PluginSignature,
Spanned, TryIntoValue, Value, ShellError, Span, Spanned, Value, IntoValue, TryIntoValue
}; };
use std::{ use std::{
collections::{btree_map, BTreeMap}, collections::{btree_map, BTreeMap},
@ -716,6 +716,7 @@ impl PluginInterface {
// Convert the call into one with a header and handle the stream, if necessary // Convert the call into one with a header and handle the stream, if necessary
let (call, writer) = match call { let (call, writer) = match call {
PluginCall::Metadata => (PluginCall::Metadata, Default::default()),
PluginCall::Signature => (PluginCall::Signature, Default::default()), PluginCall::Signature => (PluginCall::Signature, Default::default()),
PluginCall::CustomValueOp(value, op) => { PluginCall::CustomValueOp(value, op) => {
(PluginCall::CustomValueOp(value, op), Default::default()) (PluginCall::CustomValueOp(value, op), Default::default())
@ -913,6 +914,17 @@ impl PluginInterface {
self.receive_plugin_call_response(result.receiver, context, result.state) self.receive_plugin_call_response(result.receiver, context, result.state)
} }
/// Get the metadata from the plugin.
pub fn get_metadata(&self) -> Result<PluginMetadata, ShellError> {
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. /// Get the command signatures from the plugin.
pub fn get_signature(&self) -> Result<Vec<PluginSignature>, ShellError> { pub fn get_signature(&self) -> Result<Vec<PluginSignature>, ShellError> {
match self.plugin_call(PluginCall::Signature, None)? { match self.plugin_call(PluginCall::Signature, None)? {
@ -1206,6 +1218,7 @@ impl CurrentCallState {
source: &PluginSource, source: &PluginSource,
) -> Result<(), ShellError> { ) -> Result<(), ShellError> {
match call { match call {
PluginCall::Metadata => Ok(()),
PluginCall::Signature => Ok(()), PluginCall::Signature => Ok(()),
PluginCall::Run(CallInfo { call, .. }) => self.prepare_call_args(call, source), PluginCall::Run(CallInfo { call, .. }) => self.prepare_call_args(call, source),
PluginCall::CustomValueOp(_, op) => { PluginCall::CustomValueOp(_, op) => {

View File

@ -17,8 +17,8 @@ use nu_plugin_protocol::{
use nu_protocol::{ use nu_protocol::{
ast::{Math, Operator}, ast::{Math, Operator},
engine::Closure, engine::Closure,
ByteStreamType, CustomValue, IntoInterruptiblePipelineData, IntoSpanned, IntoValue, ByteStreamType, CustomValue, IntoInterruptiblePipelineData, IntoSpanned, PipelineData,
PipelineData, PluginSignature, ShellError, Span, Spanned, TryIntoValue, Value, PluginMetadata, PluginSignature, ShellError, Span, Spanned, Value, IntoValue, TryIntoValue
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
@ -1019,6 +1019,25 @@ fn start_fake_plugin_call_responder(
.expect("failed to spawn thread"); .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] #[test]
fn interface_get_signature() -> Result<(), ShellError> { fn interface_get_signature() -> Result<(), ShellError> {
let test = TestCase::new(); let test = TestCase::new();

View File

@ -7,7 +7,7 @@ use super::{PluginInterface, PluginSource};
use nu_plugin_core::CommunicationMode; use nu_plugin_core::CommunicationMode;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack}, engine::{EngineState, Stack},
PluginGcConfig, PluginIdentity, RegisteredPlugin, ShellError, PluginGcConfig, PluginIdentity, PluginMetadata, RegisteredPlugin, ShellError,
}; };
use std::{ use std::{
collections::HashMap, collections::HashMap,
@ -31,6 +31,8 @@ pub struct PersistentPlugin {
struct MutableState { struct MutableState {
/// Reference to the plugin if running /// Reference to the plugin if running
running: Option<RunningPlugin>, running: Option<RunningPlugin>,
/// Metadata for the plugin, e.g. version.
metadata: Option<PluginMetadata>,
/// Plugin's preferred communication mode (if known) /// Plugin's preferred communication mode (if known)
preferred_mode: Option<PreferredCommunicationMode>, preferred_mode: Option<PreferredCommunicationMode>,
/// Garbage collector config /// Garbage collector config
@ -59,6 +61,7 @@ impl PersistentPlugin {
identity, identity,
mutable: Mutex::new(MutableState { mutable: Mutex::new(MutableState {
running: None, running: None,
metadata: None,
preferred_mode: None, preferred_mode: None,
gc_config, gc_config,
}), }),
@ -268,6 +271,16 @@ impl RegisteredPlugin for PersistentPlugin {
self.stop_internal(true) self.stop_internal(true)
} }
fn metadata(&self) -> Option<PluginMetadata> {
self.mutable.lock().ok().and_then(|m| m.metadata.clone())
}
fn set_metadata(&self, metadata: Option<PluginMetadata>) {
if let Ok(mut mutable) = self.mutable.lock() {
mutable.metadata = metadata;
}
}
fn set_gc_config(&self, gc_config: &PluginGcConfig) { fn set_gc_config(&self, gc_config: &PluginGcConfig) {
if let Ok(mut mutable) = self.mutable.lock() { if let Ok(mut mutable) = self.mutable.lock() {
// Save the new config for future calls // Save the new config for future calls

View File

@ -5,14 +5,14 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-plugin-prot
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
name = "nu-plugin-protocol" name = "nu-plugin-protocol"
version = "0.94.3" version = "0.95.1"
[lib] [lib]
bench = false bench = false
[dependencies] [dependencies]
nu-protocol = { path = "../nu-protocol", version = "0.94.3", features = ["plugin"] } nu-protocol = { path = "../nu-protocol", version = "0.95.1", features = ["plugin"] }
nu-utils = { path = "../nu-utils", version = "0.94.3" } nu-utils = { path = "../nu-utils", version = "0.95.1" }
bincode = "1.3" bincode = "1.3"
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }

View File

@ -23,7 +23,7 @@ pub mod test_util;
use nu_protocol::{ use nu_protocol::{
ast::Operator, engine::Closure, ByteStreamType, Config, LabeledError, PipelineData, ast::Operator, engine::Closure, ByteStreamType, Config, LabeledError, PipelineData,
PluginSignature, ShellError, Span, Spanned, Value, PluginMetadata, PluginSignature, ShellError, Span, Spanned, Value,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
@ -119,6 +119,7 @@ pub struct ByteStreamInfo {
/// Calls that a plugin can execute. The type parameter determines the input type. /// Calls that a plugin can execute. The type parameter determines the input type.
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub enum PluginCall<D> { pub enum PluginCall<D> {
Metadata,
Signature, Signature,
Run(CallInfo<D>), Run(CallInfo<D>),
CustomValueOp(Spanned<PluginCustomValue>, CustomValueOp), CustomValueOp(Spanned<PluginCustomValue>, CustomValueOp),
@ -132,6 +133,7 @@ impl<D> PluginCall<D> {
f: impl FnOnce(D) -> Result<T, ShellError>, f: impl FnOnce(D) -> Result<T, ShellError>,
) -> Result<PluginCall<T>, ShellError> { ) -> Result<PluginCall<T>, ShellError> {
Ok(match self { Ok(match self {
PluginCall::Metadata => PluginCall::Metadata,
PluginCall::Signature => PluginCall::Signature, PluginCall::Signature => PluginCall::Signature,
PluginCall::Run(call) => PluginCall::Run(call.map_data(f)?), PluginCall::Run(call) => PluginCall::Run(call.map_data(f)?),
PluginCall::CustomValueOp(custom_value, op) => { PluginCall::CustomValueOp(custom_value, op) => {
@ -143,6 +145,7 @@ impl<D> PluginCall<D> {
/// The span associated with the call. /// The span associated with the call.
pub fn span(&self) -> Option<Span> { pub fn span(&self) -> Option<Span> {
match self { match self {
PluginCall::Metadata => None,
PluginCall::Signature => None, PluginCall::Signature => None,
PluginCall::Run(CallInfo { call, .. }) => Some(call.head), PluginCall::Run(CallInfo { call, .. }) => Some(call.head),
PluginCall::CustomValueOp(val, _) => Some(val.span), PluginCall::CustomValueOp(val, _) => Some(val.span),
@ -309,6 +312,7 @@ pub enum StreamMessage {
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub enum PluginCallResponse<D> { pub enum PluginCallResponse<D> {
Error(LabeledError), Error(LabeledError),
Metadata(PluginMetadata),
Signature(Vec<PluginSignature>), Signature(Vec<PluginSignature>),
Ordering(Option<Ordering>), Ordering(Option<Ordering>),
PipelineData(D), PipelineData(D),
@ -323,6 +327,7 @@ impl<D> PluginCallResponse<D> {
) -> Result<PluginCallResponse<T>, ShellError> { ) -> Result<PluginCallResponse<T>, ShellError> {
Ok(match self { Ok(match self {
PluginCallResponse::Error(err) => PluginCallResponse::Error(err), PluginCallResponse::Error(err) => PluginCallResponse::Error(err),
PluginCallResponse::Metadata(meta) => PluginCallResponse::Metadata(meta),
PluginCallResponse::Signature(sigs) => PluginCallResponse::Signature(sigs), PluginCallResponse::Signature(sigs) => PluginCallResponse::Signature(sigs),
PluginCallResponse::Ordering(ordering) => PluginCallResponse::Ordering(ordering), PluginCallResponse::Ordering(ordering) => PluginCallResponse::Ordering(ordering),
PluginCallResponse::PipelineData(input) => PluginCallResponse::PipelineData(f(input)?), PluginCallResponse::PipelineData(input) => PluginCallResponse::PipelineData(f(input)?),

View File

@ -1,6 +1,6 @@
[package] [package]
name = "nu-plugin-test-support" name = "nu-plugin-test-support"
version = "0.94.3" version = "0.95.1"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
description = "Testing support for Nushell plugins" description = "Testing support for Nushell plugins"
@ -12,14 +12,14 @@ bench = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
nu-engine = { path = "../nu-engine", version = "0.94.3", features = ["plugin"] } nu-engine = { path = "../nu-engine", version = "0.95.1", features = ["plugin"] }
nu-protocol = { path = "../nu-protocol", version = "0.94.3", features = ["plugin"] } nu-protocol = { path = "../nu-protocol", version = "0.95.1", features = ["plugin"] }
nu-parser = { path = "../nu-parser", version = "0.94.3", features = ["plugin"] } nu-parser = { path = "../nu-parser", version = "0.95.1", features = ["plugin"] }
nu-plugin = { path = "../nu-plugin", version = "0.94.3" } nu-plugin = { path = "../nu-plugin", version = "0.95.1" }
nu-plugin-core = { path = "../nu-plugin-core", version = "0.94.3" } nu-plugin-core = { path = "../nu-plugin-core", version = "0.95.1" }
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.94.3" } nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.95.1" }
nu-plugin-protocol = { path = "../nu-plugin-protocol", version = "0.94.3" } nu-plugin-protocol = { path = "../nu-plugin-protocol", version = "0.95.1" }
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.94.3" } nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.95.1" }
nu-ansi-term = { workspace = true } nu-ansi-term = { workspace = true }
similar = "2.5" similar = "2.5"

View File

@ -6,7 +6,7 @@ use std::{
use nu_plugin_engine::{GetPlugin, PluginInterface}; use nu_plugin_engine::{GetPlugin, PluginInterface};
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack}, engine::{EngineState, Stack},
PluginGcConfig, PluginIdentity, RegisteredPlugin, ShellError, PluginGcConfig, PluginIdentity, PluginMetadata, RegisteredPlugin, ShellError,
}; };
pub struct FakePersistentPlugin { pub struct FakePersistentPlugin {
@ -42,6 +42,12 @@ impl RegisteredPlugin for FakePersistentPlugin {
None None
} }
fn metadata(&self) -> Option<PluginMetadata> {
None
}
fn set_metadata(&self, _metadata: Option<PluginMetadata>) {}
fn set_gc_config(&self, _gc_config: &PluginGcConfig) { fn set_gc_config(&self, _gc_config: &PluginGcConfig) {
// We don't have a GC // We don't have a GC
} }

View File

@ -66,6 +66,10 @@
//! } //! }
//! //!
//! impl Plugin for LowercasePlugin { //! impl Plugin for LowercasePlugin {
//! fn version(&self) -> String {
//! env!("CARGO_PKG_VERSION").into()
//! }
//!
//! fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> { //! fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {
//! vec![Box::new(Lowercase)] //! vec![Box::new(Lowercase)]
//! } //! }

View File

@ -54,6 +54,10 @@ struct IntoU32;
struct IntoIntFromU32; struct IntoIntFromU32;
impl Plugin for CustomU32Plugin { impl Plugin for CustomU32Plugin {
fn version(&self) -> String {
"0.0.0".into()
}
fn commands(&self) -> Vec<Box<dyn nu_plugin::PluginCommand<Plugin = Self>>> { fn commands(&self) -> Vec<Box<dyn nu_plugin::PluginCommand<Plugin = Self>>> {
vec![Box::new(IntoU32), Box::new(IntoIntFromU32)] vec![Box::new(IntoU32), Box::new(IntoIntFromU32)]
} }

View File

@ -8,6 +8,10 @@ struct HelloPlugin;
struct Hello; struct Hello;
impl Plugin for HelloPlugin { impl Plugin for HelloPlugin {
fn version(&self) -> String {
"0.0.0".into()
}
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> { fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
vec![Box::new(Hello)] vec![Box::new(Hello)]
} }

View File

@ -59,6 +59,10 @@ impl PluginCommand for Lowercase {
} }
impl Plugin for LowercasePlugin { impl Plugin for LowercasePlugin {
fn version(&self) -> String {
"0.0.0".into()
}
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> { fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
vec![Box::new(Lowercase)] vec![Box::new(Lowercase)]
} }

View File

@ -5,16 +5,16 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-plugin"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
name = "nu-plugin" name = "nu-plugin"
version = "0.94.3" version = "0.95.1"
[lib] [lib]
bench = false bench = false
[dependencies] [dependencies]
nu-engine = { path = "../nu-engine", version = "0.94.3" } nu-engine = { path = "../nu-engine", version = "0.95.1" }
nu-protocol = { path = "../nu-protocol", version = "0.94.3" } nu-protocol = { path = "../nu-protocol", version = "0.95.1" }
nu-plugin-protocol = { path = "../nu-plugin-protocol", version = "0.94.3" } nu-plugin-protocol = { path = "../nu-plugin-protocol", version = "0.95.1" }
nu-plugin-core = { path = "../nu-plugin-core", version = "0.94.3", default-features = false } nu-plugin-core = { path = "../nu-plugin-core", version = "0.95.1", default-features = false }
log = { workspace = true } log = { workspace = true }
thiserror = "1.0" thiserror = "1.0"

View File

@ -24,6 +24,10 @@
//! struct MyCommand; //! struct MyCommand;
//! //!
//! impl Plugin for MyPlugin { //! impl Plugin for MyPlugin {
//! fn version(&self) -> String {
//! env!("CARGO_PKG_VERSION").into()
//! }
//!
//! fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> { //! fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
//! vec![Box::new(MyCommand)] //! vec![Box::new(MyCommand)]
//! } //! }

View File

@ -60,6 +60,9 @@ use crate::{EngineInterface, EvaluatedCall, Plugin};
/// } /// }
/// ///
/// # impl Plugin for LowercasePlugin { /// # impl Plugin for LowercasePlugin {
/// # fn version(&self) -> String {
/// # "0.0.0".into()
/// # }
/// # fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> { /// # fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {
/// # vec![Box::new(Lowercase)] /// # vec![Box::new(Lowercase)]
/// # } /// # }
@ -195,6 +198,9 @@ pub trait PluginCommand: Sync {
/// } /// }
/// ///
/// # impl Plugin for HelloPlugin { /// # impl Plugin for HelloPlugin {
/// # fn version(&self) -> String {
/// # "0.0.0".into()
/// # }
/// # fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> { /// # fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {
/// # vec![Box::new(Hello)] /// # vec![Box::new(Hello)]
/// # } /// # }

View File

@ -11,8 +11,8 @@ use nu_plugin_protocol::{
ProtocolInfo, ProtocolInfo,
}; };
use nu_protocol::{ use nu_protocol::{
engine::Closure, Config, LabeledError, PipelineData, PluginSignature, ShellError, Span, engine::Closure, Config, LabeledError, PipelineData, PluginMetadata, PluginSignature,
Spanned, TryIntoValue, Value, ShellError, Span, Spanned, Value, IntoValue, TryIntoValue
}; };
use std::{ use std::{
collections::{btree_map, BTreeMap, HashMap}, collections::{btree_map, BTreeMap, HashMap},
@ -29,6 +29,9 @@ use std::{
#[derive(Debug)] #[derive(Debug)]
#[doc(hidden)] #[doc(hidden)]
pub enum ReceivedPluginCall { pub enum ReceivedPluginCall {
Metadata {
engine: EngineInterface,
},
Signature { Signature {
engine: EngineInterface, engine: EngineInterface,
}, },
@ -280,8 +283,11 @@ impl InterfaceManager for EngineInterfaceManager {
} }
}; };
match call { match call {
// We just let the receiver handle it rather than trying to store signature here // Ask the plugin for metadata
// or something PluginCall::Metadata => {
self.send_plugin_call(ReceivedPluginCall::Metadata { engine: interface })
}
// Ask the plugin for signatures
PluginCall::Signature => { PluginCall::Signature => {
self.send_plugin_call(ReceivedPluginCall::Signature { engine: interface }) 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. /// Write a call response of plugin signatures.
/// ///
/// Any custom values in the examples will be rendered using `to_base_value()`. /// Any custom values in the examples will be rendered using `to_base_value()`.

View File

@ -322,6 +322,26 @@ fn manager_consume_goodbye_closes_plugin_call_channel() -> Result<(), ShellError
Ok(()) 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] #[test]
fn manager_consume_call_signature_forwards_to_receiver_with_context() -> Result<(), ShellError> { fn manager_consume_call_signature_forwards_to_receiver_with_context() -> Result<(), ShellError> {
let mut manager = TestCase::new().engine(); let mut manager = TestCase::new().engine();

View File

@ -16,7 +16,8 @@ use nu_plugin_core::{
}; };
use nu_plugin_protocol::{CallInfo, CustomValueOp, PluginCustomValue, PluginInput, PluginOutput}; use nu_plugin_protocol::{CallInfo, CustomValueOp, PluginCustomValue, PluginInput, PluginOutput};
use nu_protocol::{ 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; use thiserror::Error;
@ -52,6 +53,10 @@ pub(crate) const OUTPUT_BUFFER_SIZE: usize = 16384;
/// struct Hello; /// struct Hello;
/// ///
/// impl Plugin for HelloPlugin { /// impl Plugin for HelloPlugin {
/// fn version(&self) -> String {
/// env!("CARGO_PKG_VERSION").into()
/// }
///
/// fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> { /// fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {
/// vec![Box::new(Hello)] /// vec![Box::new(Hello)]
/// } /// }
@ -89,6 +94,23 @@ pub(crate) const OUTPUT_BUFFER_SIZE: usize = 16384;
/// # } /// # }
/// ``` /// ```
pub trait Plugin: Sync { 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<Box<dyn PluginCommand<Plugin = Self>>> { vec![] }
/// # }
/// ```
fn version(&self) -> String;
/// The commands supported by the plugin /// The commands supported by the plugin
/// ///
/// Each [`PluginCommand`] contains both the signature of the command and the functionality it /// Each [`PluginCommand`] contains both the signature of the command and the functionality it
@ -216,6 +238,7 @@ pub trait Plugin: Sync {
/// # struct MyPlugin; /// # struct MyPlugin;
/// # impl MyPlugin { fn new() -> Self { Self }} /// # impl MyPlugin { fn new() -> Self { Self }}
/// # impl Plugin for MyPlugin { /// # impl Plugin for MyPlugin {
/// # fn version(&self) -> String { "0.0.0".into() }
/// # fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {todo!();} /// # fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {todo!();}
/// # } /// # }
/// fn main() { /// fn main() {
@ -504,6 +527,12 @@ where
} }
match plugin_call { 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 // Sending the signature back to nushell to create the declaration definition
ReceivedPluginCall::Signature { engine } => { ReceivedPluginCall::Signature { engine } => {
let sigs = commands let sigs = commands

View File

@ -5,7 +5,7 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-pretty-hex"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
name = "nu-pretty-hex" name = "nu-pretty-hex"
version = "0.94.3" version = "0.95.1"
[lib] [lib]
doctest = false doctest = false

View File

@ -5,7 +5,7 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-protocol"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
name = "nu-protocol" 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 # 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 bench = false
[dependencies] [dependencies]
nu-utils = { path = "../nu-utils", version = "0.94.3" } nu-utils = { path = "../nu-utils", version = "0.95.1" }
nu-path = { path = "../nu-path", version = "0.94.3" } nu-path = { path = "../nu-path", version = "0.95.1" }
nu-system = { path = "../nu-system", version = "0.94.3" } nu-system = { path = "../nu-system", version = "0.95.1" }
nu-derive-value = { path = "../nu-derive-value", version = "0.94.3" } nu-derive-value = { path = "../nu-derive-value", version = "0.95.1" }
brotli = { workspace = true, optional = true } brotli = { workspace = true, optional = true }
byte-unit = { version = "5.1", features = [ "serde" ] } byte-unit = { version = "5.1", features = [ "serde" ] }
@ -47,7 +47,7 @@ plugin = [
serde_json = { workspace = true } serde_json = { workspace = true }
strum = "0.26" strum = "0.26"
strum_macros = "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 } pretty_assertions = { workspace = true }
rstest = { workspace = true } rstest = { workspace = true }
tempfile = { workspace = true } tempfile = { workspace = true }

View File

@ -32,8 +32,11 @@ pub enum Expr {
Keyword(Box<Keyword>), Keyword(Box<Keyword>),
ValueWithUnit(Box<ValueWithUnit>), ValueWithUnit(Box<ValueWithUnit>),
DateTime(chrono::DateTime<FixedOffset>), DateTime(chrono::DateTime<FixedOffset>),
/// The boolean is `true` if the string is quoted.
Filepath(String, bool), Filepath(String, bool),
/// The boolean is `true` if the string is quoted.
Directory(String, bool), Directory(String, bool),
/// The boolean is `true` if the string is quoted.
GlobPattern(String, bool), GlobPattern(String, bool),
String(String), String(String),
RawString(String), RawString(String),
@ -43,6 +46,8 @@ pub enum Expr {
Overlay(Option<BlockId>), // block ID of the overlay's origin module Overlay(Option<BlockId>), // block ID of the overlay's origin module
Signature(Box<Signature>), Signature(Box<Signature>),
StringInterpolation(Vec<Expression>), StringInterpolation(Vec<Expression>),
/// The boolean is `true` if the string is quoted.
GlobInterpolation(Vec<Expression>, bool),
Nothing, Nothing,
Garbage, Garbage,
} }
@ -84,6 +89,7 @@ impl Expr {
| Expr::RawString(_) | Expr::RawString(_)
| Expr::CellPath(_) | Expr::CellPath(_)
| Expr::StringInterpolation(_) | Expr::StringInterpolation(_)
| Expr::GlobInterpolation(_, _)
| Expr::Nothing => { | Expr::Nothing => {
// These expressions do not use the output of the pipeline in any meaningful way, // 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`. // so we can discard the previous output by redirecting it to `Null`.

View File

@ -232,7 +232,7 @@ impl Expression {
} }
false false
} }
Expr::StringInterpolation(items) => { Expr::StringInterpolation(items) | Expr::GlobInterpolation(items, _) => {
for i in items { for i in items {
if i.has_in_variable(working_set) { if i.has_in_variable(working_set) {
return true; return true;
@ -441,7 +441,7 @@ impl Expression {
Expr::Signature(_) => {} Expr::Signature(_) => {}
Expr::String(_) => {} Expr::String(_) => {}
Expr::RawString(_) => {} Expr::RawString(_) => {}
Expr::StringInterpolation(items) => { Expr::StringInterpolation(items) | Expr::GlobInterpolation(items, _) => {
for i in items { for i in items {
i.replace_span(working_set, replaced, new_span) i.replace_span(working_set, replaced, new_span)
} }

View File

@ -258,6 +258,7 @@ fn expr_to_string(engine_state: &EngineState, expr: &Expr) -> String {
Expr::Signature(_) => "signature".to_string(), Expr::Signature(_) => "signature".to_string(),
Expr::String(_) | Expr::RawString(_) => "string".to_string(), Expr::String(_) | Expr::RawString(_) => "string".to_string(),
Expr::StringInterpolation(_) => "string interpolation".to_string(), Expr::StringInterpolation(_) => "string interpolation".to_string(),
Expr::GlobInterpolation(_, _) => "glob interpolation".to_string(),
Expr::Subexpression(_) => "subexpression".to_string(), Expr::Subexpression(_) => "subexpression".to_string(),
Expr::Table(_) => "table".to_string(), Expr::Table(_) => "table".to_string(),
Expr::UnaryNot(_) => "unary not".to_string(), Expr::UnaryNot(_) => "unary not".to_string(),

View File

@ -81,7 +81,9 @@ pub(super) fn build_usage(comment_lines: &[&[u8]]) -> (String, String) {
usage.push_str(&comment_line); 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()) (brief_usage.to_string(), extra_usage.to_string())
} else { } else {
(usage, String::default()) (usage, String::default())

View File

@ -290,6 +290,15 @@ pub trait Eval {
Ok(Value::string(str, expr_span)) 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::<D>(state, mut_state, expr).map(|v| v.to_expanded_string(", ", &config)))
.collect::<Result<String, _>>()?;
Ok(Value::glob(str, *quoted, expr_span))
}
Expr::Overlay(_) => Self::eval_overlay(state, expr_span), Expr::Overlay(_) => Self::eval_overlay(state, expr_span),
Expr::GlobPattern(pattern, quoted) => { Expr::GlobPattern(pattern, quoted) => {
// GlobPattern is similar to Filepath // GlobPattern is similar to Filepath

Some files were not shown because too many files have changed in this diff Show More