Merge main into sort-custom

This commit is contained in:
132ikl 2024-07-02 10:35:30 -04:00
commit f940edbee2
179 changed files with 7744 additions and 1946 deletions

View File

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

View File

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

View File

@ -27,7 +27,7 @@ jobs:
# if: github.repository == 'nushell/nightly'
steps:
- name: Checkout
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
if: github.repository == 'nushell/nightly'
with:
ref: main
@ -36,10 +36,10 @@ jobs:
token: ${{ secrets.WORKFLOW_TOKEN }}
- name: Setup Nushell
uses: hustcer/setup-nu@v3.11
uses: hustcer/setup-nu@v3.12
if: github.repository == 'nushell/nightly'
with:
version: 0.93.0
version: 0.95.0
# Synchronize the main branch of nightly repo with the main branch of Nushell official repo
- name: Prepare for Nightly Release
@ -112,7 +112,7 @@ jobs:
runs-on: ${{matrix.os}}
steps:
- uses: actions/checkout@v4.1.6
- uses: actions/checkout@v4.1.7
with:
ref: main
fetch-depth: 0
@ -128,9 +128,9 @@ jobs:
rustflags: ''
- name: Setup Nushell
uses: hustcer/setup-nu@v3.11
uses: hustcer/setup-nu@v3.12
with:
version: 0.93.0
version: 0.95.0
- name: Release Nu Binary
id: nu
@ -161,7 +161,7 @@ jobs:
# REF: https://github.com/marketplace/actions/gh-release
# Create a release only in nushell/nightly repo
- name: Publish Archive
uses: softprops/action-gh-release@v2.0.5
uses: softprops/action-gh-release@v2.0.6
if: ${{ startsWith(github.repository, 'nushell/nightly') }}
with:
prerelease: true
@ -181,14 +181,14 @@ jobs:
- name: Waiting for Release
run: sleep 1800
- uses: actions/checkout@v4.1.6
- uses: actions/checkout@v4.1.7
with:
ref: main
- name: Setup Nushell
uses: hustcer/setup-nu@v3.11
uses: hustcer/setup-nu@v3.12
with:
version: 0.93.0
version: 0.95.0
# Keep the last a few releases
- name: Delete Older Releases

View File

@ -62,7 +62,7 @@ jobs:
runs-on: ${{matrix.os}}
steps:
- uses: actions/checkout@v4.1.6
- uses: actions/checkout@v4.1.7
- name: Update Rust Toolchain Target
run: |
@ -76,9 +76,9 @@ jobs:
rustflags: ''
- name: Setup Nushell
uses: hustcer/setup-nu@v3.11
uses: hustcer/setup-nu@v3.12
with:
version: 0.93.0
version: 0.95.0
- name: Release Nu Binary
id: nu
@ -91,7 +91,7 @@ jobs:
# REF: https://github.com/marketplace/actions/gh-release
- name: Publish Archive
uses: softprops/action-gh-release@v2.0.5
uses: softprops/action-gh-release@v2.0.6
if: ${{ startsWith(github.ref, 'refs/tags/') }}
with:
draft: true

View File

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

366
Cargo.lock generated
View File

@ -1117,6 +1117,36 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "curl"
version = "0.4.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e2161dd6eba090ff1594084e95fd67aeccf04382ffea77999ea94ed42ec67b6"
dependencies = [
"curl-sys",
"libc",
"openssl-probe",
"openssl-sys",
"schannel",
"socket2",
"windows-sys 0.52.0",
]
[[package]]
name = "curl-sys"
version = "0.4.73+curl-8.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "450ab250ecf17227c39afb9a2dd9261dc0035cb80f2612472fc0c4aac2dcb84d"
dependencies = [
"cc",
"libc",
"libz-sys",
"openssl-sys",
"pkg-config",
"vcpkg",
"windows-sys 0.52.0",
]
[[package]]
name = "deranged"
version = "0.3.11"
@ -1148,6 +1178,12 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "deunicode"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00"
[[package]]
name = "dialoguer"
version = "0.11.0"
@ -1227,6 +1263,12 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "doctest-file"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562"
[[package]]
name = "downcast-rs"
version = "1.2.1"
@ -1324,6 +1366,16 @@ dependencies = [
"syn 2.0.60",
]
[[package]]
name = "env_filter"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.8.4"
@ -1334,6 +1386,19 @@ dependencies = [
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"humantime",
"log",
]
[[package]]
name = "equivalent"
version = "1.0.1"
@ -1848,12 +1913,26 @@ checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7"
dependencies = [
"log",
"mac",
"markup5ever",
"markup5ever 0.11.0",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "html5ever"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4"
dependencies = [
"log",
"mac",
"markup5ever 0.12.1",
"proc-macro2",
"quote",
"syn 2.0.60",
]
[[package]]
name = "http"
version = "0.2.12"
@ -1900,6 +1979,12 @@ dependencies = [
"thiserror",
]
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
version = "0.14.28"
@ -1989,12 +2074,6 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "indoc"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
[[package]]
name = "inotify"
version = "0.9.6"
@ -2026,10 +2105,11 @@ dependencies = [
[[package]]
name = "interprocess"
version = "2.1.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4d0250d41da118226e55b3d50ca3f0d9e0a0f6829b92f543ac0054aeea1572"
checksum = "67bafc2f5dbdad79a6d925649758d5472647b416028099f0b829d1b67fdd47d3"
dependencies = [
"doctest-file",
"libc",
"recvmsg",
"widestring",
@ -2516,6 +2596,32 @@ dependencies = [
"tendril",
]
[[package]]
name = "markup5ever"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45"
dependencies = [
"log",
"phf 0.11.2",
"phf_codegen 0.11.2",
"string_cache",
"string_cache_codegen",
"tendril",
]
[[package]]
name = "markup5ever_rcdom"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18"
dependencies = [
"html5ever 0.27.0",
"markup5ever 0.12.1",
"tendril",
"xml5ever",
]
[[package]]
name = "md-5"
version = "0.10.6"
@ -2762,7 +2868,7 @@ dependencies = [
[[package]]
name = "nu"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"assert_cmd",
"crossterm",
@ -2815,7 +2921,7 @@ dependencies = [
[[package]]
name = "nu-cli"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"chrono",
"crossterm",
@ -2850,7 +2956,7 @@ dependencies = [
[[package]]
name = "nu-cmd-base"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"indexmap",
"miette",
@ -2862,7 +2968,7 @@ dependencies = [
[[package]]
name = "nu-cmd-extra"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"fancy-regex",
"heck 0.5.0",
@ -2887,7 +2993,7 @@ dependencies = [
[[package]]
name = "nu-cmd-lang"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"itertools 0.12.1",
"nu-engine",
@ -2899,7 +3005,7 @@ dependencies = [
[[package]]
name = "nu-cmd-plugin"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"itertools 0.12.1",
"nu-engine",
@ -2910,7 +3016,7 @@ dependencies = [
[[package]]
name = "nu-color-config"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"nu-ansi-term",
"nu-engine",
@ -2922,7 +3028,7 @@ dependencies = [
[[package]]
name = "nu-command"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"alphanumeric-sort",
"base64 0.22.1",
@ -2937,6 +3043,7 @@ dependencies = [
"chrono-tz 0.8.6",
"crossterm",
"csv",
"deunicode",
"dialoguer",
"digest",
"dirs-next",
@ -3031,7 +3138,7 @@ dependencies = [
[[package]]
name = "nu-derive-value"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"convert_case",
"proc-macro-error",
@ -3042,7 +3149,7 @@ dependencies = [
[[package]]
name = "nu-engine"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"nu-glob",
"nu-path",
@ -3052,7 +3159,7 @@ dependencies = [
[[package]]
name = "nu-explore"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"ansi-str",
"anyhow",
@ -3077,14 +3184,14 @@ dependencies = [
[[package]]
name = "nu-glob"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"doc-comment",
]
[[package]]
name = "nu-json"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"linked-hash-map",
"num-traits",
@ -3094,7 +3201,7 @@ dependencies = [
[[package]]
name = "nu-lsp"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"assert-json-diff",
"crossbeam-channel",
@ -3115,7 +3222,7 @@ dependencies = [
[[package]]
name = "nu-parser"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"bytesize",
"chrono",
@ -3131,7 +3238,7 @@ dependencies = [
[[package]]
name = "nu-path"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"dirs-next",
"omnipath",
@ -3140,7 +3247,7 @@ dependencies = [
[[package]]
name = "nu-plugin"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"log",
"nix",
@ -3155,7 +3262,7 @@ dependencies = [
[[package]]
name = "nu-plugin-core"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"interprocess",
"log",
@ -3169,7 +3276,7 @@ dependencies = [
[[package]]
name = "nu-plugin-engine"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"log",
"nu-engine",
@ -3184,7 +3291,7 @@ dependencies = [
[[package]]
name = "nu-plugin-protocol"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"bincode",
"nu-protocol",
@ -3196,7 +3303,7 @@ dependencies = [
[[package]]
name = "nu-plugin-test-support"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"nu-ansi-term",
"nu-cmd-lang",
@ -3214,7 +3321,7 @@ dependencies = [
[[package]]
name = "nu-pretty-hex"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"heapless",
"nu-ansi-term",
@ -3223,7 +3330,7 @@ dependencies = [
[[package]]
name = "nu-protocol"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"brotli",
"byte-unit",
@ -3256,7 +3363,7 @@ dependencies = [
[[package]]
name = "nu-std"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"log",
"miette",
@ -3267,7 +3374,7 @@ dependencies = [
[[package]]
name = "nu-system"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"chrono",
"itertools 0.12.1",
@ -3285,7 +3392,7 @@ dependencies = [
[[package]]
name = "nu-table"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"fancy-regex",
"nu-ansi-term",
@ -3299,7 +3406,7 @@ dependencies = [
[[package]]
name = "nu-term-grid"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"nu-utils",
"unicode-width",
@ -3307,7 +3414,7 @@ dependencies = [
[[package]]
name = "nu-test-support"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"nu-glob",
"nu-path",
@ -3319,7 +3426,7 @@ dependencies = [
[[package]]
name = "nu-utils"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"crossterm_winapi",
"log",
@ -3345,7 +3452,7 @@ dependencies = [
[[package]]
name = "nu_plugin_example"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"nu-cmd-lang",
"nu-plugin",
@ -3355,7 +3462,7 @@ dependencies = [
[[package]]
name = "nu_plugin_formats"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"eml-parser",
"ical",
@ -3368,7 +3475,7 @@ dependencies = [
[[package]]
name = "nu_plugin_gstat"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"git2",
"nu-plugin",
@ -3377,7 +3484,7 @@ dependencies = [
[[package]]
name = "nu_plugin_inc"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"nu-plugin",
"nu-protocol",
@ -3386,12 +3493,14 @@ dependencies = [
[[package]]
name = "nu_plugin_polars"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"chrono",
"chrono-tz 0.9.0",
"env_logger 0.11.3",
"fancy-regex",
"indexmap",
"log",
"mimalloc",
"nu-cmd-lang",
"nu-command",
@ -3401,6 +3510,7 @@ dependencies = [
"nu-plugin",
"nu-plugin-test-support",
"nu-protocol",
"nu-utils",
"num",
"polars",
"polars-arrow",
@ -3409,7 +3519,7 @@ dependencies = [
"polars-plan",
"polars-utils",
"serde",
"sqlparser 0.47.0",
"sqlparser",
"tempfile",
"typetag",
"uuid",
@ -3417,19 +3527,22 @@ dependencies = [
[[package]]
name = "nu_plugin_query"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"gjson",
"nu-plugin",
"nu-protocol",
"scraper",
"serde",
"serde_json",
"sxd-document",
"sxd-xpath",
"webpage",
]
[[package]]
name = "nu_plugin_stress_internals"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"interprocess",
"serde",
@ -3555,7 +3668,7 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
name = "nuon"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"chrono",
"fancy-regex",
@ -4019,9 +4132,9 @@ dependencies = [
[[package]]
name = "polars"
version = "0.40.0"
version = "0.41.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e148396dca5496566880fa19374f3f789a29db94e3eb458afac1497b4bac5442"
checksum = "ce49e10a756f68eb99c102c6b2a0cbc0c583a0fa7263536ad0913d94be878d2d"
dependencies = [
"getrandom",
"polars-arrow",
@ -4039,9 +4152,9 @@ dependencies = [
[[package]]
name = "polars-arrow"
version = "0.40.0"
version = "0.41.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cb5e11cd0752ae022fa6ca3afa50a14b0301b7ce53c0135828fbb0f4fa8303e"
checksum = "b436f83f62e864f0d91871e26528f2c5552c7cf07c8d77547f1b8e3fde22bd27"
dependencies = [
"ahash 0.8.11",
"atoi",
@ -4087,9 +4200,9 @@ dependencies = [
[[package]]
name = "polars-compute"
version = "0.40.0"
version = "0.41.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89fc4578f826234cdecb782952aa9c479dc49373f81694a7b439c70b6f609ba0"
checksum = "f6758f834f07e622a2f859bebb542b2b7f8879b8704dbb2b2bbab460ddcdca4b"
dependencies = [
"bytemuck",
"either",
@ -4103,9 +4216,9 @@ dependencies = [
[[package]]
name = "polars-core"
version = "0.40.0"
version = "0.41.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e490c6bace1366a558feea33d1846f749a8ca90bd72a6748752bc65bb4710b2a"
checksum = "7ed262e9bdda15a12a9bfcfc9200bec5253335633dbd86cf5b94fda0194244b3"
dependencies = [
"ahash 0.8.11",
"bitflags 2.5.0",
@ -4137,9 +4250,9 @@ dependencies = [
[[package]]
name = "polars-error"
version = "0.40.0"
version = "0.41.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08888f58e61599b00f5ea0c2ccdc796b54b9859559cc0d4582733509451fa01a"
checksum = "53e1707a17475ba5e74c349154b415e3148a1a275e395965427971b5e53ad621"
dependencies = [
"avro-schema",
"polars-arrow-format",
@ -4150,9 +4263,9 @@ dependencies = [
[[package]]
name = "polars-expr"
version = "0.40.0"
version = "0.41.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4173591920fe56ad55af025f92eb0d08421ca85705c326a640c43856094e3484"
checksum = "31a9688d5842e7a7fbad88e67a174778794a91d97d3bba1b3c09dd1656fee3b2"
dependencies = [
"ahash 0.8.11",
"bitflags 2.5.0",
@ -4170,9 +4283,9 @@ dependencies = [
[[package]]
name = "polars-io"
version = "0.40.0"
version = "0.41.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5842896aea46d975b425d63f156f412aed3cfde4c257b64fb1f43ceea288074e"
checksum = "18798dacd94fb9263f65f63f0feab0908675422646d6f7fc37043b85ff6dca35"
dependencies = [
"ahash 0.8.11",
"async-trait",
@ -4211,9 +4324,9 @@ dependencies = [
[[package]]
name = "polars-json"
version = "0.40.0"
version = "0.41.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "160cbad0145b93ac6a88639aadfa6f7d7c769d05a8674f9b7e895b398cae9901"
checksum = "044ea319f667efbf8007c4c38171c2956e0e7f9b078eb66e31e82f80d1e14b51"
dependencies = [
"ahash 0.8.11",
"chrono",
@ -4232,19 +4345,21 @@ dependencies = [
[[package]]
name = "polars-lazy"
version = "0.40.0"
version = "0.41.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e805ea2ebbc6b7749b0afb31b7fc5d32b42b57ba29b984549d43d3a16114c4a5"
checksum = "74a11994c2211f2e99d9ac31776fd7c2c0607d5fe62d5b5db9e396f7d663f3d5"
dependencies = [
"ahash 0.8.11",
"bitflags 2.5.0",
"glob",
"memchr",
"once_cell",
"polars-arrow",
"polars-core",
"polars-expr",
"polars-io",
"polars-json",
"polars-mem-engine",
"polars-ops",
"polars-pipe",
"polars-plan",
@ -4256,10 +4371,29 @@ dependencies = [
]
[[package]]
name = "polars-ops"
version = "0.40.0"
name = "polars-mem-engine"
version = "0.41.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b0aed7e169c81b98457641cf82b251f52239a668916c2e683abd1f38df00d58"
checksum = "5acd5fde6fadaddfcae3227ec5b64121007928f8e68870c80653438e20c1c587"
dependencies = [
"polars-arrow",
"polars-core",
"polars-error",
"polars-expr",
"polars-io",
"polars-json",
"polars-ops",
"polars-plan",
"polars-time",
"polars-utils",
"rayon",
]
[[package]]
name = "polars-ops"
version = "0.41.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4170c59e974727941edfb722f6d430ed623be9e7f30581ee00832c907f1b9fd"
dependencies = [
"ahash 0.8.11",
"argminmax",
@ -4293,9 +4427,9 @@ dependencies = [
[[package]]
name = "polars-parquet"
version = "0.40.0"
version = "0.41.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c70670a9e51cac66d0e77fd20b5cc957dbcf9f2660d410633862bb72f846d5b8"
checksum = "c684638c36c60c691d707d414249fe8af4a19a35a39d418464b140fe23732e5d"
dependencies = [
"ahash 0.8.11",
"async-stream",
@ -4308,9 +4442,11 @@ dependencies = [
"num-traits",
"parquet-format-safe",
"polars-arrow",
"polars-compute",
"polars-error",
"polars-utils",
"seq-macro",
"serde",
"simdutf8",
"snap",
"streaming-decompression",
@ -4319,9 +4455,9 @@ dependencies = [
[[package]]
name = "polars-pipe"
version = "0.40.0"
version = "0.41.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a40ae1b3c74ee07e2d1f7cbf56c5d6e15969e45d9b6f0903bd2acaf783ba436"
checksum = "832af9fbebc4c074d95fb19e1ef9e1bf37c343641238c2476febff296a7028ea"
dependencies = [
"crossbeam-channel",
"crossbeam-queue",
@ -4345,9 +4481,9 @@ dependencies = [
[[package]]
name = "polars-plan"
version = "0.40.0"
version = "0.41.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8daa3541ae7e9af311a4389bc2b21f83349c34c723cc67fa524cdefdaa172d90"
checksum = "801390ea815c05c9cf8337f3148090c9c10c9595a839fa0706b77cc2405b4466"
dependencies = [
"ahash 0.8.11",
"bytemuck",
@ -4375,9 +4511,9 @@ dependencies = [
[[package]]
name = "polars-row"
version = "0.40.0"
version = "0.41.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "deb285f2f3a65b00dd06bef16bb9f712dbb5478f941dab5cf74f9f016d382e40"
checksum = "dee955e91b605fc91db4d0a8ea02609d3a09ff79256d905214a2a6f758cd6f7b"
dependencies = [
"bytemuck",
"polars-arrow",
@ -4387,9 +4523,9 @@ dependencies = [
[[package]]
name = "polars-sql"
version = "0.40.0"
version = "0.41.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a724f699d194cb02c25124d3832f7d4d77f387f1a89ee42f6b9e88ec561d4ad9"
checksum = "d89c00a4b399501d5bd478e8e8022b9391047fe8570324ecba20c4e4833c0e87"
dependencies = [
"hex",
"once_cell",
@ -4397,18 +4533,20 @@ dependencies = [
"polars-core",
"polars-error",
"polars-lazy",
"polars-ops",
"polars-plan",
"polars-time",
"rand",
"serde",
"serde_json",
"sqlparser 0.39.0",
"sqlparser",
]
[[package]]
name = "polars-time"
version = "0.40.0"
version = "0.41.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87ebec238d8b6200d9f0c3ce411c8441e950bd5a7df7806b8172d06c1d5a4b97"
checksum = "9689b3aff99d64befe300495528bdc44c36d2656c3a8b242a790d4f43df027fc"
dependencies = [
"atoi",
"bytemuck",
@ -4428,9 +4566,9 @@ dependencies = [
[[package]]
name = "polars-utils"
version = "0.40.0"
version = "0.41.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34e1a907c63abf71e5f21467e2e4ff748896c28196746f631c6c25512ec6102c"
checksum = "12081e346983a91e26f395597e1d53dea1b4ecd694653aee1cc402d2fae01f04"
dependencies = [
"ahash 0.8.11",
"bytemuck",
@ -4666,7 +4804,7 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6"
dependencies = [
"env_logger",
"env_logger 0.8.4",
"log",
"rand",
]
@ -4745,21 +4883,21 @@ dependencies = [
[[package]]
name = "ratatui"
version = "0.26.2"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a564a852040e82671dc50a37d88f3aa83bbc690dfc6844cfe7a2591620206a80"
checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef"
dependencies = [
"bitflags 2.5.0",
"cassowary",
"compact_str",
"crossterm",
"indoc",
"itertools 0.12.1",
"lru",
"paste",
"stability",
"strum",
"unicode-segmentation",
"unicode-truncate",
"unicode-width",
]
@ -5218,7 +5356,7 @@ dependencies = [
"ahash 0.8.11",
"cssparser",
"ego-tree",
"html5ever",
"html5ever 0.26.0",
"once_cell",
"selectors",
"tendril",
@ -5434,9 +5572,9 @@ dependencies = [
[[package]]
name = "shadow-rs"
version = "0.28.0"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d75516bdaee8f640543ad1f6e292448c23ce57143f812c3736ab4b0874383df"
checksum = "0a600f795d0894cda22235b44eea4b85c2a35b405f65523645ac8e35b306817a"
dependencies = [
"const_format",
"is_debug",
@ -5581,15 +5719,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "sqlparser"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743b4dc2cbde11890ccb254a8fc9d537fa41b36da00de2a1c5e9848c9bc42bd7"
dependencies = [
"log",
]
[[package]]
name = "sqlparser"
version = "0.47.0"
@ -6307,6 +6436,16 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
[[package]]
name = "unicode-truncate"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5fbabedabe362c618c714dbefda9927b5afc8e2a8102f47f081089a9019226"
dependencies = [
"itertools 0.12.1",
"unicode-width",
]
[[package]]
name = "unicode-width"
version = "0.1.12"
@ -6486,9 +6625,9 @@ checksum = "425a23c7b7145bc7620c9c445817c37b1f78b6790aee9f208133f3c028975b60"
[[package]]
name = "uuid"
version = "1.8.0"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439"
dependencies = [
"getrandom",
"serde",
@ -6731,6 +6870,20 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "webpage"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70862efc041d46e6bbaa82bb9c34ae0596d090e86cbd14bd9e93b36ee6802eac"
dependencies = [
"curl",
"html5ever 0.27.0",
"markup5ever_rcdom",
"serde",
"serde_json",
"url",
]
[[package]]
name = "which"
version = "6.0.1"
@ -7143,6 +7296,17 @@ dependencies = [
"rustix",
]
[[package]]
name = "xml5ever"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69"
dependencies = [
"log",
"mac",
"markup5ever 0.12.1",
]
[[package]]
name = "xxhash-rust"
version = "0.8.10"

View File

@ -11,7 +11,7 @@ license = "MIT"
name = "nu"
repository = "https://github.com/nushell/nushell"
rust-version = "1.77.2"
version = "0.94.3"
version = "0.95.1"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -80,6 +80,7 @@ crossbeam-channel = "0.5.8"
crossterm = "0.27"
csv = "1.3"
ctrlc = "3.4"
deunicode = "1.6.0"
dialoguer = { default-features = false, version = "0.11" }
digest = { default-features = false, version = "0.10" }
dirs-next = "2.0"
@ -95,7 +96,7 @@ heck = "0.5.0"
human-date-parser = "0.1.1"
indexmap = "2.2"
indicatif = "0.17"
interprocess = "2.1.0"
interprocess = "2.2.0"
is_executable = "1.0"
itertools = "0.12"
libc = "0.2"
@ -172,7 +173,7 @@ uu_mv = "0.0.26"
uu_whoami = "0.0.26"
uu_uname = "0.0.26"
uucore = "0.0.26"
uuid = "1.8.0"
uuid = "1.9.1"
v_htmlescape = "0.15.0"
wax = "0.6"
which = "6.0.0"
@ -180,22 +181,22 @@ windows = "0.54"
winreg = "0.52"
[dependencies]
nu-cli = { path = "./crates/nu-cli", version = "0.94.3" }
nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.94.3" }
nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.94.3" }
nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.94.3", optional = true }
nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.94.3" }
nu-command = { path = "./crates/nu-command", version = "0.94.3" }
nu-engine = { path = "./crates/nu-engine", version = "0.94.3" }
nu-explore = { path = "./crates/nu-explore", version = "0.94.3" }
nu-lsp = { path = "./crates/nu-lsp/", version = "0.94.3" }
nu-parser = { path = "./crates/nu-parser", version = "0.94.3" }
nu-path = { path = "./crates/nu-path", version = "0.94.3" }
nu-plugin-engine = { path = "./crates/nu-plugin-engine", optional = true, version = "0.94.3" }
nu-protocol = { path = "./crates/nu-protocol", version = "0.94.3" }
nu-std = { path = "./crates/nu-std", version = "0.94.3" }
nu-system = { path = "./crates/nu-system", version = "0.94.3" }
nu-utils = { path = "./crates/nu-utils", version = "0.94.3" }
nu-cli = { path = "./crates/nu-cli", version = "0.95.1" }
nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.95.1" }
nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.95.1" }
nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.95.1", optional = true }
nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.95.1" }
nu-command = { path = "./crates/nu-command", version = "0.95.1" }
nu-engine = { path = "./crates/nu-engine", version = "0.95.1" }
nu-explore = { path = "./crates/nu-explore", version = "0.95.1" }
nu-lsp = { path = "./crates/nu-lsp/", version = "0.95.1" }
nu-parser = { path = "./crates/nu-parser", version = "0.95.1" }
nu-path = { path = "./crates/nu-path", version = "0.95.1" }
nu-plugin-engine = { path = "./crates/nu-plugin-engine", optional = true, version = "0.95.1" }
nu-protocol = { path = "./crates/nu-protocol", version = "0.95.1" }
nu-std = { path = "./crates/nu-std", version = "0.95.1" }
nu-system = { path = "./crates/nu-system", version = "0.95.1" }
nu-utils = { path = "./crates/nu-utils", version = "0.95.1" }
reedline = { workspace = true, features = ["bashisms", "sqlite"] }
@ -225,9 +226,9 @@ nix = { workspace = true, default-features = false, features = [
] }
[dev-dependencies]
nu-test-support = { path = "./crates/nu-test-support", version = "0.94.3" }
nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.94.3" }
nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.94.3" }
nu-test-support = { path = "./crates/nu-test-support", version = "0.95.1" }
nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.95.1" }
nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.95.1" }
assert_cmd = "2.0"
dirs-next = { workspace = true }
tango-bench = "0.5"
@ -310,4 +311,4 @@ bench = false
# Run individual benchmarks like `cargo bench -- <regex>` e.g. `cargo bench -- parse`
[[bench]]
name = "benchmarks"
harness = false
harness = false

View File

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

View File

@ -8,7 +8,7 @@ use nu_protocol::{
report_error_new, HistoryFileFormat, PipelineData,
};
#[cfg(feature = "plugin")]
use nu_utils::utils::perf;
use nu_utils::perf;
use std::path::PathBuf;
#[cfg(feature = "plugin")]
@ -53,13 +53,10 @@ pub fn read_plugin_file(
// Reading signatures from plugin registry file
// The plugin.msgpackz file stores the parsed signature collected from each registered plugin
add_plugin_file(engine_state, plugin_file.clone(), storage_path);
perf(
perf!(
"add plugin file to engine_state",
start_time,
file!(),
line!(),
column!(),
engine_state.get_config().use_ansi_coloring,
engine_state.get_config().use_ansi_coloring
);
start_time = std::time::Instant::now();
@ -137,13 +134,10 @@ pub fn read_plugin_file(
}
};
perf(
perf!(
&format!("read plugin file {}", plugin_path.display()),
start_time,
file!(),
line!(),
column!(),
engine_state.get_config().use_ansi_coloring,
engine_state.get_config().use_ansi_coloring
);
start_time = std::time::Instant::now();
@ -156,13 +150,10 @@ pub fn read_plugin_file(
return;
}
perf(
perf!(
&format!("load plugin file {}", plugin_path.display()),
start_time,
file!(),
line!(),
column!(),
engine_state.get_config().use_ansi_coloring,
engine_state.get_config().use_ansi_coloring
);
}
}
@ -344,7 +335,10 @@ pub fn migrate_old_plugin_file(engine_state: &EngineState, storage_path: &str) -
name: identity.name().to_owned(),
filename: identity.filename().to_owned(),
shell: identity.shell().map(|p| p.to_owned()),
data: PluginRegistryItemData::Valid { commands },
data: PluginRegistryItemData::Valid {
metadata: Default::default(),
commands,
},
});
}
@ -378,13 +372,10 @@ pub fn migrate_old_plugin_file(engine_state: &EngineState, storage_path: &str) -
);
}
perf(
perf!(
"migrate old plugin file",
start_time,
file!(),
line!(),
column!(),
engine_state.get_config().use_ansi_coloring,
engine_state.get_config().use_ansi_coloring
);
true
}

View File

@ -31,7 +31,7 @@ use nu_protocol::{
};
use nu_utils::{
filesystem::{have_permission, PermissionResult},
utils::perf,
perf,
};
use reedline::{
CursorConfig, CwdAwareHinter, DefaultCompleter, EditCommand, Emacs, FileBackedHistory,
@ -89,14 +89,7 @@ pub fn evaluate_repl(
if let Err(e) = convert_env_values(engine_state, &unique_stack) {
report_error_new(engine_state, &e);
}
perf(
"translate env vars",
start_time,
file!(),
line!(),
column!(),
use_color,
);
perf!("translate env vars", start_time, use_color);
// seed env vars
unique_stack.add_env_var(
@ -225,28 +218,14 @@ fn get_line_editor(
// Now that reedline is created, get the history session id and store it in engine_state
store_history_id_in_engine(engine_state, &line_editor);
perf(
"setup reedline",
start_time,
file!(),
line!(),
column!(),
use_color,
);
perf!("setup reedline", start_time, use_color);
if let Some(history) = engine_state.history_config() {
start_time = std::time::Instant::now();
line_editor = setup_history(nushell_path, engine_state, line_editor, history)?;
perf(
"setup history",
start_time,
file!(),
line!(),
column!(),
use_color,
);
perf!("setup history", start_time, use_color);
}
Ok(line_editor)
}
@ -289,28 +268,14 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
if let Err(err) = engine_state.merge_env(&mut stack, cwd) {
report_error_new(engine_state, &err);
}
perf(
"merge env",
start_time,
file!(),
line!(),
column!(),
use_color,
);
perf!("merge env", start_time, use_color);
start_time = std::time::Instant::now();
// Reset the ctrl-c handler
if let Some(ctrlc) = &mut engine_state.ctrlc {
ctrlc.store(false, Ordering::SeqCst);
}
perf(
"reset ctrlc",
start_time,
file!(),
line!(),
column!(),
use_color,
);
perf!("reset ctrlc", start_time, use_color);
start_time = std::time::Instant::now();
// Right before we start our prompt and take input from the user,
@ -320,14 +285,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
report_error_new(engine_state, &err);
}
}
perf(
"pre-prompt hook",
start_time,
file!(),
line!(),
column!(),
use_color,
);
perf!("pre-prompt hook", start_time, use_color);
start_time = std::time::Instant::now();
// Next, check all the environment variables they ask for
@ -336,14 +294,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
if let Err(error) = hook::eval_env_change_hook(env_change, engine_state, &mut stack) {
report_error_new(engine_state, &error)
}
perf(
"env-change hook",
start_time,
file!(),
line!(),
column!(),
use_color,
);
perf!("env-change hook", start_time, use_color);
let engine_reference = Arc::new(engine_state.clone());
let config = engine_state.get_config();
@ -355,14 +306,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
vi_normal: map_nucursorshape_to_cursorshape(config.cursor_shape_vi_normal),
emacs: map_nucursorshape_to_cursorshape(config.cursor_shape_emacs),
};
perf(
"get config/cursor config",
start_time,
file!(),
line!(),
column!(),
use_color,
);
perf!("get config/cursor config", start_time, use_color);
start_time = std::time::Instant::now();
// at this line we have cloned the state for the completer and the transient prompt
@ -394,14 +338,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
.with_ansi_colors(config.use_ansi_coloring)
.with_cursor_config(cursor_config);
perf(
"reedline builder",
start_time,
file!(),
line!(),
column!(),
use_color,
);
perf!("reedline builder", start_time, use_color);
let style_computer = StyleComputer::from_config(engine_state, &stack_arc);
@ -416,14 +353,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
line_editor.disable_hints()
};
perf(
"reedline coloring/style_computer",
start_time,
file!(),
line!(),
column!(),
use_color,
);
perf!("reedline coloring/style_computer", start_time, use_color);
start_time = std::time::Instant::now();
trace!("adding menus");
@ -433,14 +363,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
Reedline::create()
});
perf(
"reedline adding menus",
start_time,
file!(),
line!(),
column!(),
use_color,
);
perf!("reedline adding menus", start_time, use_color);
start_time = std::time::Instant::now();
let buffer_editor = get_editor(engine_state, &stack_arc, Span::unknown());
@ -457,14 +380,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
line_editor
};
perf(
"reedline buffer_editor",
start_time,
file!(),
line!(),
column!(),
use_color,
);
perf!("reedline buffer_editor", start_time, use_color);
if let Some(history) = engine_state.history_config() {
start_time = std::time::Instant::now();
@ -474,28 +390,14 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
}
}
perf(
"sync_history",
start_time,
file!(),
line!(),
column!(),
use_color,
);
perf!("sync_history", start_time, use_color);
}
start_time = std::time::Instant::now();
// Changing the line editor based on the found keybindings
line_editor = setup_keybindings(engine_state, line_editor);
perf(
"keybindings",
start_time,
file!(),
line!(),
column!(),
use_color,
);
perf!("keybindings", start_time, use_color);
start_time = std::time::Instant::now();
let config = &engine_state.get_config().clone();
@ -512,14 +414,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
nu_prompt,
);
perf(
"update_prompt",
start_time,
file!(),
line!(),
column!(),
use_color,
);
perf!("update_prompt", start_time, use_color);
*entry_num += 1;
@ -546,14 +441,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
// so we should avoid it or making stack cheaper to clone.
let mut stack = Arc::unwrap_or_clone(stack_arc);
perf(
"line_editor setup",
start_time,
file!(),
line!(),
column!(),
use_color,
);
perf!("line_editor setup", start_time, use_color);
let line_editor_input_time = std::time::Instant::now();
match input {
@ -590,14 +478,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
}
}
perf(
"pre_execution_hook",
start_time,
file!(),
line!(),
column!(),
use_color,
);
perf!("pre_execution_hook", start_time, use_color);
let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
repl.cursor_pos = line_editor.current_insertion_point();
@ -612,26 +493,20 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
run_ansi_sequence(VSCODE_PRE_EXECUTION_MARKER);
perf(
perf!(
"pre_execute_marker (633;C) ansi escape sequence",
start_time,
file!(),
line!(),
column!(),
use_color,
use_color
);
} else if shell_integration_osc133 {
start_time = Instant::now();
run_ansi_sequence(PRE_EXECUTION_MARKER);
perf(
perf!(
"pre_execute_marker (133;C) ansi escape sequence",
start_time,
file!(),
line!(),
column!(),
use_color,
use_color
);
}
} else if shell_integration_osc133 {
@ -639,13 +514,10 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
run_ansi_sequence(PRE_EXECUTION_MARKER);
perf(
perf!(
"pre_execute_marker (133;C) ansi escape sequence",
start_time,
file!(),
line!(),
column!(),
use_color,
use_color
);
}
@ -769,22 +641,16 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
);
}
}
perf(
perf!(
"processing line editor input",
line_editor_input_time,
file!(),
line!(),
column!(),
use_color,
use_color
);
perf(
perf!(
"time between prompts in line editor loop",
loop_start_time,
file!(),
line!(),
column!(),
use_color,
use_color
);
(true, stack, line_editor)
@ -1061,14 +927,7 @@ fn run_shell_integration_osc2(
// ESC]2;stringBEL -- Set window title to string
run_ansi_sequence(&format!("\x1b]2;{title}\x07"));
perf(
"set title with command osc2",
start_time,
file!(),
line!(),
column!(),
use_color,
);
perf!("set title with command osc2", start_time, use_color);
}
}
@ -1093,13 +952,10 @@ fn run_shell_integration_osc7(
percent_encoding::utf8_percent_encode(&path, percent_encoding::CONTROLS)
));
perf(
perf!(
"communicate path to terminal with osc7",
start_time,
file!(),
line!(),
column!(),
use_color,
use_color
);
}
}
@ -1116,13 +972,10 @@ fn run_shell_integration_osc9_9(engine_state: &EngineState, stack: &mut Stack, u
percent_encoding::utf8_percent_encode(&path, percent_encoding::CONTROLS)
));
perf(
perf!(
"communicate path to terminal with osc9;9",
start_time,
file!(),
line!(),
column!(),
use_color,
use_color
);
}
}
@ -1142,13 +995,10 @@ fn run_shell_integration_osc633(engine_state: &EngineState, stack: &mut Stack, u
VSCODE_CWD_PROPERTY_MARKER_PREFIX, path, VSCODE_CWD_PROPERTY_MARKER_SUFFIX
));
perf(
perf!(
"communicate path to terminal with osc633;P",
start_time,
file!(),
line!(),
column!(),
use_color,
use_color
);
}
}
@ -1371,13 +1221,10 @@ fn run_finaliziation_ansi_sequence(
shell_integration_osc133,
));
perf(
perf!(
"post_execute_marker (633;D) ansi escape sequences",
start_time,
file!(),
line!(),
column!(),
use_color,
use_color
);
} else if shell_integration_osc133 {
let start_time = Instant::now();
@ -1389,13 +1236,10 @@ fn run_finaliziation_ansi_sequence(
shell_integration_osc133,
));
perf(
perf!(
"post_execute_marker (133;D) ansi escape sequences",
start_time,
file!(),
line!(),
column!(),
use_color,
use_color
);
}
} else if shell_integration_osc133 {
@ -1408,13 +1252,10 @@ fn run_finaliziation_ansi_sequence(
shell_integration_osc133,
));
perf(
perf!(
"post_execute_marker (133;D) ansi escape sequences",
start_time,
file!(),
line!(),
column!(),
use_color,
use_color
);
}
}

View File

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

View File

@ -8,7 +8,7 @@ use nu_protocol::{
};
#[cfg(windows)]
use nu_utils::enable_vt_processing;
use nu_utils::utils::perf;
use nu_utils::perf;
use std::path::Path;
// This will collect environment variables from std::env and adds them to a stack.
@ -228,13 +228,10 @@ pub fn eval_source(
let _ = enable_vt_processing();
}
perf(
perf!(
&format!("eval_source {}", &fname),
start_time,
file!(),
line!(),
column!(),
engine_state.get_config().use_ansi_coloring,
engine_state.get_config().use_ansi_coloring
);
exit_code

View File

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

View File

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

View File

@ -5,7 +5,7 @@ edition = "2021"
license = "MIT"
name = "nu-cmd-extra"
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-extra"
version = "0.94.3"
version = "0.95.1"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -13,13 +13,13 @@ version = "0.94.3"
bench = false
[dependencies]
nu-cmd-base = { path = "../nu-cmd-base", version = "0.94.3" }
nu-engine = { path = "../nu-engine", version = "0.94.3" }
nu-json = { version = "0.94.3", path = "../nu-json" }
nu-parser = { path = "../nu-parser", version = "0.94.3" }
nu-pretty-hex = { version = "0.94.3", path = "../nu-pretty-hex" }
nu-protocol = { path = "../nu-protocol", version = "0.94.3" }
nu-utils = { path = "../nu-utils", version = "0.94.3" }
nu-cmd-base = { path = "../nu-cmd-base", version = "0.95.1" }
nu-engine = { path = "../nu-engine", version = "0.95.1" }
nu-json = { version = "0.95.1", path = "../nu-json" }
nu-parser = { path = "../nu-parser", version = "0.95.1" }
nu-pretty-hex = { version = "0.95.1", path = "../nu-pretty-hex" }
nu-protocol = { path = "../nu-protocol", version = "0.95.1" }
nu-utils = { path = "../nu-utils", version = "0.95.1" }
# Potential dependencies for extras
heck = { workspace = true }
@ -33,6 +33,6 @@ v_htmlescape = { workspace = true }
itertools = { workspace = true }
[dev-dependencies]
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.94.3" }
nu-command = { path = "../nu-command", version = "0.94.3" }
nu-test-support = { path = "../nu-test-support", version = "0.94.3" }
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.95.1" }
nu-command = { path = "../nu-command", version = "0.95.1" }
nu-test-support = { path = "../nu-test-support", version = "0.95.1" }

View File

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

View File

@ -1,4 +1,5 @@
use nu_engine::command_prelude::*;
use nu_protocol::engine::CommandType;
#[derive(Clone)]
pub struct Break;
@ -18,6 +19,15 @@ impl Command for Break {
.category(Category::Core)
}
fn extra_usage(&self) -> &str {
r#"This command is a parser keyword. For details, check:
https://www.nushell.sh/book/thinking_in_nu.html"#
}
fn command_type(&self) -> CommandType {
CommandType::Keyword
}
fn run(
&self,
_engine_state: &EngineState,

View File

@ -1,4 +1,5 @@
use nu_engine::command_prelude::*;
use nu_protocol::engine::CommandType;
#[derive(Clone)]
pub struct Continue;
@ -18,6 +19,14 @@ impl Command for Continue {
.category(Category::Core)
}
fn extra_usage(&self) -> &str {
r#"This command is a parser keyword. For details, check:
https://www.nushell.sh/book/thinking_in_nu.html"#
}
fn command_type(&self) -> CommandType {
CommandType::Keyword
}
fn run(
&self,
_engine_state: &EngineState,

View File

@ -23,11 +23,7 @@ impl Command for Do {
fn signature(&self) -> Signature {
Signature::build("do")
.required(
"closure",
SyntaxShape::OneOf(vec![SyntaxShape::Closure(None), SyntaxShape::Any]),
"The closure to run.",
)
.required("closure", SyntaxShape::Closure(None), "The closure to run.")
.input_output_types(vec![(Type::Any, Type::Any)])
.switch(
"ignore-errors",
@ -229,14 +225,24 @@ impl Command for Do {
result: None,
},
Example {
description: "Run the closure, with a positional parameter",
example: r#"do {|x| 100 + $x } 77"#,
description: "Run the closure with a positional, type-checked parameter",
example: r#"do {|x:int| 100 + $x } 77"#,
result: Some(Value::test_int(177)),
},
Example {
description: "Run the closure, with input",
example: r#"77 | do {|x| 100 + $in }"#,
result: None, // TODO: returns 177
description: "Run the closure with pipeline input",
example: r#"77 | do { 100 + $in }"#,
result: Some(Value::test_int(177)),
},
Example {
description: "Run the closure with a default parameter value",
example: r#"77 | do {|x=100| $x + $in }"#,
result: Some(Value::test_int(177)),
},
Example {
description: "Run the closure with two positional parameters",
example: r#"do {|x,y| $x + $y } 77 100"#,
result: Some(Value::test_int(177)),
},
Example {
description: "Run the closure and keep changes to the environment",

View File

@ -2,7 +2,7 @@ use nu_engine::{
command_prelude::*, get_eval_block, get_eval_expression, get_eval_expression_with_input,
};
use nu_protocol::{
engine::StateWorkingSet,
engine::{CommandType, StateWorkingSet},
eval_const::{eval_const_subexpression, eval_constant, eval_constant_with_input},
};
@ -41,6 +41,15 @@ impl Command for If {
.category(Category::Core)
}
fn extra_usage(&self) -> &str {
r#"This command is a parser keyword. For details, check:
https://www.nushell.sh/book/thinking_in_nu.html"#
}
fn command_type(&self) -> CommandType {
CommandType::Keyword
}
fn is_const(&self) -> bool {
true
}

View File

@ -1,4 +1,5 @@
use nu_engine::{command_prelude::*, get_eval_block};
use nu_protocol::engine::CommandType;
#[derive(Clone)]
pub struct Loop;
@ -20,6 +21,15 @@ impl Command for Loop {
.category(Category::Core)
}
fn extra_usage(&self) -> &str {
r#"This command is a parser keyword. For details, check:
https://www.nushell.sh/book/thinking_in_nu.html"#
}
fn command_type(&self) -> CommandType {
CommandType::Keyword
}
fn run(
&self,
engine_state: &EngineState,

View File

@ -1,7 +1,7 @@
use nu_engine::{
command_prelude::*, get_eval_block, get_eval_expression, get_eval_expression_with_input,
};
use nu_protocol::engine::Matcher;
use nu_protocol::engine::{CommandType, Matcher};
#[derive(Clone)]
pub struct Match;
@ -27,6 +27,15 @@ impl Command for Match {
.category(Category::Core)
}
fn extra_usage(&self) -> &str {
r#"This command is a parser keyword. For details, check:
https://www.nushell.sh/book/thinking_in_nu.html"#
}
fn command_type(&self) -> CommandType {
CommandType::Keyword
}
fn run(
&self,
engine_state: &EngineState,

View File

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

View File

@ -1,5 +1,5 @@
use nu_engine::{command_prelude::*, get_eval_block, EvalBlockFn};
use nu_protocol::engine::Closure;
use nu_protocol::engine::{Closure, CommandType};
#[derive(Clone)]
pub struct Try;
@ -31,6 +31,15 @@ impl Command for Try {
.category(Category::Core)
}
fn extra_usage(&self) -> &str {
r#"This command is a parser keyword. For details, check:
https://www.nushell.sh/book/thinking_in_nu.html"#
}
fn command_type(&self) -> CommandType {
CommandType::Keyword
}
fn run(
&self,
engine_state: &EngineState,

View File

@ -116,11 +116,18 @@ pub fn version(engine_state: &EngineState, span: Span) -> Result<PipelineData, S
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
.plugins()
.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<_>>();
record.push(

View File

@ -1,4 +1,5 @@
use nu_engine::{command_prelude::*, get_eval_block, get_eval_expression};
use nu_protocol::engine::CommandType;
#[derive(Clone)]
pub struct While;
@ -29,6 +30,15 @@ impl Command for While {
vec!["loop"]
}
fn extra_usage(&self) -> &str {
r#"This command is a parser keyword. For details, check:
https://www.nushell.sh/book/thinking_in_nu.html"#
}
fn command_type(&self) -> CommandType {
CommandType::Keyword
}
fn run(
&self,
engine_state: &EngineState,

View File

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

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

View File

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

View File

@ -31,11 +31,20 @@ pub(crate) fn modify_plugin_file(
})?
};
let file_span = custom_path.as_ref().map(|p| p.span).unwrap_or(span);
// Try to read the plugin file if it exists
let mut contents = if fs::metadata(&plugin_registry_file_path).is_ok_and(|m| m.len() > 0) {
PluginRegistryFile::read_from(
File::open(&plugin_registry_file_path).err_span(span)?,
Some(span),
File::open(&plugin_registry_file_path).map_err(|err| ShellError::IOErrorSpanned {
msg: format!(
"failed to read `{}`: {}",
plugin_registry_file_path.display(),
err
),
span: file_span,
})?,
Some(file_span),
)?
} else {
PluginRegistryFile::default()
@ -46,7 +55,14 @@ pub(crate) fn modify_plugin_file(
// Save the modified file on success
contents.write_to(
File::create(&plugin_registry_file_path).err_span(span)?,
File::create(&plugin_registry_file_path).map_err(|err| ShellError::IOErrorSpanned {
msg: format!(
"failed to create `{}`: {}",
plugin_registry_file_path.display(),
err
),
span: file_span,
})?,
Some(span),
)?;

View File

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

View File

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

View File

@ -5,7 +5,7 @@ edition = "2021"
license = "MIT"
name = "nu-command"
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-command"
version = "0.94.3"
version = "0.95.1"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -13,21 +13,21 @@ version = "0.94.3"
bench = false
[dependencies]
nu-cmd-base = { path = "../nu-cmd-base", version = "0.94.3" }
nu-color-config = { path = "../nu-color-config", version = "0.94.3" }
nu-engine = { path = "../nu-engine", version = "0.94.3" }
nu-glob = { path = "../nu-glob", version = "0.94.3" }
nu-json = { path = "../nu-json", version = "0.94.3" }
nu-parser = { path = "../nu-parser", version = "0.94.3" }
nu-path = { path = "../nu-path", version = "0.94.3" }
nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.94.3" }
nu-protocol = { path = "../nu-protocol", version = "0.94.3" }
nu-system = { path = "../nu-system", version = "0.94.3" }
nu-table = { path = "../nu-table", version = "0.94.3" }
nu-term-grid = { path = "../nu-term-grid", version = "0.94.3" }
nu-utils = { path = "../nu-utils", version = "0.94.3" }
nu-cmd-base = { path = "../nu-cmd-base", version = "0.95.1" }
nu-color-config = { path = "../nu-color-config", version = "0.95.1" }
nu-engine = { path = "../nu-engine", version = "0.95.1" }
nu-glob = { path = "../nu-glob", version = "0.95.1" }
nu-json = { path = "../nu-json", version = "0.95.1" }
nu-parser = { path = "../nu-parser", version = "0.95.1" }
nu-path = { path = "../nu-path", version = "0.95.1" }
nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.95.1" }
nu-protocol = { path = "../nu-protocol", version = "0.95.1" }
nu-system = { path = "../nu-system", version = "0.95.1" }
nu-table = { path = "../nu-table", version = "0.95.1" }
nu-term-grid = { path = "../nu-term-grid", version = "0.95.1" }
nu-utils = { path = "../nu-utils", version = "0.95.1" }
nu-ansi-term = { workspace = true }
nuon = { path = "../nuon", version = "0.94.3" }
nuon = { path = "../nuon", version = "0.95.1" }
alphanumeric-sort = { workspace = true }
base64 = { workspace = true }
@ -42,6 +42,7 @@ chrono-humanize = { workspace = true }
chrono-tz = { workspace = true }
crossterm = { workspace = true }
csv = { workspace = true }
deunicode = { workspace = true }
dialoguer = { workspace = true, default-features = false, features = ["fuzzy-select"] }
digest = { workspace = true, default-features = false }
dtparse = { workspace = true }
@ -136,8 +137,8 @@ sqlite = ["rusqlite"]
trash-support = ["trash"]
[dev-dependencies]
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.94.3" }
nu-test-support = { path = "../nu-test-support", version = "0.94.3" }
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.95.1" }
nu-test-support = { path = "../nu-test-support", version = "0.95.1" }
dirs-next = { workspace = true }
mockito = { workspace = true, default-features = false }
@ -145,4 +146,4 @@ quickcheck = { workspace = true }
quickcheck_macros = { workspace = true }
rstest = { workspace = true, default-features = false }
pretty_assertions = { workspace = true }
tempfile = { workspace = true }
tempfile = { workspace = true }

View File

@ -189,6 +189,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
Str,
StrCapitalize,
StrContains,
StrDeunicode,
StrDistance,
StrDowncase,
StrEndswith,

View File

@ -1,4 +1,5 @@
use nu_engine::{command_prelude::*, get_eval_block, redirect_env};
use nu_protocol::engine::CommandType;
#[derive(Clone)]
pub struct ExportEnv;
@ -23,6 +24,15 @@ impl Command for ExportEnv {
"Run a block and preserve its environment in a current scope."
}
fn extra_usage(&self) -> &str {
r#"This command is a parser keyword. For details, check:
https://www.nushell.sh/book/thinking_in_nu.html"#
}
fn command_type(&self) -> CommandType {
CommandType::Keyword
}
fn run(
&self,
engine_state: &EngineState,

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,
redirect_env,
};
use nu_protocol::engine::CommandType;
use std::path::PathBuf;
/// Source a file for environment variables.
@ -28,6 +29,15 @@ impl Command for SourceEnv {
"Source the environment from a source file into the current environment."
}
fn extra_usage(&self) -> &str {
r#"This command is a parser keyword. For details, check:
https://www.nushell.sh/book/thinking_in_nu.html"#
}
fn command_type(&self) -> CommandType {
CommandType::Keyword
}
fn run(
&self,
engine_state: &EngineState,

View File

@ -175,20 +175,32 @@ impl Command for Ls {
},
Example {
description: "List files and directories whose name do not contain 'bar'",
example: "ls -s | where name !~ bar",
example: "ls | where name !~ bar",
result: None,
},
Example {
description: "List all dirs in your home directory",
description: "List the full path of all dirs in your home directory",
example: "ls -a ~ | where type == dir",
result: None,
},
Example {
description:
"List all dirs in your home directory which have not been modified in 7 days",
"List only the names (not paths) of all dirs in your home directory which have not been modified in 7 days",
example: "ls -as ~ | where type == dir and modified < ((date now) - 7day)",
result: None,
},
Example {
description:
"Recursively list all files and subdirectories under the current directory using a glob pattern",
example: "ls -a **/*",
result: None,
},
Example {
description:
"Recursively list *.rs and *.toml files using the glob command",
example: "ls ...(glob **/*.{rs,toml})",
result: None,
},
Example {
description: "List given paths and show directories themselves",
example: "['/path/to/directory' '/path/to/file'] | each {|| ls -D $in } | flatten",

View File

@ -69,9 +69,9 @@ impl Command for Find {
result: None,
},
Example {
description: "Search and highlight text for a term in a string",
example: r#"'Cargo.toml' | find toml"#,
result: Some(Value::test_string("\u{1b}[37mCargo.\u{1b}[0m\u{1b}[41;37mtoml\u{1b}[0m\u{1b}[37m\u{1b}[0m".to_owned())),
description: "Search and highlight text for a term in a string. Note that regular search is case insensitive",
example: r#"'Cargo.toml' | find cargo"#,
result: Some(Value::test_string("\u{1b}[37m\u{1b}[0m\u{1b}[41;37mCargo\u{1b}[0m\u{1b}[37m.toml\u{1b}[0m".to_owned())),
},
Example {
description: "Search a number or a file size in a list of numbers",
@ -457,9 +457,10 @@ fn find_with_rest_and_highlight(
let mut output: Vec<Value> = vec![];
for line in lines {
let line = line?.to_lowercase();
let line = line?;
let lower_val = line.to_lowercase();
for term in &terms {
if line.contains(term) {
if lower_val.contains(term) {
output.push(Value::string(
highlight_search_string(
&line,

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
use chrono::{Datelike, Local, NaiveDate};
use nu_color_config::StyleComputer;
use nu_engine::command_prelude::*;
use nu_protocol::ast::{Expr, Expression};
use std::collections::VecDeque;
@ -14,6 +15,7 @@ struct Arguments {
month_names: bool,
full_year: Option<Spanned<i64>>,
week_start: Option<Spanned<String>>,
as_table: bool,
}
impl Command for Cal {
@ -26,6 +28,7 @@ impl Command for Cal {
.switch("year", "Display the year column", Some('y'))
.switch("quarter", "Display the quarter column", Some('q'))
.switch("month", "Display the month column", Some('m'))
.switch("as-table", "output as a table", Some('t'))
.named(
"full-year",
SyntaxShape::Int,
@ -43,7 +46,10 @@ impl Command for Cal {
"Display the month names instead of integers",
None,
)
.input_output_types(vec![(Type::Nothing, Type::table())])
.input_output_types(vec![
(Type::Nothing, Type::table()),
(Type::Nothing, Type::String),
])
.allow_variants_without_examples(true) // TODO: supply exhaustive examples
.category(Category::Generators)
}
@ -75,10 +81,15 @@ impl Command for Cal {
result: None,
},
Example {
description: "This month's calendar with the week starting on monday",
description: "This month's calendar with the week starting on Monday",
example: "cal --week-start mo",
result: None,
},
Example {
description: "How many 'Friday the Thirteenths' occurred in 2015?",
example: "cal --as-table --full-year 2015 | where fr == 13 | length",
result: None,
},
]
}
}
@ -101,6 +112,7 @@ pub fn cal(
quarter: call.has_flag(engine_state, stack, "quarter")?,
full_year: call.get_flag(engine_state, stack, "full-year")?,
week_start: call.get_flag(engine_state, stack, "week-start")?,
as_table: call.has_flag(engine_state, stack, "as-table")?,
};
let style_computer = &StyleComputer::from_config(engine_state, stack);
@ -131,7 +143,27 @@ pub fn cal(
style_computer,
)?;
Ok(Value::list(calendar_vec_deque.into_iter().collect(), tag).into_pipeline_data())
let mut table_no_index = Call::new(Span::unknown());
table_no_index.add_named((
Spanned {
item: "index".to_string(),
span: Span::unknown(),
},
None,
Some(Expression::new_unknown(
Expr::Bool(false),
Span::unknown(),
Type::Bool,
)),
));
let cal_table_output =
Value::list(calendar_vec_deque.into_iter().collect(), tag).into_pipeline_data();
if !arguments.as_table {
crate::Table.run(engine_state, stack, &table_no_index, cal_table_output)
} else {
Ok(cal_table_output)
}
}
fn get_invalid_year_shell_error(head: Span) -> ShellError {

View File

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

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
// it's just easier to use them like this.
"nul" => '\x00'.to_string(), // nul character, 0x00
"null_byte" => '\x00'.to_string(), // nul character, 0x00
"zero_byte" => '\x00'.to_string(), // nul character, 0x00
// This are the "normal" characters section
"newline" => '\n'.to_string(),
"enter" => '\n'.to_string(),

View File

@ -72,7 +72,9 @@ impl GuessWidth {
let mut rows = Vec::new();
while let Ok(columns) = self.read() {
rows.push(columns);
if !columns.is_empty() {
rows.push(columns);
}
}
rows
}
@ -175,34 +177,47 @@ fn separator_position(lr: &[char], p: usize, pos: &[usize], n: usize) -> usize {
fn split(line: &str, pos: &[usize], trim_space: bool) -> Vec<String> {
let mut n = 0;
let mut start = 0;
let mut start_char = 0;
let mut columns = Vec::with_capacity(pos.len() + 1);
let lr: Vec<char> = line.chars().collect();
let (line_char_boundaries, line_chars): (Vec<usize>, Vec<char>) = line.char_indices().unzip();
let mut w = 0;
for p in 0..lr.len() {
if line_chars.is_empty() || line_chars.iter().all(|&c| c.is_whitespace()) {
// current line is completely empty, or only filled with whitespace
return Vec::new();
} else if !pos.is_empty()
&& line_chars.iter().all(|&c| !c.is_whitespace())
&& pos[0] < UnicodeWidthStr::width(line)
{
// we have more than 1 column in the input, but the current line has no whitespace,
// and it is longer than the first detected column separation position
// this indicates some kind of decoration line. let's skip it
return Vec::new();
}
for p in 0..line_char_boundaries.len() {
if pos.is_empty() || n > pos.len() - 1 {
start = p;
start_char = p;
break;
}
if pos[n] <= w {
let end = separator_position(&lr, p, pos, n);
if start > end {
let end_char = separator_position(&line_chars, p, pos, n);
if start_char > end_char {
break;
}
let col = &line[start..end];
let col = &line[line_char_boundaries[start_char]..line_char_boundaries[end_char]];
let col = if trim_space { col.trim() } else { col };
columns.push(col.to_string());
n += 1;
start = end;
start_char = end_char;
}
w += UnicodeWidthStr::width(lr[p].to_string().as_str());
w += UnicodeWidthStr::width(line_chars[p].to_string().as_str());
}
// add last part.
let col = &line[start..];
let col = &line[line_char_boundaries[start_char]..];
let col = if trim_space { col.trim() } else { col };
columns.push(col.to_string());
columns
@ -423,6 +438,162 @@ D: 104792064 17042676 87749388 17% /d";
assert_eq!(got, want);
}
#[test]
fn test_guess_width_multibyte() {
let input = "A… B\nC… D";
let r = Box::new(std::io::BufReader::new(input.as_bytes())) as Box<dyn std::io::Read>;
let reader = std::io::BufReader::new(r);
let mut guess_width = GuessWidth {
reader,
pos: Vec::new(),
pre_lines: Vec::new(),
pre_count: 0,
limit_split: 0,
};
let want = vec![vec!["A…", "B"], vec!["C…", "D"]];
let got = guess_width.read_all();
assert_eq!(got, want);
}
#[test]
fn test_guess_width_combining_diacritical_marks() {
let input = "Name Surname
Ștefan Țincu ";
let r = Box::new(std::io::BufReader::new(input.as_bytes())) as Box<dyn std::io::Read>;
let reader = std::io::BufReader::new(r);
let mut guess_width = GuessWidth {
reader,
pos: Vec::new(),
pre_lines: Vec::new(),
pre_count: 0,
limit_split: 0,
};
let want = vec![vec!["Name", "Surname"], vec!["Ștefan", "Țincu"]];
let got = guess_width.read_all();
assert_eq!(got, want);
}
#[test]
fn test_guess_width_single_column() {
let input = "A
B
C";
let r = Box::new(std::io::BufReader::new(input.as_bytes())) as Box<dyn std::io::Read>;
let reader = std::io::BufReader::new(r);
let mut guess_width = GuessWidth {
reader,
pos: Vec::new(),
pre_lines: Vec::new(),
pre_count: 0,
limit_split: 0,
};
let want = vec![vec!["A"], vec!["B"], vec!["C"]];
let got = guess_width.read_all();
assert_eq!(got, want);
}
#[test]
fn test_guess_width_row_without_whitespace() {
let input = "A B C D
-------
E F G H";
let r = Box::new(std::io::BufReader::new(input.as_bytes())) as Box<dyn std::io::Read>;
let reader = std::io::BufReader::new(r);
let mut guess_width = GuessWidth {
reader,
pos: Vec::new(),
pre_lines: Vec::new(),
pre_count: 0,
limit_split: 0,
};
let want = vec![vec!["A", "B", "C", "D"], vec!["E", "F", "G", "H"]];
let got = guess_width.read_all();
assert_eq!(got, want);
}
#[test]
fn test_guess_width_row_with_single_column() {
let input = "A B C D
E
F G H I";
let r = Box::new(std::io::BufReader::new(input.as_bytes())) as Box<dyn std::io::Read>;
let reader = std::io::BufReader::new(r);
let mut guess_width = GuessWidth {
reader,
pos: Vec::new(),
pre_lines: Vec::new(),
pre_count: 0,
limit_split: 0,
};
let want = vec![
vec!["A", "B", "C", "D"],
vec!["E"],
vec!["F", "G", "H", "I"],
];
let got = guess_width.read_all();
assert_eq!(got, want);
}
#[test]
fn test_guess_width_empty_row() {
let input = "A B C D
E F G H";
let r = Box::new(std::io::BufReader::new(input.as_bytes())) as Box<dyn std::io::Read>;
let reader = std::io::BufReader::new(r);
let mut guess_width = GuessWidth {
reader,
pos: Vec::new(),
pre_lines: Vec::new(),
pre_count: 0,
limit_split: 0,
};
let want = vec![vec!["A", "B", "C", "D"], vec!["E", "F", "G", "H"]];
let got = guess_width.read_all();
assert_eq!(got, want);
}
#[test]
fn test_guess_width_row_with_only_whitespace() {
let input = "A B C D
E F G H";
let r = Box::new(std::io::BufReader::new(input.as_bytes())) as Box<dyn std::io::Read>;
let reader = std::io::BufReader::new(r);
let mut guess_width = GuessWidth {
reader,
pos: Vec::new(),
pre_lines: Vec::new(),
pre_count: 0,
limit_split: 0,
};
let want = vec![vec!["A", "B", "C", "D"], vec!["E", "F", "G", "H"]];
let got = guess_width.read_all();
assert_eq!(got, want);
}
#[test]
fn test_to_table() {
let lines = vec![

View File

@ -0,0 +1,98 @@
use deunicode::deunicode;
use nu_cmd_base::input_handler::{operate, CellPathOnlyArgs};
use nu_engine::command_prelude::*;
use nu_protocol::engine::StateWorkingSet;
#[derive(Clone)]
pub struct SubCommand;
impl Command for SubCommand {
fn name(&self) -> &str {
"str deunicode"
}
fn signature(&self) -> Signature {
Signature::build("str deunicode")
.input_output_types(vec![(Type::String, Type::String)])
.category(Category::Strings)
}
fn usage(&self) -> &str {
"Convert Unicode string to pure ASCII."
}
fn search_terms(&self) -> Vec<&str> {
vec!["convert", "ascii"]
}
fn is_const(&self) -> bool {
true
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let cell_paths: Vec<CellPath> = call.rest(engine_state, stack, 0)?;
let args = CellPathOnlyArgs::from(cell_paths);
operate(action, args, input, call.head, engine_state.ctrlc.clone())
}
fn run_const(
&self,
working_set: &StateWorkingSet,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let cell_paths: Vec<CellPath> = call.rest_const(working_set, 0)?;
let args = CellPathOnlyArgs::from(cell_paths);
operate(
action,
args,
input,
call.head,
working_set.permanent().ctrlc.clone(),
)
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "deunicode a string",
example: "'A…B' | str deunicode",
result: Some(Value::test_string("A...B")),
}]
}
}
fn action(input: &Value, _args: &CellPathOnlyArgs, head: Span) -> Value {
match input {
Value::String { val, .. } => Value::string(deunicode(val), head),
Value::Error { .. } => input.clone(),
_ => Value::error(
ShellError::OnlySupportsThisInputType {
exp_input_type: "string".into(),
wrong_type: input.get_type().to_string(),
dst_span: head,
src_span: input.span(),
},
head,
),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_examples() {
use crate::test_examples;
test_examples(SubCommand {})
}
}

View File

@ -1,5 +1,6 @@
mod case;
mod contains;
mod deunicode;
mod distance;
mod ends_with;
mod expand;
@ -15,6 +16,7 @@ mod trim;
pub use case::*;
pub use contains::SubCommand as StrContains;
pub use deunicode::SubCommand as StrDeunicode;
pub use distance::SubCommand as StrDistance;
pub use ends_with::SubCommand as StrEndswith;
pub use expand::SubCommand as StrExpand;

View File

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

View File

@ -1,8 +1,9 @@
use nu_test_support::{nu, pipeline};
// Tests against table/structured data
#[test]
fn cal_full_year() {
let actual = nu!("cal -y --full-year 2010 | first | to json -r");
let actual = nu!("cal -t -y --full-year 2010 | first | to json -r");
let first_week_2010_json =
r#"{"year":2010,"su":null,"mo":null,"tu":null,"we":null,"th":null,"fr":1,"sa":2}"#;
@ -14,7 +15,7 @@ fn cal_full_year() {
fn cal_february_2020_leap_year() {
let actual = nu!(pipeline(
r#"
cal -ym --full-year 2020 --month-names | where month == "february" | to json -r
cal --as-table -ym --full-year 2020 --month-names | where month == "february" | to json -r
"#
));
@ -27,7 +28,7 @@ fn cal_february_2020_leap_year() {
fn cal_fr_the_thirteenths_in_2015() {
let actual = nu!(pipeline(
r#"
cal --full-year 2015 | default 0 fr | where fr == 13 | length
cal --as-table --full-year 2015 | default 0 fr | where fr == 13 | length
"#
));
@ -38,7 +39,7 @@ fn cal_fr_the_thirteenths_in_2015() {
fn cal_rows_in_2020() {
let actual = nu!(pipeline(
r#"
cal --full-year 2020 | length
cal --as-table --full-year 2020 | length
"#
));
@ -49,7 +50,7 @@ fn cal_rows_in_2020() {
fn cal_week_day_start_mo() {
let actual = nu!(pipeline(
r#"
cal --full-year 2020 -m --month-names --week-start mo | where month == january | to json -r
cal --as-table --full-year 2020 -m --month-names --week-start mo | where month == january | to json -r
"#
));
@ -62,9 +63,43 @@ fn cal_week_day_start_mo() {
fn cal_sees_pipeline_year() {
let actual = nu!(pipeline(
r#"
cal --full-year 1020 | get mo | first 4 | to json -r
cal --as-table --full-year 1020 | get mo | first 4 | to json -r
"#
));
assert_eq!(actual.out, "[null,3,10,17]");
}
// Tests against default string output
#[test]
fn cal_is_string() {
let actual = nu!(pipeline(
r#"
cal | describe
"#
));
assert_eq!(actual.out, "string (stream)");
}
#[test]
fn cal_year_num_lines() {
let actual = nu!(pipeline(
r#"
cal --full-year 2024 | lines | length
"#
));
assert_eq!(actual.out, "68");
}
#[test]
fn cal_week_start_string() {
let actual = nu!(pipeline(
r#"
cal --week-start fr | lines | get 1 | split row '│' | get 2 | ansi strip | str trim
"#
));
assert_eq!(actual.out, "sa");
}

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\"]");
}
#[test]
fn find_with_bytestream_search_with_char() {
let actual =
nu!("\"ABC\" | save foo.txt; let res = open foo.txt | find abc; rm foo.txt; $res | get 0");
assert_eq!(
actual.out,
"\u{1b}[37m\u{1b}[0m\u{1b}[41;37mABC\u{1b}[0m\u{1b}[37m\u{1b}[0m"
)
}
#[test]
fn find_with_list_search_with_number() {
let actual = nu!("[1 2 3 4 5] | find 3 | get 0");

View File

@ -2,7 +2,7 @@ use nu_test_support::nu;
#[test]
fn length_columns_in_cal_table() {
let actual = nu!("cal | columns | length");
let actual = nu!("cal --as-table | columns | length");
assert_eq!(actual.out, "7");
}

View File

@ -1,4 +1,3 @@
#[cfg(not(windows))]
use nu_test_support::fs::Stub::EmptyFile;
use nu_test_support::playground::Playground;
use nu_test_support::{nu, pipeline};
@ -17,7 +16,6 @@ fn better_empty_redirection() {
assert!(!actual.out.contains('2'));
}
#[cfg(not(windows))]
#[test]
fn explicit_glob() {
Playground::setup("external with explicit glob", |dirs, sandbox| {
@ -30,15 +28,15 @@ fn explicit_glob() {
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
^ls | glob '*.txt' | length
^nu --testbin cococo ('*.txt' | into glob)
"#
));
assert_eq!(actual.out, "2");
assert!(actual.out.contains("D&D_volume_1.txt"));
assert!(actual.out.contains("D&D_volume_2.txt"));
})
}
#[cfg(not(windows))]
#[test]
fn bare_word_expand_path_glob() {
Playground::setup("bare word should do the expansion", |dirs, sandbox| {
@ -51,7 +49,7 @@ fn bare_word_expand_path_glob() {
let actual = nu!(
cwd: dirs.test(), pipeline(
"
^ls *.txt
^nu --testbin cococo *.txt
"
));
@ -60,7 +58,6 @@ fn bare_word_expand_path_glob() {
})
}
#[cfg(not(windows))]
#[test]
fn backtick_expand_path_glob() {
Playground::setup("backtick should do the expansion", |dirs, sandbox| {
@ -73,7 +70,7 @@ fn backtick_expand_path_glob() {
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
^ls `*.txt`
^nu --testbin cococo `*.txt`
"#
));
@ -82,7 +79,6 @@ fn backtick_expand_path_glob() {
})
}
#[cfg(not(windows))]
#[test]
fn single_quote_does_not_expand_path_glob() {
Playground::setup("single quote do not run the expansion", |dirs, sandbox| {
@ -95,15 +91,14 @@ fn single_quote_does_not_expand_path_glob() {
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
^ls '*.txt'
^nu --testbin cococo '*.txt'
"#
));
assert!(actual.err.contains("No such file or directory"));
assert_eq!(actual.out, "*.txt");
})
}
#[cfg(not(windows))]
#[test]
fn double_quote_does_not_expand_path_glob() {
Playground::setup("double quote do not run the expansion", |dirs, sandbox| {
@ -116,22 +111,21 @@ fn double_quote_does_not_expand_path_glob() {
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
^ls "*.txt"
^nu --testbin cococo "*.txt"
"#
));
assert!(actual.err.contains("No such file or directory"));
assert_eq!(actual.out, "*.txt");
})
}
#[cfg(not(windows))]
#[test]
fn failed_command_with_semicolon_will_not_execute_following_cmds() {
Playground::setup("external failed command with semicolon", |dirs, _| {
let actual = nu!(
cwd: dirs.test(), pipeline(
"
^ls *.abc; echo done
nu --testbin fail; echo done
"
));
@ -155,16 +149,51 @@ fn external_args_with_quoted() {
#[cfg(not(windows))]
#[test]
fn external_arg_with_long_flag_value_quoted() {
Playground::setup("external failed command with semicolon", |dirs, _| {
fn external_arg_with_option_like_embedded_quotes() {
// TODO: would be nice to make this work with cococo, but arg parsing interferes
Playground::setup(
"external arg with option like embedded quotes",
|dirs, _| {
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
^echo --foo='bar' -foo='bar'
"#
));
assert_eq!(actual.out, "--foo=bar -foo=bar");
},
)
}
#[test]
fn external_arg_with_non_option_like_embedded_quotes() {
Playground::setup(
"external arg with non option like embedded quotes",
|dirs, _| {
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
^nu --testbin cococo foo='bar' 'foo'=bar
"#
));
assert_eq!(actual.out, "foo=bar foo=bar");
},
)
}
#[test]
fn external_arg_with_string_interpolation() {
Playground::setup("external arg with string interpolation", |dirs, _| {
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
^echo --foo='bar'
^nu --testbin cococo foo=(2 + 2) $"foo=(2 + 2)" foo=$"(2 + 2)"
"#
));
assert_eq!(actual.out, "--foo=bar");
assert_eq!(actual.out, "foo=4 foo=4 foo=4");
})
}
@ -200,6 +229,99 @@ fn external_command_escape_args() {
})
}
#[test]
fn external_command_ndots_args() {
let actual = nu!(r#"
nu --testbin cococo foo/. foo/.. foo/... foo/./bar foo/../bar foo/.../bar ./bar ../bar .../bar
"#);
assert_eq!(
actual.out,
if cfg!(windows) {
// Windows is a bit weird right now, where if ndots has to fix something it's going to
// change everything to backslashes too. Would be good to fix that
r"foo/. foo/.. foo\..\.. foo/./bar foo/../bar foo\..\..\bar ./bar ../bar ..\..\bar"
} else {
r"foo/. foo/.. foo/../.. foo/./bar foo/../bar foo/../../bar ./bar ../bar ../../bar"
}
);
}
#[test]
fn external_command_url_args() {
// If ndots is not handled correctly, we can lose the double forward slashes that are needed
// here
let actual = nu!(r#"
nu --testbin cococo http://example.com http://example.com/.../foo //foo
"#);
assert_eq!(
actual.out,
"http://example.com http://example.com/.../foo //foo"
);
}
#[test]
#[cfg_attr(
not(target_os = "linux"),
ignore = "only runs on Linux, where controlling the HOME var is reliable"
)]
fn external_command_expand_tilde() {
Playground::setup("external command expand tilde", |dirs, _| {
// Make a copy of the nu executable that we can use
let mut src = std::fs::File::open(nu_test_support::fs::binaries().join("nu"))
.expect("failed to open nu");
let mut dst = std::fs::File::create_new(dirs.test().join("test_nu"))
.expect("failed to create test_nu file");
std::io::copy(&mut src, &mut dst).expect("failed to copy data for nu binary");
// Make test_nu have the same permissions so that it's executable
dst.set_permissions(
src.metadata()
.expect("failed to get nu metadata")
.permissions(),
)
.expect("failed to set permissions on test_nu");
// Close the files
drop(dst);
drop(src);
let actual = nu!(
envs: vec![
("HOME".to_string(), dirs.test().to_string_lossy().into_owned()),
],
r#"
^~/test_nu --testbin cococo hello
"#
);
assert_eq!(actual.out, "hello");
})
}
#[test]
fn external_arg_expand_tilde() {
Playground::setup("external arg expand tilde", |dirs, _| {
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
^nu --testbin cococo ~/foo ~/(2 + 2)
"#
));
let home = dirs_next::home_dir().expect("failed to find home dir");
assert_eq!(
actual.out,
format!(
"{} {}",
home.join("foo").display(),
home.join("4").display()
)
);
})
}
#[test]
fn external_command_not_expand_tilde_with_quotes() {
Playground::setup(
@ -231,21 +353,6 @@ fn external_command_receives_raw_binary_data() {
})
}
#[cfg(windows)]
#[test]
fn failed_command_with_semicolon_will_not_execute_following_cmds_windows() {
Playground::setup("external failed command with semicolon", |dirs, _| {
let actual = nu!(
cwd: dirs.test(), pipeline(
"
^cargo asdf; echo done
"
));
assert!(!actual.out.contains("done"));
})
}
#[cfg(windows)]
#[test]
fn can_run_batch_files() {

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ edition = "2021"
license = "MIT"
name = "nu-derive-value"
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-derive-value"
version = "0.94.3"
version = "0.95.1"
[lib]
proc-macro = true
@ -18,4 +18,4 @@ proc-macro2 = { workspace = true }
syn = { workspace = true }
quote = { workspace = true }
proc-macro-error = { workspace = true }
convert_case = { workspace = true }
convert_case = { workspace = true }

View File

@ -3,6 +3,7 @@ use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens};
use syn::{
spanned::Spanned, Attribute, Data, DataEnum, DataStruct, DeriveInput, Fields, Generics, Ident,
Type,
};
use crate::attributes::{self, ContainerAttributes};
@ -116,15 +117,11 @@ fn derive_struct_from_value(
/// src_span: span
/// })?,
/// )?,
/// favorite_toy: <Option<String> as nu_protocol::FromValue>::from_value(
/// record
/// .remove("favorite_toy")
/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn {
/// col_name: std::string::ToString::to_string("favorite_toy"),
/// span: std::option::Option::None,
/// src_span: span
/// })?,
/// )?,
/// favorite_toy: record
/// .remove("favorite_toy")
/// .map(|v| <#ty as nu_protocol::FromValue>::from_value(v))
/// .transpose()?
/// .flatten(),
/// })
/// }
/// }
@ -480,20 +477,29 @@ fn parse_value_via_fields(fields: &Fields, self_ident: impl ToTokens) -> TokenSt
match fields {
Fields::Named(fields) => {
let fields = fields.named.iter().map(|field| {
// TODO: handle missing fields for Options as None
let ident = field.ident.as_ref().expect("named has idents");
let ident_s = ident.to_string();
let ty = &field.ty;
quote! {
#ident: <#ty as nu_protocol::FromValue>::from_value(
record
match type_is_option(ty) {
true => quote! {
#ident: record
.remove(#ident_s)
.ok_or_else(|| nu_protocol::ShellError::CantFindColumn {
col_name: std::string::ToString::to_string(#ident_s),
span: std::option::Option::None,
src_span: span
})?,
)?
.map(|v| <#ty as nu_protocol::FromValue>::from_value(v))
.transpose()?
.flatten()
},
false => quote! {
#ident: <#ty as nu_protocol::FromValue>::from_value(
record
.remove(#ident_s)
.ok_or_else(|| nu_protocol::ShellError::CantFindColumn {
col_name: std::string::ToString::to_string(#ident_s),
span: std::option::Option::None,
src_span: span
})?,
)?
},
}
});
quote! {
@ -537,3 +543,25 @@ fn parse_value_via_fields(fields: &Fields, self_ident: impl ToTokens) -> TokenSt
},
}
}
const FULLY_QUALIFIED_OPTION: &str = "std::option::Option";
const PARTIALLY_QUALIFIED_OPTION: &str = "option::Option";
const PRELUDE_OPTION: &str = "Option";
/// Check if the field type is an `Option`.
///
/// This function checks if a given type is an `Option`.
/// We assume that an `Option` is [`std::option::Option`] because we can't see the whole code and
/// can't ask the compiler itself.
/// If the `Option` type isn't `std::option::Option`, the user will get a compile error due to a
/// type mismatch.
/// It's very unusual for people to override `Option`, so this should rarely be an issue.
///
/// When [rust#63084](https://github.com/rust-lang/rust/issues/63084) is resolved, we can use
/// [`std::any::type_name`] for a static assertion check to get a more direct error messages.
fn type_is_option(ty: &Type) -> bool {
let s = ty.to_token_stream().to_string();
s.starts_with(PRELUDE_OPTION)
|| s.starts_with(PARTIALLY_QUALIFIED_OPTION)
|| s.starts_with(FULLY_QUALIFIED_OPTION)
}

View File

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

View File

@ -5,21 +5,21 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-explore"
edition = "2021"
license = "MIT"
name = "nu-explore"
version = "0.94.3"
version = "0.95.1"
[lib]
bench = false
[dependencies]
nu-protocol = { path = "../nu-protocol", version = "0.94.3" }
nu-parser = { path = "../nu-parser", version = "0.94.3" }
nu-color-config = { path = "../nu-color-config", version = "0.94.3" }
nu-engine = { path = "../nu-engine", version = "0.94.3" }
nu-table = { path = "../nu-table", version = "0.94.3" }
nu-json = { path = "../nu-json", version = "0.94.3" }
nu-utils = { path = "../nu-utils", version = "0.94.3" }
nu-protocol = { path = "../nu-protocol", version = "0.95.1" }
nu-parser = { path = "../nu-parser", version = "0.95.1" }
nu-color-config = { path = "../nu-color-config", version = "0.95.1" }
nu-engine = { path = "../nu-engine", version = "0.95.1" }
nu-table = { path = "../nu-table", version = "0.95.1" }
nu-json = { path = "../nu-json", version = "0.95.1" }
nu-utils = { path = "../nu-utils", version = "0.95.1" }
nu-ansi-term = { workspace = true }
nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.94.3" }
nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.95.1" }
anyhow = { workspace = true }
log = { workspace = true }
@ -32,4 +32,4 @@ ansi-str = { workspace = true }
unicode-width = { workspace = true }
lscolors = { workspace = true, default-features = false, features = [
"nu-ansi-term",
] }
] }

View File

@ -131,7 +131,7 @@ impl RecordView {
Orientation::Left => (column, row),
};
if row >= layer.count_rows() || column >= layer.count_columns() {
if row >= layer.record_values.len() || column >= layer.column_names.len() {
// actually must never happen; unless cursor works incorrectly
// if being sure about cursor it can be deleted;
return Value::nothing(Span::unknown());
@ -610,7 +610,7 @@ fn estimate_page_size(area: Rect, show_head: bool) -> u16 {
/// scroll to the end of the data
fn tail_data(state: &mut RecordView, page_size: usize) {
let layer = state.get_layer_last_mut();
let count_rows = layer.count_rows();
let count_rows = layer.record_values.len();
if count_rows > page_size {
layer
.cursor
@ -722,43 +722,66 @@ fn get_percentage(value: usize, max: usize) -> usize {
}
fn transpose_table(layer: &mut RecordLayer) {
if layer.was_transposed {
transpose_from(layer);
} else {
transpose_to(layer);
}
layer.was_transposed = !layer.was_transposed;
}
fn transpose_from(layer: &mut RecordLayer) {
let count_rows = layer.record_values.len();
let count_columns = layer.column_names.len();
if layer.was_transposed {
let headers = pop_first_column(&mut layer.record_values);
let headers = headers
.into_iter()
.map(|value| match value {
Value::String { val, .. } => val,
_ => unreachable!("must never happen"),
})
.collect();
if let Some(data) = &mut layer.record_text {
pop_first_column(data);
*data = _transpose_table(data, count_rows, count_columns - 1);
}
let data = _transpose_table(&layer.record_values, count_rows, count_columns - 1);
let headers = pop_first_column(&mut layer.record_values);
let headers = headers
.into_iter()
.map(|value| match value {
Value::String { val, .. } => val,
_ => unreachable!("must never happen"),
})
.collect();
layer.record_values = data;
layer.column_names = headers;
let data = _transpose_table(&layer.record_values, count_rows, count_columns - 1);
return;
layer.record_values = data;
layer.column_names = headers;
}
fn transpose_to(layer: &mut RecordLayer) {
let count_rows = layer.record_values.len();
let count_columns = layer.column_names.len();
if let Some(data) = &mut layer.record_text {
*data = _transpose_table(data, count_rows, count_columns);
for (column, column_name) in layer.column_names.iter().enumerate() {
let value = (column_name.to_owned(), Default::default());
data[column].insert(0, value);
}
}
let mut data = _transpose_table(&layer.record_values, count_rows, count_columns);
for (column, column_name) in layer.column_names.iter().enumerate() {
let value = Value::string(column_name, NuSpan::unknown());
data[column].insert(0, value);
}
layer.record_values = data;
layer.column_names = (1..count_rows + 1 + 1).map(|i| i.to_string()).collect();
layer.was_transposed = !layer.was_transposed;
}
fn pop_first_column(values: &mut [Vec<Value>]) -> Vec<Value> {
let mut data = vec![Value::default(); values.len()];
fn pop_first_column<T>(values: &mut [Vec<T>]) -> Vec<T>
where
T: Default + Clone,
{
let mut data = vec![T::default(); values.len()];
for (row, values) in values.iter_mut().enumerate() {
data[row] = values.remove(0);
}
@ -766,12 +789,11 @@ fn pop_first_column(values: &mut [Vec<Value>]) -> Vec<Value> {
data
}
fn _transpose_table(
values: &[Vec<Value>],
count_rows: usize,
count_columns: usize,
) -> Vec<Vec<Value>> {
let mut data = vec![vec![Value::default(); count_rows]; count_columns];
fn _transpose_table<T>(values: &[Vec<T>], count_rows: usize, count_columns: usize) -> Vec<Vec<T>>
where
T: Clone + Default,
{
let mut data = vec![vec![T::default(); count_rows]; count_columns];
for (row, values) in values.iter().enumerate() {
for (column, value) in values.iter().enumerate() {
data[column][row].clone_from(value);

View File

@ -88,6 +88,7 @@ impl StatefulWidget for TableWidget<'_> {
// todo: refactoring these to methods as they have quite a bit in common.
impl<'a> TableWidget<'a> {
// header at the top; header is always 1 line
fn render_table_horizontal(self, area: Rect, buf: &mut Buffer, state: &mut TableWidgetState) {
let padding_l = self.config.column_padding_left as u16;
let padding_r = self.config.column_padding_right as u16;
@ -130,25 +131,16 @@ impl<'a> TableWidget<'a> {
}
if show_index {
let area = Rect::new(width, data_y, area.width, data_height);
width += render_index(
buf,
area,
Rect::new(width, data_y, area.width, data_height),
self.style_computer,
self.index_row,
padding_l,
padding_r,
);
width += render_vertical_line_with_split(
buf,
width,
data_y,
data_height,
show_head,
false,
separator_s,
);
width += render_split_line(buf, width, area.y, area.height, show_head, separator_s);
}
// if there is more data than we can show, add an ellipsis to the column headers to hint at that
@ -162,6 +154,11 @@ impl<'a> TableWidget<'a> {
}
for col in self.index_column..self.columns.len() {
let need_split_line = state.count_columns > 0 && width < area.width;
if need_split_line {
width += render_split_line(buf, width, area.y, area.height, show_head, separator_s);
}
let mut column = create_column(data, col);
let column_width = calculate_column_width(&column);
@ -200,6 +197,7 @@ impl<'a> TableWidget<'a> {
}
let head_iter = [(&head, head_style)].into_iter();
// we don't change width here cause the whole column have the same width; so we add it when we print data
let mut w = width;
w += render_space(buf, w, head_y, 1, padding_l);
w += render_column(buf, w, head_y, use_space, head_iter);
@ -209,10 +207,10 @@ impl<'a> TableWidget<'a> {
state.layout.push(&head, x, head_y, use_space, 1);
}
let head_rows = column.iter().map(|(t, s)| (t, *s));
let column_rows = column.iter().map(|(t, s)| (t, *s));
width += render_space(buf, width, data_y, data_height, padding_l);
width += render_column(buf, width, data_y, use_space, head_rows);
width += render_column(buf, width, data_y, use_space, column_rows);
width += render_space(buf, width, data_y, data_height, padding_r);
for (row, (text, _)) in column.iter().enumerate() {
@ -235,15 +233,7 @@ impl<'a> TableWidget<'a> {
}
if width < area.width {
width += render_vertical_line_with_split(
buf,
width,
data_y,
data_height,
show_head,
false,
separator_s,
);
width += render_split_line(buf, width, area.y, area.height, show_head, separator_s);
}
let rest = area.width.saturating_sub(width);
@ -255,6 +245,7 @@ impl<'a> TableWidget<'a> {
}
}
// header at the left; header is always 1 line
fn render_table_vertical(self, area: Rect, buf: &mut Buffer, state: &mut TableWidgetState) {
if area.width == 0 || area.height == 0 {
return;
@ -353,6 +344,9 @@ impl<'a> TableWidget<'a> {
state.count_rows = columns.len();
state.count_columns = 0;
// note: is there a time where we would have more then 1 column?
// seems like not really; cause it's literally KV table, or am I wrong?
for col in self.index_column..self.data.len() {
let mut column =
self.data[col][self.index_row..self.index_row + columns.len()].to_vec();
@ -361,6 +355,13 @@ impl<'a> TableWidget<'a> {
break;
}
// see KV comment; this block might never got used
let need_split_line = state.count_columns > 0 && left_w < area.width;
if need_split_line {
render_vertical_line(buf, area.x + left_w, area.y, area.height, separator_s);
left_w += 1;
}
let column_width = column_width as u16;
let available = area.width - left_w;
let is_last = col + 1 == self.data.len();
@ -555,6 +556,51 @@ fn render_index(
width
}
fn render_split_line(
buf: &mut Buffer,
x: u16,
y: u16,
height: u16,
has_head: bool,
style: NuStyle,
) -> u16 {
if has_head {
render_vertical_split_line(buf, x, y, height, &[0], &[2], &[], style);
} else {
render_vertical_split_line(buf, x, y, height, &[], &[], &[], style);
}
1
}
#[allow(clippy::too_many_arguments)]
fn render_vertical_split_line(
buf: &mut Buffer,
x: u16,
y: u16,
height: u16,
top_slit: &[u16],
inner_slit: &[u16],
bottom_slit: &[u16],
style: NuStyle,
) -> u16 {
render_vertical_line(buf, x, y, height, style);
for &y in top_slit {
render_top_connector(buf, x, y, style);
}
for &y in inner_slit {
render_inner_connector(buf, x, y, style);
}
for &y in bottom_slit {
render_bottom_connector(buf, x, y, style);
}
1
}
fn render_vertical_line_with_split(
buf: &mut Buffer,
x: u16,
@ -668,6 +714,12 @@ fn render_bottom_connector(buf: &mut Buffer, x: u16, y: u16, style: NuStyle) {
buf.set_span(x, y, &span, 1);
}
fn render_inner_connector(buf: &mut Buffer, x: u16, y: u16, style: NuStyle) {
let style = nu_style_to_tui(style);
let span = Span::styled("", style);
buf.set_span(x, y, &span, 1);
}
fn calculate_column_width(column: &[NuText]) -> usize {
column
.iter()

View File

@ -1,6 +1,6 @@
[package]
name = "nu-glob"
version = "0.94.3"
version = "0.95.1"
authors = ["The Nushell Project Developers", "The Rust Project Developers"]
license = "MIT/Apache-2.0"
description = """
@ -14,4 +14,4 @@ categories = ["filesystem"]
bench = false
[dev-dependencies]
doc-comment = "0.3"
doc-comment = "0.3"

View File

@ -5,7 +5,7 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-json"
edition = "2021"
license = "MIT"
name = "nu-json"
version = "0.94.3"
version = "0.95.1"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -23,5 +23,5 @@ serde = { workspace = true }
serde_json = { workspace = true }
[dev-dependencies]
# nu-path = { path="../nu-path", version = "0.94.3" }
# serde_json = "1.0"
# nu-path = { path="../nu-path", version = "0.95.1" }
# serde_json = "1.0"

View File

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

View File

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

View File

@ -26,6 +26,7 @@ pub enum FlatShape {
Flag,
Float,
Garbage,
GlobInterpolation,
GlobPattern,
Int,
InternalCall(DeclId),
@ -67,6 +68,7 @@ impl FlatShape {
FlatShape::Flag => "shape_flag",
FlatShape::Float => "shape_float",
FlatShape::Garbage => "shape_garbage",
FlatShape::GlobInterpolation => "shape_glob_interpolation",
FlatShape::GlobPattern => "shape_globpattern",
FlatShape::Int => "shape_int",
FlatShape::InternalCall(_) => "shape_internalcall",
@ -277,7 +279,7 @@ fn flatten_expression_into(
output[arg_start..].sort();
}
Expr::ExternalCall(head, args) => {
if let Expr::String(..) = &head.expr {
if let Expr::String(..) | Expr::GlobPattern(..) = &head.expr {
output.push((head.span, FlatShape::External));
} else {
flatten_expression_into(working_set, head, output);
@ -286,7 +288,7 @@ fn flatten_expression_into(
for arg in args.as_ref() {
match arg {
ExternalArgument::Regular(expr) => {
if let Expr::String(..) = &expr.expr {
if let Expr::String(..) | Expr::GlobPattern(..) = &expr.expr {
output.push((expr.span, FlatShape::ExternalArg));
} else {
flatten_expression_into(working_set, expr, output);
@ -431,6 +433,25 @@ fn flatten_expression_into(
}
output.extend(flattened);
}
Expr::GlobInterpolation(exprs, quoted) => {
let mut flattened = vec![];
for expr in exprs {
flatten_expression_into(working_set, expr, &mut flattened);
}
if *quoted {
// If we aren't a bare word interpolation, also highlight the outer quotes
output.push((
Span::new(expr.span.start, expr.span.start + 2),
FlatShape::GlobInterpolation,
));
flattened.push((
Span::new(expr.span.end - 1, expr.span.end),
FlatShape::GlobInterpolation,
));
}
output.extend(flattened);
}
Expr::Record(list) => {
let outer_span = expr.span;
let mut last_end = outer_span.start;

View File

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

View File

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

View File

@ -16,7 +16,6 @@ use nu_protocol::{
IN_VARIABLE_ID,
};
use std::{
borrow::Cow,
collections::{HashMap, HashSet},
num::ParseIntError,
str,
@ -222,6 +221,209 @@ pub(crate) fn check_call(
}
}
/// Parses a string in the arg or head position of an external call.
///
/// If the string begins with `r#`, it is parsed as a raw string. If it doesn't contain any quotes
/// or parentheses, it is parsed as a glob pattern so that tilde and glob expansion can be handled
/// by `run-external`. Otherwise, we use a custom state machine to put together an interpolated
/// string, where each balanced pair of quotes is parsed as a separate part of the string, and then
/// concatenated together.
///
/// For example, `-foo="bar\nbaz"` becomes `$"-foo=bar\nbaz"`
fn parse_external_string(working_set: &mut StateWorkingSet, span: Span) -> Expression {
let contents = &working_set.get_span_contents(span);
if contents.starts_with(b"r#") {
parse_raw_string(working_set, span)
} else if contents
.iter()
.any(|b| matches!(b, b'"' | b'\'' | b'(' | b')'))
{
enum State {
Bare {
from: usize,
},
Quote {
from: usize,
quote_char: u8,
escaped: bool,
depth: i32,
},
}
// Find the spans of parts of the string that can be parsed as their own strings for
// concatenation.
//
// By passing each of these parts to `parse_string()`, we can eliminate the quotes and also
// handle string interpolation.
let make_span = |from: usize, index: usize| Span {
start: span.start + from,
end: span.start + index,
};
let mut spans = vec![];
let mut state = State::Bare { from: 0 };
let mut index = 0;
while index < contents.len() {
let ch = contents[index];
match &mut state {
State::Bare { from } => match ch {
b'"' | b'\'' => {
// Push bare string
if index != *from {
spans.push(make_span(*from, index));
}
// then transition to other state
state = State::Quote {
from: index,
quote_char: ch,
escaped: false,
depth: 1,
};
}
b'$' => {
if let Some(&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 {
let contents = working_set.get_span_contents(span);
@ -229,8 +431,6 @@ fn parse_external_arg(working_set: &mut StateWorkingSet, span: Span) -> External
ExternalArgument::Regular(parse_dollar_expr(working_set, span))
} else if contents.starts_with(b"[") {
ExternalArgument::Regular(parse_list_expression(working_set, span, &SyntaxShape::Any))
} else if contents.starts_with(b"r#") {
ExternalArgument::Regular(parse_raw_string(working_set, span))
} else if contents.len() > 3
&& contents.starts_with(b"...")
&& (contents[3] == b'$' || contents[3] == b'[' || contents[3] == b'(')
@ -241,18 +441,7 @@ fn parse_external_arg(working_set: &mut StateWorkingSet, span: Span) -> External
&SyntaxShape::List(Box::new(SyntaxShape::Any)),
))
} else {
// Eval stage trims the quotes, so we don't have to do the same thing when parsing.
let (contents, err) = unescape_string_preserving_quotes(contents, span);
if let Some(err) = err {
working_set.error(err);
}
ExternalArgument::Regular(Expression::new(
working_set,
Expr::String(contents),
span,
Type::String,
))
ExternalArgument::Regular(parse_external_string(working_set, span))
}
}
@ -274,18 +463,7 @@ pub fn parse_external_call(working_set: &mut StateWorkingSet, spans: &[Span]) ->
let arg = parse_expression(working_set, &[head_span]);
Box::new(arg)
} else {
// Eval stage will unquote the string, so we don't bother with that here
let (contents, err) = unescape_string_preserving_quotes(&head_contents, head_span);
if let Some(err) = err {
working_set.error(err)
}
Box::new(Expression::new(
working_set,
Expr::String(contents),
head_span,
Type::String,
))
Box::new(parse_external_string(working_set, head_span))
};
let args = spans[1..]
@ -756,15 +934,12 @@ pub fn parse_internal_call(
let output = signature.get_output_type();
// storing the var ID for later due to borrowing issues
let lib_dirs_var_id = if decl.is_builtin() {
match decl.name() {
"use" | "overlay use" | "source-env" | "nu-check" => {
find_dirs_var(working_set, LIB_DIRS_VAR)
}
_ => None,
let lib_dirs_var_id = match decl.name() {
"use" | "overlay use" | "source-env" if decl.is_keyword() => {
find_dirs_var(working_set, LIB_DIRS_VAR)
}
} else {
None
"nu-check" if decl.is_builtin() => find_dirs_var(working_set, LIB_DIRS_VAR),
_ => None,
};
// The index into the positional parameter in the definition
@ -2639,23 +2814,6 @@ pub fn unescape_unquote_string(bytes: &[u8], span: Span) -> (String, Option<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 {
trace!("parsing: string");
@ -2670,6 +2828,36 @@ pub fn parse_string(working_set: &mut StateWorkingSet, span: Span) -> Expression
if bytes[0] != b'\'' && bytes[0] != b'"' && bytes[0] != b'`' && bytes.contains(&b'(') {
return parse_string_interpolation(working_set, span);
}
// Check for unbalanced quotes:
{
if bytes.starts_with(b"\"")
&& (bytes.iter().filter(|ch| **ch == b'"').count() > 1 && !bytes.ends_with(b"\""))
{
let close_delimiter_index = bytes
.iter()
.skip(1)
.position(|ch| *ch == b'"')
.expect("Already check input bytes contains at least two double quotes");
// needs `+2` rather than `+1`, because we have skip 1 to find close_delimiter_index before.
let span = Span::new(span.start + close_delimiter_index + 2, span.end);
working_set.error(ParseError::ExtraTokensAfterClosingDelimiter(span));
return garbage(working_set, span);
}
if bytes.starts_with(b"\'")
&& (bytes.iter().filter(|ch| **ch == b'\'').count() > 1 && !bytes.ends_with(b"\'"))
{
let close_delimiter_index = bytes
.iter()
.skip(1)
.position(|ch| *ch == b'\'')
.expect("Already check input bytes contains at least two double quotes");
// needs `+2` rather than `+1`, because we have skip 1 to find close_delimiter_index before.
let span = Span::new(span.start + close_delimiter_index + 2, span.end);
working_set.error(ParseError::ExtraTokensAfterClosingDelimiter(span));
return garbage(working_set, span);
}
}
let (s, err) = unescape_unquote_string(bytes, span);
if let Some(err) = err {
@ -5219,7 +5407,7 @@ pub fn parse_builtin_commands(
}
b"alias" => parse_alias(working_set, lite_command, None),
b"module" => parse_module(working_set, lite_command, None).0,
b"use" => parse_use(working_set, lite_command).0,
b"use" => parse_use(working_set, lite_command, None).0,
b"overlay" => {
if let Some(redirection) = lite_command.redirection.as_ref() {
working_set.error(redirecting_builtin_error("overlay", redirection));
@ -6012,7 +6200,7 @@ pub fn discover_captures_in_expr(
}
Expr::String(_) => {}
Expr::RawString(_) => {}
Expr::StringInterpolation(exprs) => {
Expr::StringInterpolation(exprs) | Expr::GlobInterpolation(exprs, _) => {
for expr in exprs {
discover_captures_in_expr(working_set, expr, seen, seen_blocks, output)?;
}

View File

@ -1,8 +1,8 @@
use nu_parser::*;
use nu_protocol::{
ast::{Argument, Call, Expr, ExternalArgument, PathMember, Range},
ast::{Argument, Call, Expr, Expression, ExternalArgument, PathMember, Range},
engine::{Command, EngineState, Stack, StateWorkingSet},
ParseError, PipelineData, ShellError, Signature, Span, SyntaxShape,
ParseError, PipelineData, ShellError, Signature, Span, SyntaxShape, Type,
};
use rstest::rstest;
@ -182,7 +182,7 @@ pub fn multi_test_parse_int() {
Test(
"ranges or relative paths not confused for int",
b"./a/b",
Expr::String("./a/b".into()),
Expr::GlobPattern("./a/b".into(), false),
None,
),
Test(
@ -694,6 +694,50 @@ pub fn parse_call_missing_req_flag() {
));
}
fn test_external_call(input: &str, tag: &str, f: impl FnOnce(&Expression, &[ExternalArgument])) {
let engine_state = EngineState::new();
let mut working_set = StateWorkingSet::new(&engine_state);
let block = parse(&mut working_set, None, input.as_bytes(), true);
assert!(
working_set.parse_errors.is_empty(),
"{tag}: errors: {:?}",
working_set.parse_errors
);
let pipeline = &block.pipelines[0];
assert_eq!(1, pipeline.len());
let element = &pipeline.elements[0];
match &element.expr.expr {
Expr::ExternalCall(name, args) => f(name, args),
other => {
panic!("{tag}: Unexpected expression in pipeline: {other:?}");
}
}
}
fn check_external_call_interpolation(
tag: &str,
subexpr_count: usize,
quoted: bool,
expr: &Expression,
) -> bool {
match &expr.expr {
Expr::StringInterpolation(exprs) => {
assert!(quoted, "{tag}: quoted");
assert_eq!(expr.ty, Type::String, "{tag}: expr.ty");
assert_eq!(subexpr_count, exprs.len(), "{tag}: subexpr_count");
true
}
Expr::GlobInterpolation(exprs, is_quoted) => {
assert_eq!(quoted, *is_quoted, "{tag}: quoted");
assert_eq!(expr.ty, Type::Glob, "{tag}: expr.ty");
assert_eq!(subexpr_count, exprs.len(), "{tag}: subexpr_count");
true
}
_ => false,
}
}
#[rstest]
#[case("foo-external-call", "foo-external-call", "bare word")]
#[case("^foo-external-call", "foo-external-call", "bare word with caret")]
@ -713,200 +757,370 @@ pub fn parse_call_missing_req_flag() {
r"foo\external-call",
"bare word with backslash and caret"
)]
#[case(
"^'foo external call'",
"'foo external call'",
"single quote with caret"
)]
#[case(
"^'foo/external call'",
"'foo/external call'",
"single quote with forward slash and caret"
)]
#[case(
r"^'foo\external call'",
r"'foo\external call'",
"single quote with backslash and caret"
)]
#[case(
r#"^"foo external call""#,
r#""foo external call""#,
"double quote with caret"
)]
#[case(
r#"^"foo/external call""#,
r#""foo/external call""#,
"double quote with forward slash and caret"
)]
#[case(
r#"^"foo\\external call""#,
r#""foo\external call""#,
"double quote with backslash and caret"
)]
#[case("`foo external call`", "`foo external call`", "backtick quote")]
#[case("`foo external call`", "foo external call", "backtick quote")]
#[case(
"^`foo external call`",
"`foo external call`",
"foo external call",
"backtick quote with caret"
)]
#[case(
"`foo/external call`",
"`foo/external call`",
"foo/external call",
"backtick quote with forward slash"
)]
#[case(
"^`foo/external call`",
"`foo/external call`",
"foo/external call",
"backtick quote with forward slash and caret"
)]
#[case(
r"^`foo\external call`",
r"`foo\external call`",
r"foo\external call",
"backtick quote with backslash"
)]
#[case(
r"^`foo\external call`",
r"`foo\external call`",
r"foo\external call",
"backtick quote with backslash and caret"
)]
fn test_external_call_name(#[case] input: &str, #[case] expected: &str, #[case] tag: &str) {
let engine_state = EngineState::new();
let mut working_set = StateWorkingSet::new(&engine_state);
let block = parse(&mut working_set, None, input.as_bytes(), true);
assert!(
working_set.parse_errors.is_empty(),
"{tag}: errors: {:?}",
working_set.parse_errors
);
let pipeline = &block.pipelines[0];
assert_eq!(1, pipeline.len());
let element = &pipeline.elements[0];
match &element.expr.expr {
Expr::ExternalCall(name, args) => {
match &name.expr {
Expr::String(string) => {
assert_eq!(expected, string);
}
other => {
panic!("{tag}: Unexpected expression in command name position: {other:?}");
}
}
assert_eq!(0, args.len());
}
other => {
panic!("{tag}: Unexpected expression in pipeline: {other:?}");
}
}
}
#[rstest]
#[case("^foo bar-baz", "bar-baz", "bare word")]
#[case("^foo bar/baz", "bar/baz", "bare word with forward slash")]
#[case(r"^foo bar\baz", r"bar\baz", "bare word with backslash")]
#[case("^foo 'bar baz'", "'bar baz'", "single quote")]
#[case("foo 'bar/baz'", "'bar/baz'", "single quote with forward slash")]
#[case(r"foo 'bar\baz'", r"'bar\baz'", "single quote with backslash")]
#[case(r#"^foo "bar baz""#, r#""bar baz""#, "double quote")]
#[case(r#"^foo "bar/baz""#, r#""bar/baz""#, "double quote with forward slash")]
#[case(r#"^foo "bar\\baz""#, r#""bar\baz""#, "double quote with backslash")]
#[case("^foo `bar baz`", "`bar baz`", "backtick quote")]
#[case("^foo `bar/baz`", "`bar/baz`", "backtick quote with forward slash")]
#[case(r"^foo `bar\baz`", r"`bar\baz`", "backtick quote with backslash")]
fn test_external_call_argument_regular(
pub fn test_external_call_head_glob(
#[case] input: &str,
#[case] expected: &str,
#[case] tag: &str,
) {
let engine_state = EngineState::new();
let mut working_set = StateWorkingSet::new(&engine_state);
let block = parse(&mut working_set, None, input.as_bytes(), true);
assert!(
working_set.parse_errors.is_empty(),
"{tag}: errors: {:?}",
working_set.parse_errors
);
test_external_call(input, tag, |name, args| {
match &name.expr {
Expr::GlobPattern(string, is_quoted) => {
assert_eq!(expected, string, "{tag}: incorrect name");
assert!(!*is_quoted);
}
other => {
panic!("{tag}: Unexpected expression in command name position: {other:?}");
}
}
assert_eq!(0, args.len());
})
}
let pipeline = &block.pipelines[0];
assert_eq!(1, pipeline.len());
let element = &pipeline.elements[0];
match &element.expr.expr {
Expr::ExternalCall(name, args) => {
match &name.expr {
Expr::String(string) => {
assert_eq!("foo", string, "{tag}: incorrect name");
#[rstest]
#[case(
r##"^r#'foo-external-call'#"##,
"foo-external-call",
"raw string with caret"
)]
#[case(
r##"^r#'foo/external-call'#"##,
"foo/external-call",
"raw string with forward slash and caret"
)]
#[case(
r##"^r#'foo\external-call'#"##,
r"foo\external-call",
"raw string with backslash and caret"
)]
pub fn test_external_call_head_raw_string(
#[case] input: &str,
#[case] expected: &str,
#[case] tag: &str,
) {
test_external_call(input, tag, |name, args| {
match &name.expr {
Expr::RawString(string) => {
assert_eq!(expected, string, "{tag}: incorrect name");
}
other => {
panic!("{tag}: Unexpected expression in command name position: {other:?}");
}
}
assert_eq!(0, args.len());
})
}
#[rstest]
#[case("^'foo external call'", "foo external call", "single quote with caret")]
#[case(
"^'foo/external call'",
"foo/external call",
"single quote with forward slash and caret"
)]
#[case(
r"^'foo\external call'",
r"foo\external call",
"single quote with backslash and caret"
)]
#[case(
r#"^"foo external call""#,
r#"foo external call"#,
"double quote with caret"
)]
#[case(
r#"^"foo/external call""#,
r#"foo/external call"#,
"double quote with forward slash and caret"
)]
#[case(
r#"^"foo\\external call""#,
r#"foo\external call"#,
"double quote with backslash and caret"
)]
pub fn test_external_call_head_string(
#[case] input: &str,
#[case] expected: &str,
#[case] tag: &str,
) {
test_external_call(input, tag, |name, args| {
match &name.expr {
Expr::String(string) => {
assert_eq!(expected, string);
}
other => {
panic!("{tag}: Unexpected expression in command name position: {other:?}");
}
}
assert_eq!(0, args.len());
})
}
#[rstest]
#[case(r"~/.foo/(1)", 2, false, "unquoted interpolated string")]
#[case(
r"~\.foo(2)\(1)",
4,
false,
"unquoted interpolated string with backslash"
)]
#[case(r"^~/.foo/(1)", 2, false, "unquoted interpolated string with caret")]
#[case(r#"^$"~/.foo/(1)""#, 2, true, "quoted interpolated string with caret")]
pub fn test_external_call_head_interpolated_string(
#[case] input: &str,
#[case] subexpr_count: usize,
#[case] quoted: bool,
#[case] tag: &str,
) {
test_external_call(input, tag, |name, args| {
if !check_external_call_interpolation(tag, subexpr_count, quoted, name) {
panic!("{tag}: Unexpected expression in command name position: {name:?}");
}
assert_eq!(0, args.len());
})
}
#[rstest]
#[case("^foo foo-external-call", "foo-external-call", "bare word")]
#[case(
"^foo foo/external-call",
"foo/external-call",
"bare word with forward slash"
)]
#[case(
r"^foo foo\external-call",
r"foo\external-call",
"bare word with backslash"
)]
#[case(
"^foo `foo external call`",
"foo external call",
"backtick quote with caret"
)]
#[case(
"^foo `foo/external call`",
"foo/external call",
"backtick quote with forward slash"
)]
#[case(
r"^foo `foo\external call`",
r"foo\external call",
"backtick quote with backslash"
)]
pub fn test_external_call_arg_glob(#[case] input: &str, #[case] expected: &str, #[case] tag: &str) {
test_external_call(input, tag, |name, args| {
match &name.expr {
Expr::GlobPattern(string, _) => {
assert_eq!("foo", string, "{tag}: incorrect name");
}
other => {
panic!("{tag}: Unexpected expression in command name position: {other:?}");
}
}
assert_eq!(1, args.len());
match &args[0] {
ExternalArgument::Regular(expr) => match &expr.expr {
Expr::GlobPattern(string, is_quoted) => {
assert_eq!(expected, string, "{tag}: incorrect arg");
assert!(!*is_quoted);
}
other => {
panic!("{tag}: Unexpected expression in command name position: {other:?}");
}
}
assert_eq!(1, args.len());
match &args[0] {
ExternalArgument::Regular(expr) => match &expr.expr {
Expr::String(string) => {
assert_eq!(expected, string, "{tag}: incorrect arg");
}
other => {
panic!("Unexpected expression in command arg position: {other:?}")
}
},
other @ ExternalArgument::Spread(..) => {
panic!("Unexpected external spread argument in command arg position: {other:?}")
panic!("Unexpected expression in command arg position: {other:?}")
}
},
other @ ExternalArgument::Spread(..) => {
panic!("Unexpected external spread argument in command arg position: {other:?}")
}
}
other => {
panic!("{tag}: Unexpected expression in pipeline: {other:?}");
})
}
#[rstest]
#[case(r##"^foo r#'foo-external-call'#"##, "foo-external-call", "raw string")]
#[case(
r##"^foo r#'foo/external-call'#"##,
"foo/external-call",
"raw string with forward slash"
)]
#[case(
r##"^foo r#'foo\external-call'#"##,
r"foo\external-call",
"raw string with backslash"
)]
pub fn test_external_call_arg_raw_string(
#[case] input: &str,
#[case] expected: &str,
#[case] tag: &str,
) {
test_external_call(input, tag, |name, args| {
match &name.expr {
Expr::GlobPattern(string, _) => {
assert_eq!("foo", string, "{tag}: incorrect name");
}
other => {
panic!("{tag}: Unexpected expression in command name position: {other:?}");
}
}
}
assert_eq!(1, args.len());
match &args[0] {
ExternalArgument::Regular(expr) => match &expr.expr {
Expr::RawString(string) => {
assert_eq!(expected, string, "{tag}: incorrect arg");
}
other => {
panic!("Unexpected expression in command arg position: {other:?}")
}
},
other @ ExternalArgument::Spread(..) => {
panic!("Unexpected external spread argument in command arg position: {other:?}")
}
}
})
}
#[rstest]
#[case("^foo 'foo external call'", "foo external call", "single quote")]
#[case(
"^foo 'foo/external call'",
"foo/external call",
"single quote with forward slash"
)]
#[case(
r"^foo 'foo\external call'",
r"foo\external call",
"single quote with backslash"
)]
#[case(r#"^foo "foo external call""#, r#"foo external call"#, "double quote")]
#[case(
r#"^foo "foo/external call""#,
r#"foo/external call"#,
"double quote with forward slash"
)]
#[case(
r#"^foo "foo\\external call""#,
r#"foo\external call"#,
"double quote with backslash"
)]
pub fn test_external_call_arg_string(
#[case] input: &str,
#[case] expected: &str,
#[case] tag: &str,
) {
test_external_call(input, tag, |name, args| {
match &name.expr {
Expr::GlobPattern(string, _) => {
assert_eq!("foo", string, "{tag}: incorrect name");
}
other => {
panic!("{tag}: Unexpected expression in command name position: {other:?}");
}
}
assert_eq!(1, args.len());
match &args[0] {
ExternalArgument::Regular(expr) => match &expr.expr {
Expr::String(string) => {
assert_eq!(expected, string, "{tag}: incorrect arg");
}
other => {
panic!("{tag}: Unexpected expression in command arg position: {other:?}")
}
},
other @ ExternalArgument::Spread(..) => {
panic!(
"{tag}: Unexpected external spread argument in command arg position: {other:?}"
)
}
}
})
}
#[rstest]
#[case(r"^foo ~/.foo/(1)", 2, false, "unquoted interpolated string")]
#[case(r#"^foo $"~/.foo/(1)""#, 2, true, "quoted interpolated string")]
pub fn test_external_call_arg_interpolated_string(
#[case] input: &str,
#[case] subexpr_count: usize,
#[case] quoted: bool,
#[case] tag: &str,
) {
test_external_call(input, tag, |name, args| {
match &name.expr {
Expr::GlobPattern(string, _) => {
assert_eq!("foo", string, "{tag}: incorrect name");
}
other => {
panic!("{tag}: Unexpected expression in command name position: {other:?}");
}
}
assert_eq!(1, args.len());
match &args[0] {
ExternalArgument::Regular(expr) => {
if !check_external_call_interpolation(tag, subexpr_count, quoted, expr) {
panic!("Unexpected expression in command arg position: {expr:?}")
}
}
other @ ExternalArgument::Spread(..) => {
panic!("Unexpected external spread argument in command arg position: {other:?}")
}
}
})
}
#[test]
fn test_external_call_argument_spread() {
let engine_state = EngineState::new();
let mut working_set = StateWorkingSet::new(&engine_state);
let block = parse(&mut working_set, None, b"^foo ...[a b c]", true);
assert!(
working_set.parse_errors.is_empty(),
"errors: {:?}",
working_set.parse_errors
);
let input = r"^foo ...[a b c]";
let tag = "spread";
let pipeline = &block.pipelines[0];
assert_eq!(1, pipeline.len());
let element = &pipeline.elements[0];
match &element.expr.expr {
Expr::ExternalCall(name, args) => {
match &name.expr {
Expr::String(string) => {
assert_eq!("foo", string, "incorrect name");
test_external_call(input, tag, |name, args| {
match &name.expr {
Expr::GlobPattern(string, _) => {
assert_eq!("foo", string, "incorrect name");
}
other => {
panic!("Unexpected expression in command name position: {other:?}");
}
}
assert_eq!(1, args.len());
match &args[0] {
ExternalArgument::Spread(expr) => match &expr.expr {
Expr::List(items) => {
assert_eq!(3, items.len());
// that's good enough, don't really need to go so deep into it...
}
other => {
panic!("Unexpected expression in command name position: {other:?}");
}
}
assert_eq!(1, args.len());
match &args[0] {
ExternalArgument::Spread(expr) => match &expr.expr {
Expr::List(items) => {
assert_eq!(3, items.len());
// that's good enough, don't really need to go so deep into it...
}
other => {
panic!("Unexpected expression in command arg position: {other:?}")
}
},
other @ ExternalArgument::Regular(..) => {
panic!(
"Unexpected external regular argument in command arg position: {other:?}"
)
panic!("Unexpected expression in command arg position: {other:?}")
}
},
other @ ExternalArgument::Regular(..) => {
panic!("Unexpected external regular argument in command arg position: {other:?}")
}
}
other => {
panic!("Unexpected expression in pipeline: {other:?}");
}
}
})
}
#[test]
@ -1132,6 +1346,44 @@ mod string {
assert_eq!(subexprs[0], &Expr::String("(1 + 3)(7 - 5)".to_string()));
}
#[test]
pub fn parse_string_interpolation_bare() {
let engine_state = EngineState::new();
let mut working_set = StateWorkingSet::new(&engine_state);
let block = parse(
&mut working_set,
None,
b"\"\" ++ foo(1 + 3)bar(7 - 5)",
true,
);
assert!(working_set.parse_errors.is_empty());
assert_eq!(block.len(), 1);
let pipeline = &block.pipelines[0];
assert_eq!(pipeline.len(), 1);
let element = &pipeline.elements[0];
assert!(element.redirection.is_none());
let subexprs: Vec<&Expr> = match &element.expr.expr {
Expr::BinaryOp(_, _, rhs) => match &rhs.expr {
Expr::StringInterpolation(expressions) => {
expressions.iter().map(|e| &e.expr).collect()
}
_ => panic!("Expected an `Expr::StringInterpolation`"),
},
_ => panic!("Expected an `Expr::BinaryOp`"),
};
assert_eq!(subexprs.len(), 4);
assert_eq!(subexprs[0], &Expr::String("foo".to_string()));
assert!(matches!(subexprs[1], &Expr::FullCellPath(..)));
assert_eq!(subexprs[2], &Expr::String("bar".to_string()));
assert!(matches!(subexprs[3], &Expr::FullCellPath(..)));
}
#[test]
pub fn parse_nested_expressions() {
let engine_state = EngineState::new();

View File

@ -5,7 +5,7 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-path"
edition = "2021"
license = "MIT"
name = "nu-path"
version = "0.94.3"
version = "0.95.1"
exclude = ["/fuzz"]
[lib]
@ -18,4 +18,4 @@ dirs-next = { workspace = true }
omnipath = { workspace = true }
[target.'cfg(all(unix, not(target_os = "macos"), not(target_os = "android")))'.dependencies]
pwd = { workspace = true }
pwd = { workspace = true }

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

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"
license = "MIT"
name = "nu-plugin-core"
version = "0.94.3"
version = "0.95.1"
[lib]
bench = false
[dependencies]
nu-protocol = { path = "../nu-protocol", version = "0.94.3" }
nu-plugin-protocol = { path = "../nu-plugin-protocol", version = "0.94.3", default-features = false }
nu-protocol = { path = "../nu-protocol", version = "0.95.1" }
nu-plugin-protocol = { path = "../nu-plugin-protocol", version = "0.95.1", default-features = false }
rmp-serde = { workspace = true }
serde = { workspace = true }
@ -25,4 +25,4 @@ default = ["local-socket"]
local-socket = ["interprocess", "nu-plugin-protocol/local-socket"]
[target.'cfg(target_os = "windows")'.dependencies]
windows = { workspace = true }
windows = { workspace = true }

View File

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

View File

@ -252,7 +252,7 @@ pub fn load_plugin_registry_item(
})?;
match &plugin.data {
PluginRegistryItemData::Valid { commands } => {
PluginRegistryItemData::Valid { metadata, commands } => {
let plugin = add_plugin_to_working_set(working_set, &identity)?;
// Ensure that the plugin is reset. We're going to load new signatures, so we want to
@ -260,6 +260,9 @@ pub fn load_plugin_registry_item(
// doesn't.
plugin.reset()?;
// Set the plugin metadata from the file
plugin.set_metadata(Some(metadata.clone()));
// Create the declarations from the commands
for signature in commands {
let decl = PluginDeclaration::new(plugin.clone(), signature.clone());

View File

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

View File

@ -18,7 +18,7 @@ use nu_protocol::{
ast::{Math, Operator},
engine::Closure,
ByteStreamType, CustomValue, IntoInterruptiblePipelineData, IntoSpanned, PipelineData,
PluginSignature, ShellError, Span, Spanned, Value,
PluginMetadata, PluginSignature, ShellError, Span, Spanned, Value,
};
use serde::{Deserialize, Serialize};
use std::{
@ -1019,6 +1019,25 @@ fn start_fake_plugin_call_responder(
.expect("failed to spawn thread");
}
#[test]
fn interface_get_metadata() -> Result<(), ShellError> {
let test = TestCase::new();
let manager = test.plugin("test");
let interface = manager.get_interface();
start_fake_plugin_call_responder(manager, 1, |_| {
vec![ReceivedPluginCallMessage::Response(
PluginCallResponse::Metadata(PluginMetadata::new().with_version("test")),
)]
});
let metadata = interface.get_metadata()?;
assert_eq!(Some("test"), metadata.version.as_deref());
assert!(test.has_unconsumed_write());
Ok(())
}
#[test]
fn interface_get_signature() -> Result<(), ShellError> {
let test = TestCase::new();

View File

@ -7,7 +7,7 @@ use super::{PluginInterface, PluginSource};
use nu_plugin_core::CommunicationMode;
use nu_protocol::{
engine::{EngineState, Stack},
PluginGcConfig, PluginIdentity, RegisteredPlugin, ShellError,
PluginGcConfig, PluginIdentity, PluginMetadata, RegisteredPlugin, ShellError,
};
use std::{
collections::HashMap,
@ -31,6 +31,8 @@ pub struct PersistentPlugin {
struct MutableState {
/// Reference to the plugin if running
running: Option<RunningPlugin>,
/// Metadata for the plugin, e.g. version.
metadata: Option<PluginMetadata>,
/// Plugin's preferred communication mode (if known)
preferred_mode: Option<PreferredCommunicationMode>,
/// Garbage collector config
@ -59,6 +61,7 @@ impl PersistentPlugin {
identity,
mutable: Mutex::new(MutableState {
running: None,
metadata: None,
preferred_mode: None,
gc_config,
}),
@ -268,6 +271,16 @@ impl RegisteredPlugin for PersistentPlugin {
self.stop_internal(true)
}
fn metadata(&self) -> Option<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) {
if let Ok(mut mutable) = self.mutable.lock() {
// 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"
license = "MIT"
name = "nu-plugin-protocol"
version = "0.94.3"
version = "0.95.1"
[lib]
bench = false
[dependencies]
nu-protocol = { path = "../nu-protocol", version = "0.94.3", features = ["plugin"] }
nu-utils = { path = "../nu-utils", version = "0.94.3" }
nu-protocol = { path = "../nu-protocol", version = "0.95.1", features = ["plugin"] }
nu-utils = { path = "../nu-utils", version = "0.95.1" }
bincode = "1.3"
serde = { workspace = true, features = ["derive"] }
@ -21,4 +21,4 @@ typetag = "0.2"
[features]
default = ["local-socket"]
local-socket = []
local-socket = []

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,16 +5,16 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-plugin"
edition = "2021"
license = "MIT"
name = "nu-plugin"
version = "0.94.3"
version = "0.95.1"
[lib]
bench = false
[dependencies]
nu-engine = { path = "../nu-engine", version = "0.94.3" }
nu-protocol = { path = "../nu-protocol", version = "0.94.3" }
nu-plugin-protocol = { path = "../nu-plugin-protocol", version = "0.94.3" }
nu-plugin-core = { path = "../nu-plugin-core", version = "0.94.3", default-features = false }
nu-engine = { path = "../nu-engine", version = "0.95.1" }
nu-protocol = { path = "../nu-protocol", version = "0.95.1" }
nu-plugin-protocol = { path = "../nu-plugin-protocol", version = "0.95.1" }
nu-plugin-core = { path = "../nu-plugin-core", version = "0.95.1", default-features = false }
log = { workspace = true }
thiserror = "1.0"
@ -29,4 +29,4 @@ local-socket = ["nu-plugin-core/local-socket"]
[target.'cfg(target_family = "unix")'.dependencies]
# For setting the process group ID (EnterForeground / LeaveForeground)
nix = { workspace = true, default-features = false, features = ["process"] }
nix = { workspace = true, default-features = false, features = ["process"] }

View File

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

View File

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

View File

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

View File

@ -322,6 +322,26 @@ fn manager_consume_goodbye_closes_plugin_call_channel() -> Result<(), ShellError
Ok(())
}
#[test]
fn manager_consume_call_metadata_forwards_to_receiver_with_context() -> Result<(), ShellError> {
let mut manager = TestCase::new().engine();
set_default_protocol_info(&mut manager)?;
let rx = manager
.take_plugin_call_receiver()
.expect("couldn't take receiver");
manager.consume(PluginInput::Call(0, PluginCall::Metadata))?;
match rx.try_recv().expect("call was not forwarded to receiver") {
ReceivedPluginCall::Metadata { engine } => {
assert_eq!(Some(0), engine.context);
Ok(())
}
call => panic!("wrong call type: {call:?}"),
}
}
#[test]
fn manager_consume_call_signature_forwards_to_receiver_with_context() -> Result<(), ShellError> {
let mut manager = TestCase::new().engine();

View File

@ -16,7 +16,8 @@ use nu_plugin_core::{
};
use nu_plugin_protocol::{CallInfo, CustomValueOp, PluginCustomValue, PluginInput, PluginOutput};
use nu_protocol::{
ast::Operator, CustomValue, IntoSpanned, LabeledError, PipelineData, ShellError, Spanned, Value,
ast::Operator, CustomValue, IntoSpanned, LabeledError, PipelineData, PluginMetadata,
ShellError, Spanned, Value,
};
use thiserror::Error;
@ -52,6 +53,10 @@ pub(crate) const OUTPUT_BUFFER_SIZE: usize = 16384;
/// struct Hello;
///
/// impl Plugin for HelloPlugin {
/// fn version(&self) -> String {
/// env!("CARGO_PKG_VERSION").into()
/// }
///
/// fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {
/// vec![Box::new(Hello)]
/// }
@ -89,6 +94,23 @@ pub(crate) const OUTPUT_BUFFER_SIZE: usize = 16384;
/// # }
/// ```
pub trait Plugin: Sync {
/// The version of the plugin.
///
/// The recommended implementation, which will use the version from your crate's `Cargo.toml`
/// file:
///
/// ```no_run
/// # use nu_plugin::{Plugin, PluginCommand};
/// # struct MyPlugin;
/// # impl Plugin for MyPlugin {
/// fn version(&self) -> String {
/// env!("CARGO_PKG_VERSION").into()
/// }
/// # fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> { vec![] }
/// # }
/// ```
fn version(&self) -> String;
/// The commands supported by the plugin
///
/// Each [`PluginCommand`] contains both the signature of the command and the functionality it
@ -216,6 +238,7 @@ pub trait Plugin: Sync {
/// # struct MyPlugin;
/// # impl MyPlugin { fn new() -> Self { Self }}
/// # impl Plugin for MyPlugin {
/// # fn version(&self) -> String { "0.0.0".into() }
/// # fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {todo!();}
/// # }
/// fn main() {
@ -504,6 +527,12 @@ where
}
match plugin_call {
// Send metadata back to nushell so it can be stored with the plugin signatures
ReceivedPluginCall::Metadata { engine } => {
engine
.write_metadata(PluginMetadata::new().with_version(plugin.version()))
.try_to_report(&engine)?;
}
// Sending the signature back to nushell to create the declaration definition
ReceivedPluginCall::Signature { engine } => {
let sigs = commands

View File

@ -5,7 +5,7 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-pretty-hex"
edition = "2021"
license = "MIT"
name = "nu-pretty-hex"
version = "0.94.3"
version = "0.95.1"
[lib]
doctest = false
@ -18,4 +18,4 @@ nu-ansi-term = { workspace = true }
[dev-dependencies]
heapless = { version = "0.8", default-features = false }
rand = "0.8"
rand = "0.8"

View File

@ -5,7 +5,7 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-protocol"
edition = "2021"
license = "MIT"
name = "nu-protocol"
version = "0.94.3"
version = "0.95.1"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -13,10 +13,10 @@ version = "0.94.3"
bench = false
[dependencies]
nu-utils = { path = "../nu-utils", version = "0.94.3" }
nu-path = { path = "../nu-path", version = "0.94.3" }
nu-system = { path = "../nu-system", version = "0.94.3" }
nu-derive-value = { path = "../nu-derive-value", version = "0.94.3" }
nu-utils = { path = "../nu-utils", version = "0.95.1" }
nu-path = { path = "../nu-path", version = "0.95.1" }
nu-system = { path = "../nu-system", version = "0.95.1" }
nu-derive-value = { path = "../nu-derive-value", version = "0.95.1" }
brotli = { workspace = true, optional = true }
byte-unit = { version = "5.1", features = [ "serde" ] }
@ -47,11 +47,11 @@ plugin = [
serde_json = { workspace = true }
strum = "0.26"
strum_macros = "0.26"
nu-test-support = { path = "../nu-test-support", version = "0.94.3" }
nu-test-support = { path = "../nu-test-support", version = "0.95.1" }
pretty_assertions = { workspace = true }
rstest = { workspace = true }
tempfile = { workspace = true }
os_pipe = { workspace = true }
[package.metadata.docs.rs]
all-features = true
all-features = true

View File

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

View File

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

View File

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

View File

@ -81,7 +81,9 @@ pub(super) fn build_usage(comment_lines: &[&[u8]]) -> (String, String) {
usage.push_str(&comment_line);
}
if let Some((brief_usage, extra_usage)) = usage.split_once("\n\n") {
if let Some((brief_usage, extra_usage)) = usage.split_once("\r\n\r\n") {
(brief_usage.to_string(), extra_usage.to_string())
} else if let Some((brief_usage, extra_usage)) = usage.split_once("\n\n") {
(brief_usage.to_string(), extra_usage.to_string())
} else {
(usage, String::default())

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