Merge branch 'main' into ir

This commit is contained in:
Devyn Cairns 2024-06-25 22:44:50 -07:00
commit 4fe74e3f8c
No known key found for this signature in database
211 changed files with 9263 additions and 2295 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,10 +33,10 @@ jobs:
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4.1.6
- uses: actions/checkout@v4.1.7
- name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
- name: cargo fmt
run: cargo fmt --all -- --check
@ -66,10 +66,10 @@ jobs:
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4.1.6
- uses: actions/checkout@v4.1.7
- name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
- name: Tests
run: cargo test --workspace --profile ci --exclude nu_plugin_* ${{ matrix.default-flags }}
@ -95,10 +95,10 @@ jobs:
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4.1.6
- uses: actions/checkout@v4.1.7
- name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
- name: Install Nushell
run: cargo install --path . --locked --no-default-features
@ -146,10 +146,10 @@ jobs:
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4.1.6
- uses: actions/checkout@v4.1.7
- name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
- name: Clippy
run: cargo clippy --package nu_plugin_* -- $CLIPPY_OPTIONS

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
@ -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
@ -122,7 +122,7 @@ jobs:
echo "targets = ['${{matrix.target}}']" >> rust-toolchain.toml
- name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
# WARN: Keep the rustflags to prevent from the winget submission error: `CAQuietExec: Error 0xc0000135`
with:
rustflags: ''
@ -181,7 +181,7 @@ jobs:
- name: Waiting for Release
run: sleep 1800
- uses: actions/checkout@v4.1.6
- uses: actions/checkout@v4.1.7
with:
ref: main

View File

@ -62,14 +62,14 @@ jobs:
runs-on: ${{matrix.os}}
steps:
- uses: actions/checkout@v4.1.6
- uses: actions/checkout@v4.1.7
- name: Update Rust Toolchain Target
run: |
echo "targets = ['${{matrix.target}}']" >> rust-toolchain.toml
- name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
uses: actions-rust-lang/setup-rust-toolchain@v1.9.0
# WARN: Keep the rustflags to prevent from the winget submission error: `CAQuietExec: Error 0xc0000135`
with:
cache: false

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.1
uses: crate-ci/typos@v1.22.7

167
Cargo.lock generated
View File

@ -920,6 +920,15 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "convert_case"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
@ -1218,6 +1227,12 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "doctest-file"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562"
[[package]]
name = "downcast-rs"
version = "1.2.1"
@ -1678,9 +1693,9 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
name = "git2"
version = "0.18.3"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70"
checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724"
dependencies = [
"bitflags 2.5.0",
"libc",
@ -2017,10 +2032,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",
@ -2279,9 +2295,9 @@ dependencies = [
[[package]]
name = "libgit2-sys"
version = "0.16.2+1.7.2"
version = "0.17.0+1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8"
checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224"
dependencies = [
"cc",
"libc",
@ -2753,7 +2769,7 @@ dependencies = [
[[package]]
name = "nu"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"assert_cmd",
"crossterm",
@ -2806,7 +2822,7 @@ dependencies = [
[[package]]
name = "nu-cli"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"chrono",
"crossterm",
@ -2841,7 +2857,7 @@ dependencies = [
[[package]]
name = "nu-cmd-base"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"indexmap",
"miette",
@ -2853,7 +2869,7 @@ dependencies = [
[[package]]
name = "nu-cmd-extra"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"fancy-regex",
"heck 0.5.0",
@ -2878,7 +2894,7 @@ dependencies = [
[[package]]
name = "nu-cmd-lang"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"itertools 0.12.1",
"nu-engine",
@ -2890,7 +2906,7 @@ dependencies = [
[[package]]
name = "nu-cmd-plugin"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"itertools 0.12.1",
"nu-engine",
@ -2901,7 +2917,7 @@ dependencies = [
[[package]]
name = "nu-color-config"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"nu-ansi-term",
"nu-engine",
@ -2913,7 +2929,7 @@ dependencies = [
[[package]]
name = "nu-command"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"alphanumeric-sort",
"base64 0.22.1",
@ -3011,7 +3027,7 @@ dependencies = [
"uu_mv",
"uu_uname",
"uu_whoami",
"uucore 0.0.25",
"uucore",
"uuid",
"v_htmlescape",
"wax",
@ -3020,9 +3036,20 @@ dependencies = [
"winreg",
]
[[package]]
name = "nu-derive-value"
version = "0.95.1"
dependencies = [
"convert_case",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.60",
]
[[package]]
name = "nu-engine"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"log",
"nu-glob",
@ -3033,7 +3060,7 @@ dependencies = [
[[package]]
name = "nu-explore"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"ansi-str",
"anyhow",
@ -3058,14 +3085,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",
@ -3075,7 +3102,7 @@ dependencies = [
[[package]]
name = "nu-lsp"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"assert-json-diff",
"crossbeam-channel",
@ -3096,7 +3123,7 @@ dependencies = [
[[package]]
name = "nu-parser"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"bytesize",
"chrono",
@ -3112,7 +3139,7 @@ dependencies = [
[[package]]
name = "nu-path"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"dirs-next",
"omnipath",
@ -3121,7 +3148,7 @@ dependencies = [
[[package]]
name = "nu-plugin"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"log",
"nix",
@ -3136,7 +3163,7 @@ dependencies = [
[[package]]
name = "nu-plugin-core"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"interprocess",
"log",
@ -3150,7 +3177,7 @@ dependencies = [
[[package]]
name = "nu-plugin-engine"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"log",
"nu-engine",
@ -3165,7 +3192,7 @@ dependencies = [
[[package]]
name = "nu-plugin-protocol"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"bincode",
"nu-protocol",
@ -3177,7 +3204,7 @@ dependencies = [
[[package]]
name = "nu-plugin-test-support"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"nu-ansi-term",
"nu-cmd-lang",
@ -3195,7 +3222,7 @@ dependencies = [
[[package]]
name = "nu-pretty-hex"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"heapless",
"nu-ansi-term",
@ -3204,17 +3231,19 @@ dependencies = [
[[package]]
name = "nu-protocol"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"brotli",
"byte-unit",
"chrono",
"chrono-humanize",
"convert_case",
"fancy-regex",
"indexmap",
"lru",
"miette",
"nix",
"nu-derive-value",
"nu-path",
"nu-system",
"nu-test-support",
@ -3235,7 +3264,7 @@ dependencies = [
[[package]]
name = "nu-std"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"log",
"miette",
@ -3246,7 +3275,7 @@ dependencies = [
[[package]]
name = "nu-system"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"chrono",
"itertools 0.12.1",
@ -3264,7 +3293,7 @@ dependencies = [
[[package]]
name = "nu-table"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"fancy-regex",
"nu-ansi-term",
@ -3278,7 +3307,7 @@ dependencies = [
[[package]]
name = "nu-term-grid"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"nu-utils",
"unicode-width",
@ -3286,7 +3315,7 @@ dependencies = [
[[package]]
name = "nu-test-support"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"nu-glob",
"nu-path",
@ -3298,7 +3327,7 @@ dependencies = [
[[package]]
name = "nu-utils"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"crossterm_winapi",
"log",
@ -3324,7 +3353,7 @@ dependencies = [
[[package]]
name = "nu_plugin_example"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"nu-cmd-lang",
"nu-plugin",
@ -3334,7 +3363,7 @@ dependencies = [
[[package]]
name = "nu_plugin_formats"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"eml-parser",
"ical",
@ -3347,7 +3376,7 @@ dependencies = [
[[package]]
name = "nu_plugin_gstat"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"git2",
"nu-plugin",
@ -3356,7 +3385,7 @@ dependencies = [
[[package]]
name = "nu_plugin_inc"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"nu-plugin",
"nu-protocol",
@ -3365,7 +3394,7 @@ dependencies = [
[[package]]
name = "nu_plugin_polars"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"chrono",
"chrono-tz 0.9.0",
@ -3396,7 +3425,7 @@ dependencies = [
[[package]]
name = "nu_plugin_query"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"gjson",
"nu-plugin",
@ -3408,7 +3437,7 @@ dependencies = [
[[package]]
name = "nu_plugin_stress_internals"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"interprocess",
"serde",
@ -3534,7 +3563,7 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
name = "nuon"
version = "0.94.3"
version = "0.95.1"
dependencies = [
"chrono",
"fancy-regex",
@ -6353,93 +6382,77 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uu_cp"
version = "0.0.25"
version = "0.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcbe045dc92209114afdfd366bd18f7b95dbf999f3eaa85ad6dca910b0be3d56"
checksum = "c31fc5c95f7668999e129464a29e9080f69ba01ccf7a0ae43ff2cfdb15baa340"
dependencies = [
"clap",
"filetime",
"indicatif",
"libc",
"quick-error 2.0.1",
"uucore 0.0.26",
"uucore",
"walkdir",
"xattr",
]
[[package]]
name = "uu_mkdir"
version = "0.0.25"
version = "0.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "040aa4584036b2f65e05387b0ea9ac468afce1db325743ce5f350689fd9ce4ae"
checksum = "496d95e0e3121e4d424ba62019eb84a6f1102213ca8ca16c0a2f8c652c7236c3"
dependencies = [
"clap",
"uucore 0.0.26",
"uucore",
]
[[package]]
name = "uu_mktemp"
version = "0.0.25"
version = "0.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f240a99c36d768153874d198c43605a45c86996b576262689a0f18248cc3bc57"
checksum = "a28a0d9744bdc28ceaf13f70b959bacded91aedfd008402d72fa1e3224158653"
dependencies = [
"clap",
"rand",
"tempfile",
"uucore 0.0.26",
"uucore",
]
[[package]]
name = "uu_mv"
version = "0.0.25"
version = "0.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c99fd7c75e6e85553c92537314be3d9a64b4927051aa1608513feea2f933022"
checksum = "53680908b01c5ac3cc0ee8a376de3e51a36dde2c5a5227a115a3d0977cc4539b"
dependencies = [
"clap",
"fs_extra",
"indicatif",
"uucore 0.0.26",
"uucore",
]
[[package]]
name = "uu_uname"
version = "0.0.25"
version = "0.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5951832d73199636bde6c0d61cf960932b3c4450142c290375bc10c7abed6db5"
checksum = "a7f4125fb4f286313bca8f222abaefe39db54d65179ea788c91ebd3162345f4e"
dependencies = [
"clap",
"platform-info",
"uucore 0.0.26",
"uucore",
]
[[package]]
name = "uu_whoami"
version = "0.0.25"
version = "0.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3b44166eb6335aeac42744ea368cc4c32d3f2287a4ff765a5ce44d927ab8bb4"
checksum = "7f7b313901a15cfde2d88f434fcd077903d690f73cc36d1cec20f47906960aec"
dependencies = [
"clap",
"libc",
"uucore 0.0.26",
"uucore",
"windows-sys 0.48.0",
]
[[package]]
name = "uucore"
version = "0.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23994a722acb43dbc56877e271c9723f167ae42c4c089f909b2d7dd106c3a9b4"
dependencies = [
"clap",
"glob",
"libc",
"nix",
"once_cell",
"os_display",
"uucore_procs",
"wild",
]
[[package]]
name = "uucore"
version = "0.0.26"

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
@ -39,6 +39,7 @@ members = [
"crates/nu-lsp",
"crates/nu-pretty-hex",
"crates/nu-protocol",
"crates/nu-derive-value",
"crates/nu-plugin",
"crates/nu-plugin-core",
"crates/nu-plugin-engine",
@ -74,6 +75,7 @@ chardetng = "0.1.17"
chrono = { default-features = false, version = "0.4.34" }
chrono-humanize = "0.2.3"
chrono-tz = "0.8"
convert_case = "0.6"
crossbeam-channel = "0.5.8"
crossterm = "0.27"
csv = "1.3"
@ -93,7 +95,7 @@ heck = "0.5.0"
human-date-parser = "0.1.1"
indexmap = "2.2"
indicatif = "0.17"
interprocess = "2.1.0"
interprocess = "2.2.0"
is_executable = "1.0"
itertools = "0.12"
libc = "0.2"
@ -123,11 +125,14 @@ pathdiff = "0.2"
percent-encoding = "2"
pretty_assertions = "1.4"
print-positions = "0.6"
proc-macro-error = { version = "1.0", default-features = false }
proc-macro2 = "1.0"
procfs = "0.16.0"
pwd = "1.3"
quick-xml = "0.31.0"
quickcheck = "1.0"
quickcheck_macros = "1.0"
quote = "1.0"
rand = "0.8"
ratatui = "0.26"
rayon = "1.10"
@ -147,6 +152,7 @@ serde_urlencoded = "0.7.1"
serde_yaml = "0.9"
sha2 = "0.10"
strip-ansi-escapes = "0.2.0"
syn = "2.0"
sysinfo = "0.30"
tabled = { version = "0.14.0", default-features = false }
tempfile = "3.10"
@ -159,13 +165,13 @@ unicode-segmentation = "1.11"
unicode-width = "0.1"
ureq = { version = "2.9", default-features = false }
url = "2.2"
uu_cp = "0.0.25"
uu_mkdir = "0.0.25"
uu_mktemp = "0.0.25"
uu_mv = "0.0.25"
uu_whoami = "0.0.25"
uu_uname = "0.0.25"
uucore = "0.0.25"
uu_cp = "0.0.26"
uu_mkdir = "0.0.26"
uu_mktemp = "0.0.26"
uu_mv = "0.0.26"
uu_whoami = "0.0.26"
uu_uname = "0.0.26"
uucore = "0.0.26"
uuid = "1.8.0"
v_htmlescape = "0.15.0"
wax = "0.6"
@ -174,27 +180,28 @@ windows = "0.54"
winreg = "0.52"
[dependencies]
nu-cli = { path = "./crates/nu-cli", version = "0.94.3" }
nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.94.3" }
nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.94.3" }
nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.94.3", optional = true }
nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.94.3" }
nu-command = { path = "./crates/nu-command", version = "0.94.3" }
nu-engine = { path = "./crates/nu-engine", version = "0.94.3" }
nu-explore = { path = "./crates/nu-explore", version = "0.94.3" }
nu-lsp = { path = "./crates/nu-lsp/", version = "0.94.3" }
nu-parser = { path = "./crates/nu-parser", version = "0.94.3" }
nu-path = { path = "./crates/nu-path", version = "0.94.3" }
nu-plugin-engine = { path = "./crates/nu-plugin-engine", optional = true, version = "0.94.3" }
nu-protocol = { path = "./crates/nu-protocol", version = "0.94.3" }
nu-std = { path = "./crates/nu-std", version = "0.94.3" }
nu-system = { path = "./crates/nu-system", version = "0.94.3" }
nu-utils = { path = "./crates/nu-utils", version = "0.94.3" }
nu-cli = { path = "./crates/nu-cli", version = "0.95.1" }
nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.95.1" }
nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.95.1" }
nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.95.1", optional = true }
nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.95.1" }
nu-command = { path = "./crates/nu-command", version = "0.95.1" }
nu-engine = { path = "./crates/nu-engine", version = "0.95.1" }
nu-explore = { path = "./crates/nu-explore", version = "0.95.1" }
nu-lsp = { path = "./crates/nu-lsp/", version = "0.95.1" }
nu-parser = { path = "./crates/nu-parser", version = "0.95.1" }
nu-path = { path = "./crates/nu-path", version = "0.95.1" }
nu-plugin-engine = { path = "./crates/nu-plugin-engine", optional = true, version = "0.95.1" }
nu-protocol = { path = "./crates/nu-protocol", version = "0.95.1" }
nu-std = { path = "./crates/nu-std", version = "0.95.1" }
nu-system = { path = "./crates/nu-system", version = "0.95.1" }
nu-utils = { path = "./crates/nu-utils", version = "0.95.1" }
reedline = { workspace = true, features = ["bashisms", "sqlite"] }
crossterm = { workspace = true }
ctrlc = { workspace = true }
dirs-next = { workspace = true }
log = { workspace = true }
miette = { workspace = true, features = ["fancy-no-backtrace", "fancy"] }
mimalloc = { version = "0.1.42", default-features = false, optional = true }
@ -218,9 +225,9 @@ nix = { workspace = true, default-features = false, features = [
] }
[dev-dependencies]
nu-test-support = { path = "./crates/nu-test-support", version = "0.94.3" }
nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.94.3" }
nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.94.3" }
nu-test-support = { path = "./crates/nu-test-support", version = "0.95.1" }
nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.95.1" }
nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.95.1" }
assert_cmd = "2.0"
dirs-next = { workspace = true }
tango-bench = "0.5"
@ -244,7 +251,6 @@ default = ["default-no-clipboard", "system-clipboard"]
# See https://github.com/nushell/nushell/pull/11535
default-no-clipboard = [
"plugin",
"which-support",
"trash-support",
"sqlite",
"mimalloc",
@ -264,7 +270,6 @@ system-clipboard = [
]
# Stable (Default)
which-support = ["nu-command/which-support", "nu-cmd-lang/which-support"]
trash-support = ["nu-command/trash-support", "nu-cmd-lang/trash-support"]
# SQLite commands for nushell
@ -305,4 +310,4 @@ bench = false
# Run individual benchmarks like `cargo bench -- <regex>` e.g. `cargo bench -- parse`
[[bench]]
name = "benchmarks"
harness = false
harness = false

View File

@ -52,7 +52,7 @@ To use `Nu` in GitHub Action, check [setup-nu](https://github.com/marketplace/ac
Detailed installation instructions can be found in the [installation chapter of the book](https://www.nushell.sh/book/installation.html). Nu is available via many package managers:
[![Packaging status](https://repology.org/badge/vertical-allrepos/nushell.svg)](https://repology.org/project/nushell/versions)
[![Packaging status](https://repology.org/badge/vertical-allrepos/nushell.svg?columns=3)](https://repology.org/project/nushell/versions)
For details about which platforms the Nushell team actively supports, see [our platform support policy](devdocs/PLATFORM_SUPPORT.md).
@ -222,6 +222,7 @@ Please submit an issue or PR to be added to this list.
- [clap](https://github.com/clap-rs/clap/tree/master/clap_complete_nushell)
- [Dorothy](http://github.com/bevry/dorothy)
- [Direnv](https://github.com/direnv/direnv/blob/master/docs/hook.md#nushell)
- [x-cmd](https://x-cmd.com/mod/nu)
## Contributing

View File

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

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

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

View File

@ -8,15 +8,45 @@ use nu_protocol::{
};
use std::sync::Arc;
#[derive(Default)]
pub struct EvaluateCommandsOpts {
pub table_mode: Option<Value>,
pub error_style: Option<Value>,
pub no_newline: bool,
}
/// Run a command (or commands) given to us by the user
pub fn evaluate_commands(
commands: &Spanned<String>,
engine_state: &mut EngineState,
stack: &mut Stack,
input: PipelineData,
table_mode: Option<Value>,
no_newline: bool,
opts: EvaluateCommandsOpts,
) -> Result<(), ShellError> {
let EvaluateCommandsOpts {
table_mode,
error_style,
no_newline,
} = opts;
// Handle the configured error style early
if let Some(e_style) = error_style {
match e_style.coerce_str()?.parse() {
Ok(e_style) => {
Arc::make_mut(&mut engine_state.config).error_style = e_style;
}
Err(err) => {
return Err(ShellError::GenericError {
error: "Invalid value for `--error-style`".into(),
msg: err.into(),
span: Some(e_style.span()),
help: None,
inner: vec![],
});
}
}
}
// Translate environment variables from Strings to Values
convert_env_values(engine_state, stack)?;

View File

@ -17,7 +17,7 @@ mod validation;
pub use commands::add_cli_context;
pub use completions::{FileCompletion, NuCompleter, SemanticSuggestion, SuggestionKind};
pub use config_files::eval_config_contents;
pub use eval_cmds::evaluate_commands;
pub use eval_cmds::{evaluate_commands, EvaluateCommandsOpts};
pub use eval_file::evaluate_file;
pub use menus::NuHelpCompleter;
pub use nu_cmd_base::util::get_init_cwd;

View File

@ -75,7 +75,7 @@ const DEFAULT_HELP_MENU: &str = r#"
// Adds all menus to line editor
pub(crate) fn add_menus(
mut line_editor: Reedline,
engine_state: Arc<EngineState>,
engine_state_ref: Arc<EngineState>,
stack: &Stack,
config: &Config,
) -> Result<Reedline, ShellError> {
@ -83,7 +83,7 @@ pub(crate) fn add_menus(
line_editor = line_editor.clear_menus();
for menu in &config.menus {
line_editor = add_menu(line_editor, menu, engine_state.clone(), stack, config)?
line_editor = add_menu(line_editor, menu, engine_state_ref.clone(), stack, config)?
}
// Checking if the default menus have been added from the config file
@ -93,13 +93,16 @@ pub(crate) fn add_menus(
("help_menu", DEFAULT_HELP_MENU),
];
let mut engine_state = (*engine_state_ref).clone();
let mut menu_eval_results = vec![];
for (name, definition) in default_menus {
if !config
.menus
.iter()
.any(|menu| menu.name.to_expanded_string("", config) == name)
{
let (block, _) = {
let (block, delta) = {
let mut working_set = StateWorkingSet::new(&engine_state);
let output = parse(
&mut working_set,
@ -111,15 +114,31 @@ pub(crate) fn add_menus(
(output, working_set.render())
};
engine_state.merge_delta(delta)?;
let mut temp_stack = Stack::new().capture();
let input = PipelineData::Empty;
let res = eval_block::<WithoutDebug>(&engine_state, &mut temp_stack, &block, input)?;
menu_eval_results.push(eval_block::<WithoutDebug>(
&engine_state,
&mut temp_stack,
&block,
input,
)?);
}
}
if let PipelineData::Value(value, None) = res {
for menu in create_menus(&value)? {
line_editor =
add_menu(line_editor, &menu, engine_state.clone(), stack, config)?;
}
let new_engine_state_ref = Arc::new(engine_state);
for res in menu_eval_results.into_iter() {
if let PipelineData::Value(value, None) = res {
for menu in create_menus(&value)? {
line_editor = add_menu(
line_editor,
&menu,
new_engine_state_ref.clone(),
stack,
config,
)?;
}
}
}

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

@ -763,11 +763,13 @@ fn variables_completions() {
// Test completions for $nu
let suggestions = completer.complete("$nu.", 4);
assert_eq!(15, suggestions.len());
assert_eq!(18, suggestions.len());
let expected: Vec<String> = vec![
"cache-dir".into(),
"config-path".into(),
"current-exe".into(),
"data-dir".into(),
"default-config-dir".into(),
"env-path".into(),
"history-enabled".into(),
@ -781,6 +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

@ -194,7 +194,7 @@ pub fn eval_hook(
let Some(follow) = val.get("code") else {
return Err(ShellError::CantFindColumn {
col_name: "code".into(),
span,
span: Some(span),
src_span: span,
});
};

View File

@ -20,13 +20,14 @@ pub fn get_guaranteed_cwd(engine_state: &EngineState, stack: &Stack) -> PathBuf
type MakeRangeError = fn(&str, Span) -> ShellError;
/// Returns a inclusive pair of boundary in given `range`.
pub fn process_range(range: &Range) -> Result<(isize, isize), MakeRangeError> {
match range {
Range::IntRange(range) => {
let start = range.start().try_into().unwrap_or(0);
let end = match range.end() {
Bound::Included(v) => (v + 1) as isize,
Bound::Excluded(v) => v as isize,
Bound::Included(v) => v as isize,
Bound::Excluded(v) => (v - 1) as isize,
Bound::Unbounded => isize::MAX,
};
Ok((start, end))

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 }
@ -32,11 +32,7 @@ serde_urlencoded = { workspace = true }
v_htmlescape = { workspace = true }
itertools = { workspace = true }
[features]
extra = ["default"]
default = []
[dev-dependencies]
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.94.3" }
nu-command = { path = "../nu-command", version = "0.94.3" }
nu-test-support = { path = "../nu-test-support", version = "0.94.3" }
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.95.1" }
nu-command = { path = "../nu-command", version = "0.95.1" }
nu-test-support = { path = "../nu-test-support", version = "0.95.1" }

View File

@ -1,5 +1,4 @@
use nu_engine::{command_prelude::*, get_eval_expression};
use nu_parser::parse_expression;
use nu_engine::command_prelude::*;
use nu_protocol::{ast::PathMember, engine::StateWorkingSet, ListStream};
#[derive(Clone)]
@ -57,14 +56,7 @@ impl Command for FormatPattern {
string_span.start + 1,
)?;
format(
input_val,
&ops,
engine_state,
&mut working_set,
stack,
call.head,
)
format(input_val, &ops, engine_state, call.head)
}
}
}
@ -100,8 +92,6 @@ enum FormatOperation {
FixedText(String),
// raw input is something like {column1.column2}
ValueFromColumn(String, Span),
// raw input is something like {$it.column1.column2} or {$var}.
ValueNeedEval(String, Span),
}
/// Given a pattern that is fed into the Format command, we can process it and subdivide it
@ -110,7 +100,6 @@ enum FormatOperation {
/// there without any further processing.
/// FormatOperation::ValueFromColumn contains the name of a column whose values will be
/// formatted according to the input pattern.
/// FormatOperation::ValueNeedEval contains expression which need to eval, it has the following form:
/// "$it.column1.column2" or "$variable"
fn extract_formatting_operations(
input: String,
@ -161,10 +150,17 @@ fn extract_formatting_operations(
if !column_name.is_empty() {
if column_need_eval {
output.push(FormatOperation::ValueNeedEval(
column_name.clone(),
Span::new(span_start + column_span_start, span_start + column_span_end),
));
return Err(ShellError::GenericError {
error: "Removed functionality".into(),
msg: "The ability to use variables ($it) in `format pattern` has been removed."
.into(),
span: Some(error_span),
help: Some(
"You can use other formatting options, such as string interpolation."
.into(),
),
inner: vec![],
});
} else {
output.push(FormatOperation::ValueFromColumn(
column_name.clone(),
@ -185,8 +181,6 @@ fn format(
input_data: Value,
format_operations: &[FormatOperation],
engine_state: &EngineState,
working_set: &mut StateWorkingSet,
stack: &mut Stack,
head_span: Span,
) -> Result<PipelineData, ShellError> {
let data_as_value = input_data;
@ -194,13 +188,7 @@ fn format(
// We can only handle a Record or a List of Records
match data_as_value {
Value::Record { .. } => {
match format_record(
format_operations,
&data_as_value,
engine_state,
working_set,
stack,
) {
match format_record(format_operations, &data_as_value, engine_state) {
Ok(value) => Ok(PipelineData::Value(Value::string(value, head_span), None)),
Err(value) => Err(value),
}
@ -211,13 +199,7 @@ fn format(
for val in vals.iter() {
match val {
Value::Record { .. } => {
match format_record(
format_operations,
val,
engine_state,
working_set,
stack,
) {
match format_record(format_operations, val, engine_state) {
Ok(value) => {
list.push(Value::string(value, head_span));
}
@ -256,12 +238,9 @@ fn format_record(
format_operations: &[FormatOperation],
data_as_value: &Value,
engine_state: &EngineState,
working_set: &mut StateWorkingSet,
stack: &mut Stack,
) -> Result<String, ShellError> {
let config = engine_state.get_config();
let mut output = String::new();
let eval_expression = get_eval_expression(engine_state);
for op in format_operations {
match op {
@ -283,23 +262,6 @@ fn format_record(
Err(se) => return Err(se),
}
}
FormatOperation::ValueNeedEval(_col_name, span) => {
let exp = parse_expression(working_set, &[*span]);
match working_set.parse_errors.first() {
None => {
let parsed_result = eval_expression(engine_state, stack, &exp);
if let Ok(val) = parsed_result {
output.push_str(&val.to_abbreviated_string(config))
}
}
Some(err) => {
return Err(ShellError::TypeMismatch {
err_message: format!("expression is invalid, detail message: {err:?}"),
span: *span,
})
}
}
}
}
}
Ok(output)

View File

@ -6,16 +6,16 @@ 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 }
@ -25,8 +25,7 @@ shadow-rs = { version = "0.28", default-features = false }
[features]
mimalloc = []
which-support = []
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

@ -67,8 +67,8 @@ impl Command for Def {
},
Example {
description: "Define a custom wrapper for an external command",
example: r#"def --wrapped my-echo [...rest] { echo $rest }; my-echo spam"#,
result: Some(Value::test_list(vec![Value::test_string("spam")])),
example: r#"def --wrapped my-echo [...rest] { ^echo ...$rest }; my-echo -e 'spam\tspam'"#,
result: Some(Value::test_string("spam\tspam")),
},
]
}

View File

@ -243,14 +243,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

@ -1,5 +1,6 @@
use nu_engine::{command_prelude::*, get_eval_block, get_eval_expression};
use nu_protocol::engine::CommandType;
use nu_protocol::ParseWarning;
#[derive(Clone)]
pub struct For;
@ -30,7 +31,7 @@ impl Command for For {
.required("block", SyntaxShape::Block, "The block to run.")
.switch(
"numbered",
"return a numbered item ($it.index and $it.item)",
"DEPRECATED: return a numbered item ($it.index and $it.item)",
Some('n'),
)
.creates_scope()
@ -79,6 +80,20 @@ impl Command for For {
let value = eval_expression(engine_state, stack, keyword_expr)?;
let numbered = call.has_flag(engine_state, stack, "numbered")?;
if numbered {
nu_protocol::report_error_new(
engine_state,
&ParseWarning::DeprecatedWarning {
old_command: "--numbered/-n".into(),
new_suggestion: "use `enumerate`".into(),
span: call
.get_named_arg("numbered")
.expect("`get_named_arg` found `--numbered` but still failed")
.span,
url: "See `help for` examples".into(),
},
);
}
let ctrlc = engine_state.ctrlc.clone();
let engine_state = engine_state.clone();
@ -199,8 +214,7 @@ impl Command for For {
},
Example {
description: "Number each item and print a message",
example:
"for $it in ['bob' 'fred'] --numbered { print $\"($it.index) is ($it.item)\" }",
example: r#"for $it in (['bob' 'fred'] | enumerate) { print $"($it.index) is ($it.item)" }"#,
result: None,
},
]

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
}
@ -124,6 +133,10 @@ impl Command for If {
}
}
fn search_terms(&self) -> Vec<&str> {
vec!["else", "conditional"]
}
fn examples(&self) -> Vec<Example> {
vec![
Example {

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;
@ -10,7 +10,7 @@ impl Command for Try {
}
fn usage(&self) -> &str {
"Try to run a block, if it fails optionally run a catch block."
"Try to run a block, if it fails optionally run a catch closure."
}
fn signature(&self) -> nu_protocol::Signature {
@ -18,7 +18,7 @@ impl Command for Try {
.input_output_types(vec![(Type::Any, Type::Any)])
.required("try_block", SyntaxShape::Block, "Block to run.")
.optional(
"catch_block",
"catch_closure",
SyntaxShape::Keyword(
b"catch".to_vec(),
Box::new(SyntaxShape::OneOf(vec![
@ -26,11 +26,20 @@ impl Command for Try {
SyntaxShape::Closure(Some(vec![SyntaxShape::Any])),
])),
),
"Block to run if try block fails.",
"Closure to run if try block fails.",
)
.category(Category::Core)
}
fn extra_usage(&self) -> &str {
r#"This command is a parser keyword. For details, check:
https://www.nushell.sh/book/thinking_in_nu.html"#
}
fn command_type(&self) -> CommandType {
CommandType::Keyword
}
fn run(
&self,
engine_state: &EngineState,
@ -86,9 +95,14 @@ impl Command for Try {
},
Example {
description: "Try to run a missing command",
example: "try { asdfasdf } catch { 'missing' } ",
example: "try { asdfasdf } catch { 'missing' }",
result: Some(Value::test_string("missing")),
},
Example {
description: "Try to run a missing command and report the message",
example: "try { asdfasdf } catch { |err| $err.msg }",
result: None,
},
]
}
}

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(
@ -160,11 +167,6 @@ fn features_enabled() -> Vec<String> {
// NOTE: There should be another way to know features on.
#[cfg(feature = "which-support")]
{
names.push("which".to_string());
}
#[cfg(feature = "trash-support")]
{
names.push("trash".to_string());

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

@ -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 }
@ -134,11 +134,10 @@ workspace = true
plugin = ["nu-parser/plugin"]
sqlite = ["rusqlite"]
trash-support = ["trash"]
which-support = []
[dev-dependencies]
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.94.3" }
nu-test-support = { path = "../nu-test-support", version = "0.94.3" }
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.95.1" }
nu-test-support = { path = "../nu-test-support", version = "0.95.1" }
dirs-next = { workspace = true }
mockito = { workspace = true, default-features = false }
@ -146,4 +145,4 @@ quickcheck = { workspace = true }
quickcheck_macros = { workspace = true }
rstest = { workspace = true, default-features = false }
pretty_assertions = { workspace = true }
tempfile = { workspace = true }
tempfile = { workspace = true }

View File

@ -128,48 +128,24 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value {
let range = &args.indexes;
match input {
Value::Binary { val, .. } => {
use std::cmp::{self, Ordering};
let len = val.len() as isize;
let start = if range.0 < 0 { range.0 + len } else { range.0 };
let end = if range.1 < 0 { range.1 + len } else { range.1 };
let end = if range.1 < 0 {
cmp::max(range.1 + len, 0)
} else {
range.1
};
if start < len && end >= 0 {
match start.cmp(&end) {
Ordering::Equal => Value::binary(vec![], head),
Ordering::Greater => Value::error(
ShellError::TypeMismatch {
err_message: "End must be greater than or equal to Start".to_string(),
span: head,
},
head,
),
Ordering::Less => Value::binary(
if end == isize::MAX {
val.iter()
.skip(start as usize)
.copied()
.collect::<Vec<u8>>()
} else {
val.iter()
.skip(start as usize)
.take((end - start) as usize)
.copied()
.collect()
},
head,
),
}
} else {
if start > end {
Value::binary(vec![], head)
} else {
let val_iter = val.iter().skip(start as usize);
Value::binary(
if end == isize::MAX {
val_iter.copied().collect::<Vec<u8>>()
} else {
val_iter.take((end - start + 1) as usize).copied().collect()
},
head,
)
}
}
Value::Error { .. } => input.clone(),
other => Value::error(

View File

@ -194,7 +194,7 @@ fn run_histogram(
if inputs.is_empty() {
return Err(ShellError::CantFindColumn {
col_name: col_name.clone(),
span: head_span,
span: Some(head_span),
src_span: list_span,
});
}

View File

@ -154,7 +154,7 @@ fn record_to_path_member(
let Some(value) = record.get("value") else {
return Err(ShellError::CantFindColumn {
col_name: "value".into(),
span: val_span,
span: Some(val_span),
src_span: span,
});
};

View File

@ -122,7 +122,7 @@ pub fn action(input: &Value, _args: &CellPathOnlyArgs, span: Span) -> Value {
Value::Filesize { .. } => input.clone(),
Value::Int { val, .. } => Value::filesize(*val, value_span),
Value::Float { val, .. } => Value::filesize(*val as i64, value_span),
Value::String { val, .. } => match int_from_string(val, value_span) {
Value::String { val, .. } => match i64_from_string(val, value_span) {
Ok(val) => Value::filesize(val, value_span),
Err(error) => Value::error(error, value_span),
},
@ -139,7 +139,7 @@ pub fn action(input: &Value, _args: &CellPathOnlyArgs, span: Span) -> Value {
}
}
fn int_from_string(a_string: &str, span: Span) -> Result<i64, ShellError> {
fn i64_from_string(a_string: &str, span: Span) -> Result<i64, ShellError> {
// Get the Locale so we know what the thousands separator is
let locale = get_system_locale();
@ -151,24 +151,35 @@ fn int_from_string(a_string: &str, span: Span) -> Result<i64, ShellError> {
// Hadle negative file size
if let Some(stripped_negative_string) = clean_string.strip_prefix('-') {
match stripped_negative_string.parse::<bytesize::ByteSize>() {
Ok(n) => Ok(-(n.as_u64() as i64)),
Ok(n) => i64_from_byte_size(n, true, span),
Err(_) => Err(string_convert_error(span)),
}
} else if let Some(stripped_positive_string) = clean_string.strip_prefix('+') {
match stripped_positive_string.parse::<bytesize::ByteSize>() {
Ok(n) if stripped_positive_string.starts_with(|c: char| c.is_ascii_digit()) => {
Ok(n.as_u64() as i64)
i64_from_byte_size(n, false, span)
}
_ => Err(string_convert_error(span)),
}
} else {
match clean_string.parse::<bytesize::ByteSize>() {
Ok(n) => Ok(n.as_u64() as i64),
Ok(n) => i64_from_byte_size(n, false, span),
Err(_) => Err(string_convert_error(span)),
}
}
}
fn i64_from_byte_size(
byte_size: bytesize::ByteSize,
is_negative: bool,
span: Span,
) -> Result<i64, ShellError> {
match i64::try_from(byte_size.as_u64()) {
Ok(n) => Ok(if is_negative { -n } else { n }),
Err(_) => Err(string_convert_error(span)),
}
}
fn string_convert_error(span: Span) -> ShellError {
ShellError::CantConvert {
to_type: "filesize".into(),

View File

@ -127,7 +127,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
SysTemp,
SysUsers,
UName,
Which,
};
// Help
@ -173,9 +173,6 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
))]
bind_command! { Ps };
#[cfg(feature = "which-support")]
bind_command! { Which };
// Strings
bind_command! {
Char,

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

@ -130,7 +130,7 @@ pub fn split(
Some(group_key) => Ok(group_key.coerce_string()?),
None => Err(ShellError::CantFindColumn {
col_name: column_name.item.to_string(),
span: column_name.span,
span: Some(column_name.span),
src_span: row.span(),
}),
}

View File

@ -123,7 +123,7 @@ fn validate(vec: &[Value], columns: &[String], span: Span) -> Result<(), ShellEr
if let Some(nonexistent) = nonexistent_column(columns, record.columns()) {
return Err(ShellError::CantFindColumn {
col_name: nonexistent,
span,
span: Some(span),
src_span: val_span,
});
}

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

@ -1,6 +1,7 @@
use chrono::{Datelike, Local, NaiveDate};
use nu_color_config::StyleComputer;
use nu_engine::command_prelude::*;
use nu_protocol::ast::{self, Expr, Expression};
use std::collections::VecDeque;
@ -14,6 +15,7 @@ struct Arguments {
month_names: bool,
full_year: Option<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,32 @@ pub fn cal(
style_computer,
)?;
Ok(Value::list(calendar_vec_deque.into_iter().collect(), tag).into_pipeline_data())
let mut table_no_index = ast::Call::new(Span::unknown());
table_no_index.add_named((
Spanned {
item: "index".to_string(),
span: Span::unknown(),
},
None,
Some(Expression::new_unknown(
Expr::Bool(false),
Span::unknown(),
Type::Bool,
)),
));
let cal_table_output =
Value::list(calendar_vec_deque.into_iter().collect(), tag).into_pipeline_data();
if !arguments.as_table {
crate::Table.run(
engine_state,
stack,
&(&table_no_index).into(),
cal_table_output,
)
} else {
Ok(cal_table_output)
}
}
fn get_invalid_year_shell_error(head: Span) -> ShellError {

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

@ -1,7 +1,10 @@
use super::PathSubcommandArguments;
use nu_engine::command_prelude::*;
use nu_protocol::engine::StateWorkingSet;
use std::path::{Path, PathBuf};
use std::{
io,
path::{Path, PathBuf},
};
struct Arguments {
pwd: PathBuf,
@ -36,7 +39,7 @@ impl Command for SubCommand {
fn extra_usage(&self) -> &str {
r#"This checks the file system to confirm the path's object type.
If nothing is found, an empty string will be returned."#
If the path does not exist, null will be returned."#
}
fn is_const(&self) -> bool {
@ -104,9 +107,11 @@ If nothing is found, an empty string will be returned."#
fn path_type(path: &Path, span: Span, args: &Arguments) -> Value {
let path = nu_path::expand_path_with(path, &args.pwd, true);
let meta = path.symlink_metadata();
let ty = meta.as_ref().map(get_file_type).unwrap_or("");
Value::string(ty, span)
match path.symlink_metadata() {
Ok(metadata) => Value::string(get_file_type(&metadata), span),
Err(err) if err.kind() == io::ErrorKind::NotFound => Value::nothing(span),
Err(err) => Value::error(err.into_spanned(span).into(), span),
}
}
fn get_file_type(md: &std::fs::Metadata) -> &str {

View File

@ -81,7 +81,7 @@ pub fn sort(
if let Some(nonexistent) = nonexistent_column(&sort_columns, record.columns()) {
return Err(ShellError::CantFindColumn {
col_name: nonexistent,
span,
span: Some(span),
src_span: val_span,
});
}

View File

@ -10,7 +10,6 @@ struct Arguments {
substring: String,
cell_paths: Option<Vec<CellPath>>,
case_insensitive: bool,
not_contain: bool,
}
impl CmdArgument for Arguments {
@ -40,7 +39,6 @@ impl Command for SubCommand {
"For a data structure input, check strings at the given cell paths, and replace with result.",
)
.switch("ignore-case", "search is case insensitive", Some('i'))
.switch("not", "DEPRECATED OPTION: does not contain", Some('n'))
.category(Category::Strings)
}
@ -63,27 +61,12 @@ impl Command for SubCommand {
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
if call.has_flag(engine_state, stack, "not")? {
nu_protocol::report_error_new(
engine_state,
&ShellError::GenericError {
error: "Deprecated option".into(),
msg: "`str contains --not {string}` is deprecated and will be removed in 0.95."
.into(),
span: Some(call.head),
help: Some("Please use the `not` operator instead.".into()),
inner: vec![],
},
);
}
let cell_paths: Vec<CellPath> = call.rest(engine_state, stack, 1)?;
let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
let args = Arguments {
substring: call.req::<String>(engine_state, stack, 0)?,
cell_paths,
case_insensitive: call.has_flag(engine_state, stack, "ignore-case")?,
not_contain: call.has_flag(engine_state, stack, "not")?,
};
operate(action, args, input, call.head, engine_state.ctrlc.clone())
}
@ -114,7 +97,6 @@ impl Command for SubCommand {
substring: call.req_const::<String>(working_set, 0)?,
cell_paths,
case_insensitive: call.has_flag_const(working_set, "ignore-case")?,
not_contain: call.has_flag_const(working_set, "not")?,
};
operate(
action,
@ -183,7 +165,6 @@ fn action(
input: &Value,
Arguments {
case_insensitive,
not_contain,
substring,
..
}: &Arguments,
@ -191,23 +172,11 @@ fn action(
) -> Value {
match input {
Value::String { val, .. } => Value::bool(
match case_insensitive {
true => {
if *not_contain {
!val.to_folded_case()
.contains(substring.to_folded_case().as_str())
} else {
val.to_folded_case()
.contains(substring.to_folded_case().as_str())
}
}
false => {
if *not_contain {
!val.contains(substring)
} else {
val.contains(substring)
}
}
if *case_insensitive {
val.to_folded_case()
.contains(substring.to_folded_case().as_str())
} else {
val.contains(substring)
},
head,
),

View File

@ -5,7 +5,6 @@ use nu_cmd_base::{
};
use nu_engine::command_prelude::*;
use nu_protocol::{engine::StateWorkingSet, Range};
use std::cmp::Ordering;
use unicode_segmentation::UnicodeSegmentation;
#[derive(Clone)]
@ -151,6 +150,11 @@ impl Command for SubCommand {
example: " '🇯🇵ほげ ふが ぴよ' | str substring --grapheme-clusters 4..5",
result: Some(Value::test_string("ふが")),
},
Example {
description: "sub string by negative index",
example: " 'good nushell' | str substring 5..-2",
result: Some(Value::test_string("nushel")),
},
]
}
}
@ -167,56 +171,46 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value {
options.0
};
let end: isize = if options.1 < 0 {
std::cmp::max(len + options.1, 0)
options.1 + len
} else {
options.1
};
if start < len && end >= 0 {
match start.cmp(&end) {
Ordering::Equal => Value::string("", head),
Ordering::Greater => Value::error(
ShellError::TypeMismatch {
err_message: "End must be greater than or equal to Start".to_string(),
span: head,
},
head,
),
Ordering::Less => Value::string(
{
if end == isize::MAX {
if args.graphemes {
s.graphemes(true)
.skip(start as usize)
.collect::<Vec<&str>>()
.join("")
} else {
String::from_utf8_lossy(
&s.bytes().skip(start as usize).collect::<Vec<_>>(),
)
.to_string()
}
} else if args.graphemes {
if start > end {
Value::string("", head)
} else {
Value::string(
{
if end == isize::MAX {
if args.graphemes {
s.graphemes(true)
.skip(start as usize)
.take((end - start) as usize)
.collect::<Vec<&str>>()
.join("")
} else {
String::from_utf8_lossy(
&s.bytes()
.skip(start as usize)
.take((end - start) as usize)
.collect::<Vec<_>>(),
&s.bytes().skip(start as usize).collect::<Vec<_>>(),
)
.to_string()
}
},
head,
),
}
} else {
Value::string("", head)
} else if args.graphemes {
s.graphemes(true)
.skip(start as usize)
.take((end - start + 1) as usize)
.collect::<Vec<&str>>()
.join("")
} else {
String::from_utf8_lossy(
&s.bytes()
.skip(start as usize)
.take((end - start + 1) as usize)
.collect::<Vec<_>>(),
)
.to_string()
}
},
head,
)
}
}
// Propagate errors by explicitly matching them before the final case.
@ -243,6 +237,7 @@ mod tests {
test_examples(SubCommand {})
}
#[derive(Debug)]
struct Expectation<'a> {
options: (isize, isize),
expected: &'a str,
@ -266,18 +261,19 @@ mod tests {
let word = Value::test_string("andres");
let cases = vec![
expectation("a", (0, 1)),
expectation("an", (0, 2)),
expectation("and", (0, 3)),
expectation("andr", (0, 4)),
expectation("andre", (0, 5)),
expectation("a", (0, 0)),
expectation("an", (0, 1)),
expectation("and", (0, 2)),
expectation("andr", (0, 3)),
expectation("andre", (0, 4)),
expectation("andres", (0, 5)),
expectation("andres", (0, 6)),
expectation("", (0, -6)),
expectation("a", (0, -5)),
expectation("an", (0, -4)),
expectation("and", (0, -3)),
expectation("andr", (0, -2)),
expectation("andre", (0, -1)),
expectation("a", (0, -6)),
expectation("an", (0, -5)),
expectation("and", (0, -4)),
expectation("andr", (0, -3)),
expectation("andre", (0, -2)),
expectation("andres", (0, -1)),
// str substring [ -4 , _ ]
// str substring -4 ,
expectation("dres", (-4, isize::MAX)),
@ -292,6 +288,7 @@ mod tests {
];
for expectation in &cases {
println!("{:?}", expectation);
let expected = expectation.expected;
let actual = action(
&word,

View File

@ -1,7 +1,8 @@
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::{self, Expr, Expression},
ast::{self, Expression},
did_you_mean,
process::ChildProcess,
ByteStream, NuGlob, OutDest,
@ -11,6 +12,7 @@ use nu_utils::IgnoreCaseExt;
use pathdiff::diff_paths;
use std::{
borrow::Cow,
ffi::{OsStr, OsString},
io::Write,
path::{Path, PathBuf},
process::Stdio,
@ -33,8 +35,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)
}
@ -50,42 +60,33 @@ impl Command for External {
let call = call.assert_ast_call()?;
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
};
@ -104,15 +105,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));
@ -220,76 +221,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: &ast::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(),
@ -301,31 +280,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.
///
@ -336,19 +296,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) {
@ -356,7 +318,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());
}
@ -395,23 +357,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.
///
@ -580,7 +525,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()
@ -596,17 +541,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:
@ -615,12 +561,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 {
@ -631,76 +577,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| {
@ -730,40 +639,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,4 +1,6 @@
use super::trim_cstyle_null;
use nu_engine::command_prelude::*;
use sysinfo::{CpuRefreshKind, System, MINIMUM_CPU_UPDATE_INTERVAL};
#[derive(Clone)]
pub struct SysCpu;
@ -26,7 +28,7 @@ impl Command for SysCpu {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(super::cpu(call.head).into_pipeline_data())
Ok(cpu(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
@ -37,3 +39,42 @@ impl Command for SysCpu {
}]
}
}
fn cpu(span: Span) -> Value {
let mut sys = System::new();
sys.refresh_cpu_specifics(CpuRefreshKind::everything());
// We must refresh the CPU twice a while apart to get valid usage data.
// In theory we could just sleep MINIMUM_CPU_UPDATE_INTERVAL, but I've noticed that
// that gives poor results (error of ~5%). Decided to wait 2x that long, somewhat arbitrarily
std::thread::sleep(MINIMUM_CPU_UPDATE_INTERVAL * 2);
sys.refresh_cpu_specifics(CpuRefreshKind::new().with_cpu_usage());
let cpus = sys
.cpus()
.iter()
.map(|cpu| {
// sysinfo CPU usage numbers are not very precise unless you wait a long time between refreshes.
// Round to 1DP (chosen somewhat arbitrarily) so people aren't misled by high-precision floats.
let rounded_usage = (cpu.cpu_usage() * 10.0).round() / 10.0;
let load_avg = System::load_average();
let load_avg = format!(
"{:.2}, {:.2}, {:.2}",
load_avg.one, load_avg.five, load_avg.fifteen
);
let record = record! {
"name" => Value::string(trim_cstyle_null(cpu.name()), span),
"brand" => Value::string(trim_cstyle_null(cpu.brand()), span),
"freq" => Value::int(cpu.frequency() as i64, span),
"cpu_usage" => Value::float(rounded_usage.into(), span),
"load_average" => Value::string(load_avg, span),
"vendor_id" => Value::string(trim_cstyle_null(cpu.vendor_id()), span),
};
Value::record(record, span)
})
.collect();
Value::list(cpus, span)
}

View File

@ -1,4 +1,6 @@
use super::trim_cstyle_null;
use nu_engine::command_prelude::*;
use sysinfo::Disks;
#[derive(Clone)]
pub struct SysDisks;
@ -26,7 +28,7 @@ impl Command for SysDisks {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(super::disks(call.head).into_pipeline_data())
Ok(disks(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
@ -37,3 +39,27 @@ impl Command for SysDisks {
}]
}
}
fn disks(span: Span) -> Value {
let disks = Disks::new_with_refreshed_list()
.iter()
.map(|disk| {
let device = trim_cstyle_null(disk.name().to_string_lossy());
let typ = trim_cstyle_null(disk.file_system().to_string_lossy());
let record = record! {
"device" => Value::string(device, span),
"type" => Value::string(typ, span),
"mount" => Value::string(disk.mount_point().to_string_lossy(), span),
"total" => Value::filesize(disk.total_space() as i64, span),
"free" => Value::filesize(disk.available_space() as i64, span),
"removable" => Value::bool(disk.is_removable(), span),
"kind" => Value::string(disk.kind().to_string(), span),
};
Value::record(record, span)
})
.collect();
Value::list(disks, span)
}

View File

@ -1,4 +1,7 @@
use super::trim_cstyle_null;
use chrono::{DateTime, FixedOffset, Local};
use nu_engine::command_prelude::*;
use sysinfo::System;
#[derive(Clone)]
pub struct SysHost;
@ -26,8 +29,7 @@ impl Command for SysHost {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let host = super::host(call.head);
Ok(Value::record(host, call.head).into_pipeline_data())
Ok(host(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
@ -38,3 +40,53 @@ impl Command for SysHost {
}]
}
}
fn host(span: Span) -> Value {
let mut record = Record::new();
if let Some(name) = System::name() {
record.push("name", Value::string(trim_cstyle_null(name), span));
}
if let Some(version) = System::os_version() {
record.push("os_version", Value::string(trim_cstyle_null(version), span));
}
if let Some(long_version) = System::long_os_version() {
record.push(
"long_os_version",
Value::string(trim_cstyle_null(long_version), span),
);
}
if let Some(version) = System::kernel_version() {
record.push(
"kernel_version",
Value::string(trim_cstyle_null(version), span),
);
}
if let Some(hostname) = System::host_name() {
record.push("hostname", Value::string(trim_cstyle_null(hostname), span));
}
let uptime = System::uptime()
.saturating_mul(1_000_000_000)
.try_into()
.unwrap_or(i64::MAX);
record.push("uptime", Value::duration(uptime, span));
let boot_time = boot_time()
.map(|time| Value::date(time, span))
.unwrap_or(Value::nothing(span));
record.push("boot_time", boot_time);
Value::record(record, span)
}
fn boot_time() -> Option<DateTime<FixedOffset>> {
// Broken systems can apparently return really high values.
// See: https://github.com/nushell/nushell/issues/10155
// First, try to convert u64 to i64, and then try to create a `DateTime`.
let secs = System::boot_time().try_into().ok()?;
let time = DateTime::from_timestamp(secs, 0)?;
Some(time.with_timezone(&Local).fixed_offset())
}

View File

@ -1,4 +1,5 @@
use nu_engine::command_prelude::*;
use sysinfo::System;
#[derive(Clone)]
pub struct SysMem;
@ -26,7 +27,7 @@ impl Command for SysMem {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(super::mem(call.head).into_pipeline_data())
Ok(mem(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
@ -37,3 +38,20 @@ impl Command for SysMem {
}]
}
}
fn mem(span: Span) -> Value {
let mut sys = System::new();
sys.refresh_memory();
let record = record! {
"total" => Value::filesize(sys.total_memory() as i64, span),
"free" => Value::filesize(sys.free_memory() as i64, span),
"used" => Value::filesize(sys.used_memory() as i64, span),
"available" => Value::filesize(sys.available_memory() as i64, span),
"swap total" => Value::filesize(sys.total_swap() as i64, span),
"swap free" => Value::filesize(sys.free_swap() as i64, span),
"swap used" => Value::filesize(sys.used_swap() as i64, span),
};
Value::record(record, span)
}

View File

@ -16,202 +16,6 @@ pub use sys_::Sys;
pub use temp::SysTemp;
pub use users::SysUsers;
use chrono::{DateTime, FixedOffset, Local};
use nu_protocol::{record, Record, Span, Value};
use sysinfo::{
Components, CpuRefreshKind, Disks, Networks, System, Users, MINIMUM_CPU_UPDATE_INTERVAL,
};
pub fn trim_cstyle_null(s: impl AsRef<str>) -> String {
fn trim_cstyle_null(s: impl AsRef<str>) -> String {
s.as_ref().trim_matches('\0').into()
}
pub fn disks(span: Span) -> Value {
let disks = Disks::new_with_refreshed_list()
.iter()
.map(|disk| {
let device = trim_cstyle_null(disk.name().to_string_lossy());
let typ = trim_cstyle_null(disk.file_system().to_string_lossy());
let record = record! {
"device" => Value::string(device, span),
"type" => Value::string(typ, span),
"mount" => Value::string(disk.mount_point().to_string_lossy(), span),
"total" => Value::filesize(disk.total_space() as i64, span),
"free" => Value::filesize(disk.available_space() as i64, span),
"removable" => Value::bool(disk.is_removable(), span),
"kind" => Value::string(disk.kind().to_string(), span),
};
Value::record(record, span)
})
.collect();
Value::list(disks, span)
}
pub fn net(span: Span) -> Value {
let networks = Networks::new_with_refreshed_list()
.iter()
.map(|(iface, data)| {
let record = record! {
"name" => Value::string(trim_cstyle_null(iface), span),
"sent" => Value::filesize(data.total_transmitted() as i64, span),
"recv" => Value::filesize(data.total_received() as i64, span),
};
Value::record(record, span)
})
.collect();
Value::list(networks, span)
}
pub fn cpu(span: Span) -> Value {
let mut sys = System::new();
sys.refresh_cpu_specifics(CpuRefreshKind::everything());
// We must refresh the CPU twice a while apart to get valid usage data.
// In theory we could just sleep MINIMUM_CPU_UPDATE_INTERVAL, but I've noticed that
// that gives poor results (error of ~5%). Decided to wait 2x that long, somewhat arbitrarily
std::thread::sleep(MINIMUM_CPU_UPDATE_INTERVAL * 2);
sys.refresh_cpu_specifics(CpuRefreshKind::new().with_cpu_usage());
let cpus = sys
.cpus()
.iter()
.map(|cpu| {
// sysinfo CPU usage numbers are not very precise unless you wait a long time between refreshes.
// Round to 1DP (chosen somewhat arbitrarily) so people aren't misled by high-precision floats.
let rounded_usage = (cpu.cpu_usage() * 10.0).round() / 10.0;
let load_avg = System::load_average();
let load_avg = format!(
"{:.2}, {:.2}, {:.2}",
load_avg.one, load_avg.five, load_avg.fifteen
);
let record = record! {
"name" => Value::string(trim_cstyle_null(cpu.name()), span),
"brand" => Value::string(trim_cstyle_null(cpu.brand()), span),
"freq" => Value::int(cpu.frequency() as i64, span),
"cpu_usage" => Value::float(rounded_usage.into(), span),
"load_average" => Value::string(load_avg, span),
"vendor_id" => Value::string(trim_cstyle_null(cpu.vendor_id()), span),
};
Value::record(record, span)
})
.collect();
Value::list(cpus, span)
}
pub fn mem(span: Span) -> Value {
let mut sys = System::new();
sys.refresh_memory();
let record = record! {
"total" => Value::filesize(sys.total_memory() as i64, span),
"free" => Value::filesize(sys.free_memory() as i64, span),
"used" => Value::filesize(sys.used_memory() as i64, span),
"available" => Value::filesize(sys.available_memory() as i64, span),
"swap total" => Value::filesize(sys.total_swap() as i64, span),
"swap free" => Value::filesize(sys.free_swap() as i64, span),
"swap used" => Value::filesize(sys.used_swap() as i64, span),
};
Value::record(record, span)
}
pub fn users(span: Span) -> Value {
let users = Users::new_with_refreshed_list()
.iter()
.map(|user| {
let groups = user
.groups()
.iter()
.map(|group| Value::string(trim_cstyle_null(group.name()), span))
.collect();
let record = record! {
"name" => Value::string(trim_cstyle_null(user.name()), span),
"groups" => Value::list(groups, span),
};
Value::record(record, span)
})
.collect();
Value::list(users, span)
}
pub fn host(span: Span) -> Record {
let mut record = Record::new();
if let Some(name) = System::name() {
record.push("name", Value::string(trim_cstyle_null(name), span));
}
if let Some(version) = System::os_version() {
record.push("os_version", Value::string(trim_cstyle_null(version), span));
}
if let Some(long_version) = System::long_os_version() {
record.push(
"long_os_version",
Value::string(trim_cstyle_null(long_version), span),
);
}
if let Some(version) = System::kernel_version() {
record.push(
"kernel_version",
Value::string(trim_cstyle_null(version), span),
);
}
if let Some(hostname) = System::host_name() {
record.push("hostname", Value::string(trim_cstyle_null(hostname), span));
}
let uptime = System::uptime()
.saturating_mul(1_000_000_000)
.try_into()
.unwrap_or(i64::MAX);
record.push("uptime", Value::duration(uptime, span));
let boot_time = boot_time()
.map(|time| Value::date(time, span))
.unwrap_or(Value::nothing(span));
record.push("boot_time", boot_time);
record
}
fn boot_time() -> Option<DateTime<FixedOffset>> {
// Broken systems can apparently return really high values.
// See: https://github.com/nushell/nushell/issues/10155
// First, try to convert u64 to i64, and then try to create a `DateTime`.
let secs = System::boot_time().try_into().ok()?;
let time = DateTime::from_timestamp(secs, 0)?;
Some(time.with_timezone(&Local).fixed_offset())
}
pub fn temp(span: Span) -> Value {
let components = Components::new_with_refreshed_list()
.iter()
.map(|component| {
let mut record = record! {
"unit" => Value::string(component.label(), span),
"temp" => Value::float(component.temperature().into(), span),
"high" => Value::float(component.max().into(), span),
};
if let Some(critical) = component.critical() {
record.push("critical", Value::float(critical.into(), span));
}
Value::record(record, span)
})
.collect();
Value::list(components, span)
}

View File

@ -1,4 +1,6 @@
use super::trim_cstyle_null;
use nu_engine::command_prelude::*;
use sysinfo::Networks;
#[derive(Clone)]
pub struct SysNet;
@ -26,7 +28,7 @@ impl Command for SysNet {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(super::net(call.head).into_pipeline_data())
Ok(net(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
@ -37,3 +39,20 @@ impl Command for SysNet {
}]
}
}
fn net(span: Span) -> Value {
let networks = Networks::new_with_refreshed_list()
.iter()
.map(|(iface, data)| {
let record = record! {
"name" => Value::string(trim_cstyle_null(iface), span),
"sent" => Value::filesize(data.total_transmitted() as i64, span),
"recv" => Value::filesize(data.total_received() as i64, span),
};
Value::record(record, span)
})
.collect();
Value::list(networks, span)
}

View File

@ -1,4 +1,4 @@
use nu_engine::command_prelude::*;
use nu_engine::{command_prelude::*, get_full_help};
#[derive(Clone)]
pub struct Sys;
@ -20,41 +20,17 @@ impl Command for Sys {
}
fn extra_usage(&self) -> &str {
"Note that this command may take a noticeable amount of time to run. To reduce the time taken, you can use the various `sys` sub commands to get the subset of information you are interested in."
"You must use one of the following subcommands. Using this command as-is will only produce this help message."
}
fn run(
&self,
engine_state: &EngineState,
_stack: &mut Stack,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
nu_protocol::report_error_new(
engine_state,
&ShellError::GenericError {
error: "Deprecated command".into(),
msg: "the `sys` command is deprecated, please use the new subcommands (`sys host`, `sys mem`, etc.)."
.into(),
span: Some(call.head),
help: None,
inner: vec![],
},
);
let head = call.head;
let mut host = super::host(head);
host.push("sessions", super::users(head));
let record = record! {
"host" => Value::record(host, head),
"cpu" => super::cpu(head),
"disks" => super::disks(head),
"mem" => super::mem(head),
"temp" => super::temp(head),
"net" => super::net(head),
};
Ok(Value::record(record, head).into_pipeline_data())
Ok(Value::string(get_full_help(self, engine_state, stack), call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {

View File

@ -1,4 +1,5 @@
use nu_engine::command_prelude::*;
use sysinfo::Components;
#[derive(Clone)]
pub struct SysTemp;
@ -30,7 +31,7 @@ impl Command for SysTemp {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(super::temp(call.head).into_pipeline_data())
Ok(temp(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
@ -41,3 +42,24 @@ impl Command for SysTemp {
}]
}
}
fn temp(span: Span) -> Value {
let components = Components::new_with_refreshed_list()
.iter()
.map(|component| {
let mut record = record! {
"unit" => Value::string(component.label(), span),
"temp" => Value::float(component.temperature().into(), span),
"high" => Value::float(component.max().into(), span),
};
if let Some(critical) = component.critical() {
record.push("critical", Value::float(critical.into(), span));
}
Value::record(record, span)
})
.collect();
Value::list(components, span)
}

View File

@ -1,4 +1,6 @@
use super::trim_cstyle_null;
use nu_engine::command_prelude::*;
use sysinfo::Users;
#[derive(Clone)]
pub struct SysUsers;
@ -11,7 +13,7 @@ impl Command for SysUsers {
fn signature(&self) -> Signature {
Signature::build("sys users")
.category(Category::System)
.input_output_types(vec![(Type::Nothing, Type::record())])
.input_output_types(vec![(Type::Nothing, Type::table())])
}
fn usage(&self) -> &str {
@ -25,7 +27,7 @@ impl Command for SysUsers {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(super::users(call.head).into_pipeline_data())
Ok(users(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
@ -36,3 +38,25 @@ impl Command for SysUsers {
}]
}
}
fn users(span: Span) -> Value {
let users = Users::new_with_refreshed_list()
.iter()
.map(|user| {
let groups = user
.groups()
.iter()
.map(|group| Value::string(trim_cstyle_null(group.name()), span))
.collect();
let record = record! {
"name" => Value::string(trim_cstyle_null(user.name()), span),
"groups" => Value::list(groups, span),
};
Value::record(record, span)
})
.collect();
Value::list(users, span)
}

View File

@ -92,7 +92,6 @@ fn get_entries_in_nu(
all_entries
}
#[cfg(feature = "which-support")]
fn get_first_entry_in_path(
item: &str,
span: Span,
@ -104,17 +103,6 @@ fn get_first_entry_in_path(
.ok()
}
#[cfg(not(feature = "which-support"))]
fn get_first_entry_in_path(
_item: &str,
_span: Span,
_cwd: impl AsRef<Path>,
_paths: impl AsRef<OsStr>,
) -> Option<Value> {
None
}
#[cfg(feature = "which-support")]
fn get_all_entries_in_path(
item: &str,
span: Span,
@ -129,16 +117,6 @@ fn get_all_entries_in_path(
.unwrap_or_default()
}
#[cfg(not(feature = "which-support"))]
fn get_all_entries_in_path(
_item: &str,
_span: Span,
_cwd: impl AsRef<Path>,
_paths: impl AsRef<OsStr>,
) -> Vec<Value> {
vec![]
}
#[derive(Debug)]
struct WhichArgs {
applications: Vec<Spanned<String>>,

View File

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

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

@ -31,12 +31,12 @@ fn detect_columns_with_legacy_and_flag_c() {
(
"$\"c1 c2 c3 c4 c5(char nl)a b c d e\"",
"[[c1,c3,c4,c5]; ['a b',c,d,e]]",
"0..0",
"0..1",
),
(
"$\"c1 c2 c3 c4 c5(char nl)a b c d e\"",
"[[c1,c2,c3,c4]; [a,b,c,'d e']]",
"(-2)..(-2)",
"(-2)..(-1)",
),
(
"$\"c1 c2 c3 c4 c5(char nl)a b c d e\"",
@ -72,10 +72,10 @@ drwxr-xr-x 2 root root 4.0K Mar 20 08:28 =(char nl)
drwxr-xr-x 4 root root 4.0K Mar 20 08:18 ~(char nl)
-rw-r--r-- 1 root root 3.0K Mar 20 07:23 ~asdf(char nl)\"";
let expected = "[
['column0', 'column1', 'column2', 'column3', 'column4', 'column5', 'column8'];
['drwxr-xr-x', '2', 'root', 'root', '4.0K', 'Mar 20 08:28', '='],
['drwxr-xr-x', '4', 'root', 'root', '4.0K', 'Mar 20 08:18', '~'],
['-rw-r--r--', '1', 'root', 'root', '3.0K', 'Mar 20 07:23', '~asdf']
['column0', 'column1', 'column2', 'column3', 'column4', 'column5', 'column7', 'column8'];
['drwxr-xr-x', '2', 'root', 'root', '4.0K', 'Mar 20', '08:28', '='],
['drwxr-xr-x', '4', 'root', 'root', '4.0K', 'Mar 20', '08:18', '~'],
['-rw-r--r--', '1', 'root', 'root', '3.0K', 'Mar 20', '07:23', '~asdf']
]";
let range = "5..6";
let cmd = format!(

View File

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

View File

@ -37,7 +37,7 @@ fn given_fields_can_be_column_paths() {
}
#[test]
fn can_use_variables() {
fn cant_use_variables() {
let actual = nu!(
cwd: "tests/fixtures/formats", pipeline(
r#"
@ -46,7 +46,8 @@ fn can_use_variables() {
"#
));
assert_eq!(actual.out, "nu is a new type of shell");
// TODO SPAN: This has been removed during SpanId refactor
assert!(actual.err.contains("Removed functionality"));
}
#[test]
@ -55,7 +56,7 @@ fn error_unmatched_brace() {
cwd: "tests/fixtures/formats", pipeline(
r#"
open cargo_sample.toml
| format pattern "{$it.package.name"
| format pattern "{package.name"
"#
));

View File

@ -76,6 +76,13 @@ fn into_filesize_wrong_negative_str_filesize() {
assert!(actual.err.contains("can't convert string to filesize"));
}
#[test]
fn into_filesize_large_negative_str_filesize() {
let actual = nu!("'-10000PiB' | into filesize");
assert!(actual.err.contains("can't convert string to filesize"));
}
#[test]
fn into_filesize_negative_str() {
let actual = nu!("'-1' | into filesize");
@ -104,6 +111,13 @@ fn into_filesize_wrong_positive_str_filesize() {
assert!(actual.err.contains("can't convert string to filesize"));
}
#[test]
fn into_filesize_large_positive_str_filesize() {
let actual = nu!("'+10000PiB' | into filesize");
assert!(actual.err.contains("can't convert string to filesize"));
}
#[test]
fn into_filesize_positive_str() {
let actual = nu!("'+1' | into filesize");

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

@ -125,7 +125,6 @@ mod upsert;
mod url;
mod use_;
mod where_;
#[cfg(feature = "which-support")]
mod which;
mod while_;
mod with_env;

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

@ -255,7 +255,7 @@ fn substrings_the_input() {
}
#[test]
fn substring_errors_if_start_index_is_greater_than_end_index() {
fn substring_empty_if_start_index_is_greater_than_end_index() {
Playground::setup("str_test_9", |dirs, sandbox| {
sandbox.with_files(&[FileWithContent(
"sample.toml",
@ -270,12 +270,10 @@ fn substring_errors_if_start_index_is_greater_than_end_index() {
r#"
open sample.toml
| str substring 6..4 fortune.teller.phone
| get fortune.teller.phone
"#
));
assert!(actual
.err
.contains("End must be greater than or equal to Start"))
assert_eq!(actual.out, "")
})
}
@ -375,6 +373,21 @@ fn substrings_the_input_and_treats_end_index_as_length_if_blank_end_index_given(
})
}
#[test]
fn substring_by_negative_index() {
Playground::setup("str_test_13", |dirs, _| {
let actual = nu!(
cwd: dirs.test(), "'apples' | str substring 0..-1",
);
assert_eq!(actual.out, "apples");
let actual = nu!(
cwd: dirs.test(), "'apples' | str substring 0..<-1",
);
assert_eq!(actual.out, "apple");
})
}
#[test]
fn str_reverse() {
let actual = nu!(r#"

View File

@ -906,7 +906,7 @@ fn test_cp_debug_default() {
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
if !actual
.out
.contains("copy offload: unknown, reflink: unsupported, sparse detection: no")
.contains("copy offload: yes, reflink: unsupported, sparse detection: no")
{
panic!("{}", format!("Failure: stdout was \n{}", actual.out));
}

View File

@ -0,0 +1,21 @@
[package]
authors = ["The Nushell Project Developers"]
description = "Macros implementation of #[derive(FromValue, IntoValue)]"
edition = "2021"
license = "MIT"
name = "nu-derive-value"
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-derive-value"
version = "0.95.1"
[lib]
proc-macro = true
# we can only use exposed macros in doctests really,
# so we cannot test anything useful in a doctest
doctest = false
[dependencies]
proc-macro2 = { workspace = true }
syn = { workspace = true }
quote = { workspace = true }
proc-macro-error = { workspace = true }
convert_case = { workspace = true }

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 - 2023 The Nushell Project Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,116 @@
use convert_case::Case;
use syn::{spanned::Spanned, Attribute, Fields, LitStr};
use crate::{error::DeriveError, HELPER_ATTRIBUTE};
#[derive(Debug)]
pub struct ContainerAttributes {
pub rename_all: Case,
}
impl Default for ContainerAttributes {
fn default() -> Self {
Self {
rename_all: Case::Snake,
}
}
}
impl ContainerAttributes {
pub fn parse_attrs<'a, M>(
iter: impl Iterator<Item = &'a Attribute>,
) -> Result<Self, DeriveError<M>> {
let mut container_attrs = ContainerAttributes::default();
for attr in filter(iter) {
// This is a container to allow returning derive errors inside the parse_nested_meta fn.
let mut err = Ok(());
attr.parse_nested_meta(|meta| {
let ident = meta.path.require_ident()?;
match ident.to_string().as_str() {
"rename_all" => {
// The matched case are all useful variants from `convert_case` with aliases
// that `serde` uses.
let case: LitStr = meta.value()?.parse()?;
let case = match case.value().as_str() {
"UPPER CASE" | "UPPER WITH SPACES CASE" => Case::Upper,
"lower case" | "lower with spaces case" => Case::Lower,
"Title Case" => Case::Title,
"camelCase" => Case::Camel,
"PascalCase" | "UpperCamelCase" => Case::Pascal,
"snake_case" => Case::Snake,
"UPPER_SNAKE_CASE" | "SCREAMING_SNAKE_CASE" => Case::UpperSnake,
"kebab-case" => Case::Kebab,
"COBOL-CASE" | "UPPER-KEBAB-CASE" | "SCREAMING-KEBAB-CASE" => {
Case::Cobol
}
"Train-Case" => Case::Train,
"flatcase" | "lowercase" => Case::Flat,
"UPPERFLATCASE" | "UPPERCASE" => Case::UpperFlat,
// Although very funny, we don't support `Case::{Toggle, Alternating}`,
// as we see no real benefit.
c => {
err = Err(DeriveError::InvalidAttributeValue {
value_span: case.span(),
value: Box::new(c.to_string()),
});
return Ok(()); // We stored the err in `err`.
}
};
container_attrs.rename_all = case;
}
ident => {
err = Err(DeriveError::UnexpectedAttribute {
meta_span: ident.span(),
});
}
}
Ok(())
})
.map_err(DeriveError::Syn)?;
err?; // Shortcircuit here if `err` is holding some error.
}
Ok(container_attrs)
}
}
pub fn filter<'a>(
iter: impl Iterator<Item = &'a Attribute>,
) -> impl Iterator<Item = &'a Attribute> {
iter.filter(|attr| attr.path().is_ident(HELPER_ATTRIBUTE))
}
// The deny functions are built to easily deny the use of the helper attribute if used incorrectly.
// As the usage of it gets more complex, these functions might be discarded or replaced.
/// Deny any attribute that uses the helper attribute.
pub fn deny<M>(attrs: &[Attribute]) -> Result<(), DeriveError<M>> {
match filter(attrs.iter()).next() {
Some(attr) => Err(DeriveError::InvalidAttributePosition {
attribute_span: attr.span(),
}),
None => Ok(()),
}
}
/// Deny any attributes that uses the helper attribute on any field.
pub fn deny_fields<M>(fields: &Fields) -> Result<(), DeriveError<M>> {
match fields {
Fields::Named(fields) => {
for field in fields.named.iter() {
deny(&field.attrs)?;
}
}
Fields::Unnamed(fields) => {
for field in fields.unnamed.iter() {
deny(&field.attrs)?;
}
}
Fields::Unit => (),
}
Ok(())
}

View File

@ -0,0 +1,82 @@
use std::{any, fmt::Debug, marker::PhantomData};
use proc_macro2::Span;
use proc_macro_error::{Diagnostic, Level};
#[derive(Debug)]
pub enum DeriveError<M> {
/// Marker variant, makes the `M` generic parameter valid.
_Marker(PhantomData<M>),
/// Parsing errors thrown by `syn`.
Syn(syn::parse::Error),
/// `syn::DeriveInput` was a union, currently not supported
UnsupportedUnions,
/// Only plain enums are supported right now.
UnsupportedEnums { fields_span: Span },
/// Found a `#[nu_value(x)]` attribute where `x` is unexpected.
UnexpectedAttribute { meta_span: Span },
/// Found a `#[nu_value(x)]` attribute at a invalid position.
InvalidAttributePosition { attribute_span: Span },
/// Found a valid `#[nu_value(x)]` attribute but the passed values is invalid.
InvalidAttributeValue {
value_span: Span,
value: Box<dyn Debug>,
},
}
impl<M> From<DeriveError<M>> for Diagnostic {
fn from(value: DeriveError<M>) -> Self {
let derive_name = any::type_name::<M>().split("::").last().expect("not empty");
match value {
DeriveError::_Marker(_) => panic!("used marker variant"),
DeriveError::Syn(e) => Diagnostic::spanned(e.span(), Level::Error, e.to_string()),
DeriveError::UnsupportedUnions => Diagnostic::new(
Level::Error,
format!("`{derive_name}` cannot be derived from unions"),
)
.help("consider refactoring to a struct".to_string())
.note("if you really need a union, consider opening an issue on Github".to_string()),
DeriveError::UnsupportedEnums { fields_span } => Diagnostic::spanned(
fields_span,
Level::Error,
format!("`{derive_name}` can only be derived from plain enums"),
)
.help(
"consider refactoring your data type to a struct with a plain enum as a field"
.to_string(),
)
.note("more complex enums could be implemented in the future".to_string()),
DeriveError::InvalidAttributePosition { attribute_span } => Diagnostic::spanned(
attribute_span,
Level::Error,
"invalid attribute position".to_string(),
)
.help(format!(
"check documentation for `{derive_name}` for valid placements"
)),
DeriveError::UnexpectedAttribute { meta_span } => {
Diagnostic::spanned(meta_span, Level::Error, "unknown attribute".to_string()).help(
format!("check documentation for `{derive_name}` for valid attributes"),
)
}
DeriveError::InvalidAttributeValue { value_span, value } => {
Diagnostic::spanned(value_span, Level::Error, format!("invalid value {value:?}"))
.help(format!(
"check documentation for `{derive_name}` for valid attribute values"
))
}
}
}
}

View File

@ -0,0 +1,567 @@
use convert_case::Casing;
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens};
use syn::{
spanned::Spanned, Attribute, Data, DataEnum, DataStruct, DeriveInput, Fields, Generics, Ident,
Type,
};
use crate::attributes::{self, ContainerAttributes};
#[derive(Debug)]
pub struct FromValue;
type DeriveError = super::error::DeriveError<FromValue>;
/// Inner implementation of the `#[derive(FromValue)]` macro for structs and enums.
///
/// Uses `proc_macro2::TokenStream` for better testing support, unlike `proc_macro::TokenStream`.
///
/// This function directs the `FromValue` trait derivation to the correct implementation based on
/// the input type:
/// - For structs: [`derive_struct_from_value`]
/// - For enums: [`derive_enum_from_value`]
/// - Unions are not supported and will return an error.
pub fn derive_from_value(input: TokenStream2) -> Result<TokenStream2, DeriveError> {
let input: DeriveInput = syn::parse2(input).map_err(DeriveError::Syn)?;
match input.data {
Data::Struct(data_struct) => Ok(derive_struct_from_value(
input.ident,
data_struct,
input.generics,
input.attrs,
)?),
Data::Enum(data_enum) => Ok(derive_enum_from_value(
input.ident,
data_enum,
input.generics,
input.attrs,
)?),
Data::Union(_) => Err(DeriveError::UnsupportedUnions),
}
}
/// Implements the `#[derive(FromValue)]` macro for structs.
///
/// This function ensures that the helper attribute is not used anywhere, as it is not supported for
/// structs.
/// Other than this, this function provides the impl signature for `FromValue`.
/// The implementation for `FromValue::from_value` is handled by [`struct_from_value`] and the
/// `FromValue::expected_type` is handled by [`struct_expected_type`].
fn derive_struct_from_value(
ident: Ident,
data: DataStruct,
generics: Generics,
attrs: Vec<Attribute>,
) -> Result<TokenStream2, DeriveError> {
attributes::deny(&attrs)?;
attributes::deny_fields(&data.fields)?;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let from_value_impl = struct_from_value(&data);
let expected_type_impl = struct_expected_type(&data.fields);
Ok(quote! {
#[automatically_derived]
impl #impl_generics nu_protocol::FromValue for #ident #ty_generics #where_clause {
#from_value_impl
#expected_type_impl
}
})
}
/// Implements `FromValue::from_value` for structs.
///
/// This function constructs the `from_value` function for structs.
/// The implementation is straightforward as most of the heavy lifting is handled by
/// `parse_value_via_fields`, and this function only needs to construct the signature around it.
///
/// For structs with named fields, this constructs a large return type where each field
/// contains the implementation for that specific field.
/// In structs with unnamed fields, a [`VecDeque`](std::collections::VecDeque) is used to load each
/// field one after another, and the result is used to construct the tuple.
/// For unit structs, this only checks if the input value is `Value::Nothing`.
///
/// # Examples
///
/// These examples show what the macro would generate.
///
/// Struct with named fields:
/// ```rust
/// #[derive(IntoValue)]
/// struct Pet {
/// name: String,
/// age: u8,
/// favorite_toy: Option<String>,
/// }
///
/// impl nu_protocol::FromValue for Pet {
/// fn from_value(
/// v: nu_protocol::Value
/// ) -> std::result::Result<Self, nu_protocol::ShellError> {
/// let span = v.span();
/// let mut record = v.into_record()?;
/// std::result::Result::Ok(Pet {
/// name: <String as nu_protocol::FromValue>::from_value(
/// record
/// .remove("name")
/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn {
/// col_name: std::string::ToString::to_string("name"),
/// span: std::option::Option::None,
/// src_span: span
/// })?,
/// )?,
/// age: <u8 as nu_protocol::FromValue>::from_value(
/// record
/// .remove("age")
/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn {
/// col_name: std::string::ToString::to_string("age"),
/// span: std::option::Option::None,
/// src_span: span
/// })?,
/// )?,
/// favorite_toy: record
/// .remove("favorite_toy")
/// .map(|v| <#ty as nu_protocol::FromValue>::from_value(v))
/// .transpose()?
/// .flatten(),
/// })
/// }
/// }
/// ```
///
/// Struct with unnamed fields:
/// ```rust
/// #[derive(IntoValue)]
/// struct Color(u8, u8, u8);
///
/// impl nu_protocol::FromValue for Color {
/// fn from_value(
/// v: nu_protocol::Value
/// ) -> std::result::Result<Self, nu_protocol::ShellError> {
/// let span = v.span();
/// let list = v.into_list()?;
/// let mut deque: std::collections::VecDeque<_> = std::convert::From::from(list);
/// std::result::Result::Ok(Self(
/// {
/// <u8 as nu_protocol::FromValue>::from_value(
/// deque
/// .pop_front()
/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn {
/// col_name: std::string::ToString::to_string(&0),
/// span: std::option::Option::None,
/// src_span: span
/// })?,
/// )?
/// },
/// {
/// <u8 as nu_protocol::FromValue>::from_value(
/// deque
/// .pop_front()
/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn {
/// col_name: std::string::ToString::to_string(&1),
/// span: std::option::Option::None,
/// src_span: span
/// })?,
/// )?
/// },
/// {
/// <u8 as nu_protocol::FromValue>::from_value(
/// deque
/// .pop_front()
/// .ok_or_else(|| nu_protocol::ShellError::CantFindColumn {
/// col_name: std::string::ToString::to_string(&2),
/// span: std::option::Option::None,
/// src_span: span
/// })?,
/// )?
/// }
/// ))
/// }
/// }
/// ```
///
/// Unit struct:
/// ```rust
/// #[derive(IntoValue)]
/// struct Unicorn;
///
/// impl nu_protocol::FromValue for Unicorn {
/// fn from_value(
/// v: nu_protocol::Value
/// ) -> std::result::Result<Self, nu_protocol::ShellError> {
/// match v {
/// nu_protocol::Value::Nothing {..} => Ok(Self),
/// v => std::result::Result::Err(nu_protocol::ShellError::CantConvert {
/// to_type: std::string::ToString::to_string(&<Self as nu_protocol::FromValue>::expected_type()),
/// from_type: std::string::ToString::to_string(&v.get_type()),
/// span: v.span(),
/// help: std::option::Option::None
/// })
/// }
/// }
/// }
/// ```
fn struct_from_value(data: &DataStruct) -> TokenStream2 {
let body = parse_value_via_fields(&data.fields, quote!(Self));
quote! {
fn from_value(
v: nu_protocol::Value
) -> std::result::Result<Self, nu_protocol::ShellError> {
#body
}
}
}
/// Implements `FromValue::expected_type` for structs.
///
/// This function constructs the `expected_type` function for structs.
/// The type depends on the `fields`: named fields construct a record type with every key and type
/// laid out.
/// Unnamed fields construct a custom type with the name in the format like
/// `list[type0, type1, type2]`.
/// No fields expect the `Type::Nothing`.
///
/// # Examples
///
/// These examples show what the macro would generate.
///
/// Struct with named fields:
/// ```rust
/// #[derive(IntoValue)]
/// struct Pet {
/// name: String,
/// age: u8,
/// favorite_toy: Option<String>,
/// }
///
/// impl nu_protocol::FromValue for Pet {
/// fn expected_type() -> nu_protocol::Type {
/// nu_protocol::Type::Record(
/// std::vec![
/// (
/// std::string::ToString::to_string("name"),
/// <String as nu_protocol::FromValue>::expected_type(),
/// ),
/// (
/// std::string::ToString::to_string("age"),
/// <u8 as nu_protocol::FromValue>::expected_type(),
/// ),
/// (
/// std::string::ToString::to_string("favorite_toy"),
/// <Option<String> as nu_protocol::FromValue>::expected_type(),
/// )
/// ].into_boxed_slice()
/// )
/// }
/// }
/// ```
///
/// Struct with unnamed fields:
/// ```rust
/// #[derive(IntoValue)]
/// struct Color(u8, u8, u8);
///
/// impl nu_protocol::FromValue for Color {
/// fn expected_type() -> nu_protocol::Type {
/// nu_protocol::Type::Custom(
/// std::format!(
/// "[{}, {}, {}]",
/// <u8 as nu_protocol::FromValue>::expected_type(),
/// <u8 as nu_protocol::FromValue>::expected_type(),
/// <u8 as nu_protocol::FromValue>::expected_type()
/// )
/// .into_boxed_str()
/// )
/// }
/// }
/// ```
///
/// Unit struct:
/// ```rust
/// #[derive(IntoValue)]
/// struct Unicorn;
///
/// impl nu_protocol::FromValue for Color {
/// fn expected_type() -> nu_protocol::Type {
/// nu_protocol::Type::Nothing
/// }
/// }
/// ```
fn struct_expected_type(fields: &Fields) -> TokenStream2 {
let ty = match fields {
Fields::Named(fields) => {
let fields = fields.named.iter().map(|field| {
let ident = field.ident.as_ref().expect("named has idents");
let ident_s = ident.to_string();
let ty = &field.ty;
quote! {(
std::string::ToString::to_string(#ident_s),
<#ty as nu_protocol::FromValue>::expected_type(),
)}
});
quote!(nu_protocol::Type::Record(
std::vec![#(#fields),*].into_boxed_slice()
))
}
Fields::Unnamed(fields) => {
let mut iter = fields.unnamed.iter();
let fields = fields.unnamed.iter().map(|field| {
let ty = &field.ty;
quote!(<#ty as nu_protocol::FromValue>::expected_type())
});
let mut template = String::new();
template.push('[');
if iter.next().is_some() {
template.push_str("{}")
}
iter.for_each(|_| template.push_str(", {}"));
template.push(']');
quote! {
nu_protocol::Type::Custom(
std::format!(
#template,
#(#fields),*
)
.into_boxed_str()
)
}
}
Fields::Unit => quote!(nu_protocol::Type::Nothing),
};
quote! {
fn expected_type() -> nu_protocol::Type {
#ty
}
}
}
/// Implements the `#[derive(FromValue)]` macro for enums.
///
/// This function constructs the implementation of the `FromValue` trait for enums.
/// It is designed to be on the same level as [`derive_struct_from_value`], even though this
/// function only provides the impl signature for `FromValue`.
/// The actual implementation for `FromValue::from_value` is handled by [`enum_from_value`].
///
/// Since variants are difficult to type with the current type system, this function uses the
/// default implementation for `expected_type`.
fn derive_enum_from_value(
ident: Ident,
data: DataEnum,
generics: Generics,
attrs: Vec<Attribute>,
) -> Result<TokenStream2, DeriveError> {
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let from_value_impl = enum_from_value(&data, &attrs)?;
Ok(quote! {
#[automatically_derived]
impl #impl_generics nu_protocol::FromValue for #ident #ty_generics #where_clause {
#from_value_impl
}
})
}
/// Implements `FromValue::from_value` for enums.
///
/// This function constructs the `from_value` implementation for enums.
/// It only accepts enums with unit variants, as it is currently unclear how other types of enums
/// should be represented via a `Value`.
/// This function checks that every field is a unit variant and constructs a match statement over
/// all possible variants.
/// The input value is expected to be a `Value::String` containing the name of the variant formatted
/// as defined by the `#[nu_value(rename_all = "...")]` attribute.
/// If no attribute is given, [`convert_case::Case::Snake`] is expected.
///
/// If no matching variant is found, `ShellError::CantConvert` is returned.
///
/// This is how such a derived implementation looks:
/// ```rust
/// #[derive(IntoValue)]
/// enum Weather {
/// Sunny,
/// Cloudy,
/// Raining
/// }
///
/// impl nu_protocol::IntoValue for Weather {
/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
/// let span = v.span();
/// let ty = v.get_type();
///
/// let s = v.into_string()?;
/// match s.as_str() {
/// "sunny" => std::result::Ok(Self::Sunny),
/// "cloudy" => std::result::Ok(Self::Cloudy),
/// "raining" => std::result::Ok(Self::Raining),
/// _ => std::result::Result::Err(nu_protocol::ShellError::CantConvert {
/// to_type: std::string::ToString::to_string(
/// &<Self as nu_protocol::FromValue>::expected_type()
/// ),
/// from_type: std::string::ToString::to_string(&ty),
/// span: span,help: std::option::Option::None,
/// }),
/// }
/// }
/// }
/// ```
fn enum_from_value(data: &DataEnum, attrs: &[Attribute]) -> Result<TokenStream2, DeriveError> {
let container_attrs = ContainerAttributes::parse_attrs(attrs.iter())?;
let arms: Vec<TokenStream2> = data
.variants
.iter()
.map(|variant| {
attributes::deny(&variant.attrs)?;
let ident = &variant.ident;
let ident_s = format!("{ident}")
.as_str()
.to_case(container_attrs.rename_all);
match &variant.fields {
Fields::Named(fields) => Err(DeriveError::UnsupportedEnums {
fields_span: fields.span(),
}),
Fields::Unnamed(fields) => Err(DeriveError::UnsupportedEnums {
fields_span: fields.span(),
}),
Fields::Unit => Ok(quote!(#ident_s => std::result::Result::Ok(Self::#ident))),
}
})
.collect::<Result<_, _>>()?;
Ok(quote! {
fn from_value(
v: nu_protocol::Value
) -> std::result::Result<Self, nu_protocol::ShellError> {
let span = v.span();
let ty = v.get_type();
let s = v.into_string()?;
match s.as_str() {
#(#arms,)*
_ => std::result::Result::Err(nu_protocol::ShellError::CantConvert {
to_type: std::string::ToString::to_string(
&<Self as nu_protocol::FromValue>::expected_type()
),
from_type: std::string::ToString::to_string(&ty),
span: span,
help: std::option::Option::None,
}),
}
}
})
}
/// Parses a `Value` into self.
///
/// This function handles the actual parsing of a `Value` into self.
/// It takes two parameters: `fields` and `self_ident`.
/// The `fields` parameter determines the expected type of `Value`: named fields expect a
/// `Value::Record`, unnamed fields expect a `Value::List`, and a unit expects `Value::Nothing`.
///
/// For named fields, the `fields` parameter indicates which field in the record corresponds to
/// which struct field.
/// For both named and unnamed fields, it also helps cast the type into a `FromValue` type.
/// This approach maintains
/// [hygiene](https://doc.rust-lang.org/reference/macros-by-example.html#hygiene).
///
/// The `self_ident` parameter is used to describe the identifier of the returned value.
/// For structs, `Self` is usually sufficient, but for enums, `Self::Variant` may be needed in the
/// future.
///
/// This function is more complex than the equivalent for `IntoValue` due to error handling
/// requirements.
/// For missing fields, `ShellError::CantFindColumn` is used, and for unit structs,
/// `ShellError::CantConvert` is used.
/// The implementation avoids local variables for fields to prevent accidental shadowing, ensuring
/// that poorly named fields don't cause issues.
/// While this style is not typically recommended in handwritten Rust, it is acceptable for code
/// generation.
fn parse_value_via_fields(fields: &Fields, self_ident: impl ToTokens) -> TokenStream2 {
match fields {
Fields::Named(fields) => {
let fields = fields.named.iter().map(|field| {
let ident = field.ident.as_ref().expect("named has idents");
let ident_s = ident.to_string();
let ty = &field.ty;
match type_is_option(ty) {
true => quote! {
#ident: record
.remove(#ident_s)
.map(|v| <#ty as nu_protocol::FromValue>::from_value(v))
.transpose()?
.flatten()
},
false => quote! {
#ident: <#ty as nu_protocol::FromValue>::from_value(
record
.remove(#ident_s)
.ok_or_else(|| nu_protocol::ShellError::CantFindColumn {
col_name: std::string::ToString::to_string(#ident_s),
span: std::option::Option::None,
src_span: span
})?,
)?
},
}
});
quote! {
let span = v.span();
let mut record = v.into_record()?;
std::result::Result::Ok(#self_ident {#(#fields),*})
}
}
Fields::Unnamed(fields) => {
let fields = fields.unnamed.iter().enumerate().map(|(i, field)| {
let ty = &field.ty;
quote! {{
<#ty as nu_protocol::FromValue>::from_value(
deque
.pop_front()
.ok_or_else(|| nu_protocol::ShellError::CantFindColumn {
col_name: std::string::ToString::to_string(&#i),
span: std::option::Option::None,
src_span: span
})?,
)?
}}
});
quote! {
let span = v.span();
let list = v.into_list()?;
let mut deque: std::collections::VecDeque<_> = std::convert::From::from(list);
std::result::Result::Ok(#self_ident(#(#fields),*))
}
}
Fields::Unit => quote! {
match v {
nu_protocol::Value::Nothing {..} => Ok(#self_ident),
v => std::result::Result::Err(nu_protocol::ShellError::CantConvert {
to_type: std::string::ToString::to_string(&<Self as nu_protocol::FromValue>::expected_type()),
from_type: std::string::ToString::to_string(&v.get_type()),
span: v.span(),
help: std::option::Option::None
})
}
},
}
}
const FULLY_QUALIFIED_OPTION: &str = "std::option::Option";
const PARTIALLY_QUALIFIED_OPTION: &str = "option::Option";
const PRELUDE_OPTION: &str = "Option";
/// Check if the field type is an `Option`.
///
/// This function checks if a given type is an `Option`.
/// We assume that an `Option` is [`std::option::Option`] because we can't see the whole code and
/// can't ask the compiler itself.
/// If the `Option` type isn't `std::option::Option`, the user will get a compile error due to a
/// type mismatch.
/// It's very unusual for people to override `Option`, so this should rarely be an issue.
///
/// When [rust#63084](https://github.com/rust-lang/rust/issues/63084) is resolved, we can use
/// [`std::any::type_name`] for a static assertion check to get a more direct error messages.
fn type_is_option(ty: &Type) -> bool {
let s = ty.to_token_stream().to_string();
s.starts_with(PRELUDE_OPTION)
|| s.starts_with(PARTIALLY_QUALIFIED_OPTION)
|| s.starts_with(FULLY_QUALIFIED_OPTION)
}

View File

@ -0,0 +1,266 @@
use convert_case::Casing;
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens};
use syn::{
spanned::Spanned, Attribute, Data, DataEnum, DataStruct, DeriveInput, Fields, Generics, Ident,
Index,
};
use crate::attributes::{self, ContainerAttributes};
#[derive(Debug)]
pub struct IntoValue;
type DeriveError = super::error::DeriveError<IntoValue>;
/// Inner implementation of the `#[derive(IntoValue)]` macro for structs and enums.
///
/// Uses `proc_macro2::TokenStream` for better testing support, unlike `proc_macro::TokenStream`.
///
/// This function directs the `IntoValue` trait derivation to the correct implementation based on
/// the input type:
/// - For structs: [`struct_into_value`]
/// - For enums: [`enum_into_value`]
/// - Unions are not supported and will return an error.
pub fn derive_into_value(input: TokenStream2) -> Result<TokenStream2, DeriveError> {
let input: DeriveInput = syn::parse2(input).map_err(DeriveError::Syn)?;
match input.data {
Data::Struct(data_struct) => Ok(struct_into_value(
input.ident,
data_struct,
input.generics,
input.attrs,
)?),
Data::Enum(data_enum) => Ok(enum_into_value(
input.ident,
data_enum,
input.generics,
input.attrs,
)?),
Data::Union(_) => Err(DeriveError::UnsupportedUnions),
}
}
/// Implements the `#[derive(IntoValue)]` macro for structs.
///
/// Automatically derives the `IntoValue` trait for any struct where each field implements
/// `IntoValue`.
/// For structs with named fields, the derived implementation creates a `Value::Record` using the
/// struct fields as keys.
/// Each field value is converted using the `IntoValue::into_value` method.
/// For structs with unnamed fields, this generates a `Value::List` with each field in the list.
/// For unit structs, this generates `Value::Nothing`, because there is no data.
///
/// Note: The helper attribute `#[nu_value(...)]` is currently not allowed on structs.
///
/// # Examples
///
/// These examples show what the macro would generate.
///
/// Struct with named fields:
/// ```rust
/// #[derive(IntoValue)]
/// struct Pet {
/// name: String,
/// age: u8,
/// favorite_toy: Option<String>,
/// }
///
/// impl nu_protocol::IntoValue for Pet {
/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
/// nu_protocol::Value::record(nu_protocol::record! {
/// "name" => nu_protocol::IntoValue::into_value(self.name, span),
/// "age" => nu_protocol::IntoValue::into_value(self.age, span),
/// "favorite_toy" => nu_protocol::IntoValue::into_value(self.favorite_toy, span),
/// }, span)
/// }
/// }
/// ```
///
/// Struct with unnamed fields:
/// ```rust
/// #[derive(IntoValue)]
/// struct Color(u8, u8, u8);
///
/// impl nu_protocol::IntoValue for Color {
/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
/// nu_protocol::Value::list(vec![
/// nu_protocol::IntoValue::into_value(self.0, span),
/// nu_protocol::IntoValue::into_value(self.1, span),
/// nu_protocol::IntoValue::into_value(self.2, span),
/// ], span)
/// }
/// }
/// ```
///
/// Unit struct:
/// ```rust
/// #[derive(IntoValue)]
/// struct Unicorn;
///
/// impl nu_protocol::IntoValue for Unicorn {
/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
/// nu_protocol::Value::nothing(span)
/// }
/// }
/// ```
fn struct_into_value(
ident: Ident,
data: DataStruct,
generics: Generics,
attrs: Vec<Attribute>,
) -> Result<TokenStream2, DeriveError> {
attributes::deny(&attrs)?;
attributes::deny_fields(&data.fields)?;
let record = match &data.fields {
Fields::Named(fields) => {
let accessor = fields
.named
.iter()
.map(|field| field.ident.as_ref().expect("named has idents"))
.map(|ident| quote!(self.#ident));
fields_return_value(&data.fields, accessor)
}
Fields::Unnamed(fields) => {
let accessor = fields
.unnamed
.iter()
.enumerate()
.map(|(n, _)| Index::from(n))
.map(|index| quote!(self.#index));
fields_return_value(&data.fields, accessor)
}
Fields::Unit => quote!(nu_protocol::Value::nothing(span)),
};
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
Ok(quote! {
#[automatically_derived]
impl #impl_generics nu_protocol::IntoValue for #ident #ty_generics #where_clause {
fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
#record
}
}
})
}
/// Implements the `#[derive(IntoValue)]` macro for enums.
///
/// This function implements the derive macro `IntoValue` for enums.
/// Currently, only unit enum variants are supported as it is not clear how other types of enums
/// should be represented in a `Value`.
/// For simple enums, we represent the enum as a `Value::String`. For other types of variants, we return an error.
/// The variant name will be case-converted as described by the `#[nu_value(rename_all = "...")]` helper attribute.
/// If no attribute is used, the default is `case_convert::Case::Snake`.
/// The implementation matches over all variants, uses the appropriate variant name, and constructs a `Value::String`.
///
/// This is how such a derived implementation looks:
/// ```rust
/// #[derive(IntoValue)]
/// enum Weather {
/// Sunny,
/// Cloudy,
/// Raining
/// }
///
/// impl nu_protocol::IntoValue for Weather {
/// fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
/// match self {
/// Self::Sunny => nu_protocol::Value::string("sunny", span),
/// Self::Cloudy => nu_protocol::Value::string("cloudy", span),
/// Self::Raining => nu_protocol::Value::string("raining", span),
/// }
/// }
/// }
/// ```
fn enum_into_value(
ident: Ident,
data: DataEnum,
generics: Generics,
attrs: Vec<Attribute>,
) -> Result<TokenStream2, DeriveError> {
let container_attrs = ContainerAttributes::parse_attrs(attrs.iter())?;
let arms: Vec<TokenStream2> = data
.variants
.into_iter()
.map(|variant| {
attributes::deny(&variant.attrs)?;
let ident = variant.ident;
let ident_s = format!("{ident}")
.as_str()
.to_case(container_attrs.rename_all);
match &variant.fields {
// In the future we can implement more complexe enums here.
Fields::Named(fields) => Err(DeriveError::UnsupportedEnums {
fields_span: fields.span(),
}),
Fields::Unnamed(fields) => Err(DeriveError::UnsupportedEnums {
fields_span: fields.span(),
}),
Fields::Unit => {
Ok(quote!(Self::#ident => nu_protocol::Value::string(#ident_s, span)))
}
}
})
.collect::<Result<_, _>>()?;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
Ok(quote! {
impl #impl_generics nu_protocol::IntoValue for #ident #ty_generics #where_clause {
fn into_value(self, span: nu_protocol::Span) -> nu_protocol::Value {
match self {
#(#arms,)*
}
}
}
})
}
/// Constructs the final `Value` that the macro generates.
///
/// This function handles the construction of the final `Value` that the macro generates.
/// It is currently only used for structs but may be used for enums in the future.
/// The function takes two parameters: the `fields`, which allow iterating over each field of a data
/// type, and the `accessor`.
/// The fields determine whether we need to generate a `Value::Record`, `Value::List`, or
/// `Value::Nothing`.
/// For named fields, they are also directly used to generate the record key.
///
/// The `accessor` parameter generalizes how the data is accessed.
/// For named fields, this is usually the name of the fields preceded by `self` in a struct, and
/// maybe something else for enums.
/// For unnamed fields, this should be an iterator similar to the one with named fields, but
/// accessing tuple fields, so we get `self.n`.
/// For unit structs, this parameter is ignored.
/// By using the accessor like this, we can have the same code for structs and enums with data
/// variants in the future.
fn fields_return_value(
fields: &Fields,
accessor: impl Iterator<Item = impl ToTokens>,
) -> TokenStream2 {
match fields {
Fields::Named(fields) => {
let items: Vec<TokenStream2> = fields
.named
.iter()
.zip(accessor)
.map(|(field, accessor)| {
let ident = field.ident.as_ref().expect("named has idents");
let field = ident.to_string();
quote!(#field => nu_protocol::IntoValue::into_value(#accessor, span))
})
.collect();
quote! {
nu_protocol::Value::record(nu_protocol::record! {
#(#items),*
}, span)
}
}
Fields::Unnamed(fields) => {
let items =
fields.unnamed.iter().zip(accessor).map(
|(_, accessor)| quote!(nu_protocol::IntoValue::into_value(#accessor, span)),
);
quote!(nu_protocol::Value::list(std::vec![#(#items),*], span))
}
Fields::Unit => quote!(nu_protocol::Value::nothing(span)),
}
}

View File

@ -0,0 +1,69 @@
//! Macro implementations of `#[derive(FromValue, IntoValue)]`.
//!
//! As this crate is a [`proc_macro`] crate, it is only allowed to export
//! [procedural macros](https://doc.rust-lang.org/reference/procedural-macros.html).
//! Therefore, it only exports [`IntoValue`] and [`FromValue`].
//!
//! To get documentation for other functions and types used in this crate, run
//! `cargo doc -p nu-derive-value --document-private-items`.
//!
//! This crate uses a lot of
//! [`proc_macro2::TokenStream`](https://docs.rs/proc-macro2/1.0.24/proc_macro2/struct.TokenStream.html)
//! as `TokenStream2` to allow testing the behavior of the macros directly, including the output
//! token stream or if the macro errors as expected.
//! The tests for functionality can be found in `nu_protocol::value::test_derive`.
//!
//! This documentation is often less reference-heavy than typical Rust documentation.
//! This is because this crate is a dependency for `nu_protocol`, and linking to it would create a
//! cyclic dependency.
//! Also all examples in the documentation aren't tested as this crate cannot be compiled as a
//! normal library very easily.
//! This might change in the future if cargo allows building a proc-macro crate differently for
//! `cfg(doctest)` as they are already doing for `cfg(test)`.
//!
//! The generated code from the derive macros tries to be as
//! [hygienic](https://doc.rust-lang.org/reference/macros-by-example.html#hygiene) as possible.
//! This ensures that the macro can be called anywhere without requiring specific imports.
//! This results in obtuse code, which isn't recommended for manual, handwritten Rust
//! but ensures that no other code may influence this generated code or vice versa.
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use proc_macro_error::{proc_macro_error, Diagnostic};
mod attributes;
mod error;
mod from;
mod into;
#[cfg(test)]
mod tests;
const HELPER_ATTRIBUTE: &str = "nu_value";
/// Derive macro generating an impl of the trait `IntoValue`.
///
/// For further information, see the docs on the trait itself.
#[proc_macro_derive(IntoValue, attributes(nu_value))]
#[proc_macro_error]
pub fn derive_into_value(input: TokenStream) -> TokenStream {
let input = TokenStream2::from(input);
let output = match into::derive_into_value(input) {
Ok(output) => output,
Err(e) => Diagnostic::from(e).abort(),
};
TokenStream::from(output)
}
/// Derive macro generating an impl of the trait `FromValue`.
///
/// For further information, see the docs on the trait itself.
#[proc_macro_derive(FromValue, attributes(nu_value))]
#[proc_macro_error]
pub fn derive_from_value(input: TokenStream) -> TokenStream {
let input = TokenStream2::from(input);
let output = match from::derive_from_value(input) {
Ok(output) => output,
Err(e) => Diagnostic::from(e).abort(),
};
TokenStream::from(output)
}

View File

@ -0,0 +1,157 @@
// These tests only check that the derive macros throw the relevant errors.
// Functionality of the derived types is tested in nu_protocol::value::test_derive.
use crate::error::DeriveError;
use crate::from::derive_from_value;
use crate::into::derive_into_value;
use quote::quote;
#[test]
fn unsupported_unions() {
let input = quote! {
#[nu_value]
union SomeUnion {
f1: u32,
f2: f32,
}
};
let from_res = derive_from_value(input.clone());
assert!(
matches!(from_res, Err(DeriveError::UnsupportedUnions)),
"expected `DeriveError::UnsupportedUnions`, got {:?}",
from_res
);
let into_res = derive_into_value(input);
assert!(
matches!(into_res, Err(DeriveError::UnsupportedUnions)),
"expected `DeriveError::UnsupportedUnions`, got {:?}",
into_res
);
}
#[test]
fn unsupported_enums() {
let input = quote! {
#[nu_value(rename_all = "SCREAMING_SNAKE_CASE")]
enum ComplexEnum {
Unit,
Unnamed(u32, f32),
Named {
u: u32,
f: f32,
}
}
};
let from_res = derive_from_value(input.clone());
assert!(
matches!(from_res, Err(DeriveError::UnsupportedEnums { .. })),
"expected `DeriveError::UnsupportedEnums`, got {:?}",
from_res
);
let into_res = derive_into_value(input);
assert!(
matches!(into_res, Err(DeriveError::UnsupportedEnums { .. })),
"expected `DeriveError::UnsupportedEnums`, got {:?}",
into_res
);
}
#[test]
fn unexpected_attribute() {
let input = quote! {
#[nu_value(what)]
enum SimpleEnum {
A,
B,
}
};
let from_res = derive_from_value(input.clone());
assert!(
matches!(from_res, Err(DeriveError::UnexpectedAttribute { .. })),
"expected `DeriveError::UnexpectedAttribute`, got {:?}",
from_res
);
let into_res = derive_into_value(input);
assert!(
matches!(into_res, Err(DeriveError::UnexpectedAttribute { .. })),
"expected `DeriveError::UnexpectedAttribute`, got {:?}",
into_res
);
}
#[test]
fn deny_attribute_on_structs() {
let input = quote! {
#[nu_value]
struct SomeStruct;
};
let from_res = derive_from_value(input.clone());
assert!(
matches!(from_res, Err(DeriveError::InvalidAttributePosition { .. })),
"expected `DeriveError::InvalidAttributePosition`, got {:?}",
from_res
);
let into_res = derive_into_value(input);
assert!(
matches!(into_res, Err(DeriveError::InvalidAttributePosition { .. })),
"expected `DeriveError::InvalidAttributePosition`, got {:?}",
into_res
);
}
#[test]
fn deny_attribute_on_fields() {
let input = quote! {
struct SomeStruct {
#[nu_value]
field: ()
}
};
let from_res = derive_from_value(input.clone());
assert!(
matches!(from_res, Err(DeriveError::InvalidAttributePosition { .. })),
"expected `DeriveError::InvalidAttributePosition`, got {:?}",
from_res
);
let into_res = derive_into_value(input);
assert!(
matches!(into_res, Err(DeriveError::InvalidAttributePosition { .. })),
"expected `DeriveError::InvalidAttributePosition`, got {:?}",
into_res
);
}
#[test]
fn invalid_attribute_value() {
let input = quote! {
#[nu_value(rename_all = "CrazY-CasE")]
enum SimpleEnum {
A,
B
}
};
let from_res = derive_from_value(input.clone());
assert!(
matches!(from_res, Err(DeriveError::InvalidAttributeValue { .. })),
"expected `DeriveError::InvalidAttributeValue`, got {:?}",
from_res
);
let into_res = derive_into_value(input);
assert!(
matches!(into_res, Err(DeriveError::InvalidAttributeValue { .. })),
"expected `DeriveError::InvalidAttributeValue`, got {:?}",
into_res
);
}

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" }
log = { workspace = true }
[features]

View File

@ -392,6 +392,7 @@ fn compile_expression(
Expr::Overlay(_) => Err(CompileError::Todo("Overlay")),
Expr::Signature(_) => Err(CompileError::Todo("Signature")),
Expr::StringInterpolation(_) => Err(CompileError::Todo("StringInterpolation")),
Expr::GlobInterpolation(_, _) => Err(CompileError::Todo("GlobInterpolation")),
Expr::Nothing => lit(builder, Literal::Nothing),
Expr::Garbage => Err(CompileError::Garbage),
}

View File

@ -3,7 +3,7 @@ use nu_protocol::{
ast::{Argument, Call, Expr, Expression, RecordItem},
debugger::WithoutDebug,
engine::{Command, EngineState, Stack, UNKNOWN_SPAN_ID},
record, Category, Example, IntoPipelineData, PipelineData, Signature, Span, SpanId,
record, Category, Example, IntoPipelineData, PipelineData, Signature, Span, SpanId, Spanned,
SyntaxShape, Type, Value,
};
use std::{collections::HashMap, fmt::Write};
@ -299,16 +299,37 @@ fn get_documentation(
}
if let Some(result) = &example.result {
let mut table_call = Call::new(Span::unknown());
if example.example.ends_with("--collapse") {
// collapse the result
table_call.add_named((
Spanned {
item: "collapse".to_string(),
span: Span::unknown(),
},
None,
None,
))
} else {
// expand the result
table_call.add_named((
Spanned {
item: "expand".to_string(),
span: Span::unknown(),
},
None,
None,
))
}
let table = engine_state
.find_decl("table".as_bytes(), &[])
.and_then(|decl_id| {
let call = Call::new(Span::unknown());
engine_state
.get_decl(decl_id)
.run(
engine_state,
stack,
&(&call).into(),
&(&table_call).into(),
PipelineData::Value(result.clone(), None),
)
.ok()

View File

@ -208,11 +208,13 @@ fn eval_external(
) -> Result<PipelineData, ShellError> {
let decl_id = engine_state
.find_decl("run-external".as_bytes(), &[])
.ok_or(ShellError::ExternalNotSupported { span: head.span })?;
.ok_or(ShellError::ExternalNotSupported {
span: head.span(&engine_state),
})?;
let command = engine_state.get_decl(decl_id);
let mut call = Call::new(head.span);
let mut call = Call::new(head.span(&engine_state));
call.add_positional(head.clone());
@ -712,7 +714,7 @@ impl Eval for EvalRuntime {
args: &[ExternalArgument],
_: Span,
) -> Result<Value, ShellError> {
let span = head.span;
let span = head.span(&engine_state);
// FIXME: protect this collect with ctrl-c
eval_external(engine_state, stack, head, args, PipelineData::empty())?.into_value(span)
}
@ -779,9 +781,11 @@ impl Eval for EvalRuntime {
let var_info = engine_state.get_var(*var_id);
if var_info.mutable {
stack.add_var(*var_id, rhs);
Ok(Value::nothing(lhs.span))
Ok(Value::nothing(lhs.span(&engine_state)))
} else {
Err(ShellError::AssignmentRequiresMutableVar { lhs_span: lhs.span })
Err(ShellError::AssignmentRequiresMutableVar {
lhs_span: lhs.span(&engine_state),
})
}
}
Expr::FullCellPath(cell_path) => {
@ -797,7 +801,7 @@ impl Eval for EvalRuntime {
// Reject attempts to assign to the entire $env
if cell_path.tail.is_empty() {
return Err(ShellError::CannotReplaceEnv {
span: cell_path.head.span,
span: cell_path.head.span(&engine_state),
});
}
@ -837,15 +841,21 @@ impl Eval for EvalRuntime {
lhs.upsert_data_at_cell_path(&cell_path.tail, rhs)?;
stack.add_var(*var_id, lhs);
}
Ok(Value::nothing(cell_path.head.span))
Ok(Value::nothing(cell_path.head.span(&engine_state)))
} else {
Err(ShellError::AssignmentRequiresMutableVar { lhs_span: lhs.span })
Err(ShellError::AssignmentRequiresMutableVar {
lhs_span: lhs.span(&engine_state),
})
}
}
_ => Err(ShellError::AssignmentRequiresVar { lhs_span: lhs.span }),
_ => Err(ShellError::AssignmentRequiresVar {
lhs_span: lhs.span(&engine_state),
}),
}
}
_ => Err(ShellError::AssignmentRequiresVar { lhs_span: lhs.span }),
_ => Err(ShellError::AssignmentRequiresVar {
lhs_span: lhs.span(&engine_state),
}),
}
}
@ -882,8 +892,8 @@ impl Eval for EvalRuntime {
Ok(Value::string(name, span))
}
fn unreachable(expr: &Expression) -> Result<Value, ShellError> {
Ok(Value::nothing(expr.span))
fn unreachable(engine_state: &EngineState, expr: &Expression) -> Result<Value, ShellError> {
Ok(Value::nothing(expr.span(&engine_state)))
}
}

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

@ -27,7 +27,7 @@ impl NuCmd {
}
impl ViewCommand for NuCmd {
type View = NuView<'static>;
type View = NuView;
fn name(&self) -> &'static str {
Self::NAME
@ -72,12 +72,12 @@ impl ViewCommand for NuCmd {
}
}
pub enum NuView<'a> {
Records(RecordView<'a>),
pub enum NuView {
Records(RecordView),
Preview(Preview),
}
impl View for NuView<'_> {
impl View for NuView {
fn draw(&mut self, f: &mut Frame, area: Rect, cfg: ViewConfig<'_>, layout: &mut Layout) {
match self {
NuView::Records(v) => v.draw(f, area, cfg, layout),

View File

@ -30,7 +30,7 @@ impl TableCmd {
}
impl ViewCommand for TableCmd {
type View = RecordView<'static>;
type View = RecordView;
fn name(&self) -> &'static str {
Self::NAME

View File

@ -22,7 +22,7 @@ impl TryCmd {
}
impl ViewCommand for TryCmd {
type View = TryView<'static>;
type View = TryView;
fn name(&self) -> &'static str {
Self::NAME

View File

@ -23,23 +23,20 @@ use nu_protocol::{
Config, Record, Span, Value,
};
use ratatui::{layout::Rect, widgets::Block};
use std::{borrow::Cow, collections::HashMap};
use std::collections::HashMap;
pub use self::table_widget::Orientation;
#[derive(Debug, Clone)]
pub struct RecordView<'a> {
layer_stack: Vec<RecordLayer<'a>>,
pub struct RecordView {
layer_stack: Vec<RecordLayer>,
mode: UIMode,
orientation: Orientation,
cfg: ExploreConfig,
}
impl<'a> RecordView<'a> {
pub fn new(
columns: impl Into<Cow<'a, [String]>>,
records: impl Into<Cow<'a, [Vec<Value>]>>,
) -> Self {
impl RecordView {
pub fn new(columns: Vec<String>, records: Vec<Vec<Value>>) -> Self {
Self {
layer_stack: vec![RecordLayer::new(columns, records)],
mode: UIMode::View,
@ -64,13 +61,13 @@ impl<'a> RecordView<'a> {
}
// todo: rename to get_layer
pub fn get_layer_last(&self) -> &RecordLayer<'a> {
pub fn get_layer_last(&self) -> &RecordLayer {
self.layer_stack
.last()
.expect("we guarantee that 1 entry is always in a list")
}
pub fn get_layer_last_mut(&mut self) -> &mut RecordLayer<'a> {
pub fn get_layer_last_mut(&mut self) -> &mut RecordLayer {
self.layer_stack
.last_mut()
.expect("we guarantee that 1 entry is always in a list")
@ -134,32 +131,39 @@ impl<'a> RecordView<'a> {
Orientation::Left => (column, row),
};
if layer.records.len() > row && layer.records[row].len() > column {
layer.records[row][column].clone()
} else {
Value::nothing(Span::unknown())
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());
}
layer.record_values[row][column].clone()
}
/// Create a table widget.
/// WARNING: this is currently really slow on large data sets.
/// It creates a string representation of every cell in the table and looks at every row for lscolorize.
fn create_table_widget(&'a self, cfg: ViewConfig<'a>) -> TableWidget<'a> {
let layer = self.get_layer_last();
let mut data = convert_records_to_string(&layer.records, cfg.nu_config, cfg.style_computer);
lscolorize(&layer.columns, &mut data, cfg.lscolors);
let headers = layer.columns.as_ref();
fn create_table_widget<'a>(&'a mut self, cfg: ViewConfig<'a>) -> TableWidget<'a> {
let style = self.cfg.table;
let style_computer = cfg.style_computer;
let Position { row, column } = self.get_window_origin();
let layer = self.get_layer_last_mut();
if layer.record_text.is_none() {
let mut data =
convert_records_to_string(&layer.record_values, cfg.nu_config, cfg.style_computer);
lscolorize(&layer.column_names, &mut data, cfg.lscolors);
layer.record_text = Some(data);
}
let headers = &layer.column_names;
let data = layer.record_text.as_ref().expect("always ok");
TableWidget::new(
headers,
data,
style_computer,
row,
column,
self.cfg.table,
style,
layer.orientation,
)
}
@ -197,7 +201,7 @@ impl<'a> RecordView<'a> {
}
}
impl View for RecordView<'_> {
impl View for RecordView {
fn draw(&mut self, f: &mut Frame, area: Rect, cfg: ViewConfig<'_>, layout: &mut Layout) {
let mut table_layout = TableWidgetState::default();
// TODO: creating the table widget is O(N) where N is the number of cells in the grid.
@ -265,7 +269,7 @@ impl View for RecordView<'_> {
let style_computer = StyleComputer::new(&dummy_engine_state, &dummy_stack, HashMap::new());
let data = convert_records_to_string(
&self.get_layer_last().records,
&self.get_layer_last().record_values,
&nu_protocol::Config::default(),
&style_computer,
);
@ -274,7 +278,7 @@ impl View for RecordView<'_> {
}
fn show_data(&mut self, pos: usize) -> bool {
let data = &self.get_layer_last().records;
let data = &self.get_layer_last().record_values;
let mut i = 0;
for (row, cells) in data.iter().enumerate() {
@ -332,30 +336,35 @@ enum UIMode {
}
#[derive(Debug, Clone)]
pub struct RecordLayer<'a> {
columns: Cow<'a, [String]>,
records: Cow<'a, [Vec<Value>]>,
pub struct RecordLayer {
column_names: Vec<String>,
// These are the raw records in the current layer. The sole reason we keep this around is so we can return the original value
// if it's being peeked. Otherwise we could accept an iterator over it.
// or if it could be Clonable we could do that anyway;
// cause it would keep memory footprint lower while keep everything working
// (yee would make return O(n); we would need to traverse iterator once again; but maybe worth it)
record_values: Vec<Vec<Value>>,
// This is the text representation of the record values (the actual text that will be displayed to users).
// It's an Option because we need configuration to set it and we (currently) don't have access to configuration when things are created.
record_text: Option<Vec<Vec<NuText>>>,
orientation: Orientation,
name: Option<String>,
was_transposed: bool,
cursor: WindowCursor2D,
}
impl<'a> RecordLayer<'a> {
fn new(
columns: impl Into<Cow<'a, [String]>>,
records: impl Into<Cow<'a, [Vec<Value>]>>,
) -> Self {
let columns = columns.into();
let records = records.into();
impl RecordLayer {
fn new(columns: Vec<String>, records: Vec<Vec<Value>>) -> Self {
// TODO: refactor so this is fallible and returns a Result instead of panicking
let cursor =
WindowCursor2D::new(records.len(), columns.len()).expect("Failed to create cursor");
let column_names = columns.iter().map(|s| strip_string(s)).collect();
Self {
columns,
records,
column_names,
record_values: records,
record_text: None,
cursor,
orientation: Orientation::Top,
name: None,
@ -369,21 +378,21 @@ impl<'a> RecordLayer<'a> {
fn count_rows(&self) -> usize {
match self.orientation {
Orientation::Top => self.records.len(),
Orientation::Left => self.columns.len(),
Orientation::Top => self.record_values.len(),
Orientation::Left => self.column_names.len(),
}
}
fn count_columns(&self) -> usize {
match self.orientation {
Orientation::Top => self.columns.len(),
Orientation::Left => self.records.len(),
Orientation::Top => self.column_names.len(),
Orientation::Left => self.record_values.len(),
}
}
fn get_column_header(&self) -> Option<String> {
let col = self.cursor.column();
self.columns.get(col).map(|header| header.to_string())
self.column_names.get(col).map(|header| header.to_string())
}
fn reset_cursor(&mut self) {
@ -570,13 +579,13 @@ fn handle_key_event_cursor_mode(
}
}
fn create_layer(value: Value) -> Result<RecordLayer<'static>> {
fn create_layer(value: Value) -> Result<RecordLayer> {
let (columns, values) = collect_input(value)?;
Ok(RecordLayer::new(columns, values))
}
fn push_layer(view: &mut RecordView<'_>, mut next_layer: RecordLayer<'static>) {
fn push_layer(view: &mut RecordView, mut next_layer: RecordLayer) {
let layer = view.get_layer_last();
let header = layer.get_column_header();
@ -599,9 +608,9 @@ fn estimate_page_size(area: Rect, show_head: bool) -> u16 {
}
/// scroll to the end of the data
fn tail_data(state: &mut RecordView<'_>, page_size: usize) {
fn tail_data(state: &mut RecordView, page_size: usize) {
let layer = state.get_layer_last_mut();
let count_rows = layer.records.len();
let count_rows = layer.record_values.len();
if count_rows > page_size {
layer
.cursor
@ -620,6 +629,7 @@ fn convert_records_to_string(
row.iter()
.map(|value| {
let text = value.clone().to_abbreviated_string(cfg);
let text = strip_string(&text);
let float_precision = cfg.float_precision as usize;
make_styled_string(style_computer, text, Some(value), float_precision)
@ -649,12 +659,16 @@ fn build_last_value(v: &RecordView) -> Value {
fn build_table_as_list(v: &RecordView) -> Value {
let layer = v.get_layer_last();
let cols = &layer.columns;
let vals = layer
.records
.record_values
.iter()
.map(|vals| {
let record = cols.iter().cloned().zip(vals.iter().cloned()).collect();
let record = layer
.column_names
.iter()
.cloned()
.zip(vals.iter().cloned())
.collect();
Value::record(record, NuSpan::unknown())
})
.collect();
@ -665,16 +679,15 @@ fn build_table_as_list(v: &RecordView) -> Value {
fn build_table_as_record(v: &RecordView) -> Value {
let layer = v.get_layer_last();
let record = if let Some(row) = layer.records.first() {
layer
.columns
let mut record = Record::new();
if let Some(row) = layer.record_values.first() {
record = layer
.column_names
.iter()
.cloned()
.zip(row.iter().cloned())
.collect()
} else {
Record::new()
};
.collect();
}
Value::record(record, NuSpan::unknown())
}
@ -708,47 +721,67 @@ fn get_percentage(value: usize, max: usize) -> usize {
((value as f32 / max as f32) * 100.0).floor() as usize
}
fn transpose_table(layer: &mut RecordLayer<'_>) {
let count_rows = layer.records.len();
let count_columns = layer.columns.len();
fn transpose_table(layer: &mut RecordLayer) {
if layer.was_transposed {
let data = match &mut layer.records {
Cow::Owned(data) => data,
Cow::Borrowed(_) => unreachable!("must never happen"),
};
let headers = pop_first_column(data);
let headers = headers
.into_iter()
.map(|value| match value {
Value::String { val, .. } => val,
_ => unreachable!("must never happen"),
})
.collect();
let data = _transpose_table(data, count_rows, count_columns - 1);
layer.records = Cow::Owned(data);
layer.columns = Cow::Owned(headers);
transpose_from(layer);
} else {
let mut data = _transpose_table(&layer.records, count_rows, count_columns);
for (column, column_name) in layer.columns.iter().enumerate() {
let value = Value::string(column_name, NuSpan::unknown());
data[column].insert(0, value);
}
layer.records = Cow::Owned(data);
layer.columns = (1..count_rows + 1 + 1).map(|i| i.to_string()).collect();
transpose_to(layer);
}
layer.was_transposed = !layer.was_transposed;
}
fn pop_first_column(values: &mut [Vec<Value>]) -> Vec<Value> {
let mut data = vec![Value::default(); values.len()];
fn transpose_from(layer: &mut RecordLayer) {
let count_rows = layer.record_values.len();
let count_columns = layer.column_names.len();
if let Some(data) = &mut layer.record_text {
pop_first_column(data);
*data = _transpose_table(data, count_rows, count_columns - 1);
}
let headers = pop_first_column(&mut layer.record_values);
let headers = headers
.into_iter()
.map(|value| match value {
Value::String { val, .. } => val,
_ => unreachable!("must never happen"),
})
.collect();
let data = _transpose_table(&layer.record_values, count_rows, count_columns - 1);
layer.record_values = data;
layer.column_names = headers;
}
fn transpose_to(layer: &mut RecordLayer) {
let count_rows = layer.record_values.len();
let count_columns = layer.column_names.len();
if let Some(data) = &mut layer.record_text {
*data = _transpose_table(data, count_rows, count_columns);
for (column, column_name) in layer.column_names.iter().enumerate() {
let value = (column_name.to_owned(), Default::default());
data[column].insert(0, value);
}
}
let mut data = _transpose_table(&layer.record_values, count_rows, count_columns);
for (column, column_name) in layer.column_names.iter().enumerate() {
let value = Value::string(column_name, NuSpan::unknown());
data[column].insert(0, value);
}
layer.record_values = data;
layer.column_names = (1..count_rows + 1 + 1).map(|i| i.to_string()).collect();
}
fn pop_first_column<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);
}
@ -756,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);
@ -770,3 +802,9 @@ fn _transpose_table(
data
}
fn strip_string(text: &str) -> String {
String::from_utf8(strip_ansi_escapes::strip(text))
.map_err(|_| ())
.unwrap_or_else(|_| text.to_owned())
}

View File

@ -13,15 +13,12 @@ use ratatui::{
text::Span,
widgets::{Block, Borders, Paragraph, StatefulWidget, Widget},
};
use std::{
borrow::Cow,
cmp::{max, Ordering},
};
use std::cmp::{max, Ordering};
#[derive(Debug, Clone)]
pub struct TableWidget<'a> {
columns: Cow<'a, [String]>,
data: Cow<'a, [Vec<NuText>]>,
columns: &'a [String],
data: &'a [Vec<NuText>],
index_row: usize,
index_column: usize,
config: TableConfig,
@ -39,8 +36,8 @@ pub enum Orientation {
impl<'a> TableWidget<'a> {
#[allow(clippy::too_many_arguments)]
pub fn new(
columns: impl Into<Cow<'a, [String]>>,
data: impl Into<Cow<'a, [Vec<NuText>]>>,
columns: &'a [String],
data: &'a [Vec<NuText>],
style_computer: &'a StyleComputer<'a>,
index_row: usize,
index_column: usize,
@ -48,8 +45,8 @@ impl<'a> TableWidget<'a> {
header_position: Orientation,
) -> Self {
Self {
columns: columns.into(),
data: data.into(),
columns,
data,
style_computer,
index_row,
index_column,
@ -91,6 +88,7 @@ impl StatefulWidget for TableWidget<'_> {
// todo: refactoring these to methods as they have quite a bit in common.
impl<'a> TableWidget<'a> {
// header at the top; header is always 1 line
fn render_table_horizontal(self, area: Rect, buf: &mut Buffer, state: &mut TableWidgetState) {
let padding_l = self.config.column_padding_left as u16;
let padding_r = self.config.column_padding_right as u16;
@ -133,25 +131,16 @@ impl<'a> TableWidget<'a> {
}
if show_index {
let area = Rect::new(width, data_y, area.width, data_height);
width += render_index(
buf,
area,
Rect::new(width, data_y, area.width, data_height),
self.style_computer,
self.index_row,
padding_l,
padding_r,
);
width += render_vertical_line_with_split(
buf,
width,
data_y,
data_height,
show_head,
false,
separator_s,
);
width += render_split_line(buf, width, area.y, area.height, show_head, separator_s);
}
// if there is more data than we can show, add an ellipsis to the column headers to hint at that
@ -165,6 +154,11 @@ impl<'a> TableWidget<'a> {
}
for col in self.index_column..self.columns.len() {
let need_split_line = state.count_columns > 0 && width < area.width;
if need_split_line {
width += render_split_line(buf, width, area.y, area.height, show_head, separator_s);
}
let mut column = create_column(data, col);
let column_width = calculate_column_width(&column);
@ -197,22 +191,26 @@ impl<'a> TableWidget<'a> {
}
if show_head {
let mut header = [head_row_text(&head, self.style_computer)];
let head_style = head_style(&head, self.style_computer);
if head_width > use_space as usize {
truncate_str(&mut header[0].0, use_space as usize)
truncate_str(&mut head, use_space as usize)
}
let head_iter = [(&head, head_style)].into_iter();
// we don't change width here cause the whole column have the same width; so we add it when we print data
let mut w = width;
w += render_space(buf, w, head_y, 1, padding_l);
w += render_column(buf, w, head_y, use_space, &header);
w += render_column(buf, w, head_y, use_space, head_iter);
w += render_space(buf, w, head_y, 1, padding_r);
let x = w - padding_r - use_space;
state.layout.push(&header[0].0, x, head_y, use_space, 1);
state.layout.push(&head, x, head_y, use_space, 1);
}
let column_rows = column.iter().map(|(t, s)| (t, *s));
width += render_space(buf, width, data_y, data_height, padding_l);
width += render_column(buf, width, data_y, use_space, &column);
width += render_column(buf, width, data_y, use_space, column_rows);
width += render_space(buf, width, data_y, data_height, padding_r);
for (row, (text, _)) in column.iter().enumerate() {
@ -235,15 +233,7 @@ impl<'a> TableWidget<'a> {
}
if width < area.width {
width += render_vertical_line_with_split(
buf,
width,
data_y,
data_height,
show_head,
false,
separator_s,
);
width += render_split_line(buf, width, area.y, area.height, show_head, separator_s);
}
let rest = area.width.saturating_sub(width);
@ -255,6 +245,7 @@ impl<'a> TableWidget<'a> {
}
}
// header at the left; header is always 1 line
fn render_table_vertical(self, area: Rect, buf: &mut Buffer, state: &mut TableWidgetState) {
if area.width == 0 || area.height == 0 {
return;
@ -305,10 +296,9 @@ impl<'a> TableWidget<'a> {
return;
}
let columns = columns
let columns_iter = columns
.iter()
.map(|s| head_row_text(s, self.style_computer))
.collect::<Vec<_>>();
.map(|s| (s.clone(), head_style(s, self.style_computer)));
if !show_index {
let x = area.x + left_w;
@ -326,12 +316,12 @@ impl<'a> TableWidget<'a> {
let x = area.x + left_w;
left_w += render_space(buf, x, area.y, 1, padding_l);
let x = area.x + left_w;
left_w += render_column(buf, x, area.y, columns_width as u16, &columns);
left_w += render_column(buf, x, area.y, columns_width as u16, columns_iter);
let x = area.x + left_w;
left_w += render_space(buf, x, area.y, 1, padding_r);
let layout_x = left_w - padding_r - columns_width as u16;
for (i, (text, _)) in columns.iter().enumerate() {
for (i, text) in columns.iter().enumerate() {
state
.layout
.push(text, layout_x, area.y + i as u16, columns_width as u16, 1);
@ -354,6 +344,9 @@ impl<'a> TableWidget<'a> {
state.count_rows = columns.len();
state.count_columns = 0;
// note: is there a time where we would have more then 1 column?
// seems like not really; cause it's literally KV table, or am I wrong?
for col in self.index_column..self.data.len() {
let mut column =
self.data[col][self.index_row..self.index_row + columns.len()].to_vec();
@ -362,6 +355,13 @@ impl<'a> TableWidget<'a> {
break;
}
// see KV comment; this block might never got used
let need_split_line = state.count_columns > 0 && left_w < area.width;
if need_split_line {
render_vertical_line(buf, area.x + left_w, area.y, area.height, separator_s);
left_w += 1;
}
let column_width = column_width as u16;
let available = area.width - left_w;
let is_last = col + 1 == self.data.len();
@ -377,10 +377,12 @@ impl<'a> TableWidget<'a> {
break;
}
let head_rows = column.iter().map(|(t, s)| (t, *s));
let x = area.x + left_w;
left_w += render_space(buf, x, area.y, area.height, padding_l);
let x = area.x + left_w;
left_w += render_column(buf, x, area.y, column_width, &column);
left_w += render_column(buf, x, area.y, column_width, head_rows);
let x = area.x + left_w;
left_w += render_space(buf, x, area.y, area.height, padding_r);
@ -554,6 +556,51 @@ fn render_index(
width
}
fn render_split_line(
buf: &mut Buffer,
x: u16,
y: u16,
height: u16,
has_head: bool,
style: NuStyle,
) -> u16 {
if has_head {
render_vertical_split_line(buf, x, y, height, &[0], &[2], &[], style);
} else {
render_vertical_split_line(buf, x, y, height, &[], &[], &[], style);
}
1
}
#[allow(clippy::too_many_arguments)]
fn render_vertical_split_line(
buf: &mut Buffer,
x: u16,
y: u16,
height: u16,
top_slit: &[u16],
inner_slit: &[u16],
bottom_slit: &[u16],
style: NuStyle,
) -> u16 {
render_vertical_line(buf, x, y, height, style);
for &y in top_slit {
render_top_connector(buf, x, y, style);
}
for &y in inner_slit {
render_inner_connector(buf, x, y, style);
}
for &y in bottom_slit {
render_bottom_connector(buf, x, y, style);
}
1
}
fn render_vertical_line_with_split(
buf: &mut Buffer,
x: u16,
@ -619,14 +666,14 @@ fn repeat_vertical(
c: char,
style: TextStyle,
) {
let text = std::iter::repeat(c)
.take(width as usize)
.collect::<String>();
let text = String::from(c);
let style = text_style_to_tui_style(style);
let span = Span::styled(text, style);
let span = Span::styled(&text, style);
for row in 0..height {
buf.set_span(x, y + row, &span, width);
for col in 0..width {
buf.set_span(x + col, y + row, &span, 1);
}
}
}
@ -667,6 +714,12 @@ fn render_bottom_connector(buf: &mut Buffer, x: u16, y: u16, style: NuStyle) {
buf.set_span(x, y, &span, 1);
}
fn render_inner_connector(buf: &mut Buffer, x: u16, y: u16, style: NuStyle) {
let style = nu_style_to_tui(style);
let span = Span::styled("", style);
buf.set_span(x, y, &span, 1);
}
fn calculate_column_width(column: &[NuText]) -> usize {
column
.iter()
@ -676,35 +729,28 @@ fn calculate_column_width(column: &[NuText]) -> usize {
.unwrap_or(0)
}
fn render_column(
fn render_column<T, S>(
buf: &mut ratatui::buffer::Buffer,
x: u16,
y: u16,
available_width: u16,
rows: &[NuText],
) -> u16 {
for (row, (text, style)) in rows.iter().enumerate() {
let style = text_style_to_tui_style(*style);
let text = strip_string(text);
let span = Span::styled(text, style);
rows: impl Iterator<Item = (T, S)>,
) -> u16
where
T: AsRef<str>,
S: Into<TextStyle>,
{
for (row, (text, style)) in rows.enumerate() {
let style = text_style_to_tui_style(style.into());
let span = Span::styled(text.as_ref(), style);
buf.set_span(x, y + row as u16, &span, available_width);
}
available_width
}
fn strip_string(text: &str) -> String {
String::from_utf8(strip_ansi_escapes::strip(text))
.map_err(|_| ())
.unwrap_or_else(|_| text.to_owned())
}
fn head_row_text(head: &str, style_computer: &StyleComputer) -> NuText {
(
String::from(head),
TextStyle::with_style(
Alignment::Center,
style_computer.compute("header", &Value::string(head, nu_protocol::Span::unknown())),
),
)
fn head_style(head: &str, style_computer: &StyleComputer) -> TextStyle {
let style =
style_computer.compute("header", &Value::string(head, nu_protocol::Span::unknown()));
TextStyle::with_style(Alignment::Center, style)
}

View File

@ -16,21 +16,21 @@ use ratatui::{
};
use std::cmp::min;
pub struct TryView<'a> {
pub struct TryView {
input: Value,
command: String,
reactive: bool,
table: Option<RecordView<'a>>,
immediate: bool,
table: Option<RecordView>,
view_mode: bool,
border_color: Style,
}
impl<'a> TryView<'a> {
impl TryView {
pub fn new(input: Value) -> Self {
Self {
input,
table: None,
reactive: false,
immediate: false,
border_color: Style::default(),
view_mode: false,
command: String::new(),
@ -48,7 +48,7 @@ impl<'a> TryView<'a> {
}
}
impl View for TryView<'_> {
impl View for TryView {
fn draw(&mut self, f: &mut Frame, area: Rect, cfg: ViewConfig<'_>, layout: &mut Layout) {
let border_color = self.border_color;
@ -178,7 +178,7 @@ impl View for TryView<'_> {
if !self.command.is_empty() {
self.command.pop();
if self.reactive {
if self.immediate {
match self.try_run(engine_state, stack) {
Ok(_) => info.report = Some(Report::default()),
Err(err) => info.report = Some(Report::error(format!("Error: {err}"))),
@ -191,7 +191,7 @@ impl View for TryView<'_> {
KeyCode::Char(c) => {
self.command.push(*c);
if self.reactive {
if self.immediate {
match self.try_run(engine_state, stack) {
Ok(_) => info.report = Some(Report::default()),
Err(err) => info.report = Some(Report::error(format!("Error: {err}"))),
@ -235,7 +235,7 @@ impl View for TryView<'_> {
fn setup(&mut self, config: ViewConfig<'_>) {
self.border_color = nu_style_to_tui(config.explore_config.table.separator_style);
self.reactive = config.explore_config.try_reactive;
self.immediate = config.explore_config.try_reactive;
let mut r = RecordView::new(vec![], vec![]);
r.setup(config);
@ -253,7 +253,7 @@ fn run_command(
input: &Value,
engine_state: &EngineState,
stack: &mut Stack,
) -> Result<RecordView<'static>> {
) -> Result<RecordView> {
let pipeline = run_command_with_value(command, input, engine_state, stack)?;
let is_record = matches!(pipeline, PipelineData::Value(Value::Record { .. }, ..));

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"

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